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

Powered by Google App Engine
This is Rietveld