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

Powered by Google App Engine
This is Rietveld