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: Created June 18, 2018, 2:49 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);
29 const writeFile = promisify(fs.writeFile);
30 const glob = promisify(require("glob").glob); 30 const glob = promisify(require("glob").glob);
31 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 const headers = ["Filename", "StringID", "Description", "Placeholders",
37 defaultLocale]; 36 defaultLocale];
38 let outputFileName = "translations-{repo}-{hash}.csv"; 37 let outputFileName = "translations.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 */ 43 {
45 function exportTranslations(filesFilter) 44 glob(`${localesDir}/**/*.json`).then((filePaths) =>
46 {
47 let mercurialCommands = [];
48 // Get Hash
49 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
51 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) =>
53 {
54 // Remove line endings and "+" sign from the end of the hash
55 let [hash, filePath] = outputs.map((item) =>
56 item.stdout.replace(/\+\n|\n$/, ""));
57 // Update name of the file to be output
58 outputFileName = outputFileName.replace("{hash}", hash);
59 outputFileName = outputFileName.replace("{repo}", path.basename(filePath));
60
61 // Read all available locales and default files
62 return glob(`${localesDir}/**/*.json`, {});
63 }).then((filePaths) =>
64 { 45 {
65 // Reading all existing translations files 46 // Reading all existing translations files
66 return Promise.all(filePaths.map((filePath) => readJsonPromised(filePath))); 47 return Promise.all(filePaths.map((filePath) => readJson(filePath)));
67 }).then(csvFromJsonFileObjects); 48 }).then(csvFromJsonFileObjects);
68 } 49 }
69 50
70 /** 51 /**
71 * Creating Matrix which reflects output CSV file 52 * Creating Matrix which reflects output CSV file
72 * @param {Array} fileObjects - array of file objects created by readJson 53 * @param {Object[]} fileObjects - array of file objects created by readJson
73 */ 54 */
74 function csvFromJsonFileObjects(fileObjects) 55 function csvFromJsonFileObjects(fileObjects)
75 { 56 {
76 let locales = []; 57 const locales = [];
58 const fileNames = [];
77 // Create Object tree from the Objects array, for easier search 59 // Create Object tree from the Objects array, for easier search
78 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} 60 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}}
79 let dataTreeObj = fileObjects.reduce((accumulator, fileObject) => 61 const dataTreeObj = Object.create(null);
80 { 62 for (const fileObject of fileObjects)
81 if (!fileObject) 63 {
82 return accumulator; 64 const {fileName, locale, strings} = fileObject;
83 65
84 let {fileName, locale} = fileObject; 66 if (locale != defaultLocale && !locales.includes(locale))
85 if (!locales.includes(locale))
86 locales.push(locale); 67 locales.push(locale);
87 68
88 if (!accumulator[fileName]) 69 if ((!filesFilter.length || filesFilter.includes(fileName)) &&
89 { 70 !fileNames.includes(fileName))
90 accumulator[fileName] = {}; 71 fileNames.push(fileName);
91 } 72
92 accumulator[fileName][locale] = fileObject.strings; 73 if (!(fileName in dataTreeObj))
93 return accumulator; 74 dataTreeObj[fileName] = Object.create(null);
94 }, {}); 75
95 76 dataTreeObj[fileName][locale] = strings;
96 let fileNames = Object.keys(dataTreeObj); 77 }
97 if (filesFilter.length)
98 fileNames = fileNames.filter((item) => filesFilter.includes(item));
99
100 locales = locales.filter((locale) => locale != defaultLocale).sort();
101 // Create two dimensional strings array that reflects CSV structure 78 // Create two dimensional strings array that reflects CSV structure
102 let csvArray = [headers.concat(locales)]; 79 const csvArray = [headers.concat(locales)];
103 for (let fileName of fileNames) 80 for (const fileName of fileNames)
104 { 81 {
105 let strings = dataTreeObj[fileName][defaultLocale]; 82 const strings = dataTreeObj[fileName][defaultLocale];
106 for (let stringID of Object.keys(strings)) 83 for (const stringID of Object.keys(strings))
107 { 84 {
108 let fileObj = dataTreeObj[fileName]; 85 const fileObj = dataTreeObj[fileName];
109 let {description, message, placeholders} = strings[stringID]; 86 const {description, message, placeholders} = strings[stringID];
110 let row = [fileName, stringID, description || "", 87 const row = [fileName, stringID, description || "",
111 JSON.stringify(placeholders), message]; 88 JSON.stringify(placeholders), message];
112 89
113 for (let locale of locales) 90 for (const locale of locales)
114 { 91 {
115 let localeFileObj = fileObj[locale]; 92 const localeFileObj = fileObj[locale];
116 let isTranslated = !!(localeFileObj && localeFileObj[stringID]); 93 const isTranslated = !!(localeFileObj && localeFileObj[stringID]);
117 row.push(isTranslated ? localeFileObj[stringID].message : ""); 94 row.push(isTranslated ? localeFileObj[stringID].message : "");
118 } 95 }
119 csvArray.push(row); 96 csvArray.push(row);
120 } 97 }
121 } 98 }
122 arrayToCsv(csvArray); 99 arrayToCsv(csvArray);
123 } 100 }
124 101
125 /** 102 /**
126 * Import strings from the CSV file 103 * Import strings from the CSV file
127 * @param {string} filePath - CSV file path to import from 104 * @param {string} filePath - CSV file path to import from
128 */ 105 */
129 function importTranslations(filePath) 106 function importTranslations(filePath)
130 { 107 {
131 readFile(filePath, "utf8").then((fileObjects) => 108 readFile(filePath, "utf8").then((fileObjects) =>
132 { 109 {
133 return csvParser(fileObjects); 110 return csvParser(fileObjects);
134 }).then((dataMatrix) => 111 }).then((dataMatrix) =>
135 { 112 {
136 let headLocales = dataMatrix.shift().slice(4); 113 const headLocales = dataMatrix.shift().slice(4);
137 let dataTreeObj = {}; 114 const dataTreeObj = {};
138 for (let rowId in dataMatrix) 115 for (const rowId in dataMatrix)
139 { 116 {
140 let row = dataMatrix[rowId]; 117 const row = dataMatrix[rowId];
141 let [currentFilename, stringId, description, placeholder, ...messages] = 118 let [currentFilename, stringId, description, placeholder, ...messages] =
142 row; 119 row;
143 if (!stringId) 120 if (!stringId)
144 continue; 121 continue;
145 122
146 stringId = stringId.trim(); 123 stringId = stringId.trim();
147 // Check if it's the filename row 124 // Check if it's the filename row
148 if (!dataTreeObj[currentFilename]) 125 if (!dataTreeObj[currentFilename])
149 dataTreeObj[currentFilename] = {}; 126 dataTreeObj[currentFilename] = {};
150 127
151 description = description.trim(); 128 description = description.trim();
152 for (let i = 0; i < headLocales.length; i++) 129 for (let i = 0; i < headLocales.length; i++)
153 { 130 {
154 let locale = headLocales[i].trim(); 131 const locale = headLocales[i].trim();
155 let message = messages[i].trim(); 132 const message = messages[i].trim();
156 if (!message) 133 if (!message)
157 continue; 134 continue;
158 135
159 // Create Object tree from the Objects array, for easier search 136 // Create Object tree from the Objects array, for easier search
160 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} 137 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}}
161 if (!dataTreeObj[currentFilename][locale]) 138 if (!dataTreeObj[currentFilename][locale])
162 dataTreeObj[currentFilename][locale] = {}; 139 dataTreeObj[currentFilename][locale] = {};
163 140
164 let localeObj = dataTreeObj[currentFilename][locale]; 141 const localeObj = dataTreeObj[currentFilename][locale];
165 localeObj[stringId] = {}; 142 localeObj[stringId] = {};
166 let stringObj = localeObj[stringId]; 143 const stringObj = localeObj[stringId];
167 144
168 // We keep string descriptions only in default locale files 145 // We keep string descriptions only in default locale files
169 if (locale == defaultLocale) 146 if (locale == defaultLocale && description)
170 stringObj.description = description; 147 stringObj.description = description;
171 148
172 stringObj.message = message; 149 stringObj.message = message;
173 if (placeholder) 150 if (placeholder)
174 stringObj.placeholders = JSON.parse(placeholder); 151 stringObj.placeholders = JSON.parse(placeholder);
175 } 152 }
176 } 153 }
177 writeJson(dataTreeObj); 154 writeJson(dataTreeObj);
178 }); 155 });
179 } 156 }
180 157
181 /** 158 /**
182 * Write locale files according to dataTreeObj 159 * Write locale files according to dataTreeObj
183 * @param {Object} dataTreeObj - ex.: 160 * @param {Object} dataTreeObj - ex.:
184 * {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} 161 * {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}}
185 */ 162 */
186 function writeJson(dataTreeObj) 163 function writeJson(dataTreeObj)
187 { 164 {
188 for (let fileName in dataTreeObj) 165 for (const fileName in dataTreeObj)
189 { 166 {
190 for (let locale in dataTreeObj[fileName]) 167 for (const locale in dataTreeObj[fileName])
191 { 168 {
192 let filePath = path.join(localesDir, locale, fileName); 169 const filePath = path.join(localesDir, locale, fileName);
193 let orderedJSON = orderJSON(dataTreeObj[fileName][locale]); 170 const sortedJson = sortJson(dataTreeObj[fileName][locale]);
194 let fileString = JSON.stringify(orderedJSON, null, 2); 171 let fileString = JSON.stringify(sortedJson, null, 2);
195 172
196 // Newline at end of file to match Coding Style 173 // Newline at end of file to match Coding Style
197 if (locale == defaultLocale) 174 if (locale == defaultLocale)
198 fileString += "\n"; 175 fileString += "\n";
199 fs.writeFile(filePath, fileString, "utf8", (err) => 176 writeFile(filePath, fileString, "utf8").then(() =>
200 { 177 {
201 if (err) 178 console.log(`Updated: ${filePath}`); // eslint-disable-line no-console
202 { 179 }).catch((err) =>
203 console.error(err); 180 {
204 } 181 console.error(err);
205 else
206 {
207 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 }
209 }); 182 });
210 } 183 }
211 } 184 }
212 } 185 }
213 186
214 /** 187 /**
215 * This function currently rely on nodeJS to sort the object by keys 188 * This function currently relies on V8 to sort the object by keys
216 * @param {Object} unordered - json object 189 * @param {Object} unordered - json object
217 * @returns {Object} 190 * @returns {Object}
218 */ 191 */
219 function orderJSON(unordered) 192 function sortJson(unordered)
220 { 193 {
221 const ordered = {}; 194 const ordered = {};
222 for (let key of Object.keys(unordered).sort()) 195 for (const key of Object.keys(unordered).sort())
223 { 196 {
224 ordered[key] = unordered[key]; 197 ordered[key] = unordered[key];
225 if (unordered[key].placeholders) 198 if (unordered[key].placeholders)
226 ordered[key].placeholders = orderJSON(unordered[key].placeholders); 199 ordered[key].placeholders = sortJson(unordered[key].placeholders);
227 200
228 ordered[key] = unordered[key]; 201 ordered[key] = unordered[key];
229 } 202 }
230 return ordered; 203 return ordered;
231 } 204 }
232 205
233 /** 206 /**
234 * Convert two dimensional array to the CSV file 207 * Convert two dimensional array to the CSV file
235 * @param {Array} csvArray - array to convert from 208 * @param {Object[]} csvArray - array to convert from
236 */ 209 */
237 function arrayToCsv(csvArray) 210 function arrayToCsv(csvArray)
238 { 211 {
239 csv.stringify(csvArray, (err, output) => 212 csv.stringify(csvArray, (err, output) =>
240 { 213 {
241 fs.writeFile(outputFileName, output, "utf8", (error) => 214 writeFile(outputFileName, output, "utf8").then(() =>
242 { 215 {
243 if (!error) 216 // eslint-disable-next-line no-console
244 console.log(`${outputFileName} is created`); 217 console.log(`${outputFileName} is created`);
245 else 218 }).catch((error) =>
246 console.error(error); 219 {
220 console.error(error);
247 }); 221 });
248 }); 222 });
249 } 223 }
250 224
251 /** 225 /**
252 * Reads JSON file and assign filename and locale to it 226 * Reads JSON file and assign filename and locale to it
253 * @param {string} filePath - ex.: "locales/en_US/desktop-options.json" 227 * @param {string} filePath - ex.: "locales/en_US/desktop-options.json"
254 * @param {function} callback - fileName, locale and strings of locale file 228 * @returns {Promise<Object>} resolves fileName, locale and strings of the
255 * Parameters: 229 * locale file
256 * * Error message 230 */
257 * * Object containing fileName, locale and strings 231 function readJson(filePath)
258 */ 232 {
259 function readJson(filePath, callback) 233 return readFile(filePath, "utf8").then((data) =>
260 { 234 {
261 let {dir, base} = path.parse(filePath); 235 const {dir, base} = path.parse(filePath);
262 fs.readFile(filePath, "utf8", (err, data) => 236 const locale = dir.split(path.sep).pop();
263 { 237 const strings = JSON.parse(data);
264 if (err) 238 return {fileName: base, locale, strings};
265 {
266 callback(err);
267 }
268 else
269 {
270 callback(null, {fileName: base, locale: dir.split("/").pop(),
271 strings: JSON.parse(data)});
272 }
273 }); 239 });
274 } 240 }
275 241
276 /** 242 /**
277 * Exit process and log error message 243 * Exit process and log error message
278 * @param {String} error error message 244 * @param {String} error error message
279 */ 245 */
280 function exitProcess(error) 246 function exitProcess(error)
281 { 247 {
282 console.error(error); 248 console.error(error);
283 process.exit(); 249 process.exit(1);
284 } 250 }
285 251
286 // CLI 252 // CLI
287 let helpText = ` 253 const helpText = `
288 About: Converts locale files between CSV and JSON formats 254 About: Converts locale files between CSV and JSON formats
289 Usage: csv-export.js [option] [argument] 255 Usage: csv-export.js [option] [argument]
290 Options: 256 Options:
291 -f [FILENAME] Name of the files to be exported ex.: -f firstRun.json 257 -f [FILENAME] Name of the files to be exported ex.: -f firstRun.json
292 option can be used multiple times. 258 option can be used multiple times.
293 If omitted all files are being exported 259 If omitted all files are being exported
294 260
295 -o [FILENAME] Output filename ex.: 261 -o [FILENAME] Output filename ex.:
296 -f firstRun.json -o {hash}-firstRun.csv 262 -f firstRun.json -o firstRun.csv
297 Placeholders:
298 {hash} - Mercurial current revision hash
299 {repo} - Name of the "Default" repository
300 If omitted the output fileName is set to 263 If omitted the output fileName is set to
301 translations-{repo}-{hash}.csv 264 translations.csv
302 265
303 -i [FILENAME] Import file path ex: -i issue-reporter.csv 266 -i [FILENAME] Import file path ex: -i issue-reporter.csv
304 `; 267 `;
305 268
306 let argv = process.argv.slice(2); 269 const argv = process.argv.slice(2);
307 let stopExportScript = false; 270 let stopExportScript = false;
308 // Filter to be used export to the fileNames inside 271 // Filter to be used export to the fileNames inside
309 let filesFilter = []; 272 const filesFilter = [];
310 273
311 for (let i = 0; i < argv.length; i++) 274 for (let i = 0; i < argv.length; i++)
312 { 275 {
313 switch (argv[i]) 276 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 { 277 {
315 case "-h": 278 case "-h":
316 console.log(helpText); 279 console.log(helpText); // eslint-disable-line no-console
317 stopExportScript = true; 280 stopExportScript = true;
318 break; 281 break;
319 case "-f": 282 case "-f":
320 // check if argument following option is specified
321 if (!argv[i + 1]) 283 if (!argv[i + 1])
322 { 284 {
323 exitProcess("Please specify the input filename"); 285 exitProcess("Please specify the input filename");
324 } 286 }
325 else 287 filesFilter.push(argv[i + 1]);
326 {
327 filesFilter.push(argv[i + 1]);
328 }
329 break; 288 break;
330 case "-o": 289 case "-o":
331 if (!argv[i + 1]) 290 if (!argv[i + 1])
332 { 291 {
333 exitProcess("Please specify the output filename"); 292 exitProcess("Please specify the output filename");
334 } 293 }
335 else 294 outputFileName = argv[i + 1];
336 {
337 outputFileName = argv[i + 1];
338 }
339 break; 295 break;
340 case "-i": 296 case "-i":
341 if (!argv[i + 1]) 297 if (!argv[i + 1])
342 { 298 {
343 exitProcess("Please specify the import file"); 299 exitProcess("Please specify the import file");
344 } 300 }
345 else 301 const importFile = argv[i + 1];
346 { 302 importTranslations(importFile);
347 let importFile = argv[i + 1]; 303 stopExportScript = true;
348 importTranslations(importFile);
349 stopExportScript = true;
350 }
351 break; 304 break;
352 } 305 }
353 } 306 }
354 307
355 if (!stopExportScript) 308 if (!stopExportScript)
356 exportTranslations(filesFilter); 309 exportTranslations(filesFilter);
LEFTRIGHT

Powered by Google App Engine
This is Rietveld