Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: build/csv-export.js

Issue 29636585: Issue 6171 - create CSV exporter and importer for translations (Closed)
Left Patch Set: Moved the script into build directory and updated the Readme Created May 4, 2018, 1:25 p.m.
Right Patch Set: Addressed Thomas comments Created May 16, 2018, 5:05 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « README.md ('k') | package.json » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-present eyeo GmbH 3 * Copyright (C) 2006-present eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 /* globals require, process */ 18 /* globals process */
19 19
20 "use strict"; 20 "use strict";
21 21
22 const fs = require("fs"); 22 const fs = require("fs");
23 const path = require("path"); 23 const path = require("path");
24 const csv = require("csv"); 24 const csv = require("csv");
25 const {promisify} = require("util"); 25 const {promisify} = require("util");
26 const execFile = promisify(require("child_process").execFile); 26 const execFile = promisify(require("child_process").execFile);
27 const csvParser = promisify(csv.parse); 27 const csvParser = promisify(csv.parse);
28 const readDir = promisify(fs.readdir);
29 const readFile = promisify(fs.readFile); 28 const readFile = promisify(fs.readFile);
30 const glob = promisify(require("glob").glob); 29 const glob = promisify(require("glob").glob);
31 const readJsonPromised = promisify(readJson); 30 const readJsonPromised = promisify(readJson);
32 31
33 const localesDir = "locale"; 32 const localesDir = "locale";
34 const defaultLocale = "en_US"; 33 const defaultLocale = "en_US";
35 34
36 let headers = ["filename", "StringID", "Description", "Placeholders", 35 let headers = ["Filename", "StringID", "Description", "Placeholders",
37 defaultLocale]; 36 defaultLocale];
38 let outputFileName = "translations-{repo}-{hash}.csv"; 37 let outputFileName = "translations-{repo}-{hash}.csv";
39 38
40 /** 39 /**
41 * Export existing translation - files into CSV file 40 * Export existing translation - files into CSV file
42 * @param {string[]} [filesFilter] - fileNames filter, if omitted all files 41 */
43 * will be exported 42 function exportTranslations()
44 */
45 function exportTranslations(filesFilter)
46 { 43 {
47 let mercurialCommands = []; 44 let mercurialCommands = [];
48 // Get Hash 45 // Get Hash
49 mercurialCommands.push(execFile("hg", ["id", "-i"])); 46 mercurialCommands.push(execFile("hg", ["id", "-i"]));
saroyanm 2018/05/15 18:59:49 This will not work in git. Alternative: mercuri
saroyanm 2018/05/17 17:27:37 As discussed: We better remove this mercurial spec
50 // Get repo path 47 // Get repo path
51 mercurialCommands.push(execFile("hg", ["paths", "default"])); 48 mercurialCommands.push(execFile("hg", ["paths", "default"]));
saroyanm 2018/05/15 18:59:49 This will not work in git. Alternative: mercurial
52 Promise.all(mercurialCommands).then((outputs) => 49 Promise.all(mercurialCommands).then((outputs) =>
53 { 50 {
54 // Remove line endings and "+" sign from the end of the hash 51 // Remove line endings and "+" sign from the end of the hash
55 let [hash, filePath] = outputs.map((item) => 52 let [hash, filePath] = outputs.map((output) =>
56 item.stdout.replace(/\+\n|\n$/, "")); 53 output.stdout.replace(/\+\n|\n$/, ""));
57 // Update name of the file to be output 54 // Update name of the file to be output
58 outputFileName = outputFileName.replace("{hash}", hash); 55 outputFileName = outputFileName.replace("{hash}", hash);
59 outputFileName = outputFileName.replace("{repo}", path.basename(filePath)); 56 outputFileName = outputFileName.replace("{repo}", path.basename(filePath));
60 57
61 // Read all available locales and default files 58 // Read all available locales and default files
62 return glob(`${localesDir}/**/*.json`, {}); 59 return glob(`${localesDir}/**/*.json`);
63 }).then((filePaths) => 60 }).then((filePaths) =>
64 { 61 {
65 // Reading all existing translations files 62 // Reading all existing translations files
66 return Promise.all(filePaths.map((filePath) => readJsonPromised(filePath))); 63 return Promise.all(filePaths.map((filePath) => readJsonPromised(filePath)));
67 }).then(csvFromJsonFileObjects); 64 }).then(csvFromJsonFileObjects);
68 } 65 }
69 66
70 /** 67 /**
71 * Creating Matrix which reflects output CSV file 68 * Creating Matrix which reflects output CSV file
72 * @param {Array} fileObjects - array of file objects created by readJson 69 * @param {Object[]} fileObjects - array of file objects created by readJson
73 */ 70 */
74 function csvFromJsonFileObjects(fileObjects) 71 function csvFromJsonFileObjects(fileObjects)
75 { 72 {
76 let locales = []; 73 let locales = [];
77 // Create Object tree from the Objects array, for easier search 74 // Create Object tree from the Objects array, for easier search
78 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} 75 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}}
79 let dataTreeObj = fileObjects.reduce((accumulator, fileObject) => 76 let dataTreeObj = Object.create(null);
80 { 77 for (let fileObject of fileObjects)
81 if (!fileObject) 78 {
82 return accumulator; 79 const {fileName, locale, strings} = fileObject;
83 80
84 let {fileName, locale} = fileObject;
85 if (!locales.includes(locale)) 81 if (!locales.includes(locale))
86 locales.push(locale); 82 locales.push(locale);
87 83
88 if (!accumulator[fileName]) 84 if (!dataTreeObj[fileName])
89 { 85 dataTreeObj[fileName] = {};
90 accumulator[fileName] = {}; 86 if (!dataTreeObj[fileName][locale])
91 } 87 dataTreeObj[fileName][locale] = {};
92 accumulator[fileName][locale] = fileObject.strings; 88 dataTreeObj[fileName][locale] = strings;
93 return accumulator; 89 }
94 }, {});
95 90
96 let fileNames = Object.keys(dataTreeObj); 91 let fileNames = Object.keys(dataTreeObj);
97 if (filesFilter.length) 92 if (filesFilter.length)
98 fileNames = fileNames.filter((item) => filesFilter.includes(item)); 93 fileNames = fileNames.filter((item) => filesFilter.includes(item));
99 94
100 locales = locales.filter((locale) => locale != defaultLocale).sort(); 95 locales = locales.filter((locale) => locale != defaultLocale).sort();
101 // Create two dimensional strings array that reflects CSV structure 96 // Create two dimensional strings array that reflects CSV structure
102 let csvArray = [headers.concat(locales)]; 97 let csvArray = [headers.concat(locales)];
103 for (let fileName of fileNames) 98 for (let fileName of fileNames)
104 { 99 {
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
183 * @param {Object} dataTreeObj - ex.: 178 * @param {Object} dataTreeObj - ex.:
184 * {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} 179 * {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}}
185 */ 180 */
186 function writeJson(dataTreeObj) 181 function writeJson(dataTreeObj)
187 { 182 {
188 for (let fileName in dataTreeObj) 183 for (let fileName in dataTreeObj)
189 { 184 {
190 for (let locale in dataTreeObj[fileName]) 185 for (let locale in dataTreeObj[fileName])
191 { 186 {
192 let filePath = path.join(localesDir, locale, fileName); 187 let filePath = path.join(localesDir, locale, fileName);
193 let orderedJSON = orderJSON(dataTreeObj[fileName][locale]); 188 let sortedJSON = orderJSON(dataTreeObj[fileName][locale]);
194 let fileString = JSON.stringify(orderedJSON, null, 2); 189 let fileString = JSON.stringify(sortedJSON, null, 2);
195 190
196 // Newline at end of file to match Coding Style 191 // Newline at end of file to match Coding Style
197 if (locale == defaultLocale) 192 if (locale == defaultLocale)
198 fileString += "\n"; 193 fileString += "\n";
199 fs.writeFile(filePath, fileString, "utf8", (err) => 194 fs.writeFile(filePath, fileString, "utf8", (err) =>
200 { 195 {
201 if (err) 196 if (err)
202 { 197 {
203 console.error(err); 198 console.error(err);
204 } 199 }
205 else 200 else
206 { 201 {
207 console.log(`Updated: ${filePath}`); 202 console.log(`Updated: ${filePath}`);
saroyanm 2018/05/04 13:56:27 console.log doesn't pass eslint -> Unexpected cons
Thomas Greiner 2018/05/22 17:22:49 Agreed, feel free to use "eslint-disable" to ignor
saroyanm 2018/06/05 15:03:46 Done.
208 } 203 }
209 }); 204 });
210 } 205 }
211 } 206 }
212 } 207 }
213 208
214 /** 209 /**
215 * This function currently rely on nodeJS to sort the object by keys 210 * This function currently relies on V8 to sort the object by keys
216 * @param {Object} unordered - json object 211 * @param {Object} unordered - json object
217 * @returns {Object} 212 * @returns {Object}
218 */ 213 */
219 function orderJSON(unordered) 214 function orderJSON(unordered)
220 { 215 {
221 const ordered = {}; 216 const ordered = {};
222 for (let key of Object.keys(unordered).sort()) 217 for (let key of Object.keys(unordered).sort())
223 { 218 {
224 ordered[key] = unordered[key]; 219 ordered[key] = unordered[key];
225 if (unordered[key].placeholders) 220 if (unordered[key].placeholders)
226 ordered[key].placeholders = orderJSON(unordered[key].placeholders); 221 ordered[key].placeholders = orderJSON(unordered[key].placeholders);
227 222
228 ordered[key] = unordered[key]; 223 ordered[key] = unordered[key];
229 } 224 }
230 return ordered; 225 return ordered;
231 } 226 }
232 227
233 /** 228 /**
234 * Convert two dimensional array to the CSV file 229 * Convert two dimensional array to the CSV file
235 * @param {Array} csvArray - array to convert from 230 * @param {Object[]} csvArray - array to convert from
236 */ 231 */
237 function arrayToCsv(csvArray) 232 function arrayToCsv(csvArray)
238 { 233 {
239 csv.stringify(csvArray, (err, output) => 234 csv.stringify(csvArray, (err, output) =>
240 { 235 {
241 fs.writeFile(outputFileName, output, "utf8", (error) => 236 fs.writeFile(outputFileName, output, "utf8", (error) =>
242 { 237 {
243 if (!error) 238 if (!error)
244 console.log(`${outputFileName} is created`); 239 console.log(`${outputFileName} is created`);
245 else 240 else
(...skipping 14 matching lines...) Expand all
260 { 255 {
261 let {dir, base} = path.parse(filePath); 256 let {dir, base} = path.parse(filePath);
262 fs.readFile(filePath, "utf8", (err, data) => 257 fs.readFile(filePath, "utf8", (err, data) =>
263 { 258 {
264 if (err) 259 if (err)
265 { 260 {
266 callback(err); 261 callback(err);
267 } 262 }
268 else 263 else
269 { 264 {
270 callback(null, {fileName: base, locale: dir.split("/").pop(), 265 let locale = dir.split(path.sep).pop();
271 strings: JSON.parse(data)}); 266 let strings = JSON.parse(data);
267 callback(null, {fileName: base, locale, strings});
272 } 268 }
273 }); 269 });
274 } 270 }
275 271
276 /** 272 /**
277 * Exit process and log error message 273 * Exit process and log error message
278 * @param {String} error error message 274 * @param {String} error error message
279 */ 275 */
280 function exitProcess(error) 276 function exitProcess(error)
281 { 277 {
282 console.error(error); 278 console.error(error);
283 process.exit(); 279 process.exit(1);
284 } 280 }
285 281
286 // CLI 282 // CLI
287 let helpText = ` 283 let helpText = `
288 About: Converts locale files between CSV and JSON formats 284 About: Converts locale files between CSV and JSON formats
289 Usage: csv-export.js [option] [argument] 285 Usage: csv-export.js [option] [argument]
290 Options: 286 Options:
291 -f [FILENAME] Name of the files to be exported ex.: -f firstRun.json 287 -f [FILENAME] Name of the files to be exported ex.: -f firstRun.json
292 option can be used multiple times. 288 option can be used multiple times.
293 If omitted all files are being exported 289 If omitted all files are being exported
294 290
295 -o [FILENAME] Output filename ex.: 291 -o [FILENAME] Output filename ex.:
296 -f firstRun.json -o {hash}-firstRun.csv 292 -f firstRun.json -o {hash}-firstRun.csv
297 Placeholders: 293 Placeholders:
298 {hash} - Mercurial current revision hash 294 {hash} - Mercurial current revision hash
299 {repo} - Name of the "Default" repository 295 {repo} - Name of the "Default" repository
300 If omitted the output fileName is set to 296 If omitted the output fileName is set to
301 translations-{repo}-{hash}.csv 297 translations-{repo}-{hash}.csv
302 298
303 -i [FILENAME] Import file path ex: -i issue-reporter.csv 299 -i [FILENAME] Import file path ex: -i issue-reporter.csv
304 `; 300 `;
305 301
306 let argv = process.argv.slice(2); 302 let argv = process.argv.slice(2);
307 let stopExportScript = false; 303 let stopExportScript = false;
308 // Filter to be used export to the fileNames inside 304 // Filter to be used export to the fileNames inside
309 let filesFilter = []; 305 let filesFilter = [];
310 306
311 for (let i = 0; i < argv.length; i++) 307 for (let i = 0; i < argv.length; i++)
312 { 308 {
313 switch (argv[i]) 309 switch (argv[i])
saroyanm 2018/05/04 13:51:11 It would be great to use a library like [minimist
Thomas Greiner 2018/05/07 15:16:39 Agreed.
314 { 310 {
315 case "-h": 311 case "-h":
316 console.log(helpText); 312 console.log(helpText);
317 stopExportScript = true; 313 stopExportScript = true;
318 break; 314 break;
319 case "-f": 315 case "-f":
320 // check if argument following option is specified
321 if (!argv[i + 1]) 316 if (!argv[i + 1])
322 { 317 {
323 exitProcess("Please specify the input filename"); 318 exitProcess("Please specify the input filename");
324 } 319 }
325 else 320 filesFilter.push(argv[i + 1]);
326 {
327 filesFilter.push(argv[i + 1]);
328 }
329 break; 321 break;
330 case "-o": 322 case "-o":
331 if (!argv[i + 1]) 323 if (!argv[i + 1])
332 { 324 {
333 exitProcess("Please specify the output filename"); 325 exitProcess("Please specify the output filename");
334 } 326 }
335 else 327 outputFileName = argv[i + 1];
336 {
337 outputFileName = argv[i + 1];
338 }
339 break; 328 break;
340 case "-i": 329 case "-i":
341 if (!argv[i + 1]) 330 if (!argv[i + 1])
342 { 331 {
343 exitProcess("Please specify the import file"); 332 exitProcess("Please specify the import file");
344 } 333 }
345 else 334 let importFile = argv[i + 1];
346 { 335 importTranslations(importFile);
347 let importFile = argv[i + 1]; 336 stopExportScript = true;
348 importTranslations(importFile);
349 stopExportScript = true;
350 }
351 break; 337 break;
352 } 338 }
353 } 339 }
354 340
355 if (!stopExportScript) 341 if (!stopExportScript)
356 exportTranslations(filesFilter); 342 exportTranslations(filesFilter);
LEFTRIGHT

Powered by Google App Engine
This is Rietveld