@xndrjs/i18n 0.3.3 → 0.5.0-alpha.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.
Files changed (61) hide show
  1. package/README.md +193 -44
  2. package/dist/codegen/index.d.ts +85 -0
  3. package/dist/codegen/index.js +181 -0
  4. package/dist/codegen/index.js.map +1 -0
  5. package/dist/index.d.ts +34 -18
  6. package/dist/index.js +174 -145
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +86 -4
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +6 -1
  11. package/src/IcuTranslationProviderMulti.test.ts +48 -1
  12. package/src/IcuTranslationProviderMulti.ts +41 -61
  13. package/src/IcuTranslationProviderSingle.test.ts +67 -0
  14. package/src/IcuTranslationProviderSingle.ts +27 -51
  15. package/src/audit/audit-dictionaries.ts +4 -1
  16. package/src/audit/run-audit.test.ts +35 -1
  17. package/src/audit/run-audit.ts +15 -7
  18. package/src/codegen/codegen-config-schema.ts +68 -1
  19. package/src/codegen/config.test.ts +174 -39
  20. package/src/codegen/config.ts +14 -5
  21. package/src/codegen/constants.ts +8 -0
  22. package/src/codegen/delivery-artifacts.test.ts +142 -0
  23. package/src/codegen/delivery-artifacts.ts +124 -0
  24. package/src/codegen/emit/dictionary-file.test.ts +190 -0
  25. package/src/codegen/emit/dictionary-file.ts +165 -2
  26. package/src/codegen/emit/dictionary-schema-file.ts +5 -0
  27. package/src/codegen/emit/instance-file.ts +119 -38
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
  29. package/src/codegen/emit/namespace-loaders-file.ts +118 -32
  30. package/src/codegen/emit/types-file.test.ts +114 -0
  31. package/src/codegen/emit/types-file.ts +48 -11
  32. package/src/codegen/generate-i18n-types.test.ts +765 -22
  33. package/src/codegen/generate-i18n-types.ts +111 -58
  34. package/src/codegen/icu-analysis.ts +9 -2
  35. package/src/codegen/locale-fallback.ts +19 -10
  36. package/src/codegen/locale-policy.ts +4 -0
  37. package/src/codegen/paths.ts +9 -5
  38. package/src/codegen/project-locales-set-namespace.test.ts +11 -9
  39. package/src/codegen/read-dictionary.test.ts +474 -2
  40. package/src/codegen/read-dictionary.ts +164 -15
  41. package/src/codegen/write-file-if-changed.test.ts +42 -0
  42. package/src/codegen/write-file-if-changed.ts +20 -0
  43. package/src/codegen-config/build-config.ts +34 -0
  44. package/src/codegen-config/codegen-config.test.ts +32 -0
  45. package/src/codegen-config/index.ts +21 -0
  46. package/src/codegen-config/write-config.ts +8 -0
  47. package/src/deep-freeze.test.ts +13 -0
  48. package/src/deep-freeze.ts +5 -0
  49. package/src/format-core.ts +91 -0
  50. package/src/index.ts +8 -5
  51. package/src/project-locales.test.ts +129 -10
  52. package/src/project-locales.ts +90 -3
  53. package/src/setup/setup-i18n.test.ts +1 -1
  54. package/src/setup/setup-i18n.ts +9 -32
  55. package/src/types.ts +19 -4
  56. package/src/validation/normalize.ts +4 -0
  57. package/dist/types-C1CpXVOJ.d.ts +0 -83
  58. package/src/ensure-namespace.integration.test.ts +0 -66
  59. package/src/ensure-namespace.test.ts +0 -125
  60. package/src/ensure-namespace.ts +0 -70
  61. /package/src/{setup → codegen-config}/type-names.ts +0 -0
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+ import { describe, expect, it } from "vitest";
3
+ import { formatTypesFile } from "./types-file.js";
4
+
5
+ const projectRoot = "/project";
6
+ const typesOutputPath = path.join(projectRoot, "src/i18n/generated/i18n-types.generated.ts");
7
+
8
+ describe("formatTypesFile", () => {
9
+ it("emits explicit Partial schema types for multi-namespace", () => {
10
+ const output = formatTypesFile({
11
+ isSingle: false,
12
+ entries: [
13
+ { namespace: "default", filePath: "src/i18n/translations/default.json" },
14
+ { namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
15
+ ],
16
+ projectRoot,
17
+ typesOutputPath,
18
+ paramsTypeName: "AppParams",
19
+ schemaTypeName: "AppSchema",
20
+ localeTypeName: "AppLocale",
21
+ localeFallbackConstName: "LOCALE_FALLBACK",
22
+ localeFallbackTypeName: "AppLocaleFallback",
23
+ paramsByNamespace: {
24
+ default: {
25
+ login_button: "never",
26
+ welcome: "{ name: string }",
27
+ },
28
+ billing: {
29
+ invoice_summary: "{ count: number }",
30
+ },
31
+ },
32
+ requestLocaleUnion: "'en' | 'it'",
33
+ hasLazy: true,
34
+ loadOnInitSet: new Set(["default"]),
35
+ lazyEntries: [{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" }],
36
+ });
37
+
38
+ expect(output).toContain("login_button: Partial<Record<AppLocale, string>>;");
39
+ expect(output).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
40
+ expect(output).not.toContain("typeof import");
41
+ });
42
+
43
+ it("emits explicit Partial schema types for single-file mode", () => {
44
+ const output = formatTypesFile({
45
+ isSingle: true,
46
+ entries: [{ namespace: "translations", filePath: "src/i18n/translations/translations.yaml" }],
47
+ projectRoot,
48
+ typesOutputPath,
49
+ paramsTypeName: "AppParams",
50
+ schemaTypeName: "AppSchema",
51
+ localeTypeName: "AppLocale",
52
+ localeFallbackConstName: "LOCALE_FALLBACK",
53
+ localeFallbackTypeName: "AppLocaleFallback",
54
+ paramsByNamespace: {
55
+ translations: {
56
+ welcome: "{ name: string }",
57
+ },
58
+ },
59
+ requestLocaleUnion: "'en' | 'it'",
60
+ hasLazy: false,
61
+ loadOnInitSet: new Set(),
62
+ lazyEntries: [],
63
+ });
64
+
65
+ expect(output).toContain(
66
+ "export type AppSchema = {\n welcome: Partial<Record<AppLocale, string>>;\n};"
67
+ );
68
+ expect(output).not.toContain("typeof import");
69
+ });
70
+
71
+ it("emits delivery area type for custom delivery", () => {
72
+ const output = formatTypesFile({
73
+ isSingle: false,
74
+ entries: [
75
+ { namespace: "default", filePath: "src/i18n/translations/default.json" },
76
+ { namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
77
+ ],
78
+ projectRoot,
79
+ typesOutputPath,
80
+ paramsTypeName: "AppParams",
81
+ schemaTypeName: "AppSchema",
82
+ localeTypeName: "AppLocale",
83
+ localeFallbackConstName: "LOCALE_FALLBACK",
84
+ localeFallbackTypeName: "AppLocaleFallback",
85
+ localeFallback: {
86
+ "en-US": null,
87
+ it: "en-US",
88
+ fr: null,
89
+ },
90
+ paramsByNamespace: {
91
+ default: { some_key: "never" },
92
+ billing: { invoice_summary: "{ count: number }" },
93
+ },
94
+ requestLocaleUnion: "'en-US' | 'fr' | 'it'",
95
+ deliveryAreaTypeName: "AppDeliveryArea",
96
+ deliveryAreaUnion: "'eu' | 'us'",
97
+ deliveryArtifacts: {
98
+ eu: ["it", "fr"],
99
+ us: ["en-US"],
100
+ },
101
+ hasLazy: true,
102
+ loadOnInitSet: new Set(["default"]),
103
+ lazyEntries: [{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" }],
104
+ });
105
+
106
+ expect(output).toContain("export type AppLocale = 'en-US' | 'fr' | 'it';");
107
+ expect(output).toContain("export type AppDeliveryArea = 'eu' | 'us';");
108
+ expect(output).toContain("export const LOCALE_DELIVERY_AREA = {");
109
+ expect(output).toContain('"it": "eu"');
110
+ expect(output).toContain('"en-US": "us"');
111
+ expect(output).toContain("} as const satisfies Record<AppLocale, AppDeliveryArea>;");
112
+ expect(output).toContain("some_key: Partial<Record<AppLocale, string>>;");
113
+ });
114
+ });
@@ -1,8 +1,12 @@
1
- import path from "node:path";
1
+ import { formatLocaleDeliveryAreaBlock, type DeliveryArtifactsMap } from "../delivery-artifacts.js";
2
2
  import { formatLocaleFallbackBlock } from "../locale-fallback.js";
3
- import { GENERATED_FILE_BANNER, toImportPath } from "../paths.js";
3
+ import { GENERATED_FILE_BANNER } from "../paths.js";
4
4
  import type { NamespaceEntry } from "../types.js";
5
5
 
6
+ /**
7
+ * Emits the generated `i18n-types.generated.ts` module: schema, params, locale/area
8
+ * unions, fallback constants, and lazy-load type aliases.
9
+ */
6
10
  export function formatLazyTypesBlock(
7
11
  loadOnInitSet: Set<string>,
8
12
  lazyEntries: NamespaceEntry[],
@@ -21,6 +25,13 @@ export function formatLazyTypesBlock(
21
25
  );
22
26
  }
23
27
 
28
+ export function formatLocaleTemplateType(localeTypeName: string, hasLocaleUnion: boolean): string {
29
+ if (!hasLocaleUnion) {
30
+ return "Partial<Record<string, string>>";
31
+ }
32
+ return `Partial<Record<${localeTypeName}, string>>`;
33
+ }
34
+
24
35
  export interface TypesFileOptions {
25
36
  isSingle: boolean;
26
37
  entries: NamespaceEntry[];
@@ -34,6 +45,10 @@ export interface TypesFileOptions {
34
45
  localeFallback?: Record<string, string | null> | undefined;
35
46
  paramsByNamespace: Record<string, Record<string, string>>;
36
47
  requestLocaleUnion: string;
48
+ deliveryAreaTypeName?: string;
49
+ deliveryAreaUnion?: string;
50
+ deliveryArtifacts?: DeliveryArtifactsMap;
51
+ localeDeliveryAreaConstName?: string;
37
52
  hasLazy: boolean;
38
53
  loadOnInitSet: Set<string>;
39
54
  lazyEntries: NamespaceEntry[];
@@ -43,8 +58,6 @@ export function formatTypesFile(options: TypesFileOptions): string {
43
58
  const {
44
59
  isSingle,
45
60
  entries,
46
- projectRoot,
47
- typesOutputPath,
48
61
  paramsTypeName,
49
62
  schemaTypeName,
50
63
  localeTypeName,
@@ -53,16 +66,36 @@ export function formatTypesFile(options: TypesFileOptions): string {
53
66
  localeFallback,
54
67
  paramsByNamespace,
55
68
  requestLocaleUnion,
69
+ deliveryAreaTypeName,
70
+ deliveryAreaUnion,
71
+ deliveryArtifacts,
72
+ localeDeliveryAreaConstName = "LOCALE_DELIVERY_AREA",
56
73
  hasLazy,
57
74
  loadOnInitSet,
58
75
  lazyEntries,
59
76
  } = options;
60
77
 
78
+ const hasLocaleUnion = Boolean(requestLocaleUnion);
79
+ const localeTemplateType = formatLocaleTemplateType(localeTypeName, hasLocaleUnion);
80
+
61
81
  const localeBlock = requestLocaleUnion
62
82
  ? `${localeFallback ? formatLocaleFallbackBlock(localeFallback, localeFallbackConstName, localeFallbackTypeName) : ""}` +
63
83
  `export type ${localeTypeName} = ${requestLocaleUnion};\n\n`
64
84
  : "";
65
85
 
86
+ const deliveryAreaBlock =
87
+ deliveryAreaTypeName && deliveryAreaUnion
88
+ ? `export type ${deliveryAreaTypeName} = ${deliveryAreaUnion};\n\n` +
89
+ (deliveryArtifacts
90
+ ? formatLocaleDeliveryAreaBlock(
91
+ deliveryArtifacts,
92
+ localeDeliveryAreaConstName,
93
+ localeTypeName,
94
+ deliveryAreaTypeName
95
+ )
96
+ : "")
97
+ : "";
98
+
66
99
  let paramsBlock: string;
67
100
  let schemaBlock: string;
68
101
 
@@ -75,11 +108,11 @@ export function formatTypesFile(options: TypesFileOptions): string {
75
108
 
76
109
  paramsBlock = `export type ${paramsTypeName} = {\n${paramsLines}\n};`;
77
110
 
78
- const importPath = toImportPath(
79
- typesOutputPath,
80
- path.resolve(projectRoot, entries[0]!.filePath)
81
- );
82
- schemaBlock = `export type ${schemaTypeName} = typeof import('${importPath}.json');`;
111
+ const schemaLines = Object.keys(keyTypes)
112
+ .map((key) => ` ${key}: ${localeTemplateType};`)
113
+ .join("\n");
114
+
115
+ schemaBlock = `export type ${schemaTypeName} = {\n${schemaLines}\n};`;
83
116
  } else {
84
117
  const namespaceBlocks = entries
85
118
  .map((entry) => {
@@ -95,8 +128,11 @@ export function formatTypesFile(options: TypesFileOptions): string {
95
128
 
96
129
  const schemaLines = entries
97
130
  .map((entry) => {
98
- const importPath = toImportPath(typesOutputPath, path.resolve(projectRoot, entry.filePath));
99
- return ` ${entry.namespace}: typeof import('${importPath}.json');`;
131
+ const keyTypes = paramsByNamespace[entry.namespace] ?? {};
132
+ const lines = Object.keys(keyTypes)
133
+ .map((key) => ` ${key}: ${localeTemplateType};`)
134
+ .join("\n");
135
+ return ` ${entry.namespace}: {\n${lines}\n };`;
100
136
  })
101
137
  .join("\n");
102
138
 
@@ -111,6 +147,7 @@ export function formatTypesFile(options: TypesFileOptions): string {
111
147
  `${GENERATED_FILE_BANNER}` +
112
148
  `export const I18N_MODE = '${isSingle ? "single" : "multi"}' as const;\n\n` +
113
149
  `${localeBlock}` +
150
+ `${deliveryAreaBlock}` +
114
151
  `${paramsBlock}\n\n` +
115
152
  `${schemaBlock}\n` +
116
153
  `${lazyTypesBlock}`