OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
| 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)), |
| 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) |
| 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}); |
| 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] = {}; |
| 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]) |
| 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) |
| 232 { |
| 233 if (!err) |
| 234 console.log(`${outputFileName} is created`); |
| 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" |
| 243 * @return {Promise<Object>} fileName, locale and Strings of locale file |
| 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. |
| 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 |
| 288 */ |
| 289 function readDir(dir) |
| 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) => |
| 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 |
| 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"); |
| 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 |