Skip to content

Commit 0d5f2da

Browse files
committed
Move translation resource script
1 parent 005daa8 commit 0d5f2da

File tree

7 files changed

+1067
-838
lines changed

7 files changed

+1067
-838
lines changed

apps/meteor/.scripts/translation-fix-order.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

apps/meteor/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"translation-diff": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-diff.ts",
4848
"translation-check": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-check.ts",
4949
"translation-replace-sprintf-params": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/replaceTranslationSprintfParams.ts",
50-
"translation-fix-order": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-fix-order.ts",
5150
"version": "node .scripts/version.js",
5251
"set-version": "node .scripts/set-version.js",
5352
"release": "meteor yarn set-version --silent",

packages/i18n/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727
],
2828
"scripts": {
2929
"build": "rm -rf dist && tsc -p tsconfig.build.json && node --experimental-transform-types ./src/scripts/build.mts",
30-
"lint": "eslint .",
31-
"lint:fix": "eslint . --fix",
30+
"translation-check": "node --experimental-transform-types ./src/scripts/check.mts",
31+
"lint": "eslint . && node --experimental-transform-types ./src/scripts/check.mts",
32+
"lint:fix": "eslint . --fix && node --experimental-transform-types ./src/scripts/check.mts --fix",
3233
"test": "jest",
3334
"testunit": "jest"
3435
},

packages/i18n/src/locales/de-IN.i18n.json

Lines changed: 798 additions & 798 deletions
Large diffs are not rendered by default.

packages/i18n/src/scripts/build.mts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2-
import { basename, dirname, join } from 'node:path';
2+
import { join } from 'node:path';
33
import { fileURLToPath } from 'node:url';
44

5+
import { distDirectory, languageFromBasename, resourceBasename, resourcesDirectory } from './common.mts';
56
import { normalizeI18nInterpolations } from './normalize.mts';
67

7-
export async function build() {
8-
const rootDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
9-
const resourcesDirectory = join(rootDirectory, 'src', 'locales');
10-
const distDirectory = join(rootDirectory, 'dist');
11-
8+
async function build() {
129
// read all files in the src/locales directory
1310
const resourceFiles = await readdir(resourcesDirectory);
1411
const resources = await Promise.all(
1512
resourceFiles.map(async (file) => ({
16-
language: basename(file, '.i18n.json'),
13+
language: languageFromBasename(file),
1714
content: JSON.parse(await readFile(join(resourcesDirectory, file), 'utf8')),
1815
})),
1916
);
@@ -51,7 +48,7 @@ export async function build() {
5148
// ./resources/*.i18n.json
5249
await mkdir(join(distDirectory, 'resources'), { recursive: true });
5350
for await (const resource of resources) {
54-
await writeFile(join(distDirectory, 'resources', `${resource.language}.i18n.json`), JSON.stringify(resource.content, null, 2));
51+
await writeFile(join(distDirectory, 'resources', resourceBasename(resource.language)), JSON.stringify(resource.content, null, 2));
5552
}
5653

5754
// ./resources
@@ -109,7 +106,11 @@ export default languages;`,
109106

110107
if (import.meta.url.startsWith('file:')) {
111108
const modulePath = fileURLToPath(import.meta.url);
109+
112110
if (process.argv[1] === modulePath) {
113-
build();
111+
build().catch((error) => {
112+
console.error(error);
113+
process.exit(1);
114+
});
114115
}
115116
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { argv, stderr, stdout } from 'node:process';
2+
import { fileURLToPath } from 'node:url';
3+
import { formatWithOptions, parseArgs, styleText } from 'node:util';
4+
5+
import { baseLanguage, getResourceLanguages, readResource, writeResource } from './common.mts';
6+
7+
type TaskOptions = {
8+
fix?: boolean;
9+
};
10+
11+
let errorCount = 0;
12+
13+
const describeTask =
14+
(
15+
task: string,
16+
fn: () => AsyncGenerator<{
17+
lint: (collectError: (format?: any, ...param: any[]) => void) => Promise<void>;
18+
fix: () => Promise<void>;
19+
}>,
20+
) =>
21+
async (options: TaskOptions) => {
22+
for await (const result of fn()) {
23+
if (!result) continue;
24+
25+
if (options.fix) {
26+
await result.fix();
27+
console.log(styleText('blue', '✔', { stream: stdout }), styleText('gray', `${task}:`, { stream: stdout }), 'fixes applied');
28+
} else {
29+
const stderrSupportsColor = styleText('blue', `.`, { stream: stderr }) !== '.';
30+
31+
await result.lint((_format, ...param) => {
32+
console.error(
33+
styleText('red', '✘', { stream: stderr }),
34+
styleText('gray', `${task}:`, { stream: stderr }),
35+
formatWithOptions({ colors: stderrSupportsColor }, _format, ...param),
36+
);
37+
errorCount++;
38+
});
39+
}
40+
}
41+
};
42+
43+
/**
44+
* Sort keys of the base language (en) alphabetically
45+
* and write back the sorted resource file if necessary
46+
*/
47+
const sortBaseKeys = describeTask('sort-base-keys', async function* () {
48+
const baseResource = await readResource(baseLanguage);
49+
50+
const keys = Object.keys(baseResource);
51+
const sortedKeys = keys.toSorted((a, b) => a.toLowerCase().localeCompare(b.toLowerCase(), 'en'));
52+
53+
if (keys.join(',') === sortedKeys.join(',')) return;
54+
55+
yield {
56+
lint: async (collectError) => {
57+
for (let i = 0; i < keys.length; i++) {
58+
const key = keys[i];
59+
const beforeKey = keys.at(i - 1);
60+
const j = sortedKeys.indexOf(key);
61+
const expectedBeforeKey = sortedKeys.at(j - 1);
62+
63+
if (beforeKey !== expectedBeforeKey) {
64+
if (expectedBeforeKey) {
65+
collectError('%o should be after %o', keys[i], expectedBeforeKey);
66+
} else {
67+
collectError('%o should be the first key', keys[i]);
68+
}
69+
}
70+
}
71+
},
72+
fix: async () => {
73+
const sortedResource: Record<string, unknown> = {};
74+
for (const key of sortedKeys) {
75+
sortedResource[key] = baseResource[key];
76+
}
77+
78+
await writeResource(baseLanguage, sortedResource);
79+
},
80+
};
81+
});
82+
83+
/**
84+
* Apply the order of the base language (en) to all other languages
85+
*/
86+
const sortKeys = describeTask('sort-keys', async function* () {
87+
const baseResource = await readResource(baseLanguage);
88+
const baseKeys = new Set(Object.keys(baseResource));
89+
90+
const languages = await getResourceLanguages();
91+
92+
for await (const language of languages) {
93+
if (language === baseLanguage) continue;
94+
95+
const resource = await readResource(language);
96+
const resourceKeys = new Set(Object.keys(resource));
97+
const extraKeys = resourceKeys.difference(baseKeys);
98+
99+
const sortedResource: Record<string, unknown> = {};
100+
101+
for (const key of baseKeys) {
102+
if (!resourceKeys.has(key)) continue;
103+
sortedResource[key] = resource[key];
104+
}
105+
106+
for (const key of extraKeys) {
107+
sortedResource[key] = resource[key];
108+
}
109+
110+
if (Object.keys(resource).join(',') === Object.keys(sortedResource).join(',')) continue;
111+
112+
yield {
113+
lint: async (collectError) => {
114+
const keys = Object.keys(resource);
115+
const sortedKeys = Object.keys(sortedResource);
116+
117+
for (let i = 0; i < keys.length; i++) {
118+
const key = keys[i];
119+
if (extraKeys.has(key)) continue;
120+
121+
const j = sortedKeys.indexOf(key);
122+
const expectedBeforeKey = sortedKeys.at(j - 1);
123+
const beforeKey = keys.at(i - 1);
124+
125+
if (beforeKey !== expectedBeforeKey) {
126+
if (expectedBeforeKey) {
127+
collectError('%s: %o should be after %o', language, keys[i], expectedBeforeKey);
128+
} else {
129+
collectError('%s: %o should be the first key', language, keys[i]);
130+
}
131+
}
132+
}
133+
},
134+
fix: async () => {
135+
await writeResource(language, sortedResource);
136+
},
137+
};
138+
}
139+
});
140+
141+
/**
142+
* Wipes extra keys from all language files that are not present in the base language (en)
143+
*/
144+
const wipeExtraKeys = describeTask('wipe-extra-keys', async function* () {
145+
const baseResource = await readResource(baseLanguage);
146+
const baseKeys = new Set(Object.keys(baseResource));
147+
148+
const languages = await getResourceLanguages();
149+
150+
for await (const language of languages) {
151+
if (language === baseLanguage) continue;
152+
153+
const resource = await readResource(language);
154+
const resourceKeys = new Set(Object.keys(resource));
155+
156+
if (resourceKeys.difference(baseKeys).size === 0) continue;
157+
158+
yield {
159+
lint: async (collectError) => {
160+
const extraKeys = resourceKeys.difference(baseKeys);
161+
for (const key of extraKeys) {
162+
collectError('%s: has extra key %o', language, key);
163+
}
164+
},
165+
fix: async () => {
166+
const wipedResource: Record<string, unknown> = {};
167+
// Traversing the own resource keys to preserve the original order
168+
for (const key of Object.keys(resource)) {
169+
if (!baseKeys.has(key)) continue;
170+
wipedResource[key] = resource[key];
171+
}
172+
173+
await writeResource(language, wipedResource);
174+
},
175+
};
176+
}
177+
});
178+
179+
const tasksByName = {
180+
'sort-base-keys': sortBaseKeys,
181+
'sort-keys': sortKeys,
182+
'wipe-extra-keys': wipeExtraKeys,
183+
} as const;
184+
185+
async function check({ fix, task }: { fix?: boolean; task?: string[] } = {}) {
186+
// We're lenient by default excluding 'sort-base-keys' from the default tasks
187+
const tasks = new Set<keyof typeof tasksByName>(['sort-keys', 'wipe-extra-keys']);
188+
189+
if (task?.length) {
190+
tasks.clear();
191+
task.filter((taskName): taskName is keyof typeof tasksByName => taskName in tasksByName).forEach((taskName) => tasks.add(taskName));
192+
}
193+
194+
if (tasks.size === 0) {
195+
throw new Error('No valid tasks selected.');
196+
}
197+
198+
for await (const taskName of tasks) {
199+
const task = tasksByName[taskName];
200+
await task({ fix });
201+
}
202+
203+
if (errorCount > 0) {
204+
throw new Error(`${errorCount} error(s) found.`);
205+
}
206+
}
207+
208+
if (import.meta.url.startsWith('file:')) {
209+
const modulePath = fileURLToPath(import.meta.url);
210+
211+
if (argv[1] === modulePath) {
212+
const { values } = parseArgs({
213+
args: argv.slice(2),
214+
options: {
215+
fix: { type: 'boolean', short: 'f' },
216+
task: {
217+
type: 'string',
218+
multiple: true,
219+
short: 't',
220+
choices: Object.keys(tasksByName),
221+
},
222+
},
223+
});
224+
225+
check(values).catch((error) => {
226+
console.error(error);
227+
process.exit(1);
228+
});
229+
}
230+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { readdir, readFile, writeFile } from 'node:fs/promises';
2+
import { basename, dirname, join } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
export const baseLanguage = 'en';
6+
7+
export async function getResourceLanguages() {
8+
const resourceFiles = await readdir(resourcesDirectory);
9+
return resourceFiles.map((file) => languageFromBasename(file));
10+
}
11+
12+
export async function readResource(language: string) {
13+
return JSON.parse(await readFile(join(resourcesDirectory, resourceBasename(language)), 'utf8'));
14+
}
15+
16+
export async function writeResource(language: string, resource: unknown) {
17+
const content = JSON.stringify(resource, null, 2);
18+
return writeFile(join(resourcesDirectory, resourceBasename(language)), content, 'utf8');
19+
}
20+
21+
export const rootDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
22+
export const resourcesDirectory = join(rootDirectory, 'src', 'locales');
23+
export const distDirectory = join(rootDirectory, 'dist');
24+
25+
export const resourceBasename = (language: string) => `${language}.i18n.json`;
26+
export const languageFromBasename = (path: string) => basename(path, '.i18n.json');

0 commit comments

Comments
 (0)