@xndrjs/i18n 0.5.0 → 0.6.0-alpha.1
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 +67 -35
- package/dist/codegen/index.d.ts +5 -3
- package/dist/codegen/index.js +15 -2
- package/dist/codegen/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen/codegen-config-schema.ts +27 -1
- package/src/codegen/config.test.ts +79 -1
- package/src/codegen/config.ts +10 -1
- package/src/codegen/delivery-artifacts.test.ts +24 -0
- package/src/codegen/delivery-artifacts.ts +29 -0
- package/src/codegen/emit/dictionary-file.test.ts +32 -0
- package/src/codegen/emit/dictionary-file.ts +6 -2
- package/src/codegen/emit/dictionary-schema-file.ts +2 -2
- package/src/codegen/emit/namespace-loaders-file.test.ts +53 -1
- package/src/codegen/emit/namespace-loaders-file.ts +95 -7
- package/src/codegen/emit/types-file.test.ts +5 -0
- package/src/codegen/emit/types-file.ts +19 -6
- package/src/codegen/generate-i18n-types.test.ts +82 -56
- package/src/codegen/generate-i18n-types.ts +15 -4
- package/src/codegen-config/index.ts +1 -0
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
2
10
|
import { tmpdir } from "node:os";
|
|
3
11
|
import { join } from "node:path";
|
|
4
12
|
import { spawnSync } from "node:child_process";
|
|
@@ -168,6 +176,7 @@ describe("generate-i18n-types", () => {
|
|
|
168
176
|
expect(schema).toContain("mode: 'multi' as const");
|
|
169
177
|
expect(schema).toContain("validateExternalDictionary");
|
|
170
178
|
expect(schema).toContain("validateExternalNamespace");
|
|
179
|
+
expect(schema).toContain("validateExternalNamespaceCore");
|
|
171
180
|
expect(schema).toContain('"name": "string"');
|
|
172
181
|
});
|
|
173
182
|
|
|
@@ -228,6 +237,7 @@ describe("generate-i18n-types", () => {
|
|
|
228
237
|
expect(schema).toContain("mode: 'single' as const");
|
|
229
238
|
expect(schema).toContain("validateExternalDictionary");
|
|
230
239
|
expect(schema).not.toContain("validateExternalNamespaceImpl");
|
|
240
|
+
expect(schema).not.toContain("validateExternalNamespaceCore");
|
|
231
241
|
});
|
|
232
242
|
|
|
233
243
|
it("generates flat types for single-file config", () => {
|
|
@@ -1038,7 +1048,6 @@ describe("generate-i18n-types", () => {
|
|
|
1038
1048
|
user: "src/i18n/translations/user.json",
|
|
1039
1049
|
billing: "src/i18n/translations/billing.json",
|
|
1040
1050
|
},
|
|
1041
|
-
loadOnInit: ["default"],
|
|
1042
1051
|
delivery: "split-by-locale",
|
|
1043
1052
|
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1044
1053
|
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
@@ -1070,34 +1079,28 @@ describe("generate-i18n-types", () => {
|
|
|
1070
1079
|
).toThrow();
|
|
1071
1080
|
|
|
1072
1081
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1073
|
-
const dictionary = readFileSync(
|
|
1074
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1075
|
-
"utf8"
|
|
1076
|
-
);
|
|
1077
1082
|
const loaders = readFileSync(
|
|
1078
1083
|
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1079
1084
|
"utf8"
|
|
1080
1085
|
);
|
|
1081
1086
|
|
|
1087
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1088
|
+
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1089
|
+
|
|
1082
1090
|
expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
|
|
1083
1091
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1084
1092
|
expect(types).not.toContain("typeof import");
|
|
1085
1093
|
expect(types).toContain("export type AppLocale = 'en' | 'it'");
|
|
1086
1094
|
expect(types).toContain("invoice_summary: { count: number }");
|
|
1087
1095
|
|
|
1088
|
-
expect(
|
|
1089
|
-
expect(
|
|
1090
|
-
expect(dictionary).toContain(
|
|
1091
|
-
"export function defaultDictionaryFor(locale: AppLocale): InitialSchema"
|
|
1092
|
-
);
|
|
1093
|
-
expect(dictionary).toContain("default: defaultByLocale[locale],");
|
|
1094
|
-
expect(dictionary).not.toContain("as unknown as");
|
|
1095
|
-
expect(dictionary).not.toContain("export const defaultDictionary");
|
|
1096
|
-
expect(dictionary).not.toContain("billingEn");
|
|
1096
|
+
expect(types).toContain("export type LoadOnInitNamespace = never;");
|
|
1097
|
+
expect(types).toContain("export type LazyNamespace = 'default' | 'user' | 'billing';");
|
|
1097
1098
|
|
|
1098
1099
|
expect(loaders).toContain(
|
|
1099
1100
|
"[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;"
|
|
1100
1101
|
);
|
|
1102
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
1103
|
+
expect(loaders).toContain("default: (locale) => {");
|
|
1101
1104
|
expect(loaders).toContain("billing: (locale) => {");
|
|
1102
1105
|
expect(loaders).toContain('case "it":');
|
|
1103
1106
|
expect(loaders).toContain(
|
|
@@ -1111,6 +1114,42 @@ describe("generate-i18n-types", () => {
|
|
|
1111
1114
|
expect(loaders).not.toContain("import('./translations/billing.json')");
|
|
1112
1115
|
});
|
|
1113
1116
|
|
|
1117
|
+
it("omits dictionaryOutput in split mode and removes stale dictionary at the default path", () => {
|
|
1118
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1119
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
1120
|
+
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
1121
|
+
|
|
1122
|
+
writeFileSync(
|
|
1123
|
+
join(tempDir, "src/i18n/translations/default.json"),
|
|
1124
|
+
JSON.stringify({
|
|
1125
|
+
welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
|
|
1126
|
+
})
|
|
1127
|
+
);
|
|
1128
|
+
writeFileSync(
|
|
1129
|
+
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1130
|
+
"// stale dictionary manifest\n"
|
|
1131
|
+
);
|
|
1132
|
+
writeFileSync(
|
|
1133
|
+
join(tempDir, "i18n.codegen.json"),
|
|
1134
|
+
JSON.stringify({
|
|
1135
|
+
namespaces: {
|
|
1136
|
+
default: "src/i18n/translations/default.json",
|
|
1137
|
+
},
|
|
1138
|
+
delivery: "split-by-locale",
|
|
1139
|
+
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1140
|
+
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
1141
|
+
namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
|
|
1142
|
+
paramsTypeName: "AppParams",
|
|
1143
|
+
schemaTypeName: "AppSchema",
|
|
1144
|
+
})
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
const result = runCodegen(tempDir);
|
|
1148
|
+
expect(result.status).toBe(0);
|
|
1149
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1150
|
+
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1114
1153
|
it("writes delivery json under deliveryOutput when it differs from typesOutput directory", () => {
|
|
1115
1154
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1116
1155
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
@@ -1136,7 +1175,6 @@ describe("generate-i18n-types", () => {
|
|
|
1136
1175
|
default: "src/i18n/translations/default.json",
|
|
1137
1176
|
billing: "src/i18n/translations/billing.yaml",
|
|
1138
1177
|
},
|
|
1139
|
-
loadOnInit: ["default"],
|
|
1140
1178
|
delivery: "split-by-locale",
|
|
1141
1179
|
deliveryOutput: "public/i18n",
|
|
1142
1180
|
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
@@ -1162,20 +1200,15 @@ describe("generate-i18n-types", () => {
|
|
|
1162
1200
|
) as Record<string, Record<string, string>>;
|
|
1163
1201
|
expect(billingEn.invoice_summary).toEqual({ en: "You have {count} invoices" });
|
|
1164
1202
|
|
|
1165
|
-
const dictionary = readFileSync(
|
|
1166
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1167
|
-
"utf8"
|
|
1168
|
-
);
|
|
1169
1203
|
const loaders = readFileSync(
|
|
1170
1204
|
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1171
1205
|
"utf8"
|
|
1172
1206
|
);
|
|
1173
1207
|
|
|
1174
|
-
expect(dictionary).
|
|
1175
|
-
|
|
1176
|
-
)
|
|
1177
|
-
|
|
1178
|
-
"import defaultIt from '../../../public/i18n/translations/default.it.json';"
|
|
1208
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1209
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
1210
|
+
expect(loaders).toContain(
|
|
1211
|
+
"return import('../../../public/i18n/translations/default.en.json').then((m) => m.default);"
|
|
1179
1212
|
);
|
|
1180
1213
|
expect(loaders).toContain(
|
|
1181
1214
|
"return import('../../../public/i18n/translations/billing.en.json').then((m) => m.default);"
|
|
@@ -1274,17 +1307,15 @@ describe("generate-i18n-types", () => {
|
|
|
1274
1307
|
expect(enSplit.invoice_summary).toEqual({ en: "You have {count} invoices" });
|
|
1275
1308
|
|
|
1276
1309
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1277
|
-
const dictionary = readFileSync(
|
|
1278
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1279
|
-
"utf8"
|
|
1280
|
-
);
|
|
1281
1310
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1282
1311
|
expect(types).not.toContain("typeof import");
|
|
1283
|
-
expect(
|
|
1284
|
-
expect(dictionary).
|
|
1285
|
-
|
|
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"
|
|
1286
1317
|
);
|
|
1287
|
-
expect(
|
|
1318
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
1288
1319
|
});
|
|
1289
1320
|
|
|
1290
1321
|
it("emits defaultDictionaryFor in single mode with split-by-locale delivery", () => {
|
|
@@ -1413,7 +1444,6 @@ describe("generate-i18n-types", () => {
|
|
|
1413
1444
|
default: "src/i18n/translations/default.json",
|
|
1414
1445
|
billing: "src/i18n/translations/billing.json",
|
|
1415
1446
|
},
|
|
1416
|
-
loadOnInit: ["default"],
|
|
1417
1447
|
delivery: "custom",
|
|
1418
1448
|
deliveryArtifacts: {
|
|
1419
1449
|
eu: ["it", "fr"],
|
|
@@ -1461,10 +1491,6 @@ describe("generate-i18n-types", () => {
|
|
|
1461
1491
|
).toThrow();
|
|
1462
1492
|
|
|
1463
1493
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1464
|
-
const dictionary = readFileSync(
|
|
1465
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1466
|
-
"utf8"
|
|
1467
|
-
);
|
|
1468
1494
|
const loaders = readFileSync(
|
|
1469
1495
|
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1470
1496
|
"utf8"
|
|
@@ -1488,6 +1514,11 @@ describe("generate-i18n-types", () => {
|
|
|
1488
1514
|
).toThrow();
|
|
1489
1515
|
|
|
1490
1516
|
expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
|
|
1517
|
+
expect(types).toContain("export const DELIVERY_ARTIFACTS = {");
|
|
1518
|
+
expect(types).toContain('"eu": ["fr", "it"] as const');
|
|
1519
|
+
expect(types).toContain('"us": ["en-US"] as const');
|
|
1520
|
+
expect(types).toContain("} as const satisfies Record<AppDeliveryArea, readonly AppLocale[]>;");
|
|
1521
|
+
expect(types).toContain("export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;");
|
|
1491
1522
|
expect(types).toContain("export const LOCALE_DELIVERY_AREA = {");
|
|
1492
1523
|
expect(types).toContain('"it": "eu"');
|
|
1493
1524
|
expect(types).toContain('"fr": "eu"');
|
|
@@ -1501,19 +1532,17 @@ describe("generate-i18n-types", () => {
|
|
|
1501
1532
|
expect(types).not.toContain("typeof import");
|
|
1502
1533
|
expect(types).toContain("invoice_summary: { count: number }");
|
|
1503
1534
|
|
|
1504
|
-
expect(
|
|
1505
|
-
expect(
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
);
|
|
1509
|
-
expect(dictionary).toContain("default: defaultByArea[area],");
|
|
1510
|
-
expect(dictionary).not.toContain("as unknown as");
|
|
1511
|
-
expect(dictionary).not.toContain("export const defaultDictionary");
|
|
1512
|
-
expect(dictionary).not.toContain("billingEu");
|
|
1535
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1536
|
+
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1537
|
+
|
|
1538
|
+
expect(types).toContain("export type LoadOnInitNamespace = never;");
|
|
1539
|
+
expect(types).toContain("export type LazyNamespace = 'default' | 'billing';");
|
|
1513
1540
|
|
|
1514
1541
|
expect(loaders).toContain(
|
|
1515
1542
|
"[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
|
|
1516
1543
|
);
|
|
1544
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
1545
|
+
expect(loaders).toContain("default: (area) => {");
|
|
1517
1546
|
expect(loaders).toContain("billing: (area) => {");
|
|
1518
1547
|
expect(loaders).toContain('case "eu":');
|
|
1519
1548
|
expect(loaders).toContain('case "us":');
|
|
@@ -1630,19 +1659,16 @@ describe("generate-i18n-types", () => {
|
|
|
1630
1659
|
expect(usSplit.invoice_summary).toEqual({ "en-US": "You have {count} invoices" });
|
|
1631
1660
|
|
|
1632
1661
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1633
|
-
const dictionary = readFileSync(
|
|
1634
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1635
|
-
"utf8"
|
|
1636
|
-
);
|
|
1637
1662
|
expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
|
|
1638
1663
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1639
1664
|
expect(types).not.toContain("typeof import");
|
|
1640
|
-
expect(
|
|
1641
|
-
expect(
|
|
1642
|
-
|
|
1643
|
-
|
|
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"
|
|
1644
1670
|
);
|
|
1645
|
-
expect(
|
|
1671
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
1646
1672
|
});
|
|
1647
1673
|
|
|
1648
1674
|
it("emits defaultDictionaryFor in single mode with custom delivery", () => {
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
loadConfig,
|
|
5
5
|
resolveDeliveryOutputDir,
|
|
6
|
+
resolveDictionaryOutputPath,
|
|
6
7
|
resolveLoadOnInit,
|
|
7
8
|
resolveNamespaces,
|
|
8
9
|
} from "./config.js";
|
|
@@ -98,7 +99,7 @@ function main() {
|
|
|
98
99
|
const { loadOnInitSet, lazyEntries, hasLazy } = resolveLoadOnInit(config, entries, isSingle);
|
|
99
100
|
|
|
100
101
|
const typesOutputPath = path.resolve(projectRoot, config.typesOutput);
|
|
101
|
-
const dictionaryOutputPath = path.resolve(projectRoot, config
|
|
102
|
+
const dictionaryOutputPath = path.resolve(projectRoot, resolveDictionaryOutputPath(config));
|
|
102
103
|
const instanceOutputPath = path.resolve(projectRoot, config.instanceOutput);
|
|
103
104
|
const typesModule = toModuleBasename(typesOutputPath);
|
|
104
105
|
|
|
@@ -188,15 +189,21 @@ function main() {
|
|
|
188
189
|
});
|
|
189
190
|
|
|
190
191
|
writeFileIfChanged(typesOutputPath, typesContent);
|
|
191
|
-
writeFileIfChanged(dictionaryOutputPath, dictionaryContent);
|
|
192
|
-
writeFileIfChanged(instanceOutputPath, instanceContent);
|
|
193
192
|
|
|
194
193
|
const generatedFiles = [
|
|
195
194
|
path.relative(projectRoot, typesOutputPath),
|
|
196
|
-
path.relative(projectRoot, dictionaryOutputPath),
|
|
197
195
|
path.relative(projectRoot, instanceOutputPath),
|
|
198
196
|
];
|
|
199
197
|
|
|
198
|
+
if (dictionaryContent !== null) {
|
|
199
|
+
writeFileIfChanged(dictionaryOutputPath, dictionaryContent);
|
|
200
|
+
generatedFiles.splice(1, 0, path.relative(projectRoot, dictionaryOutputPath));
|
|
201
|
+
} else if (fs.existsSync(dictionaryOutputPath)) {
|
|
202
|
+
fs.unlinkSync(dictionaryOutputPath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
writeFileIfChanged(instanceOutputPath, instanceContent);
|
|
206
|
+
|
|
200
207
|
if (config.dictionarySchemaOutput) {
|
|
201
208
|
const dictionarySchemaOutputPath = path.resolve(projectRoot, config.dictionarySchemaOutput);
|
|
202
209
|
const dictionarySpecBlock = formatDictionarySpecBlock(isSingle, entries, argsSpecByNamespace);
|
|
@@ -226,10 +233,14 @@ function main() {
|
|
|
226
233
|
loadersOutputPath: namespaceLoadersOutputPath,
|
|
227
234
|
lazyEntries: lazyEntriesWithPaths,
|
|
228
235
|
schemaTypeName,
|
|
236
|
+
paramsTypeName,
|
|
229
237
|
localeTypeName,
|
|
238
|
+
localeFallbackConstName,
|
|
239
|
+
hasLocaleFallback: Boolean(config.localeFallback),
|
|
230
240
|
typesModule,
|
|
231
241
|
importExtension,
|
|
232
242
|
projectRoot,
|
|
243
|
+
isSingle,
|
|
233
244
|
delivery,
|
|
234
245
|
splitPathsByNamespace,
|
|
235
246
|
...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
|