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