@xndrjs/i18n 0.6.1 → 0.7.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 +154 -141
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +268 -69
- package/dist/index.js +572 -65
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +23 -7
- package/dist/validation/index.js +196 -89
- package/dist/validation/index.js.map +1 -1
- package/package.json +1 -1
- package/src/IcuTranslationProviderMulti.test.ts +80 -234
- package/src/IcuTranslationProviderMulti.ts +101 -112
- package/src/IcuTranslationProviderSingle.test.ts +30 -309
- package/src/IcuTranslationProviderSingle.ts +42 -75
- package/src/builder-load-registry.ts +18 -0
- package/src/builder-loaders.ts +18 -0
- package/src/builder-multi.ts +481 -0
- package/src/builder-types.test.ts +80 -0
- package/src/builder-types.ts +24 -0
- package/src/builder.test.ts +421 -0
- package/src/builder.ts +112 -0
- package/src/codegen/delivery-artifacts.test.ts +2 -1
- package/src/codegen/delivery-artifacts.ts +2 -1
- package/src/codegen/emit/dictionary-file.test.ts +0 -7
- package/src/codegen/emit/dictionary-schema-file.ts +55 -42
- package/src/codegen/emit/instance-file.test.ts +88 -0
- package/src/codegen/emit/instance-file.ts +164 -17
- package/src/codegen/emit/namespace-loaders-file.test.ts +4 -18
- package/src/codegen/emit/namespace-loaders-file.ts +4 -54
- package/src/codegen/emit/types-file.test.ts +0 -2
- package/src/codegen/generate-i18n-types.test.ts +8 -303
- package/src/codegen/generate-i18n-types.ts +16 -1
- package/src/codegen/project-locales-set-namespace.test.ts +25 -32
- package/src/engine.ts +71 -0
- package/src/index.ts +45 -7
- package/src/patch-key.test.ts +186 -0
- package/src/patch-key.ts +140 -0
- package/src/project-locales.test.ts +16 -6
- package/src/project-locales.ts +17 -6
- package/src/scope-multi.ts +124 -0
- package/src/scope-single.ts +85 -0
- package/src/scope-types.ts +27 -0
- package/src/scope.test.ts +481 -0
- package/src/single-builder.ts +153 -0
- package/src/types.ts +16 -0
- package/src/validation/create-normalized-schema.ts +1 -1
- package/src/validation/errors.ts +2 -0
- package/src/validation/index.ts +135 -27
- package/src/validation/normalize.ts +176 -46
- package/src/validation/types.ts +4 -0
- package/src/validation/validate-normalized.ts +43 -1
- package/src/validation/validation.test.ts +200 -7
|
@@ -174,39 +174,12 @@ describe("generate-i18n-types", () => {
|
|
|
174
174
|
const schema = readFileSync(join(tempDir, "src/i18n/dictionary-schema.generated.ts"), "utf8");
|
|
175
175
|
expect(schema).toContain("export const DICTIONARY_SPEC");
|
|
176
176
|
expect(schema).toContain("mode: 'multi' as const");
|
|
177
|
-
expect(schema).toContain("
|
|
178
|
-
expect(schema).toContain("
|
|
179
|
-
expect(schema).toContain("
|
|
177
|
+
expect(schema).toContain("validateExternalDictionaryPartial");
|
|
178
|
+
expect(schema).toContain("validateExternalNamespacePartial");
|
|
179
|
+
expect(schema).toContain("validateExternalKey");
|
|
180
180
|
expect(schema).toContain('"name": "string"');
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
it("does not generate dictionary schema file when dictionarySchemaOutput is omitted", () => {
|
|
184
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
185
|
-
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
186
|
-
|
|
187
|
-
writeFileSync(
|
|
188
|
-
join(tempDir, "src/i18n/translations/translations.json"),
|
|
189
|
-
JSON.stringify({
|
|
190
|
-
login_button: { en: "Login" },
|
|
191
|
-
})
|
|
192
|
-
);
|
|
193
|
-
writeFileSync(
|
|
194
|
-
join(tempDir, "i18n.codegen.json"),
|
|
195
|
-
JSON.stringify({
|
|
196
|
-
dictionary: "src/i18n/translations/translations.json",
|
|
197
|
-
typesOutput: "src/i18n/i18n-types.generated.ts",
|
|
198
|
-
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
199
|
-
instanceOutput: "src/i18n/instance.generated.ts",
|
|
200
|
-
paramsTypeName: "AppParams",
|
|
201
|
-
schemaTypeName: "AppSchema",
|
|
202
|
-
})
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
const result = runCodegen(tempDir);
|
|
206
|
-
expect(result.status).toBe(0);
|
|
207
|
-
expect(result.stdout).not.toContain("dictionary-schema.generated.ts");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
183
|
it("generates single-mode dictionary schema without namespace validator", () => {
|
|
211
184
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
212
185
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
@@ -235,9 +208,8 @@ describe("generate-i18n-types", () => {
|
|
|
235
208
|
|
|
236
209
|
const schema = readFileSync(join(tempDir, "src/i18n/dictionary-schema.generated.ts"), "utf8");
|
|
237
210
|
expect(schema).toContain("mode: 'single' as const");
|
|
238
|
-
expect(schema).toContain("
|
|
239
|
-
expect(schema).
|
|
240
|
-
expect(schema).not.toContain("validateExternalNamespaceCore");
|
|
211
|
+
expect(schema).toContain("validateExternalDictionaryPartial");
|
|
212
|
+
expect(schema).toContain("validateExternalKey");
|
|
241
213
|
});
|
|
242
214
|
|
|
243
215
|
it("generates flat types for single-file config", () => {
|
|
@@ -273,12 +245,9 @@ describe("generate-i18n-types", () => {
|
|
|
273
245
|
expect(factory).toContain("export function createI18n(");
|
|
274
246
|
expect(factory).toContain("export function projectDictionaryLocales(");
|
|
275
247
|
expect(factory).toContain("dictionary: AppSchema");
|
|
276
|
-
expect(factory).not.toContain("export function projectNamespaceLocales(");
|
|
277
|
-
expect(factory).not.toContain("projectDictionaryLocalesCore");
|
|
278
248
|
expect(factory).toContain("IcuTranslationProviderSingle");
|
|
279
249
|
expect(types).toContain("login_button: never;");
|
|
280
250
|
expect(types).toContain("welcome: { name: string };");
|
|
281
|
-
expect(types).not.toContain("default: {");
|
|
282
251
|
});
|
|
283
252
|
|
|
284
253
|
it("fails with a non-zero exit code on malformed ICU", () => {
|
|
@@ -412,10 +381,7 @@ describe("generate-i18n-types", () => {
|
|
|
412
381
|
|
|
413
382
|
const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
|
|
414
383
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
415
|
-
expect(types).not.toContain("export const LOCALE_FALLBACK");
|
|
416
|
-
expect(factory).not.toContain("localeFallback:");
|
|
417
384
|
expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales)");
|
|
418
|
-
expect(factory).not.toContain("LOCALE_FALLBACK");
|
|
419
385
|
expect(types).toContain("export type AppLocale = 'en' | 'it'");
|
|
420
386
|
});
|
|
421
387
|
|
|
@@ -498,9 +464,7 @@ describe("generate-i18n-types", () => {
|
|
|
498
464
|
expect(types).toContain("export type LazyNamespace = 'billing'");
|
|
499
465
|
expect(types).toContain("export type InitialSchema = Pick<AppSchema, LoadOnInitNamespace>");
|
|
500
466
|
expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
|
|
501
|
-
expect(dictionary).not.toContain("billingNs");
|
|
502
467
|
expect(factory).toContain("dictionary: InitialSchema,");
|
|
503
|
-
expect(factory).not.toContain("import { dictionary }");
|
|
504
468
|
expect(loaders).toContain("export const namespaceLoaders");
|
|
505
469
|
expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
|
|
506
470
|
expect(loaders).toContain("import('./translations/billing.json')");
|
|
@@ -536,16 +500,12 @@ describe("generate-i18n-types", () => {
|
|
|
536
500
|
const result = runCodegen(tempDir);
|
|
537
501
|
expect(result.status).toBe(0);
|
|
538
502
|
|
|
539
|
-
const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
|
|
540
503
|
const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
|
|
541
504
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
542
505
|
|
|
543
|
-
expect(types).not.toContain("LazyNamespace");
|
|
544
506
|
expect(dictionary).toContain("export const defaultDictionary: AppSchema");
|
|
545
507
|
expect(dictionary).toContain("billingNs");
|
|
546
508
|
expect(factory).toContain("dictionary: AppSchema,");
|
|
547
|
-
expect(factory).not.toContain("import { dictionary }");
|
|
548
|
-
expect(result.stdout).not.toContain("namespace-loaders.generated.ts");
|
|
549
509
|
});
|
|
550
510
|
|
|
551
511
|
it("fails when loadOnInit is used in single mode", () => {
|
|
@@ -754,13 +714,8 @@ describe("generate-i18n-types", () => {
|
|
|
754
714
|
const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
|
|
755
715
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
756
716
|
expect(dictionary).toContain("from './i18n-types.generated'");
|
|
757
|
-
expect(factory).not.toContain("from './dictionary.generated'");
|
|
758
717
|
expect(factory).toContain("from './i18n-types.generated'");
|
|
759
718
|
expect(factory).toContain("dictionary: AppSchema,");
|
|
760
|
-
expect(dictionary).not.toContain(".generated.js");
|
|
761
|
-
expect(dictionary).not.toContain(".generated.ts");
|
|
762
|
-
expect(factory).not.toContain(".generated.js");
|
|
763
|
-
expect(factory).not.toContain(".generated.ts");
|
|
764
719
|
});
|
|
765
720
|
|
|
766
721
|
it("supports importExtension .ts", () => {
|
|
@@ -788,7 +743,6 @@ describe("generate-i18n-types", () => {
|
|
|
788
743
|
expect(result.status).toBe(0);
|
|
789
744
|
|
|
790
745
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
791
|
-
expect(factory).not.toContain("from './dictionary.generated.ts'");
|
|
792
746
|
expect(factory).toContain("from './i18n-types.generated.ts'");
|
|
793
747
|
});
|
|
794
748
|
|
|
@@ -817,7 +771,6 @@ describe("generate-i18n-types", () => {
|
|
|
817
771
|
expect(result.status).toBe(0);
|
|
818
772
|
|
|
819
773
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
820
|
-
expect(factory).not.toContain("from './dictionary.generated.js'");
|
|
821
774
|
expect(factory).toContain("from './i18n-types.generated.js'");
|
|
822
775
|
});
|
|
823
776
|
|
|
@@ -896,7 +849,6 @@ describe("generate-i18n-types", () => {
|
|
|
896
849
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
897
850
|
expect(dictionary).toContain("from './translations/translations.json'");
|
|
898
851
|
expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
|
|
899
|
-
expect(types).not.toContain("typeof import");
|
|
900
852
|
});
|
|
901
853
|
|
|
902
854
|
it("supports mixed json and yaml namespaces", () => {
|
|
@@ -943,7 +895,6 @@ describe("generate-i18n-types", () => {
|
|
|
943
895
|
expect(dictionary).toContain("from './translations/billing.json'");
|
|
944
896
|
expect(types).toContain("login_button: Partial<Record<AppLocale, string>>;");
|
|
945
897
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
946
|
-
expect(types).not.toContain("typeof import");
|
|
947
898
|
});
|
|
948
899
|
|
|
949
900
|
it("generates lazy loaders that import compiled json for yaml namespaces", () => {
|
|
@@ -989,7 +940,6 @@ describe("generate-i18n-types", () => {
|
|
|
989
940
|
"utf8"
|
|
990
941
|
);
|
|
991
942
|
expect(loaders).toContain("import('./translations/billing.json')");
|
|
992
|
-
expect(loaders).not.toContain("billing.yaml");
|
|
993
943
|
});
|
|
994
944
|
|
|
995
945
|
it("fails when dictionary extension is unsupported", () => {
|
|
@@ -1085,11 +1035,9 @@ describe("generate-i18n-types", () => {
|
|
|
1085
1035
|
);
|
|
1086
1036
|
|
|
1087
1037
|
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1088
|
-
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1089
1038
|
|
|
1090
1039
|
expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
|
|
1091
1040
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1092
|
-
expect(types).not.toContain("typeof import");
|
|
1093
1041
|
expect(types).toContain("export type AppLocale = 'en' | 'it'");
|
|
1094
1042
|
expect(types).toContain("invoice_summary: { count: number }");
|
|
1095
1043
|
|
|
@@ -1099,7 +1047,7 @@ describe("generate-i18n-types", () => {
|
|
|
1099
1047
|
expect(loaders).toContain(
|
|
1100
1048
|
"[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;"
|
|
1101
1049
|
);
|
|
1102
|
-
expect(loaders).toContain("export
|
|
1050
|
+
expect(loaders).toContain("export const defaultLazyNamespaces");
|
|
1103
1051
|
expect(loaders).toContain("default: (locale) => {");
|
|
1104
1052
|
expect(loaders).toContain("billing: (locale) => {");
|
|
1105
1053
|
expect(loaders).toContain('case "it":');
|
|
@@ -1111,7 +1059,6 @@ describe("generate-i18n-types", () => {
|
|
|
1111
1059
|
"return import('./translations/billing.en.json').then((m) => m.default);"
|
|
1112
1060
|
);
|
|
1113
1061
|
expect(loaders).toContain("user: (locale) => {");
|
|
1114
|
-
expect(loaders).not.toContain("import('./translations/billing.json')");
|
|
1115
1062
|
});
|
|
1116
1063
|
|
|
1117
1064
|
it("omits dictionaryOutput in split mode and removes stale dictionary at the default path", () => {
|
|
@@ -1147,7 +1094,6 @@ describe("generate-i18n-types", () => {
|
|
|
1147
1094
|
const result = runCodegen(tempDir);
|
|
1148
1095
|
expect(result.status).toBe(0);
|
|
1149
1096
|
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1150
|
-
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1151
1097
|
});
|
|
1152
1098
|
|
|
1153
1099
|
it("writes delivery json under deliveryOutput when it differs from typesOutput directory", () => {
|
|
@@ -1206,7 +1152,7 @@ describe("generate-i18n-types", () => {
|
|
|
1206
1152
|
);
|
|
1207
1153
|
|
|
1208
1154
|
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1209
|
-
expect(loaders).toContain("export
|
|
1155
|
+
expect(loaders).toContain("export const defaultLazyNamespaces");
|
|
1210
1156
|
expect(loaders).toContain(
|
|
1211
1157
|
"return import('../../../public/i18n/translations/default.en.json').then((m) => m.default);"
|
|
1212
1158
|
);
|
|
@@ -1265,112 +1211,6 @@ describe("generate-i18n-types", () => {
|
|
|
1265
1211
|
expect(types).toContain("export type AppLocale = 'de-CH' | 'de-DE' | 'en' | 'it'");
|
|
1266
1212
|
});
|
|
1267
1213
|
|
|
1268
|
-
it("splits yaml sources into per-locale json without a canonical intermediate", () => {
|
|
1269
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1270
|
-
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
1271
|
-
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
1272
|
-
|
|
1273
|
-
writeFileSync(
|
|
1274
|
-
join(tempDir, "src/i18n/translations/billing.yaml"),
|
|
1275
|
-
`invoice_summary:
|
|
1276
|
-
en: You have {count} invoices
|
|
1277
|
-
it: Hai {count} fatture
|
|
1278
|
-
`
|
|
1279
|
-
);
|
|
1280
|
-
writeFileSync(
|
|
1281
|
-
join(tempDir, "i18n.codegen.json"),
|
|
1282
|
-
JSON.stringify({
|
|
1283
|
-
namespaces: {
|
|
1284
|
-
billing: "src/i18n/translations/billing.yaml",
|
|
1285
|
-
},
|
|
1286
|
-
delivery: "split-by-locale",
|
|
1287
|
-
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1288
|
-
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
1289
|
-
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
1290
|
-
paramsTypeName: "AppParams",
|
|
1291
|
-
schemaTypeName: "AppSchema",
|
|
1292
|
-
})
|
|
1293
|
-
);
|
|
1294
|
-
|
|
1295
|
-
const result = runCodegen(tempDir);
|
|
1296
|
-
expect(result.status).toBe(0);
|
|
1297
|
-
expect(result.stdout).toContain("billing.en.json");
|
|
1298
|
-
expect(result.stdout).toContain("billing.it.json");
|
|
1299
|
-
|
|
1300
|
-
expect(() =>
|
|
1301
|
-
readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
|
|
1302
|
-
).toThrow();
|
|
1303
|
-
|
|
1304
|
-
const enSplit = JSON.parse(
|
|
1305
|
-
readFileSync(join(tempDir, "src/i18n/generated/translations/billing.en.json"), "utf8")
|
|
1306
|
-
) as Record<string, Record<string, string>>;
|
|
1307
|
-
expect(enSplit.invoice_summary).toEqual({ en: "You have {count} invoices" });
|
|
1308
|
-
|
|
1309
|
-
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1310
|
-
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1311
|
-
expect(types).not.toContain("typeof import");
|
|
1312
|
-
expect(types).toContain("export type LazyNamespace = 'billing';");
|
|
1313
|
-
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1314
|
-
const loaders = readFileSync(
|
|
1315
|
-
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1316
|
-
"utf8"
|
|
1317
|
-
);
|
|
1318
|
-
expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
1319
|
-
});
|
|
1320
|
-
|
|
1321
|
-
it("emits defaultDictionaryFor in single mode with split-by-locale delivery", () => {
|
|
1322
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1323
|
-
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
1324
|
-
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
1325
|
-
|
|
1326
|
-
const sourcePath = join(tempDir, "src/i18n/translations/translations.json");
|
|
1327
|
-
writeFileSync(
|
|
1328
|
-
sourcePath,
|
|
1329
|
-
JSON.stringify({
|
|
1330
|
-
welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
|
|
1331
|
-
})
|
|
1332
|
-
);
|
|
1333
|
-
writeFileSync(
|
|
1334
|
-
join(tempDir, "i18n.codegen.json"),
|
|
1335
|
-
JSON.stringify({
|
|
1336
|
-
dictionary: "src/i18n/translations/translations.json",
|
|
1337
|
-
delivery: "split-by-locale",
|
|
1338
|
-
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1339
|
-
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
1340
|
-
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
1341
|
-
paramsTypeName: "AppParams",
|
|
1342
|
-
schemaTypeName: "AppSchema",
|
|
1343
|
-
})
|
|
1344
|
-
);
|
|
1345
|
-
|
|
1346
|
-
const result = runCodegen(tempDir);
|
|
1347
|
-
expect(result.status).toBe(0);
|
|
1348
|
-
|
|
1349
|
-
const sourceAfter = JSON.parse(readFileSync(sourcePath, "utf8")) as Record<
|
|
1350
|
-
string,
|
|
1351
|
-
Record<string, string>
|
|
1352
|
-
>;
|
|
1353
|
-
expect(sourceAfter.welcome).toEqual({
|
|
1354
|
-
en: "Welcome {name}!",
|
|
1355
|
-
it: "Benvenuto {name}!",
|
|
1356
|
-
});
|
|
1357
|
-
|
|
1358
|
-
const dictionary = readFileSync(
|
|
1359
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1360
|
-
"utf8"
|
|
1361
|
-
);
|
|
1362
|
-
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1363
|
-
expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
|
|
1364
|
-
expect(types).not.toContain("typeof import");
|
|
1365
|
-
expect(dictionary).toContain("import defaultEn from './translations/translations.en.json';");
|
|
1366
|
-
expect(dictionary).toContain("import defaultIt from './translations/translations.it.json';");
|
|
1367
|
-
expect(dictionary).toContain(
|
|
1368
|
-
"export function defaultDictionaryFor(locale: AppLocale): AppSchema"
|
|
1369
|
-
);
|
|
1370
|
-
expect(dictionary).toContain("return defaultByLocale[locale];");
|
|
1371
|
-
expect(dictionary).not.toContain("export const defaultDictionary");
|
|
1372
|
-
});
|
|
1373
|
-
|
|
1374
1214
|
it("keeps canonical output when delivery is explicitly set to canonical", () => {
|
|
1375
1215
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1376
1216
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
@@ -1408,12 +1248,8 @@ describe("generate-i18n-types", () => {
|
|
|
1408
1248
|
const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
|
|
1409
1249
|
|
|
1410
1250
|
expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
|
|
1411
|
-
expect(dictionary).not.toContain("defaultDictionaryFor");
|
|
1412
1251
|
expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
|
|
1413
1252
|
expect(loaders).toContain("import('./translations/billing.json')");
|
|
1414
|
-
expect(loaders).not.toContain("billing: {");
|
|
1415
|
-
expect(result.stdout).not.toContain(".en.json");
|
|
1416
|
-
expect(result.stdout).not.toContain(".it.json");
|
|
1417
1253
|
});
|
|
1418
1254
|
|
|
1419
1255
|
it("generates custom delivery with per-area json files", () => {
|
|
@@ -1529,11 +1365,9 @@ describe("generate-i18n-types", () => {
|
|
|
1529
1365
|
expect(types).toContain('"it": "en-US"');
|
|
1530
1366
|
expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
|
|
1531
1367
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1532
|
-
expect(types).not.toContain("typeof import");
|
|
1533
1368
|
expect(types).toContain("invoice_summary: { count: number }");
|
|
1534
1369
|
|
|
1535
1370
|
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1536
|
-
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1537
1371
|
|
|
1538
1372
|
expect(types).toContain("export type LoadOnInitNamespace = never;");
|
|
1539
1373
|
expect(types).toContain("export type LazyNamespace = 'default' | 'billing';");
|
|
@@ -1541,7 +1375,7 @@ describe("generate-i18n-types", () => {
|
|
|
1541
1375
|
expect(loaders).toContain(
|
|
1542
1376
|
"[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
|
|
1543
1377
|
);
|
|
1544
|
-
expect(loaders).toContain("export
|
|
1378
|
+
expect(loaders).toContain("export const defaultLazyNamespaces");
|
|
1545
1379
|
expect(loaders).toContain("default: (area) => {");
|
|
1546
1380
|
expect(loaders).toContain("billing: (area) => {");
|
|
1547
1381
|
expect(loaders).toContain('case "eu":');
|
|
@@ -1552,7 +1386,6 @@ describe("generate-i18n-types", () => {
|
|
|
1552
1386
|
expect(loaders).toContain(
|
|
1553
1387
|
"return import('./translations/billing.us.json').then((m) => m.default);"
|
|
1554
1388
|
);
|
|
1555
|
-
expect(loaders).not.toContain("import('./translations/billing.json')");
|
|
1556
1389
|
|
|
1557
1390
|
expect(factory).toContain("export function projectDictionaryForDeliveryArea(");
|
|
1558
1391
|
expect(factory).toContain("export function projectNamespaceForDeliveryArea(");
|
|
@@ -1604,134 +1437,6 @@ describe("generate-i18n-types", () => {
|
|
|
1604
1437
|
expect(result.stderr).toContain("en-US");
|
|
1605
1438
|
});
|
|
1606
1439
|
|
|
1607
|
-
it("splits yaml sources into per-area json without a canonical intermediate", () => {
|
|
1608
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1609
|
-
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
1610
|
-
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
1611
|
-
|
|
1612
|
-
writeFileSync(
|
|
1613
|
-
join(tempDir, "src/i18n/translations/billing.yaml"),
|
|
1614
|
-
`invoice_summary:
|
|
1615
|
-
en-US: You have {count} invoices
|
|
1616
|
-
it: Hai {count} fatture
|
|
1617
|
-
`
|
|
1618
|
-
);
|
|
1619
|
-
writeFileSync(
|
|
1620
|
-
join(tempDir, "i18n.codegen.json"),
|
|
1621
|
-
JSON.stringify({
|
|
1622
|
-
namespaces: {
|
|
1623
|
-
billing: "src/i18n/translations/billing.yaml",
|
|
1624
|
-
},
|
|
1625
|
-
delivery: "custom",
|
|
1626
|
-
deliveryArtifacts: {
|
|
1627
|
-
eu: ["it"],
|
|
1628
|
-
us: ["en-US"],
|
|
1629
|
-
},
|
|
1630
|
-
localeFallback: {
|
|
1631
|
-
"en-US": null,
|
|
1632
|
-
it: "en-US",
|
|
1633
|
-
},
|
|
1634
|
-
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1635
|
-
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
1636
|
-
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
1637
|
-
paramsTypeName: "AppParams",
|
|
1638
|
-
schemaTypeName: "AppSchema",
|
|
1639
|
-
})
|
|
1640
|
-
);
|
|
1641
|
-
|
|
1642
|
-
const result = runCodegen(tempDir);
|
|
1643
|
-
expect(result.status).toBe(0);
|
|
1644
|
-
expect(result.stdout).toContain("billing.eu.json");
|
|
1645
|
-
expect(result.stdout).toContain("billing.us.json");
|
|
1646
|
-
|
|
1647
|
-
expect(() =>
|
|
1648
|
-
readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
|
|
1649
|
-
).toThrow();
|
|
1650
|
-
|
|
1651
|
-
const euSplit = JSON.parse(
|
|
1652
|
-
readFileSync(join(tempDir, "src/i18n/generated/translations/billing.eu.json"), "utf8")
|
|
1653
|
-
) as Record<string, Record<string, string>>;
|
|
1654
|
-
expect(euSplit.invoice_summary).toEqual({ it: "Hai {count} fatture" });
|
|
1655
|
-
|
|
1656
|
-
const usSplit = JSON.parse(
|
|
1657
|
-
readFileSync(join(tempDir, "src/i18n/generated/translations/billing.us.json"), "utf8")
|
|
1658
|
-
) as Record<string, Record<string, string>>;
|
|
1659
|
-
expect(usSplit.invoice_summary).toEqual({ "en-US": "You have {count} invoices" });
|
|
1660
|
-
|
|
1661
|
-
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1662
|
-
expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
|
|
1663
|
-
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1664
|
-
expect(types).not.toContain("typeof import");
|
|
1665
|
-
expect(types).toContain("export type LazyNamespace = 'billing';");
|
|
1666
|
-
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1667
|
-
const loaders = readFileSync(
|
|
1668
|
-
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1669
|
-
"utf8"
|
|
1670
|
-
);
|
|
1671
|
-
expect(loaders).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
1672
|
-
});
|
|
1673
|
-
|
|
1674
|
-
it("emits defaultDictionaryFor in single mode with custom delivery", () => {
|
|
1675
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1676
|
-
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
1677
|
-
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
1678
|
-
|
|
1679
|
-
const sourcePath = join(tempDir, "src/i18n/translations/translations.json");
|
|
1680
|
-
writeFileSync(
|
|
1681
|
-
sourcePath,
|
|
1682
|
-
JSON.stringify({
|
|
1683
|
-
some_key: { it: "Ciao", "en-US": "Hello" },
|
|
1684
|
-
})
|
|
1685
|
-
);
|
|
1686
|
-
writeFileSync(
|
|
1687
|
-
join(tempDir, "i18n.codegen.json"),
|
|
1688
|
-
JSON.stringify({
|
|
1689
|
-
dictionary: "src/i18n/translations/translations.json",
|
|
1690
|
-
delivery: "custom",
|
|
1691
|
-
deliveryArtifacts: {
|
|
1692
|
-
eu: ["it"],
|
|
1693
|
-
us: ["en-US"],
|
|
1694
|
-
},
|
|
1695
|
-
localeFallback: {
|
|
1696
|
-
"en-US": null,
|
|
1697
|
-
it: "en-US",
|
|
1698
|
-
},
|
|
1699
|
-
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1700
|
-
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
1701
|
-
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
1702
|
-
paramsTypeName: "AppParams",
|
|
1703
|
-
schemaTypeName: "AppSchema",
|
|
1704
|
-
})
|
|
1705
|
-
);
|
|
1706
|
-
|
|
1707
|
-
const result = runCodegen(tempDir);
|
|
1708
|
-
expect(result.status).toBe(0);
|
|
1709
|
-
|
|
1710
|
-
const sourceAfter = JSON.parse(readFileSync(sourcePath, "utf8")) as Record<
|
|
1711
|
-
string,
|
|
1712
|
-
Record<string, string>
|
|
1713
|
-
>;
|
|
1714
|
-
expect(sourceAfter.some_key).toEqual({
|
|
1715
|
-
it: "Ciao",
|
|
1716
|
-
"en-US": "Hello",
|
|
1717
|
-
});
|
|
1718
|
-
|
|
1719
|
-
const dictionary = readFileSync(
|
|
1720
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1721
|
-
"utf8"
|
|
1722
|
-
);
|
|
1723
|
-
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1724
|
-
expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
|
|
1725
|
-
expect(types).not.toContain("typeof import");
|
|
1726
|
-
expect(dictionary).toContain("import defaultEu from './translations/translations.eu.json';");
|
|
1727
|
-
expect(dictionary).toContain("import defaultUs from './translations/translations.us.json';");
|
|
1728
|
-
expect(dictionary).toContain(
|
|
1729
|
-
"export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
|
|
1730
|
-
);
|
|
1731
|
-
expect(dictionary).toContain("return defaultByArea[area];");
|
|
1732
|
-
expect(dictionary).not.toContain("export const defaultDictionary");
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
1440
|
it("fails when yaml dictionary has invalid shape", () => {
|
|
1736
1441
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1737
1442
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
@@ -16,7 +16,7 @@ import { formatInstanceFile } from "./emit/instance-file.js";
|
|
|
16
16
|
import { formatNamespaceLoadersFile } from "./emit/namespace-loaders-file.js";
|
|
17
17
|
import { formatTypesFile } from "./emit/types-file.js";
|
|
18
18
|
import { analyzeDictionaries } from "./icu-analysis.js";
|
|
19
|
-
import { getDeliveryArtifactsIssues } from "./delivery-artifacts.js";
|
|
19
|
+
import { getDeliveryArtifactsIssues, getDeliveryArtifactsTypeName } from "./delivery-artifacts.js";
|
|
20
20
|
import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
|
|
21
21
|
import { enrichLocaleFallback } from "./locale-policy.js";
|
|
22
22
|
import { reportCodegenIssues, resolveImportExtension, toModuleBasename } from "./paths.js";
|
|
@@ -176,6 +176,15 @@ function main() {
|
|
|
176
176
|
isSingle,
|
|
177
177
|
hasLazy,
|
|
178
178
|
typesOutputPath,
|
|
179
|
+
...(hasLazy
|
|
180
|
+
? {
|
|
181
|
+
namespaceLoadersOutputPath: path.resolve(
|
|
182
|
+
projectRoot,
|
|
183
|
+
config.namespaceLoadersOutput ??
|
|
184
|
+
path.join(path.dirname(config.instanceOutput), "namespace-loaders.generated.ts")
|
|
185
|
+
),
|
|
186
|
+
}
|
|
187
|
+
: {}),
|
|
179
188
|
paramsTypeName,
|
|
180
189
|
schemaTypeName,
|
|
181
190
|
localeTypeName,
|
|
@@ -186,6 +195,12 @@ function main() {
|
|
|
186
195
|
namespaceNames: entries.map((entry) => entry.namespace),
|
|
187
196
|
importExtension,
|
|
188
197
|
delivery,
|
|
198
|
+
...(delivery === "custom" && deliveryAreaTypeName
|
|
199
|
+
? {
|
|
200
|
+
deliveryAreaTypeName,
|
|
201
|
+
deliveryArtifactsTypeName: getDeliveryArtifactsTypeName(deliveryAreaTypeName),
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
189
204
|
});
|
|
190
205
|
|
|
191
206
|
writeFileIfChanged(typesOutputPath, typesContent);
|
|
@@ -95,7 +95,7 @@ function writeTscProject(tempDir: string) {
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
describe("codegen
|
|
98
|
+
describe("codegen projectNamespaceLocales + load + scope.set", () => {
|
|
99
99
|
describe("generated instance output", () => {
|
|
100
100
|
let tempDir: string;
|
|
101
101
|
|
|
@@ -111,53 +111,46 @@ describe("codegen projectDictionaryLocales + setNamespace", () => {
|
|
|
111
111
|
|
|
112
112
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
113
113
|
expect(factory).toContain('dictionary: AppSchema["billing"]');
|
|
114
|
+
expect(factory).toContain("export function projectNamespaceLocales(");
|
|
114
115
|
expect(factory).toContain(
|
|
115
116
|
"projectDictionaryLocalesCore(dictionary, locales, LOCALE_FALLBACK)"
|
|
116
117
|
);
|
|
117
118
|
});
|
|
118
119
|
|
|
119
|
-
it("passes tsc when hydrating with projectNamespaceLocales
|
|
120
|
+
it("passes tsc when hydrating with projectNamespaceLocales via load() and scope.set()", () => {
|
|
120
121
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
|
|
121
122
|
setupMultiCodegenFixture(tempDir);
|
|
122
123
|
|
|
123
124
|
writeFileSync(
|
|
124
125
|
join(tempDir, "src/hydrate-billing.ts"),
|
|
125
126
|
[
|
|
126
|
-
`import {
|
|
127
|
+
`import { createI18nMultiBuilder, IcuTranslationProviderMulti } from "@xndrjs/i18n";`,
|
|
128
|
+
`import { projectNamespaceLocales } from "./i18n/instance.generated.js";`,
|
|
127
129
|
`import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
|
|
130
|
+
`import type { AppSchema, AppParams } from "./i18n/i18n-types.generated.js";`,
|
|
128
131
|
`import billingDictionary from "./i18n/translations/billing.json";`,
|
|
129
132
|
``,
|
|
130
|
-
`
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
it("passes tsc when replacing the full schema with projectDictionaryLocales before setAll", () => {
|
|
142
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
|
|
143
|
-
setupMultiCodegenFixture(tempDir);
|
|
144
|
-
|
|
145
|
-
writeFileSync(
|
|
146
|
-
join(tempDir, "src/hydrate-all.ts"),
|
|
147
|
-
[
|
|
148
|
-
`import { createI18n, projectDictionaryLocales } from "./i18n/instance.generated.js";`,
|
|
149
|
-
`import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
|
|
150
|
-
`import type { AppSchema } from "./i18n/i18n-types.generated.js";`,
|
|
151
|
-
`import billingDictionary from "./i18n/translations/billing.json";`,
|
|
152
|
-
`import defaultNs from "./i18n/translations/default.json";`,
|
|
133
|
+
`async function hydrateBilling() {`,
|
|
134
|
+
` const billingEn = projectNamespaceLocales(billingDictionary, ["en"]);`,
|
|
135
|
+
` const engine = new IcuTranslationProviderMulti<AppSchema, AppParams>(defaultDictionary);`,
|
|
136
|
+
` const view = await createI18nMultiBuilder(engine, {`,
|
|
137
|
+
` namespaceLoaders: {`,
|
|
138
|
+
` billing: async () => billingEn,`,
|
|
139
|
+
` },`,
|
|
140
|
+
` })`,
|
|
141
|
+
` .withNamespaces(["billing"])`,
|
|
142
|
+
` .withLocale("en")`,
|
|
143
|
+
` .load();`,
|
|
153
144
|
``,
|
|
154
|
-
`
|
|
155
|
-
`
|
|
156
|
-
`
|
|
157
|
-
`}
|
|
145
|
+
` view.set(`,
|
|
146
|
+
` "billing",`,
|
|
147
|
+
` "invoice_summary",`,
|
|
148
|
+
` "You have {count, plural, one {1 invoice} other {{count} invoices}}"`,
|
|
149
|
+
` );`,
|
|
150
|
+
` return view.t("billing", "invoice_summary", { count: 1 });`,
|
|
151
|
+
`}`,
|
|
158
152
|
``,
|
|
159
|
-
`
|
|
160
|
-
`i18n.setAll(projectDictionaryLocales(fullDictionary, ["en"]));`,
|
|
153
|
+
`export { hydrateBilling };`,
|
|
161
154
|
].join("\n")
|
|
162
155
|
);
|
|
163
156
|
|