@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.
@@ -1,4 +1,12 @@
1
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
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(dictionary).toContain("import defaultEn from './translations/default.en.json';");
1089
- expect(dictionary).toContain("import defaultIt from './translations/default.it.json';");
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).toContain(
1175
- "import defaultEn from '../../../public/i18n/translations/default.en.json';"
1176
- );
1177
- expect(dictionary).toContain(
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(dictionary).toContain("import billingEn from './translations/billing.en.json';");
1284
- expect(dictionary).toContain(
1285
- "export function defaultDictionaryFor(locale: AppLocale): AppSchema"
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(dictionary).toContain("billing: billingByLocale[locale],");
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(dictionary).toContain("import defaultEu from './translations/default.eu.json';");
1505
- expect(dictionary).toContain("import defaultUs from './translations/default.us.json';");
1506
- expect(dictionary).toContain(
1507
- "export function defaultDictionaryFor(area: AppDeliveryArea): InitialSchema"
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(dictionary).toContain("import billingEu from './translations/billing.eu.json';");
1641
- expect(dictionary).toContain("import billingUs from './translations/billing.us.json';");
1642
- expect(dictionary).toContain(
1643
- "export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
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(dictionary).toContain("billing: billingByArea[area],");
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.dictionaryOutput);
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 } : {}),
@@ -7,6 +7,7 @@ export {
7
7
  codegenConfigKeys,
8
8
  DELIVERY_MODES,
9
9
  resolveDeliveryOutputDir,
10
+ resolveDictionaryOutputPath,
10
11
  } from "../codegen/codegen-config-schema.js";
11
12
 
12
13
  export type { DeliveryArtifactsMap } from "../codegen/delivery-artifacts.js";