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