| 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 |