1import fs from "fs"; 2import path from "path"; 3import xml2js from "xml2js"; 4 5function main() { 6 const args = process.argv.slice(2); 7 if (args.length !== 3) { 8 console.log("Usage:"); 9 console.log("\tnode generateLocalizedDiagnosticMessages.js <lcl source directory> <output directory> <generated diagnostics map file>"); 10 return; 11 } 12 13 const inputPath = args[0]; 14 const outputPath = args[1]; 15 const diagnosticsMapFilePath = args[2]; 16 17 // generate the lcg file for enu 18 generateLCGFile(); 19 20 // generate other langs 21 fs.readdir(inputPath, (err, files) => { 22 handleError(err); 23 files.forEach(visitDirectory); 24 }); 25 26 return; 27 28 /** 29 * @param {string} name 30 */ 31 function visitDirectory(name) { 32 const inputFilePath = path.join(inputPath, name, "diagnosticMessages", "diagnosticMessages.generated.json.lcl"); 33 34 fs.readFile(inputFilePath, (err, data) => { 35 handleError(err); 36 xml2js.parseString(data.toString(), (err, result) => { 37 handleError(err); 38 if (!result || !result.LCX || !result.LCX.$ || !result.LCX.$.TgtCul) { 39 console.error("Unexpected XML file structure. Expected to find result.LCX.$.TgtCul."); 40 process.exit(1); 41 } 42 const outputDirectoryName = getPreferredLocaleName(result.LCX.$.TgtCul).toLowerCase(); 43 if (!outputDirectoryName) { 44 console.error(`Invalid output locale name for '${result.LCX.$.TgtCul}'.`); 45 process.exit(1); 46 } 47 writeFile(path.join(outputPath, outputDirectoryName, "diagnosticMessages.generated.json"), xmlObjectToString(result)); 48 }); 49 }); 50 } 51 52 /** 53 * A locale name is based on the language tagging conventions of RFC 4646 (Windows Vista 54 * and later), and is represented by LOCALE_SNAME. 55 * Generally, the pattern <language>-<REGION> is used. Here, language is a lowercase ISO 639 56 * language code. The codes from ISO 639-1 are used when available. Otherwise, codes from 57 * ISO 639-2/T are used. REGION specifies an uppercase ISO 3166-1 country/region identifier. 58 * For example, the locale name for English (United States) is "en-US" and the locale name 59 * for Divehi (Maldives) is "dv-MV". 60 * 61 * If the locale is a neutral locale (no region), the LOCALE_SNAME value follows the 62 * pattern <language>. If it is a neutral locale for which the script is significant, the 63 * pattern is <language>-<Script>. 64 * 65 * More at https://msdn.microsoft.com/en-us/library/windows/desktop/dd373814(v=vs.85).aspx 66 * 67 * Most of the languages we support are neutral locales, so we want to use the language name. 68 * There are three exceptions, zh-CN, zh-TW and pt-BR. 69 * 70 * @param {string} localeName 71 */ 72 function getPreferredLocaleName(localeName) { 73 switch (localeName) { 74 case "zh-CN": 75 case "zh-TW": 76 case "pt-BR": 77 return localeName; 78 default: 79 return localeName.split("-")[0]; 80 } 81 } 82 83 /** 84 * @param {null | object} err 85 */ 86 function handleError(err) { 87 if (err) { 88 console.error(err); 89 process.exit(1); 90 } 91 } 92 93 /** 94 * @param {any} o 95 */ 96 function xmlObjectToString(o) { 97 /** @type {any} */ 98 const out = {}; 99 for (const item of o.LCX.Item[0].Item[0].Item) { 100 let ItemId = item.$.ItemId; 101 let val = item.Str[0].Tgt ? item.Str[0].Tgt[0].Val[0] : item.Str[0].Val[0]; 102 103 if (typeof ItemId !== "string" || typeof val !== "string") { 104 console.error("Unexpected XML file structure"); 105 process.exit(1); 106 } 107 108 if (ItemId.charAt(0) === ";") { 109 ItemId = ItemId.slice(1); // remove leading semicolon 110 } 111 112 val = val.replace(/]5D;/, "]"); // unescape `]` 113 out[ItemId] = val; 114 } 115 return JSON.stringify(out, undefined, 2); 116 } 117 118 119 /** 120 * @param {string} directoryPath 121 * @param {() => void} action 122 */ 123 function ensureDirectoryExists(directoryPath, action) { 124 fs.exists(directoryPath, exists => { 125 if (!exists) { 126 const basePath = path.dirname(directoryPath); 127 if (basePath !== directoryPath) { 128 return ensureDirectoryExists(basePath, () => fs.mkdir(directoryPath, action)); 129 } 130 } 131 action(); 132 }); 133 } 134 135 /** 136 * @param {string} fileName 137 * @param {string} contents 138 */ 139 function writeFile(fileName, contents) { 140 ensureDirectoryExists(path.dirname(fileName), () => { 141 fs.writeFile(fileName, contents, handleError); 142 }); 143 } 144 145 /** 146 * @param {Record<string, string>} o 147 */ 148 function objectToList(o) { 149 const list = []; 150 for (const key in o) { 151 list.push({ key, value: o[key] }); 152 } 153 return list; 154 } 155 156 function generateLCGFile() { 157 return fs.readFile(diagnosticsMapFilePath, (err, data) => { 158 handleError(err); 159 writeFile( 160 path.join(outputPath, "enu", "diagnosticMessages.generated.json.lcg"), 161 getLCGFileXML( 162 objectToList(JSON.parse(data.toString())) 163 .sort((a, b) => a.key > b.key ? 1 : -1) // lcg sorted by property keys 164 .reduce((s, { key, value }) => s + getItemXML(key, value), "") 165 )); 166 }); 167 168 /** 169 * @param {string} key 170 * @param {string} value 171 */ 172 function getItemXML(key, value) { 173 // escape entrt value 174 value = value.replace(/]/g, "]5D;"); 175 176 return ` 177 <Item ItemId=";${key}" ItemType="0" PsrId="306" Leaf="true"> 178 <Str Cat="Text"> 179 <Val><![CDATA[${value}]]></Val> 180 </Str> 181 <Disp Icon="Str" /> 182 </Item>`; 183 } 184 185 /** 186 * @param {string} items 187 */ 188 function getLCGFileXML(items) { 189 return `<?xml version="1.0" encoding="utf-8"?> 190<LCX SchemaVersion="6.0" Name="diagnosticMessages.generated.json" PsrId="306" FileType="1" SrcCul="en-US" xmlns="http://schemas.microsoft.com/locstudio/2006/6/lcx"> 191 <OwnedComments> 192 <Cmt Name="Dev" /> 193 <Cmt Name="LcxAdmin" /> 194 <Cmt Name="Rccx" /> 195 </OwnedComments> 196 <Item ItemId=";String Table" ItemType="0" PsrId="306" Leaf="false"> 197 <Disp Icon="Expand" Expand="true" Disp="true" LocTbl="false" /> 198 <Item ItemId=";Strings" ItemType="0" PsrId="306" Leaf="false"> 199 <Disp Icon="Str" Disp="true" LocTbl="false" />${items} 200 </Item> 201 </Item> 202</LCX>`; 203 } 204 } 205} 206 207main(); 208