@xndrjs/i18n 0.2.0 → 0.3.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/README.md +58 -8
- package/bin/audit.mjs +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/{types-CFWVT2PP.d.ts → types-C1CpXVOJ.d.ts} +7 -1
- package/dist/validation/index.d.ts +3 -2
- package/dist/validation/index.js +125 -28
- package/dist/validation/index.js.map +1 -1
- package/package.json +5 -3
- package/src/IcuTranslationProviderSingle.test.ts +54 -0
- package/src/audit/audit-dictionaries.test.ts +134 -0
- package/src/audit/audit-dictionaries.ts +172 -0
- package/src/audit/run-audit.test.ts +136 -0
- package/src/audit/run-audit.ts +81 -0
- package/src/codegen/emit/dictionary-file.ts +7 -3
- package/src/codegen/emit/dictionary-schema-file.ts +6 -4
- package/src/codegen/emit/instance-file.ts +10 -5
- package/src/codegen/emit/namespace-loaders-file.ts +10 -6
- package/src/codegen/generate-i18n-types.test.ts +496 -0
- package/src/codegen/generate-i18n-types.ts +33 -5
- package/src/codegen/icu-analysis.ts +22 -7
- package/src/codegen/locale-policy.test.ts +35 -0
- package/src/codegen/locale-policy.ts +26 -0
- package/src/codegen/paths.ts +27 -1
- package/src/codegen/read-dictionary.test.ts +104 -0
- package/src/codegen/read-dictionary.ts +133 -0
- package/src/codegen/types.ts +5 -0
- package/src/ensure-namespace.test.ts +1 -1
- package/src/icu/extract-variables.test.ts +199 -0
- package/src/icu/extract-variables.ts +160 -20
- package/src/icu/parse-template.ts +9 -2
- package/src/setup/setup-i18n.ts +7 -4
- package/src/validation/normalize.ts +17 -10
- package/src/validation/validation.test.ts +104 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VariableSpec } from "../../icu/extract-variables.js";
|
|
2
|
-
import { GENERATED_FILE_BANNER } from "../paths.js";
|
|
3
|
-
import type { NamespaceEntry } from "../types.js";
|
|
2
|
+
import { GENERATED_FILE_BANNER, toRelativeModuleImport } from "../paths.js";
|
|
3
|
+
import type { ImportExtension, NamespaceEntry } from "../types.js";
|
|
4
4
|
|
|
5
5
|
function formatVariableSpecObject(spec: VariableSpec): string {
|
|
6
6
|
const entries = Object.entries(spec);
|
|
@@ -70,8 +70,10 @@ export function formatDictionarySchemaFile(
|
|
|
70
70
|
schemaTypeName: string,
|
|
71
71
|
typesModule: string,
|
|
72
72
|
isSingle: boolean,
|
|
73
|
-
dictionarySpecBlock: string
|
|
73
|
+
dictionarySpecBlock: string,
|
|
74
|
+
importExtension: ImportExtension
|
|
74
75
|
): string {
|
|
76
|
+
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
75
77
|
const namespaceImport = isSingle
|
|
76
78
|
? ""
|
|
77
79
|
: ` validateExternalNamespace as validateExternalNamespaceImpl,\n`;
|
|
@@ -99,7 +101,7 @@ export function formatDictionarySchemaFile(
|
|
|
99
101
|
` type NormalizedDictionary,\n` +
|
|
100
102
|
` type ValidationResult,\n` +
|
|
101
103
|
`} from '@xndrjs/i18n/validation';\n` +
|
|
102
|
-
`import type { ${schemaTypeName} } from '
|
|
104
|
+
`import type { ${schemaTypeName} } from '${typesImport}';\n\n` +
|
|
103
105
|
`${dictionarySpecBlock}\n` +
|
|
104
106
|
`export function normalizeExternalDictionary(\n` +
|
|
105
107
|
` input: unknown,\n` +
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { GENERATED_FILE_BANNER, toModuleBasename } from "../paths.js";
|
|
1
|
+
import { GENERATED_FILE_BANNER, toModuleBasename, toRelativeModuleImport } from "../paths.js";
|
|
2
|
+
import type { ImportExtension } from "../types.js";
|
|
2
3
|
|
|
3
4
|
export interface InstanceFileOptions {
|
|
4
5
|
isSingle: boolean;
|
|
@@ -11,6 +12,7 @@ export interface InstanceFileOptions {
|
|
|
11
12
|
localeFallbackConstName: string;
|
|
12
13
|
factoryName: string;
|
|
13
14
|
hasLocaleFallback: boolean;
|
|
15
|
+
importExtension: ImportExtension;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
@@ -25,15 +27,18 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
25
27
|
localeFallbackConstName,
|
|
26
28
|
factoryName,
|
|
27
29
|
hasLocaleFallback,
|
|
30
|
+
importExtension,
|
|
28
31
|
} = options;
|
|
29
32
|
|
|
30
33
|
const providerClass = isSingle ? "IcuTranslationProviderSingle" : "IcuTranslationProviderMulti";
|
|
31
34
|
const typesModule = toModuleBasename(typesOutputPath);
|
|
32
35
|
const dictionaryModule = toModuleBasename(dictionaryOutputPath);
|
|
36
|
+
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
37
|
+
const dictionaryImport = toRelativeModuleImport(dictionaryModule, importExtension);
|
|
33
38
|
const initialDictionaryType = hasLazy ? "InitialSchema" : schemaTypeName;
|
|
34
39
|
const initialDictionaryImport = hasLazy
|
|
35
|
-
? `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema } from '
|
|
36
|
-
: `import type { ${paramsTypeName}, ${schemaTypeName} } from '
|
|
40
|
+
? `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema } from '${typesImport}';\n`
|
|
41
|
+
: `import type { ${paramsTypeName}, ${schemaTypeName} } from '${typesImport}';\n`;
|
|
37
42
|
const providerTypeArgs = hasLocaleFallback
|
|
38
43
|
? `${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, typeof ${localeFallbackConstName}`
|
|
39
44
|
: `${schemaTypeName}, ${paramsTypeName}`;
|
|
@@ -41,13 +46,13 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
41
46
|
? `, {\n localeFallback: ${localeFallbackConstName},\n }`
|
|
42
47
|
: "";
|
|
43
48
|
const fallbackImport = hasLocaleFallback
|
|
44
|
-
? `import { ${localeFallbackConstName}, type ${localeTypeName} } from '
|
|
49
|
+
? `import { ${localeFallbackConstName}, type ${localeTypeName} } from '${typesImport}';\n`
|
|
45
50
|
: "";
|
|
46
51
|
|
|
47
52
|
return (
|
|
48
53
|
`${GENERATED_FILE_BANNER}` +
|
|
49
54
|
`import { ${providerClass} } from '@xndrjs/i18n';\n` +
|
|
50
|
-
`import { dictionary } from '
|
|
55
|
+
`import { dictionary } from '${dictionaryImport}';\n` +
|
|
51
56
|
initialDictionaryImport +
|
|
52
57
|
fallbackImport +
|
|
53
58
|
`\n` +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { GENERATED_FILE_BANNER, toImportPath } from "../paths.js";
|
|
2
|
-
import type { NamespaceEntry } from "../types.js";
|
|
1
|
+
import { GENERATED_FILE_BANNER, toImportPath, toRelativeModuleImport } from "../paths.js";
|
|
2
|
+
import type { ImportExtension, NamespaceEntry } from "../types.js";
|
|
3
3
|
|
|
4
4
|
export function formatNamespaceLoadersFile(
|
|
5
5
|
loadersOutputPath: string,
|
|
@@ -8,8 +8,12 @@ export function formatNamespaceLoadersFile(
|
|
|
8
8
|
typesModule: string,
|
|
9
9
|
dictionarySchemaModule: string,
|
|
10
10
|
instanceModule: string,
|
|
11
|
-
factoryName: string
|
|
11
|
+
factoryName: string,
|
|
12
|
+
importExtension: ImportExtension
|
|
12
13
|
): string {
|
|
14
|
+
const dictionarySchemaImport = toRelativeModuleImport(dictionarySchemaModule, importExtension);
|
|
15
|
+
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
16
|
+
const instanceImport = toRelativeModuleImport(instanceModule, importExtension);
|
|
13
17
|
const loaderEntries = lazyEntries
|
|
14
18
|
.map((entry) => {
|
|
15
19
|
const importPath = toImportPath(loadersOutputPath, entry.absolutePath);
|
|
@@ -20,9 +24,9 @@ export function formatNamespaceLoadersFile(
|
|
|
20
24
|
return (
|
|
21
25
|
`${GENERATED_FILE_BANNER}` +
|
|
22
26
|
`import { ensureNamespacesLoadedImpl } from '@xndrjs/i18n';\n` +
|
|
23
|
-
`import { validateExternalNamespace } from '
|
|
24
|
-
`import type { ${schemaTypeName}, LazyNamespace } from '
|
|
25
|
-
`import type { ${factoryName} } from '
|
|
27
|
+
`import { validateExternalNamespace } from '${dictionarySchemaImport}';\n` +
|
|
28
|
+
`import type { ${schemaTypeName}, LazyNamespace } from '${typesImport}';\n` +
|
|
29
|
+
`import type { ${factoryName} } from '${instanceImport}';\n\n` +
|
|
26
30
|
`export const namespaceLoaders: Record<\n` +
|
|
27
31
|
` LazyNamespace,\n` +
|
|
28
32
|
` () => Promise<${schemaTypeName}[LazyNamespace]>\n` +
|
|
@@ -294,6 +294,73 @@ describe("generate-i18n-types", () => {
|
|
|
294
294
|
);
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
it("enriches LOCALE_FALLBACK with null for dictionary locales missing from config", () => {
|
|
298
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
299
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
300
|
+
|
|
301
|
+
writeFileSync(
|
|
302
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
login_button: { en: "Login", fr: "Connexion" },
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
writeFileSync(
|
|
308
|
+
join(tempDir, "i18n.codegen.json"),
|
|
309
|
+
JSON.stringify({
|
|
310
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
311
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
312
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
313
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
314
|
+
paramsTypeName: "AppParams",
|
|
315
|
+
schemaTypeName: "AppSchema",
|
|
316
|
+
localeFallback: {
|
|
317
|
+
en: null,
|
|
318
|
+
it: "en",
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const result = runCodegen(tempDir);
|
|
324
|
+
expect(result.status).toBe(0);
|
|
325
|
+
|
|
326
|
+
const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
|
|
327
|
+
expect(types).toContain('"fr": null');
|
|
328
|
+
expect(types).toContain('"it": "en"');
|
|
329
|
+
expect(types).toContain("export type AppLocale = 'en' | 'fr' | 'it'");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("does not emit LOCALE_FALLBACK when localeFallback is absent from config", () => {
|
|
333
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
334
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
335
|
+
|
|
336
|
+
writeFileSync(
|
|
337
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
338
|
+
JSON.stringify({
|
|
339
|
+
login_button: { en: "Login", it: "Accedi" },
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
writeFileSync(
|
|
343
|
+
join(tempDir, "i18n.codegen.json"),
|
|
344
|
+
JSON.stringify({
|
|
345
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
346
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
347
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
348
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
349
|
+
paramsTypeName: "AppParams",
|
|
350
|
+
schemaTypeName: "AppSchema",
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const result = runCodegen(tempDir);
|
|
355
|
+
expect(result.status).toBe(0);
|
|
356
|
+
|
|
357
|
+
const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
|
|
358
|
+
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
359
|
+
expect(types).not.toContain("export const LOCALE_FALLBACK");
|
|
360
|
+
expect(factory).not.toContain("localeFallback:");
|
|
361
|
+
expect(types).toContain("export type AppLocale = 'en' | 'it'");
|
|
362
|
+
});
|
|
363
|
+
|
|
297
364
|
it("fails on circular locale fallback in config", () => {
|
|
298
365
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
299
366
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
@@ -448,6 +515,128 @@ describe("generate-i18n-types", () => {
|
|
|
448
515
|
expect(result.stderr).toContain("loadOnInit");
|
|
449
516
|
});
|
|
450
517
|
|
|
518
|
+
it("infers number when English plural and Italian simple interpolation share a key", () => {
|
|
519
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
520
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
521
|
+
|
|
522
|
+
writeFileSync(
|
|
523
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
524
|
+
JSON.stringify({
|
|
525
|
+
invoice_count: {
|
|
526
|
+
en: "You have {count, plural, one {1 invoice} other {# invoices}}",
|
|
527
|
+
it: "Hai {count} fatture",
|
|
528
|
+
},
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
writeFileSync(
|
|
532
|
+
join(tempDir, "i18n.codegen.json"),
|
|
533
|
+
JSON.stringify({
|
|
534
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
535
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
536
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
537
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
538
|
+
paramsTypeName: "AppParams",
|
|
539
|
+
schemaTypeName: "AppSchema",
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const result = runCodegen(tempDir);
|
|
544
|
+
expect(result.status).toBe(0);
|
|
545
|
+
|
|
546
|
+
const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
|
|
547
|
+
expect(types).toContain("invoice_count: { count: number }");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("fails when plural and select disagree on the same variable across locales", () => {
|
|
551
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
552
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
553
|
+
|
|
554
|
+
writeFileSync(
|
|
555
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
556
|
+
JSON.stringify({
|
|
557
|
+
broken: {
|
|
558
|
+
en: "{myVar, select, other {x}}",
|
|
559
|
+
it: "{myVar, plural, one {1} other {#}}",
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
writeFileSync(
|
|
564
|
+
join(tempDir, "i18n.codegen.json"),
|
|
565
|
+
JSON.stringify({
|
|
566
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
567
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
568
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
569
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
570
|
+
paramsTypeName: "AppParams",
|
|
571
|
+
schemaTypeName: "AppSchema",
|
|
572
|
+
})
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
const result = runCodegen(tempDir);
|
|
576
|
+
expect(result.status).not.toBe(0);
|
|
577
|
+
expect(result.stderr).toContain('Incompatible ICU variable "myVar"');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("fails when plural and selectordinal disagree on the same variable across locales", () => {
|
|
581
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
582
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
583
|
+
|
|
584
|
+
writeFileSync(
|
|
585
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
586
|
+
JSON.stringify({
|
|
587
|
+
broken: {
|
|
588
|
+
en: "{rank, plural, one {1} other {#}}",
|
|
589
|
+
it: "{rank, selectordinal, one {#°} other {#°}}",
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
writeFileSync(
|
|
594
|
+
join(tempDir, "i18n.codegen.json"),
|
|
595
|
+
JSON.stringify({
|
|
596
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
597
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
598
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
599
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
600
|
+
paramsTypeName: "AppParams",
|
|
601
|
+
schemaTypeName: "AppSchema",
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const result = runCodegen(tempDir);
|
|
606
|
+
expect(result.status).not.toBe(0);
|
|
607
|
+
expect(result.stderr).toContain('Incompatible ICU variable "rank"');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("fails when select and number format disagree on the same variable across locales", () => {
|
|
611
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
612
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
613
|
+
|
|
614
|
+
writeFileSync(
|
|
615
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
616
|
+
JSON.stringify({
|
|
617
|
+
broken: {
|
|
618
|
+
en: "{value, select, other {x}}",
|
|
619
|
+
it: "{value, number}",
|
|
620
|
+
},
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
writeFileSync(
|
|
624
|
+
join(tempDir, "i18n.codegen.json"),
|
|
625
|
+
JSON.stringify({
|
|
626
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
627
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
628
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
629
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
630
|
+
paramsTypeName: "AppParams",
|
|
631
|
+
schemaTypeName: "AppSchema",
|
|
632
|
+
})
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const result = runCodegen(tempDir);
|
|
636
|
+
expect(result.status).not.toBe(0);
|
|
637
|
+
expect(result.stderr).toContain('Incompatible ICU variable "value"');
|
|
638
|
+
});
|
|
639
|
+
|
|
451
640
|
it("keeps multi mode when namespaces has a single entry", () => {
|
|
452
641
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
453
642
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
@@ -479,4 +668,311 @@ describe("generate-i18n-types", () => {
|
|
|
479
668
|
expect(types).toContain("default: {");
|
|
480
669
|
expect(factory).toContain("IcuTranslationProviderMulti");
|
|
481
670
|
});
|
|
671
|
+
|
|
672
|
+
it("omits import extension by default between generated modules", () => {
|
|
673
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
674
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
675
|
+
|
|
676
|
+
writeFileSync(
|
|
677
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
678
|
+
JSON.stringify({ welcome: { en: "Welcome {name}!" } })
|
|
679
|
+
);
|
|
680
|
+
writeFileSync(
|
|
681
|
+
join(tempDir, "i18n.codegen.json"),
|
|
682
|
+
JSON.stringify({
|
|
683
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
684
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
685
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
686
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
687
|
+
paramsTypeName: "AppParams",
|
|
688
|
+
schemaTypeName: "AppSchema",
|
|
689
|
+
})
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const result = runCodegen(tempDir);
|
|
693
|
+
expect(result.status).toBe(0);
|
|
694
|
+
|
|
695
|
+
const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
|
|
696
|
+
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
697
|
+
expect(dictionary).toContain("from './i18n-types.generated'");
|
|
698
|
+
expect(factory).toContain("from './dictionary.generated'");
|
|
699
|
+
expect(factory).toContain("from './i18n-types.generated'");
|
|
700
|
+
expect(dictionary).not.toContain(".generated.js");
|
|
701
|
+
expect(dictionary).not.toContain(".generated.ts");
|
|
702
|
+
expect(factory).not.toContain(".generated.js");
|
|
703
|
+
expect(factory).not.toContain(".generated.ts");
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("supports importExtension .ts", () => {
|
|
707
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
708
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
709
|
+
|
|
710
|
+
writeFileSync(
|
|
711
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
712
|
+
JSON.stringify({ welcome: { en: "Welcome {name}!" } })
|
|
713
|
+
);
|
|
714
|
+
writeFileSync(
|
|
715
|
+
join(tempDir, "i18n.codegen.json"),
|
|
716
|
+
JSON.stringify({
|
|
717
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
718
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
719
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
720
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
721
|
+
importExtension: ".ts",
|
|
722
|
+
paramsTypeName: "AppParams",
|
|
723
|
+
schemaTypeName: "AppSchema",
|
|
724
|
+
})
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const result = runCodegen(tempDir);
|
|
728
|
+
expect(result.status).toBe(0);
|
|
729
|
+
|
|
730
|
+
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
731
|
+
expect(factory).toContain("from './dictionary.generated.ts'");
|
|
732
|
+
expect(factory).toContain("from './i18n-types.generated.ts'");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("supports importExtension .js for NodeNext ESM projects", () => {
|
|
736
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
737
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
738
|
+
|
|
739
|
+
writeFileSync(
|
|
740
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
741
|
+
JSON.stringify({ welcome: { en: "Welcome {name}!" } })
|
|
742
|
+
);
|
|
743
|
+
writeFileSync(
|
|
744
|
+
join(tempDir, "i18n.codegen.json"),
|
|
745
|
+
JSON.stringify({
|
|
746
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
747
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
748
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
749
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
750
|
+
importExtension: ".js",
|
|
751
|
+
paramsTypeName: "AppParams",
|
|
752
|
+
schemaTypeName: "AppSchema",
|
|
753
|
+
})
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
const result = runCodegen(tempDir);
|
|
757
|
+
expect(result.status).toBe(0);
|
|
758
|
+
|
|
759
|
+
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
760
|
+
expect(factory).toContain("from './dictionary.generated.js'");
|
|
761
|
+
expect(factory).toContain("from './i18n-types.generated.js'");
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("fails when importExtension is invalid", () => {
|
|
765
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
766
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
767
|
+
|
|
768
|
+
writeFileSync(
|
|
769
|
+
join(tempDir, "src/i18n/translations/translations.json"),
|
|
770
|
+
JSON.stringify({ welcome: { en: "Welcome {name}!" } })
|
|
771
|
+
);
|
|
772
|
+
writeFileSync(
|
|
773
|
+
join(tempDir, "i18n.codegen.json"),
|
|
774
|
+
JSON.stringify({
|
|
775
|
+
dictionary: "src/i18n/translations/translations.json",
|
|
776
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
777
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
778
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
779
|
+
importExtension: ".mjs",
|
|
780
|
+
paramsTypeName: "AppParams",
|
|
781
|
+
schemaTypeName: "AppSchema",
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
const result = runCodegen(tempDir);
|
|
786
|
+
expect(result.status).not.toBe(0);
|
|
787
|
+
expect(result.stderr).toContain('importExtension must be "none", ".ts", or ".js"');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("compiles yaml dictionaries to json and generates json imports", () => {
|
|
791
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
792
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
793
|
+
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
794
|
+
|
|
795
|
+
writeFileSync(
|
|
796
|
+
join(tempDir, "src/i18n/translations/translations.yaml"),
|
|
797
|
+
`welcome:
|
|
798
|
+
en: |
|
|
799
|
+
Line one
|
|
800
|
+
Line two
|
|
801
|
+
it: |
|
|
802
|
+
Riga uno
|
|
803
|
+
Riga due
|
|
804
|
+
`
|
|
805
|
+
);
|
|
806
|
+
writeFileSync(
|
|
807
|
+
join(tempDir, "i18n.codegen.json"),
|
|
808
|
+
JSON.stringify({
|
|
809
|
+
dictionary: "src/i18n/translations/translations.yaml",
|
|
810
|
+
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
811
|
+
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
812
|
+
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
813
|
+
paramsTypeName: "AppParams",
|
|
814
|
+
schemaTypeName: "AppSchema",
|
|
815
|
+
})
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
const result = runCodegen(tempDir);
|
|
819
|
+
expect(result.status).toBe(0);
|
|
820
|
+
expect(result.stdout).toContain(
|
|
821
|
+
"Compiled: src/i18n/translations/translations.yaml → src/i18n/generated/translations/translations.json"
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
const compiled = JSON.parse(
|
|
825
|
+
readFileSync(join(tempDir, "src/i18n/generated/translations/translations.json"), "utf8")
|
|
826
|
+
) as Record<string, Record<string, string>>;
|
|
827
|
+
expect(compiled.welcome?.en).toBe("Line one\nLine two\n");
|
|
828
|
+
|
|
829
|
+
const dictionary = readFileSync(
|
|
830
|
+
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
831
|
+
"utf8"
|
|
832
|
+
);
|
|
833
|
+
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
834
|
+
expect(dictionary).toContain("from './translations/translations.json'");
|
|
835
|
+
expect(types).toContain("typeof import('./translations/translations.json')");
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("supports mixed json and yaml namespaces", () => {
|
|
839
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
840
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
841
|
+
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
842
|
+
|
|
843
|
+
writeFileSync(
|
|
844
|
+
join(tempDir, "src/i18n/translations/default.json"),
|
|
845
|
+
JSON.stringify({
|
|
846
|
+
login_button: { en: "Login", it: "Accedi" },
|
|
847
|
+
})
|
|
848
|
+
);
|
|
849
|
+
writeFileSync(
|
|
850
|
+
join(tempDir, "src/i18n/translations/billing.yaml"),
|
|
851
|
+
`invoice_summary:
|
|
852
|
+
en: You have {count} invoices
|
|
853
|
+
`
|
|
854
|
+
);
|
|
855
|
+
writeFileSync(
|
|
856
|
+
join(tempDir, "i18n.codegen.json"),
|
|
857
|
+
JSON.stringify({
|
|
858
|
+
namespaces: {
|
|
859
|
+
default: "src/i18n/translations/default.json",
|
|
860
|
+
billing: "src/i18n/translations/billing.yaml",
|
|
861
|
+
},
|
|
862
|
+
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
863
|
+
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
864
|
+
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
865
|
+
paramsTypeName: "AppParams",
|
|
866
|
+
schemaTypeName: "AppSchema",
|
|
867
|
+
})
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
const result = runCodegen(tempDir);
|
|
871
|
+
expect(result.status).toBe(0);
|
|
872
|
+
|
|
873
|
+
const dictionary = readFileSync(
|
|
874
|
+
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
875
|
+
"utf8"
|
|
876
|
+
);
|
|
877
|
+
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
878
|
+
expect(dictionary).toContain("from '../translations/default.json'");
|
|
879
|
+
expect(dictionary).toContain("from './translations/billing.json'");
|
|
880
|
+
expect(types).toContain("default: typeof import('../translations/default.json')");
|
|
881
|
+
expect(types).toContain("billing: typeof import('./translations/billing.json')");
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("generates lazy loaders that import compiled json for yaml namespaces", () => {
|
|
885
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
886
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
887
|
+
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
888
|
+
|
|
889
|
+
writeFileSync(
|
|
890
|
+
join(tempDir, "src/i18n/translations/default.json"),
|
|
891
|
+
JSON.stringify({
|
|
892
|
+
login_button: { en: "Login" },
|
|
893
|
+
})
|
|
894
|
+
);
|
|
895
|
+
writeFileSync(
|
|
896
|
+
join(tempDir, "src/i18n/translations/billing.yaml"),
|
|
897
|
+
`invoice_summary:
|
|
898
|
+
en: You have {count} invoices
|
|
899
|
+
`
|
|
900
|
+
);
|
|
901
|
+
writeFileSync(
|
|
902
|
+
join(tempDir, "i18n.codegen.json"),
|
|
903
|
+
JSON.stringify({
|
|
904
|
+
namespaces: {
|
|
905
|
+
default: "src/i18n/translations/default.json",
|
|
906
|
+
billing: "src/i18n/translations/billing.yaml",
|
|
907
|
+
},
|
|
908
|
+
loadOnInit: ["default"],
|
|
909
|
+
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
910
|
+
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
911
|
+
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
912
|
+
dictionarySchemaOutput: "src/i18n/generated/dictionary-schema.generated.ts",
|
|
913
|
+
namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
|
|
914
|
+
paramsTypeName: "AppParams",
|
|
915
|
+
schemaTypeName: "AppSchema",
|
|
916
|
+
})
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
const result = runCodegen(tempDir);
|
|
920
|
+
expect(result.status).toBe(0);
|
|
921
|
+
|
|
922
|
+
const loaders = readFileSync(
|
|
923
|
+
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
924
|
+
"utf8"
|
|
925
|
+
);
|
|
926
|
+
expect(loaders).toContain("import('./translations/billing.json')");
|
|
927
|
+
expect(loaders).not.toContain("billing.yaml");
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it("fails when dictionary extension is unsupported", () => {
|
|
931
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
932
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
933
|
+
|
|
934
|
+
writeFileSync(join(tempDir, "src/i18n/translations/translations.toml"), "welcome = {}");
|
|
935
|
+
writeFileSync(
|
|
936
|
+
join(tempDir, "i18n.codegen.json"),
|
|
937
|
+
JSON.stringify({
|
|
938
|
+
dictionary: "src/i18n/translations/translations.toml",
|
|
939
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
940
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
941
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
942
|
+
paramsTypeName: "AppParams",
|
|
943
|
+
schemaTypeName: "AppSchema",
|
|
944
|
+
})
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
const result = runCodegen(tempDir);
|
|
948
|
+
expect(result.status).not.toBe(0);
|
|
949
|
+
expect(result.stderr).toContain("unsupported dictionary extension");
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it("fails when yaml dictionary has invalid shape", () => {
|
|
953
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
954
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
955
|
+
|
|
956
|
+
writeFileSync(
|
|
957
|
+
join(tempDir, "src/i18n/translations/translations.yaml"),
|
|
958
|
+
`welcome:
|
|
959
|
+
en: 123
|
|
960
|
+
`
|
|
961
|
+
);
|
|
962
|
+
writeFileSync(
|
|
963
|
+
join(tempDir, "i18n.codegen.json"),
|
|
964
|
+
JSON.stringify({
|
|
965
|
+
dictionary: "src/i18n/translations/translations.yaml",
|
|
966
|
+
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
967
|
+
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
968
|
+
instanceOutput: "src/i18n/instance.generated.ts",
|
|
969
|
+
paramsTypeName: "AppParams",
|
|
970
|
+
schemaTypeName: "AppSchema",
|
|
971
|
+
})
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
const result = runCodegen(tempDir);
|
|
975
|
+
expect(result.status).not.toBe(0);
|
|
976
|
+
expect(result.stderr).toContain("must be a string");
|
|
977
|
+
});
|
|
482
978
|
});
|