Modernize localize script, use new XML library

This file is pretty much the same as it was when it was committed in
2017; these days, we can write clearer code with async/await and new FS
APIs.

Additionally, we can improve the performance of this script by using a
newer/faster/maintained XML library. This will enable us to run the
script unconditionally in a later commit.
This commit is contained in:
Jake Bailey 2022-10-09 20:12:45 -07:00
parent 394c4ae68b
commit aec2761d31
3 changed files with 100 additions and 157 deletions

110
package-lock.json generated
View File

@ -25,7 +25,6 @@
"@types/node": "latest",
"@types/source-map-support": "latest",
"@types/which": "^2.0.1",
"@types/xml2js": "^0.4.11",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
"@typescript-eslint/utils": "^5.33.1",
@ -41,6 +40,7 @@
"eslint-plugin-jsdoc": "^39.3.6",
"eslint-plugin-local": "^1.0.0",
"eslint-plugin-no-null": "^1.0.2",
"fast-xml-parser": "^4.0.11",
"fs-extra": "^9.1.0",
"glob": "latest",
"hereby": "^1.6.4",
@ -53,8 +53,7 @@
"node-fetch": "^3.2.10",
"source-map-support": "latest",
"typescript": "^4.8.4",
"which": "^2.0.2",
"xml2js": "^0.4.23"
"which": "^2.0.2"
},
"engines": {
"node": ">=4.2.0"
@ -479,15 +478,6 @@
"integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==",
"dev": true
},
"node_modules/@types/xml2js": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz",
"integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz",
@ -2161,6 +2151,22 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fast-xml-parser": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz",
"integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==",
"dev": true,
"dependencies": {
"strnum": "^1.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
},
"funding": {
"type": "paypal",
"url": "https://paypal.me/naturalintelligence"
}
},
"node_modules/fastest-levenshtein": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@ -3942,12 +3948,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@ -4124,6 +4124,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
"dev": true
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -4457,28 +4463,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"dev": true,
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@ -4876,15 +4860,6 @@
"integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==",
"dev": true
},
"@types/xml2js": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz",
"integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@typescript-eslint/eslint-plugin": {
"version": "5.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz",
@ -6019,6 +5994,15 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"fast-xml-parser": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz",
"integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==",
"dev": true,
"requires": {
"strnum": "^1.0.5"
}
},
"fastest-levenshtein": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@ -7264,12 +7248,6 @@
"is-regex": "^1.1.4"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@ -7407,6 +7385,12 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
"strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -7665,22 +7649,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"dev": true,
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
}
},
"xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"dev": true
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@ -51,7 +51,6 @@
"@types/node": "latest",
"@types/source-map-support": "latest",
"@types/which": "^2.0.1",
"@types/xml2js": "^0.4.11",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
"@typescript-eslint/utils": "^5.33.1",
@ -67,6 +66,7 @@
"eslint-plugin-jsdoc": "^39.3.6",
"eslint-plugin-local": "^1.0.0",
"eslint-plugin-no-null": "^1.0.2",
"fast-xml-parser": "^4.0.11",
"fs-extra": "^9.1.0",
"glob": "latest",
"hereby": "^1.6.4",
@ -79,8 +79,7 @@
"node-fetch": "^3.2.10",
"source-map-support": "latest",
"typescript": "^4.8.4",
"which": "^2.0.2",
"xml2js": "^0.4.23"
"which": "^2.0.2"
},
"scripts": {
"test": "hereby runtests-parallel --light=false",

View File

@ -1,8 +1,28 @@
import fs from "fs";
import path from "path";
import xml2js from "xml2js";
import { XMLParser } from "fast-xml-parser";
function main() {
/** @typedef {{
LCX: {
$_TgtCul: string;
Item: {
Item: {
Item: {
$_ItemId: string;
Str: {
Val: string;
Tgt: {
Val: string;
};
};
}[];
};
};
}
}} ParsedLCL */
void 0;
async function main() {
const args = process.argv.slice(2);
if (args.length !== 3) {
console.log("Usage:");
@ -15,38 +35,33 @@ function main() {
const diagnosticsMapFilePath = args[2];
// generate the lcg file for enu
generateLCGFile();
await generateLCGFile();
// generate other langs
fs.readdir(inputPath, (err, files) => {
handleError(err);
files.forEach(visitDirectory);
});
const files = await fs.promises.readdir(inputPath);
await Promise.all(files.map(visitDirectory));
return;
/**
* @param {string} name
*/
function visitDirectory(name) {
async function visitDirectory(name) {
const inputFilePath = path.join(inputPath, name, "diagnosticMessages", "diagnosticMessages.generated.json.lcl");
fs.readFile(inputFilePath, (err, data) => {
handleError(err);
xml2js.parseString(data.toString(), (err, result) => {
handleError(err);
if (!result || !result.LCX || !result.LCX.$ || !result.LCX.$.TgtCul) {
console.error("Unexpected XML file structure. Expected to find result.LCX.$.TgtCul.");
process.exit(1);
}
const outputDirectoryName = getPreferredLocaleName(result.LCX.$.TgtCul).toLowerCase();
if (!outputDirectoryName) {
console.error(`Invalid output locale name for '${result.LCX.$.TgtCul}'.`);
process.exit(1);
}
writeFile(path.join(outputPath, outputDirectoryName, "diagnosticMessages.generated.json"), xmlObjectToString(result));
});
});
const contents = await fs.promises.readFile(inputFilePath);
/** @type {ParsedLCL} */
// eslint-disable-next-line local/object-literal-surrounding-space
const result = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "$_"}).parse(contents);
if (!result || !result.LCX || !result.LCX.$_TgtCul) {
console.error("Unexpected XML file structure. Expected to find result.LCX.$_TgtCul.");
process.exit(1);
}
const outputDirectoryName = getPreferredLocaleName(result.LCX.$_TgtCul).toLowerCase();
if (!outputDirectoryName) {
console.error(`Invalid output locale name for '${result.LCX.$_TgtCul}'.`);
process.exit(1);
}
await writeFile(path.join(outputPath, outputDirectoryName, "diagnosticMessages.generated.json"), xmlObjectToString(result));
}
/**
@ -81,24 +96,14 @@ function main() {
}
/**
* @param {null | object} err
*/
function handleError(err) {
if (err) {
console.error(err);
process.exit(1);
}
}
/**
* @param {any} o
* @param {ParsedLCL} o
*/
function xmlObjectToString(o) {
/** @type {any} */
const out = {};
for (const item of o.LCX.Item[0].Item[0].Item) {
let ItemId = item.$.ItemId;
let val = item.Str[0].Tgt ? item.Str[0].Tgt[0].Val[0] : item.Str[0].Val[0];
for (const item of o.LCX.Item.Item.Item) {
let ItemId = item.$_ItemId;
let val = item.Str.Tgt ? item.Str.Tgt.Val : item.Str.Val;
if (typeof ItemId !== "string" || typeof val !== "string") {
console.error("Unexpected XML file structure");
@ -115,55 +120,26 @@ function main() {
return JSON.stringify(out, undefined, 2);
}
/**
* @param {string} directoryPath
* @param {() => void} action
*/
function ensureDirectoryExists(directoryPath, action) {
fs.exists(directoryPath, exists => {
if (!exists) {
const basePath = path.dirname(directoryPath);
if (basePath !== directoryPath) {
return ensureDirectoryExists(basePath, () => fs.mkdir(directoryPath, action));
}
}
action();
});
}
/**
* @param {string} fileName
* @param {string} contents
*/
function writeFile(fileName, contents) {
ensureDirectoryExists(path.dirname(fileName), () => {
fs.writeFile(fileName, contents, handleError);
});
async function writeFile(fileName, contents) {
await fs.promises.mkdir(path.dirname(fileName), { recursive: true });
await fs.promises.writeFile(fileName, contents);
}
/**
* @param {Record<string, string>} o
*/
function objectToList(o) {
const list = [];
for (const key in o) {
list.push({ key, value: o[key] });
}
return list;
}
function generateLCGFile() {
return fs.readFile(diagnosticsMapFilePath, (err, data) => {
handleError(err);
writeFile(
path.join(outputPath, "enu", "diagnosticMessages.generated.json.lcg"),
getLCGFileXML(
objectToList(JSON.parse(data.toString()))
.sort((a, b) => a.key > b.key ? 1 : -1) // lcg sorted by property keys
.reduce((s, { key, value }) => s + getItemXML(key, value), "")
));
});
async function generateLCGFile() {
const contents = await fs.promises.readFile(diagnosticsMapFilePath, "utf-8");
await writeFile(
path.join(outputPath, "enu", "diagnosticMessages.generated.json.lcg"),
getLCGFileXML(
Object.entries(JSON.parse(contents))
.sort((a, b) => a[0] > b[0] ? 1 : -1) // lcg sorted by property keys
.reduce((s, [key, value]) => s + getItemXML(key, value), "")
),
);
return;
/**
* @param {string} key
@ -204,4 +180,4 @@ function main() {
}
}
main();
await main();