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