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