/
generate_translation_keys.js
130 lines (120 loc) · 3.8 KB
/
generate_translation_keys.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* 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)]);