You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/buildtools/generate_translation_keys.js

131 lines
3.8 KiB

/**
* Generating translations keys:
*
* This code walk through all the files in client directory and its children
* Get the all keys called by our makeT utils function
* And add only the new one on our en.client.json file
*
*/
const fs = require("fs");
const path = require("path");
const Parser = require("i18next-scanner").Parser;
const englishKeys = require("../static/locales/en.client.json");
const _ = require("lodash");
const parser = new Parser({
keySeparator: "/",
nsSeparator: null,
});
async function* walk(dirs) {
for (const dir of dirs) {
for await (const d of await fs.promises.opendir(dir)) {
const entry = path.join(dir, d.name);
if (d.isDirectory()) yield* walk([entry]);
else if (d.isFile()) yield entry;
}
}
}
const customHandler = (fileName) => (key, options) => {
const keyWithFile = `${fileName}/${key}`;
if (Object.keys(options).includes("count") === true) {
const keyOne = `${keyWithFile}_one`;
const keyOther = `${keyWithFile}_other`;
parser.set(keyOne, key);
parser.set(keyOther, key);
} else {
parser.set(keyWithFile, key);
}
};
function sort(obj) {
if (typeof obj !== "object" || Array.isArray(obj))
return obj;
const sortedObject = {};
const keys = Object.keys(obj).sort();
keys.forEach(key => sortedObject[key] = sort(obj[key]));
return sortedObject;
}
const getKeysFromFile = (filePath, fileName) => {
const content = fs.readFileSync(filePath, "utf-8");
parser.parseFuncFromString(
content,
{
list: [
"i18next.t",
"t", // To match the file-level t function created with makeT
],
},
customHandler(fileName)
);
const keys = parser.get({ sort: true });
return keys;
};
// It is highly desirable to retain existing order, to not generate
// unnecessary merges/conflicts, so we do a specialized merge.
function merge(target, scanned) {
let merges = 0;
for (const key of Object.keys(scanned)) {
if (!(key in target)) {
console.log("Merging key", {key});
target[key] = scanned[key];
merges++;
} else if (typeof target[key] === 'object') {
merges += merge(target[key], scanned[key]);
} else if (scanned[key] !== target[key]) {
if (!key.endsWith('_one')) {
console.log("Value difference", {key, value: target[key]});
}
}
}
return merges;
}
// Look for keys that are listed in json file but not found in source
// code. These may be stale and need deleting in weblate.
function reportUnrecognizedKeys(originalKeys, foundKeys) {
let unknowns = 0;
for (const section of Object.keys(originalKeys)) {
if (!(section in foundKeys)) {
console.log("Unknown section found", {section});
unknowns++;
} else {
for (const key of Object.keys(originalKeys[section])) {
if (!(key in foundKeys[section])) {
console.log("Unknown key found", {section, key});
unknowns++;
}
}
}
}
return unknowns;
}
async function walkTranslation(dirs) {
const originalKeys = _.cloneDeep(englishKeys);
for await (const p of walk(dirs)) {
const { name } = path.parse(p);
if (p.endsWith('.map')) { continue; }
getKeysFromFile(p, name);
}
const keys = parser.get({ sort: true });
const foundKeys = _.cloneDeep(keys.en.translation);
const mergeCount = merge(englishKeys, sort(keys.en.translation));
await fs.promises.writeFile(
"static/locales/en.client.json",
JSON.stringify(englishKeys, null, 4) + '\n', // match weblate's default
"utf-8"
);
// Now, print a report of unrecognized keys - candidates
// for deletion in weblate.
const unknownCount = reportUnrecognizedKeys(originalKeys, foundKeys);
console.log(`Found ${unknownCount} unknown key(s).`);
console.log(`Make ${mergeCount} merge(s).`);
}
walkTranslation(["_build/app/client", ...process.argv.slice(2)]);