• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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