Left: | ||
Right: |
OLD | NEW |
---|---|
(Empty) | |
1 /* | |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | |
saroyanm
2018/02/28 20:57:34
Moving this file to the buildtools directory still
Thomas Greiner
2018/03/19 18:28:04
What does this have to do with buildtools? I was t
Thomas Greiner
2018/03/19 18:54:13
Mind mentioning this script in the README? Preferr
saroyanm
2018/04/26 17:53:52
Acknowledged, I'll move this to the "build" direct
saroyanm
2018/04/26 17:53:52
Good point. I'll add information in the README as
saroyanm
2018/05/04 13:51:10
Done.
saroyanm
2018/05/04 13:51:10
Done.
| |
3 * Copyright (C) 2006-present eyeo GmbH | |
4 * | |
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 | |
7 * published by the Free Software Foundation. | |
8 * | |
9 * Adblock Plus is distributed in the hope that it will be useful, | |
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 * GNU General Public License for more details. | |
13 * | |
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/>. | |
16 */ | |
17 | |
18 const fs = require("fs"); | |
19 const {exec} = require("child_process"); | |
20 const path = require("path"); | |
21 const csv = require("csv"); | |
22 const {promisify} = require("util"); | |
23 const csvParser = promisify(csv.parse); | |
24 | |
25 | |
26 const localesDir = "locale"; | |
27 const defaultLocale = "en_US"; | |
28 | |
29 // ex.: desktop-options.json | |
30 let fileNames = []; | |
31 // List of all available locale codes | |
32 let locales = []; | |
33 | |
34 let headers = ["StringID", "Description", "Placeholders", defaultLocale]; | |
35 let outputFileName = "translations-{repo}-{hash}.csv"; | |
36 | |
37 /** | |
38 * Export existing translation - files into CSV file | |
39 * @param {string[]} [filesFilter] - fileNames filter, if omitted all files | |
40 * will be exported | |
41 */ | |
42 function exportTranslations(filesFilter) | |
43 { | |
44 let mercurialCommands = []; | |
45 // Get Hash | |
46 mercurialCommands.push(executeMercurial(["id", "-i"])); | |
47 // Get repo path | |
48 mercurialCommands.push(executeMercurial(["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((item) => item.replace(/\+\n|\n$/, "")); | |
53 // Update name of the file to be output | |
54 outputFileName = outputFileName.replace("{hash}", hash); | |
55 outputFileName = outputFileName.replace("{repo}", path.basename(filePath)); | |
56 | |
57 // Read all available locales and default files | |
58 return Promise.all([readDir(path.join(localesDir, defaultLocale)), | |
saroyanm
2018/05/04 13:51:08
I think we should use module like glob -> https://
Thomas Greiner
2018/05/07 15:16:33
Acknowledged.
| |
59 readDir(localesDir)]); | |
60 }).then((files) => | |
61 { | |
62 [fileNames, locales] = files; | |
63 // Filter files | |
64 if (filesFilter.length) | |
65 fileNames = fileNames.filter((item) => filesFilter.includes(item)); | |
66 | |
67 let readJsonPromises = []; | |
68 for(let fileName of fileNames) | |
69 { | |
70 for(let locale of locales) | |
71 { | |
72 readJsonPromises.push(readJson(locale, fileName)); | |
73 } | |
74 } | |
75 | |
76 // Reading all existing translations files | |
77 return Promise.all(readJsonPromises); | |
78 }).then(csvFromJsonFileObjects); | |
79 } | |
80 | |
81 /** | |
82 * Creating Matrix which reflects output CSV file | |
83 * @param {Array} fileObjects - array of file objects created by readJson | |
84 * @return {Array} Matrix | |
85 */ | |
86 function csvFromJsonFileObjects(fileObjects) | |
87 { | |
88 // Create Object tree from the Objects array, for easier search | |
89 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} | |
90 let dataTreeObj = fileObjects.reduce((accumulator, fileObject) => | |
91 { | |
92 if (!fileObject) | |
93 return accumulator; | |
94 | |
95 let {fileName, locale} = fileObject; | |
96 if (!accumulator[fileName]) | |
97 { | |
98 accumulator[fileName] = {}; | |
99 } | |
100 accumulator[fileName][locale] = fileObject.strings; | |
101 return accumulator; | |
102 }, {}); | |
103 | |
104 // Create two dimensional strings array that reflects CSV structure | |
105 let translationLocales = locales.filter((locale) => locale != defaultLocale); | |
106 let csvArray = [headers.concat(translationLocales)]; | |
107 for (let fileName of fileNames) | |
saroyanm
2018/05/04 13:51:10
I have this information in fileObjects, I shouldn'
Thomas Greiner
2018/05/07 15:16:33
Acknowledged.
| |
108 { | |
109 csvArray.push([fileName]); | |
110 let strings = dataTreeObj[fileName][defaultLocale]; | |
111 for (let stringID of Object.keys(strings)) | |
112 { | |
113 let fileObj = dataTreeObj[fileName]; | |
114 let {description, message, placeholders} = strings[stringID]; | |
115 let row = [stringID, description || "", JSON.stringify(placeholders), | |
116 message]; | |
117 | |
118 for (let locale of translationLocales) | |
119 { | |
120 let localeFileObj = fileObj[locale]; | |
121 let isTranslated = !!(localeFileObj && localeFileObj[stringID]); | |
122 row.push(isTranslated ? localeFileObj[stringID].message : ""); | |
123 } | |
124 csvArray.push(row); | |
125 } | |
126 } | |
127 arrayToCsv(csvArray); | |
128 } | |
129 | |
130 /** | |
131 * Import strings from the CSV file | |
132 * @param {string} filePath - CSV file path to import from | |
133 */ | |
134 function importTranslations(filePath) | |
135 { | |
136 readFile(filePath).then((fileObjects) => | |
137 { | |
138 return csvParser(fileObjects, {relax_column_count: true}); | |
Thomas Greiner
2018/03/19 18:54:13
Why do we end up with an inconsistent number of co
saroyanm
2018/04/26 17:53:51
Meeting note: We will use new column called filena
saroyanm
2018/05/04 13:51:08
Done.
saroyanm
2018/05/04 13:51:08
Apparently we were generating right amount of comm
| |
139 }).then((dataMatrix) => | |
140 { | |
141 let headers = dataMatrix.shift(); | |
142 let [headId, headDescription, headPlaceholder, ...headLocales] = headers; | |
143 let dataTreeObj = {}; | |
144 let currentFilename = ""; | |
145 for(let rowId in dataMatrix) | |
146 { | |
147 let row = dataMatrix[rowId]; | |
148 let [stringId, description, placeholder, ...messages] = row; | |
149 if (!stringId) | |
150 continue; | |
151 | |
152 stringId = stringId.trim(); | |
153 // Check if it's the filename row | |
154 if (stringId.endsWith(".json")) | |
155 { | |
156 currentFilename = stringId; | |
157 dataTreeObj[currentFilename] = {}; | |
158 continue; | |
159 } | |
160 | |
161 description = description.trim(); | |
162 for (let i = 0; i < headLocales.length; i++) | |
163 { | |
164 let locale = headLocales[i].trim(); | |
165 let message = messages[i].trim(); | |
166 if (!message) | |
167 continue; | |
168 | |
169 // Create Object tree from the Objects array, for easier search | |
170 // ex.: {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} | |
171 if (!dataTreeObj[currentFilename][locale]) | |
172 dataTreeObj[currentFilename][locale] = {}; | |
173 | |
174 let localeObj = dataTreeObj[currentFilename][locale]; | |
175 localeObj[stringId] = {}; | |
Thomas Greiner
2018/03/19 18:28:02
Detail: You're referencing `localeObj[stringId]` a
saroyanm
2018/04/26 17:53:52
Acknowledged.
saroyanm
2018/05/04 13:51:09
Done.
| |
176 | |
177 // We keep string descriptions only in default locale files | |
178 if (locale == defaultLocale) | |
179 localeObj[stringId].description = description; | |
180 | |
181 localeObj[stringId].message = message; | |
182 | |
183 if (placeholder) | |
184 localeObj[stringId].placeholders = JSON.parse(placeholder); | |
185 } | |
186 } | |
187 writeJson(dataTreeObj); | |
188 }); | |
189 } | |
190 | |
191 /** | |
192 * Write locale files according to dataTreeObj | |
193 * @param {Object} dataTreeObj - ex.: | |
194 * {dektop-options.json: {en_US: {...}, {de: {...}, {ru: {...}}} | |
195 */ | |
196 function writeJson(dataTreeObj) | |
197 { | |
198 for (let fileName in dataTreeObj) | |
199 { | |
200 for (let locale in dataTreeObj[fileName]) | |
saroyanm
2018/02/28 20:57:34
When writing to the file we should first find a wa
Thomas Greiner
2018/03/19 18:28:04
We cannot rely on the order of object properties b
saroyanm
2018/04/26 17:53:52
Meeting note: This is required, because we don't w
saroyanm
2018/05/04 13:51:11
Done.
| |
201 { | |
202 let filePath = path.join(localesDir, locale, fileName); | |
203 let fileString = JSON.stringify(dataTreeObj[fileName][locale], null, 2); | |
204 | |
205 // Newline at end of file to match Coding Style | |
206 if (locale == defaultLocale) | |
207 fileString += "\n"; | |
208 fs.writeFile(filePath, fileString, "utf8", (err) => | |
209 { | |
210 if (err) | |
211 { | |
212 console.error(err); | |
213 } | |
214 else | |
215 { | |
216 console.log(`Updated: ${filePath}`); | |
217 } | |
218 }); | |
219 } | |
220 } | |
221 } | |
222 | |
223 /** | |
224 * Convert two dimensional array to the CSV file | |
225 * @param {string[][]} csvArray - array to convert from | |
226 */ | |
227 function arrayToCsv(csvArray) | |
228 { | |
229 csv.stringify(csvArray, (err, output) => | |
230 { | |
231 fs.writeFile(outputFileName, output, "utf8", function (err) | |
Thomas Greiner
2018/03/19 18:28:03
Coding style: `function (err)` violates the follow
saroyanm
2018/04/26 17:53:51
Acknowledged.
saroyanm
2018/05/04 13:51:08
Done.
| |
232 { | |
233 if (!err) | |
234 console.log(`${outputFileName} is created`); | |
Thomas Greiner
2018/03/19 18:28:04
We should only ignore errors in exceptional cases.
saroyanm
2018/04/26 17:53:51
Acknowledged.
saroyanm
2018/05/04 13:51:07
Done.
| |
235 }); | |
236 }); | |
237 } | |
238 | |
239 /** | |
240 * Reads JSON file and assign filename and locale to it | |
241 * @param {string} locale - ex.: "en_US", "de"... | |
242 * @param {string} file - ex.: "desktop-options.json" | |
Thomas Greiner
2018/03/19 18:28:04
Detail: Again, "file" is not what you call it in t
saroyanm
2018/05/04 13:51:07
Done.
| |
243 * @return {Promise<Object>} fileName, locale and Strings of locale file | |
Thomas Greiner
2018/03/19 18:28:03
Typo: Replace "Strings" with "strings"
saroyanm
2018/05/04 13:51:10
Done.
| |
244 */ | |
245 function readJson(locale, fileName) | |
246 { | |
247 return new Promise((resolve, reject) => | |
248 { | |
249 let filePath = path.join(localesDir, locale, fileName); | |
250 fs.readFile(filePath, (err, data) => | |
251 { | |
252 if (err) | |
253 { | |
254 reject(err); | |
255 } | |
256 else | |
257 { | |
258 resolve({fileName, locale, strings: JSON.parse(data)}); | |
259 } | |
260 }); | |
261 // Continue Promise.All even if rejected. | |
Thomas Greiner
2018/03/19 18:28:02
Detail: Why? Not being able to read from a JSON fi
saroyanm
2018/05/04 13:51:11
Done, beforehand I was using locales and filenames
| |
262 }).catch(reason => {}); | |
263 } | |
264 | |
265 /** | |
266 * Reads file | |
267 * @param {string} filePath | |
268 * @return {Promise<Object>} contents of file in given location | |
269 */ | |
270 function readFile(filePath) | |
271 { | |
272 return new Promise((resolve, reject) => | |
273 { | |
274 fs.readFile(filePath, "utf8", (err, data) => | |
275 { | |
276 if (err) | |
277 reject(err); | |
278 else | |
279 resolve(data); | |
280 }); | |
281 }); | |
282 } | |
283 | |
284 /** | |
285 * Read files and folder names inside of the directory | |
286 * @param {string} dir - path of the folder | |
287 * @return {Promise<Object>} array of folders | |
Thomas Greiner
2018/03/19 18:28:03
Detail: The return type is `Promise<string[]>`.
Thomas Greiner
2018/03/19 18:28:03
Suggestion: Technically, those can be either folde
saroyanm
2018/05/04 13:51:09
Irrelevant in the new patch.
| |
288 */ | |
289 function readDir(dir) | |
Thomas Greiner
2018/03/19 18:28:04
Suggestion: You could avoid having to write such f
saroyanm
2018/04/26 17:53:52
Agree.
saroyanm
2018/05/04 13:51:07
Done.
| |
290 { | |
291 return new Promise((resolve, reject) => | |
292 { | |
293 fs.readdir(dir, (err, folders) => | |
294 { | |
295 if (err) | |
296 reject(err); | |
297 else | |
298 resolve(folders); | |
299 }); | |
300 }); | |
301 } | |
302 | |
303 /** | |
304 * Executing mercurial commands on the system level | |
305 * @param {string} command - mercurial command ex.:"hg ..." | |
306 * @return {Promise<Object>} output of the command | |
307 */ | |
308 function executeMercurial(commands) | |
309 { | |
310 return new Promise((resolve, reject) => | |
311 { | |
312 exec(`hg ${commands.join(" ")}`, (err, output) => | |
Thomas Greiner
2018/03/19 18:28:04
Detail: `child_process.execFile()` already does wh
saroyanm
2018/05/04 13:51:08
Done.
| |
313 { | |
314 if (err) | |
315 reject(err); | |
316 else | |
317 resolve(output); | |
318 }); | |
319 }); | |
320 } | |
321 | |
322 // CLI | |
323 let helpText = ` | |
324 About: Converts locale files between CSV and JSON formats | |
325 Usage: csv-export.js [option] [argument] | |
326 Options: | |
327 -f [FILENAME] Name of the files to be exported ex.: -f firstRun.json | |
328 option can be used multiple times. | |
329 If omitted all files are being exported | |
330 | |
331 -o [FILENAME] Output filename ex.: | |
332 -f firstRun.json -o {hash}-firstRun.csv | |
Thomas Greiner
2018/03/19 18:28:02
Detail: Be careful when passing arguments like tha
saroyanm
2018/05/04 13:51:10
Not sure I understand the comment. I'm not passing
Thomas Greiner
2018/05/07 15:16:33
I'm referring to the CLI argument `-o {hash}-first
| |
333 Placeholders: | |
334 {hash} - Mercurial current revision hash | |
335 {repo} - Name of the "Default" repository | |
336 If omitted the output fileName is set to | |
337 translations-{repo}-{hash}.csv | |
338 | |
339 -i [FILENAME] Import file path ex: -i issue-reporter.csv | |
340 `; | |
341 | |
342 let arguments = process.argv.slice(2); | |
343 let stopExportScript = false; | |
344 // Filter to be used export to the fileNames inside | |
345 let filesFilter = []; | |
346 | |
347 for (let i = 0; i < arguments.length; i++) | |
348 { | |
349 switch (arguments[i]) | |
350 { | |
351 case "-h": | |
352 console.log(helpText); | |
353 stopExportScript = true; | |
354 break; | |
355 case "-f": | |
356 // check if argument following option is specified | |
357 if (!arguments[i + 1]) | |
358 { | |
359 process.exit("Please specify the input filename"); | |
Thomas Greiner
2018/03/19 18:28:04
This is not how you call `process.exit()`.
See ht
saroyanm
2018/05/04 13:51:10
Done.
| |
360 } | |
361 else | |
362 { | |
363 filesFilter.push(arguments[i + 1]); | |
364 } | |
365 break; | |
366 case "-o": | |
367 if (!arguments[i + 1]) | |
368 { | |
369 process.exit("Please specify the output filename"); | |
370 } | |
371 else | |
372 { | |
373 outputFileName = arguments[i + 1]; | |
374 } | |
375 break; | |
376 case "-i": | |
377 if (!arguments[i + 1]) | |
378 { | |
379 process.exit("Please specify the import file"); | |
380 } | |
381 else | |
382 { | |
383 let importFile = arguments[i + 1]; | |
384 importTranslations(importFile); | |
385 stopExportScript = true; | |
386 } | |
387 break; | |
388 } | |
389 } | |
390 | |
391 if (!stopExportScript) | |
392 exportTranslations(filesFilter); | |
OLD | NEW |