@jskit-ai/jskit-catalog 0.1.300 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/catalog/packages.json +243 -234
- package/package.json +1 -1
- package/scripts/i18n.mjs +387 -0
- package/scripts/jskit.mjs +70 -2
package/package.json
CHANGED
package/scripts/i18n.mjs
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveInstalledPackage } from "./project-packages.mjs";
|
|
4
|
+
|
|
5
|
+
// `jskit i18n export|check <locale>`: an app translates JSKIT by supplying
|
|
6
|
+
// src/locales/<locale>/jskit.json, keyed exactly like the merged locales/en.json
|
|
7
|
+
// catalogs of its installed @jskit-ai packages.
|
|
8
|
+
|
|
9
|
+
const JSKIT_SCOPE_PREFIX = "@jskit-ai/";
|
|
10
|
+
const LOCALE_CATALOG_EXPORT = "./locales/en.json";
|
|
11
|
+
// The sections the app's runtime follows, so the export holds what the app renders.
|
|
12
|
+
const RUNTIME_DEPENDENCY_FIELDS = Object.freeze(["dependencies", "optionalDependencies", "peerDependencies"]);
|
|
13
|
+
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function canonicalLocale(code = "") {
|
|
19
|
+
const value = String(code || "").trim();
|
|
20
|
+
try {
|
|
21
|
+
const [canonical] = Intl.getCanonicalLocales(value);
|
|
22
|
+
if (canonical) return canonical;
|
|
23
|
+
} catch {
|
|
24
|
+
// Reported below.
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Invalid locale code "${value}": use a BCP 47 tag such as fr or pt-BR.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function translationFile(projectRoot, locale) {
|
|
30
|
+
return path.join(path.resolve(projectRoot), "src", "locales", locale, "jskit.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Mirror of @jskit-ai/kernel/shared/i18n ---------------------------------------
|
|
34
|
+
// @jskit-ai/jskit-catalog has no dependencies (apps run it through npx), so it
|
|
35
|
+
// cannot import the kernel. These copies must match kernel/shared/i18n exactly;
|
|
36
|
+
// test/i18n.test.js runs the same messages through both.
|
|
37
|
+
|
|
38
|
+
function mergeCatalogs(...catalogs) {
|
|
39
|
+
const merged = {};
|
|
40
|
+
for (const catalog of catalogs) {
|
|
41
|
+
if (!isRecord(catalog)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const [name, value] of Object.entries(catalog)) {
|
|
45
|
+
merged[name] = isRecord(value) && isRecord(merged[name]) ? mergeCatalogs(merged[name], value) : value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function flattenCatalog(catalog, prefix = "") {
|
|
52
|
+
const flat = {};
|
|
53
|
+
if (!isRecord(catalog)) {
|
|
54
|
+
return flat;
|
|
55
|
+
}
|
|
56
|
+
for (const [name, value] of Object.entries(catalog)) {
|
|
57
|
+
const key = prefix ? `${prefix}.${name}` : name;
|
|
58
|
+
if (isRecord(value)) {
|
|
59
|
+
Object.assign(flat, flattenCatalog(value, key));
|
|
60
|
+
} else {
|
|
61
|
+
flat[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return flat;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function placeholderEnd(template, start) {
|
|
68
|
+
let index = start + 1;
|
|
69
|
+
let quoted = false;
|
|
70
|
+
while (index < template.length) {
|
|
71
|
+
const char = template[index];
|
|
72
|
+
if (char === "'") {
|
|
73
|
+
quoted = !quoted;
|
|
74
|
+
} else if (char === "}" && !quoted) {
|
|
75
|
+
return index;
|
|
76
|
+
}
|
|
77
|
+
index += 1;
|
|
78
|
+
}
|
|
79
|
+
throw new SyntaxError(`Unclosed placeholder in message: ${template}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseMessage(template = "") {
|
|
83
|
+
const source = String(template);
|
|
84
|
+
const choices = [[]];
|
|
85
|
+
let text = "";
|
|
86
|
+
const flush = () => {
|
|
87
|
+
if (text) {
|
|
88
|
+
choices.at(-1).push({ text });
|
|
89
|
+
text = "";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let index = 0;
|
|
93
|
+
while (index < source.length) {
|
|
94
|
+
const char = source[index];
|
|
95
|
+
if (char === "{") {
|
|
96
|
+
const end = placeholderEnd(source, index);
|
|
97
|
+
const body = source.slice(index + 1, end).trim();
|
|
98
|
+
flush();
|
|
99
|
+
choices.at(-1).push(body.startsWith("'") ? { text: body.slice(1, -1) } : { param: body });
|
|
100
|
+
index = end + 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === "|") {
|
|
104
|
+
flush();
|
|
105
|
+
choices.push([]);
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
text += char;
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
flush();
|
|
113
|
+
if (choices.length > 1) {
|
|
114
|
+
for (const parts of choices) {
|
|
115
|
+
if (parts[0]?.text !== undefined) parts[0] = { text: parts[0].text.trimStart() };
|
|
116
|
+
const last = parts.length - 1;
|
|
117
|
+
if (parts[last]?.text !== undefined) parts[last] = { text: parts[last].text.trimEnd() };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return choices;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function messageParams(template = "") {
|
|
124
|
+
const names = new Set();
|
|
125
|
+
for (const parts of parseMessage(template)) {
|
|
126
|
+
for (const part of parts) {
|
|
127
|
+
if (part.param !== undefined) names.add(part.param);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...names].sort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Installed catalogs ------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function localeCatalogTarget(manifest) {
|
|
136
|
+
const target = isRecord(manifest.exports) ? manifest.exports[LOCALE_CATALOG_EXPORT] : undefined;
|
|
137
|
+
return typeof target === "string" ? target : isRecord(target) ? target.default : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function readJsonFile(file) {
|
|
141
|
+
const contents = await readFile(file, "utf8");
|
|
142
|
+
let value;
|
|
143
|
+
try {
|
|
144
|
+
value = JSON.parse(contents);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(`${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
147
|
+
}
|
|
148
|
+
if (!isRecord(value)) {
|
|
149
|
+
throw new TypeError(`${file} must contain a JSON object.`);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Walks the app's runtime dependency graph (local packages included) and returns the
|
|
155
|
+
// English catalog of every @jskit-ai package that exports ./locales/en.json, in
|
|
156
|
+
// package order, so the merge is deterministic.
|
|
157
|
+
async function collectInstalledCatalogs(projectRoot) {
|
|
158
|
+
const root = path.resolve(projectRoot);
|
|
159
|
+
const rootManifest = await realpath(path.join(root, "package.json")).catch(() => null);
|
|
160
|
+
if (!rootManifest) {
|
|
161
|
+
throw new Error(`JSKIT project root has no package.json: ${root}`);
|
|
162
|
+
}
|
|
163
|
+
const pending = [rootManifest];
|
|
164
|
+
const visited = new Set();
|
|
165
|
+
const catalogs = [];
|
|
166
|
+
while (pending.length > 0) {
|
|
167
|
+
const manifestPath = pending.pop();
|
|
168
|
+
if (visited.has(manifestPath)) continue;
|
|
169
|
+
visited.add(manifestPath);
|
|
170
|
+
const manifest = await readJsonFile(manifestPath);
|
|
171
|
+
const directory = path.dirname(manifestPath);
|
|
172
|
+
const target = localeCatalogTarget(manifest);
|
|
173
|
+
if (String(manifest.name || "").startsWith(JSKIT_SCOPE_PREFIX) && target) {
|
|
174
|
+
const file = path.join(directory, target);
|
|
175
|
+
catalogs.push({ packageId: manifest.name, file, catalog: await readJsonFile(file) });
|
|
176
|
+
}
|
|
177
|
+
for (const field of RUNTIME_DEPENDENCY_FIELDS) {
|
|
178
|
+
for (const name of Object.keys(isRecord(manifest[field]) ? manifest[field] : {})) {
|
|
179
|
+
const resolved = await resolveInstalledPackage(directory, name);
|
|
180
|
+
if (resolved && !visited.has(resolved)) pending.push(resolved);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return catalogs.sort((left, right) => left.packageId.localeCompare(right.packageId) || left.file.localeCompare(right.file));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function readInstalledEnglish(projectRoot) {
|
|
188
|
+
const catalogs = await collectInstalledCatalogs(projectRoot);
|
|
189
|
+
if (catalogs.length < 1) {
|
|
190
|
+
throw new Error(`No installed @jskit-ai package ships ${LOCALE_CATALOG_EXPORT.slice(2)} under ${path.resolve(projectRoot)}; run npm install first.`);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
packageIds: [...new Set(catalogs.map((entry) => entry.packageId))],
|
|
194
|
+
english: flattenCatalog(mergeCatalogs(...catalogs.map((entry) => entry.catalog)))
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readTranslationFile(file) {
|
|
199
|
+
try {
|
|
200
|
+
return await readJsonFile(file);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error?.code === "ENOENT") return null;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Rebuilds a nested catalog from flat keys in their given order. A key that is both
|
|
208
|
+
// a message and a group of messages cannot be written, so it stops the export.
|
|
209
|
+
function nestCatalog(flat, file) {
|
|
210
|
+
const keys = new Set(Object.keys(flat));
|
|
211
|
+
const nested = {};
|
|
212
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
213
|
+
const segments = key.split(".");
|
|
214
|
+
let current = nested;
|
|
215
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
216
|
+
const group = segments.slice(0, index).join(".");
|
|
217
|
+
if (keys.has(group)) {
|
|
218
|
+
throw new Error(`${file} cannot hold both ${group} and ${key}; rename or remove one, then export again.`);
|
|
219
|
+
}
|
|
220
|
+
current = current[segments[index - 1]] ??= {};
|
|
221
|
+
}
|
|
222
|
+
current[segments.at(-1)] = value;
|
|
223
|
+
}
|
|
224
|
+
return nested;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function relativeFile(projectRoot, file) {
|
|
228
|
+
return path.relative(path.resolve(projectRoot), file).split(path.sep).join("/");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- export ------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
async function exportTranslations({ projectRoot = process.cwd(), locale } = {}) {
|
|
234
|
+
const code = canonicalLocale(locale);
|
|
235
|
+
const file = translationFile(projectRoot, code);
|
|
236
|
+
const { packageIds, english } = await readInstalledEnglish(projectRoot);
|
|
237
|
+
const existing = flattenCatalog(await readTranslationFile(file));
|
|
238
|
+
const merged = {};
|
|
239
|
+
let added = 0;
|
|
240
|
+
let kept = 0;
|
|
241
|
+
for (const [key, text] of Object.entries(english)) {
|
|
242
|
+
if (Object.hasOwn(existing, key)) {
|
|
243
|
+
merged[key] = existing[key];
|
|
244
|
+
kept += 1;
|
|
245
|
+
} else {
|
|
246
|
+
merged[key] = text;
|
|
247
|
+
added += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const extra = Object.keys(existing).filter((key) => !Object.hasOwn(english, key));
|
|
251
|
+
for (const key of extra) merged[key] = existing[key];
|
|
252
|
+
|
|
253
|
+
const contents = `${JSON.stringify(nestCatalog(merged, relativeFile(projectRoot, file)), null, 2)}\n`;
|
|
254
|
+
const previous = await readFile(file, "utf8").catch(() => null);
|
|
255
|
+
if (contents !== previous) {
|
|
256
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
257
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
258
|
+
try {
|
|
259
|
+
await writeFile(temporary, contents, "utf8");
|
|
260
|
+
await rename(temporary, file);
|
|
261
|
+
} finally {
|
|
262
|
+
await rm(temporary, { force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { locale: code, file, packageIds, added, kept, extra: extra.length, changed: contents !== previous };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatExportSummary(result, projectRoot = process.cwd()) {
|
|
269
|
+
const name = relativeFile(projectRoot, result.file);
|
|
270
|
+
const source = `${result.packageIds.length} JSKIT package(s): ${result.added} added, ${result.kept} kept, ${result.extra} extra.`;
|
|
271
|
+
const lines = [result.changed ? `Wrote ${name} from ${source}` : `${name} is already up to date with ${source}`];
|
|
272
|
+
if (result.extra > 0) {
|
|
273
|
+
lines.push(`Extra keys are kept; jskit i18n check ${result.locale} lists them.`);
|
|
274
|
+
}
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- check -------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
function describeParams(names) {
|
|
281
|
+
return names.map((name) => `{${name}}`).join(", ");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function placeholderProblem(englishText, translation) {
|
|
285
|
+
const englishChoices = parseMessage(englishText).length;
|
|
286
|
+
const translatedChoices = parseMessage(translation).length;
|
|
287
|
+
const problems = [];
|
|
288
|
+
if (englishChoices !== translatedChoices) {
|
|
289
|
+
problems.push(englishChoices > 1
|
|
290
|
+
? `${translatedChoices} plural choice(s), English has ${englishChoices}`
|
|
291
|
+
: `${translatedChoices} plural choices, English has none (write a literal | as {'|'})`);
|
|
292
|
+
}
|
|
293
|
+
const expected = messageParams(englishText);
|
|
294
|
+
const actual = messageParams(translation);
|
|
295
|
+
const missing = expected.filter((name) => !actual.includes(name));
|
|
296
|
+
const unexpected = actual.filter((name) => !expected.includes(name));
|
|
297
|
+
if (missing.length > 0) problems.push(`missing ${describeParams(missing)}`);
|
|
298
|
+
if (unexpected.length > 0) problems.push(`unexpected ${describeParams(unexpected)}`);
|
|
299
|
+
return problems.join("; ");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function hasWords(englishText) {
|
|
303
|
+
return /\p{L}/u.test(parseMessage(englishText).flat().map((part) => part.text ?? "").join(""));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function checkTranslations({ projectRoot = process.cwd(), locale } = {}) {
|
|
307
|
+
const code = canonicalLocale(locale);
|
|
308
|
+
const file = translationFile(projectRoot, code);
|
|
309
|
+
const { packageIds, english } = await readInstalledEnglish(projectRoot);
|
|
310
|
+
const document = await readTranslationFile(file);
|
|
311
|
+
if (!document) {
|
|
312
|
+
throw new Error(`${relativeFile(projectRoot, file)} does not exist; run jskit i18n export ${code} first.`);
|
|
313
|
+
}
|
|
314
|
+
const translations = flattenCatalog(document);
|
|
315
|
+
const missing = [];
|
|
316
|
+
const invalid = [];
|
|
317
|
+
const mismatched = [];
|
|
318
|
+
const untranslated = [];
|
|
319
|
+
for (const [key, text] of Object.entries(english)) {
|
|
320
|
+
if (!Object.hasOwn(translations, key)) {
|
|
321
|
+
missing.push(key);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const translation = translations[key];
|
|
325
|
+
if (typeof translation !== "string") {
|
|
326
|
+
invalid.push(`${key}: not a string`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
let problem;
|
|
330
|
+
try {
|
|
331
|
+
problem = placeholderProblem(text, translation);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
invalid.push(`${key}: ${error.message}`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (problem) {
|
|
337
|
+
mismatched.push(`${key}: ${problem}`);
|
|
338
|
+
} else if (translation === text && hasWords(text)) {
|
|
339
|
+
untranslated.push(key);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const extra = Object.keys(translations).filter((key) => !Object.hasOwn(english, key));
|
|
343
|
+
return {
|
|
344
|
+
locale: code,
|
|
345
|
+
file,
|
|
346
|
+
packageIds,
|
|
347
|
+
total: Object.keys(english).length,
|
|
348
|
+
missing,
|
|
349
|
+
invalid,
|
|
350
|
+
mismatched,
|
|
351
|
+
extra,
|
|
352
|
+
untranslated,
|
|
353
|
+
ok: missing.length === 0 && invalid.length === 0 && mismatched.length === 0
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function formatCheckReport(result, projectRoot = process.cwd()) {
|
|
358
|
+
const groups = [
|
|
359
|
+
["Missing keys", result.missing],
|
|
360
|
+
["Invalid messages", result.invalid],
|
|
361
|
+
["Placeholder mismatches", result.mismatched],
|
|
362
|
+
["Extra keys (not used by the installed packages)", result.extra],
|
|
363
|
+
["Untranslated, same as English (warning)", result.untranslated]
|
|
364
|
+
];
|
|
365
|
+
const lines = [];
|
|
366
|
+
for (const [title, entries] of groups) {
|
|
367
|
+
if (entries.length < 1) continue;
|
|
368
|
+
lines.push(`${title} (${entries.length}):`, ...entries.map((entry) => ` ${entry}`));
|
|
369
|
+
}
|
|
370
|
+
const name = relativeFile(projectRoot, result.file);
|
|
371
|
+
const scope = `the English of ${result.packageIds.length} JSKIT package(s), ${result.total} message(s)`;
|
|
372
|
+
lines.push(result.ok
|
|
373
|
+
? `${name} covers ${scope}.`
|
|
374
|
+
: `${name} does not match ${scope}.`);
|
|
375
|
+
return lines.join("\n");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export {
|
|
379
|
+
canonicalLocale,
|
|
380
|
+
checkTranslations,
|
|
381
|
+
collectInstalledCatalogs,
|
|
382
|
+
exportTranslations,
|
|
383
|
+
formatCheckReport,
|
|
384
|
+
formatExportSummary,
|
|
385
|
+
messageParams,
|
|
386
|
+
parseMessage
|
|
387
|
+
};
|
package/scripts/jskit.mjs
CHANGED
|
@@ -4,21 +4,37 @@ import { readFile } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
canonicalLocale,
|
|
9
|
+
checkTranslations,
|
|
10
|
+
exportTranslations,
|
|
11
|
+
formatCheckReport,
|
|
12
|
+
formatExportSummary
|
|
13
|
+
} from "./i18n.mjs";
|
|
7
14
|
import { checkProject, updateProject } from "./project-packages.mjs";
|
|
8
15
|
|
|
9
16
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
10
17
|
const PACKAGE_ROOT = path.resolve(path.dirname(SCRIPT_PATH), "..");
|
|
11
18
|
const CATALOG_PATH = path.join(PACKAGE_ROOT, "catalog", "packages.json");
|
|
19
|
+
const I18N_SUBCOMMANDS = new Set(["export", "check"]);
|
|
12
20
|
|
|
13
21
|
function usage() {
|
|
14
22
|
return [
|
|
15
23
|
"Usage:",
|
|
16
24
|
" jskit update [--root <path>] [--no-install] [--manifest <path> ...]",
|
|
17
25
|
" jskit check [--root <path>] [--manifest <path> ...]",
|
|
26
|
+
" jskit i18n export <locale> [--root <path>]",
|
|
27
|
+
" jskit i18n check <locale> [--root <path>]",
|
|
18
28
|
"",
|
|
19
29
|
"Commands:",
|
|
20
|
-
" update
|
|
21
|
-
" check
|
|
30
|
+
" update Align root and workspace @jskit-ai/* declarations to this published catalog, then run npm install.",
|
|
31
|
+
" check Fail when manifests or package-lock.json contain stale, mixed, or private singleton JSKIT stacks.",
|
|
32
|
+
" i18n export Write the English messages of the installed JSKIT packages to src/locales/<locale>/jskit.json",
|
|
33
|
+
" for translators, keeping existing translations and extra keys.",
|
|
34
|
+
" i18n check Compare src/locales/<locale>/jskit.json with the installed packages' English; fail on missing keys,",
|
|
35
|
+
" invalid messages or placeholder mismatches, and list extra keys and untranslated messages.",
|
|
36
|
+
"",
|
|
37
|
+
"<locale> is a BCP 47 language tag, such as fr or pt-BR."
|
|
22
38
|
].join("\n");
|
|
23
39
|
}
|
|
24
40
|
|
|
@@ -33,6 +49,7 @@ function parseArgs(argv = []) {
|
|
|
33
49
|
projectRoot: process.cwd(),
|
|
34
50
|
install: true
|
|
35
51
|
};
|
|
52
|
+
const positionals = [];
|
|
36
53
|
|
|
37
54
|
while (args.length > 0) {
|
|
38
55
|
const argument = String(args.shift() || "").trim();
|
|
@@ -40,6 +57,10 @@ function parseArgs(argv = []) {
|
|
|
40
57
|
options.help = true;
|
|
41
58
|
continue;
|
|
42
59
|
}
|
|
60
|
+
if (command === "i18n" && argument && !argument.startsWith("-")) {
|
|
61
|
+
positionals.push(argument);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
43
64
|
if (argument === "--no-install") {
|
|
44
65
|
options.install = false;
|
|
45
66
|
continue;
|
|
@@ -68,6 +89,9 @@ function parseArgs(argv = []) {
|
|
|
68
89
|
if (options.help || command === "help") {
|
|
69
90
|
return options;
|
|
70
91
|
}
|
|
92
|
+
if (command === "i18n") {
|
|
93
|
+
return parseI18nArgs(options, positionals);
|
|
94
|
+
}
|
|
71
95
|
if (command !== "update" && command !== "check") {
|
|
72
96
|
throw new Error(usage());
|
|
73
97
|
}
|
|
@@ -77,6 +101,46 @@ function parseArgs(argv = []) {
|
|
|
77
101
|
return options;
|
|
78
102
|
}
|
|
79
103
|
|
|
104
|
+
function parseI18nArgs(options, positionals) {
|
|
105
|
+
const [subcommandArgument = "", locale, unexpected] = positionals;
|
|
106
|
+
const subcommand = subcommandArgument.toLowerCase();
|
|
107
|
+
if (!I18N_SUBCOMMANDS.has(subcommand)) {
|
|
108
|
+
throw new Error(usage());
|
|
109
|
+
}
|
|
110
|
+
if (unexpected !== undefined) {
|
|
111
|
+
throw new Error(`Unknown argument: ${unexpected}`);
|
|
112
|
+
}
|
|
113
|
+
if (!options.install) {
|
|
114
|
+
throw new Error("--no-install is only valid with jskit update.");
|
|
115
|
+
}
|
|
116
|
+
if (options.additionalManifests) {
|
|
117
|
+
throw new Error("--manifest is only valid with jskit update and jskit check.");
|
|
118
|
+
}
|
|
119
|
+
if (!locale) {
|
|
120
|
+
throw new Error(`jskit i18n ${subcommand} needs a locale, such as fr or pt-BR.`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
command: "i18n",
|
|
124
|
+
subcommand,
|
|
125
|
+
locale: canonicalLocale(locale),
|
|
126
|
+
projectRoot: options.projectRoot
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runI18n({ subcommand, locale, projectRoot }) {
|
|
131
|
+
if (subcommand === "export") {
|
|
132
|
+
const result = await exportTranslations({ projectRoot, locale });
|
|
133
|
+
process.stdout.write(`${formatExportSummary(result, projectRoot)}\n`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const result = await checkTranslations({ projectRoot, locale });
|
|
137
|
+
const report = formatCheckReport(result, projectRoot);
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
throw new Error(report);
|
|
140
|
+
}
|
|
141
|
+
process.stdout.write(`${report}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
80
144
|
async function readCatalog() {
|
|
81
145
|
return JSON.parse(await readFile(CATALOG_PATH, "utf8"));
|
|
82
146
|
}
|
|
@@ -87,6 +151,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
87
151
|
process.stdout.write(`${usage()}\n`);
|
|
88
152
|
return;
|
|
89
153
|
}
|
|
154
|
+
if (options.command === "i18n") {
|
|
155
|
+
await runI18n(options);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
90
158
|
|
|
91
159
|
const catalog = await readCatalog();
|
|
92
160
|
if (options.command === "update") {
|