@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
@@ -1,11 +1,15 @@
1
1
  import { GENERATED_FILE_BANNER, toModuleBasename, toRelativeModuleImport } from "../paths.js";
2
+ import type { DeliveryMode } from "../codegen-config-schema.js";
2
3
  import type { ImportExtension } from "../types.js";
3
4
 
5
+ /**
6
+ * Emits the generated instance module: `createI18n` factory wrapping the ICU provider
7
+ * plus runtime projection helpers (`projectDictionaryLocales`, etc.).
8
+ */
4
9
  export interface InstanceFileOptions {
5
10
  isSingle: boolean;
6
11
  hasLazy: boolean;
7
12
  typesOutputPath: string;
8
- dictionaryOutputPath: string;
9
13
  paramsTypeName: string;
10
14
  schemaTypeName: string;
11
15
  localeTypeName: string;
@@ -15,6 +19,7 @@ export interface InstanceFileOptions {
15
19
  hasLocaleType: boolean;
16
20
  namespaceNames: string[];
17
21
  importExtension: ImportExtension;
22
+ delivery: DeliveryMode;
18
23
  }
19
24
 
20
25
  export function formatInstanceFile(options: InstanceFileOptions): string {
@@ -22,7 +27,6 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
22
27
  isSingle,
23
28
  hasLazy,
24
29
  typesOutputPath,
25
- dictionaryOutputPath,
26
30
  paramsTypeName,
27
31
  schemaTypeName,
28
32
  localeTypeName,
@@ -32,71 +36,115 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
32
36
  hasLocaleType,
33
37
  namespaceNames,
34
38
  importExtension,
39
+ delivery,
35
40
  } = options;
36
41
 
42
+ const emitDeliveryAreaHelpers = delivery === "custom";
37
43
  const providerClass = isSingle ? "IcuTranslationProviderSingle" : "IcuTranslationProviderMulti";
38
44
  const typesModule = toModuleBasename(typesOutputPath);
39
- const dictionaryModule = toModuleBasename(dictionaryOutputPath);
40
45
  const typesImport = toRelativeModuleImport(typesModule, importExtension);
41
- const dictionaryImport = toRelativeModuleImport(dictionaryModule, importExtension);
42
- const initialDictionaryType = hasLazy ? "InitialSchema" : schemaTypeName;
43
- const initialDictionaryImport = hasLazy
46
+ const dictionaryParamType = hasLazy ? "InitialSchema" : schemaTypeName;
47
+ const schemaTypesImport = hasLazy
44
48
  ? `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema } from '${typesImport}';\n`
45
49
  : `import type { ${paramsTypeName}, ${schemaTypeName} } from '${typesImport}';\n`;
46
50
  const providerTypeArgs = hasLocaleFallback
47
51
  ? `${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, typeof ${localeFallbackConstName}`
48
52
  : `${schemaTypeName}, ${paramsTypeName}`;
49
53
  const providerOptions = hasLocaleFallback
50
- ? `, {\n localeFallback: ${localeFallbackConstName},\n }`
51
- : "";
54
+ ? `, {\n localeFallback: ${localeFallbackConstName},\n ...options,\n }`
55
+ : `, options`;
52
56
  const typesImportLine = hasLocaleType
53
57
  ? hasLocaleFallback
54
58
  ? `import { ${localeFallbackConstName}, type ${localeTypeName} } from '${typesImport}';\n`
55
59
  : `import type { ${localeTypeName} } from '${typesImport}';\n`
56
60
  : "";
57
61
  const localesParamType = hasLocaleType ? `readonly ${localeTypeName}[]` : "readonly string[]";
58
- const projectLocalesFallbackArg = hasLocaleFallback ? `, ${localeFallbackConstName}` : "";
59
- const coreImports = isSingle
60
- ? "projectLocales as projectLocalesCore"
61
- : "projectLocales as projectLocalesCore, projectNamespacesLocales as projectNamespacesLocalesCore";
62
- const projectLocalesBlock = isSingle
63
- ? `\n` +
64
- `export function projectLocales(\n` +
65
- ` dictionary: ${schemaTypeName},\n` +
66
- ` locales: ${localesParamType},\n` +
67
- `): ${schemaTypeName} {\n` +
68
- ` return projectLocalesCore(dictionary, locales${projectLocalesFallbackArg});\n` +
69
- `}\n`
70
- : formatMultiProjectLocalesBlock(
62
+ const fallbackArg = hasLocaleFallback ? `, ${localeFallbackConstName}` : "";
63
+ const coreImports = formatCoreImports(isSingle, emitDeliveryAreaHelpers);
64
+ const projectionBlock = isSingle
65
+ ? formatSingleProjectionBlock(
66
+ schemaTypeName,
67
+ localesParamType,
68
+ fallbackArg,
69
+ emitDeliveryAreaHelpers
70
+ )
71
+ : formatMultiProjectionBlock(
71
72
  schemaTypeName,
72
73
  namespaceNames,
73
74
  localesParamType,
74
- projectLocalesFallbackArg
75
+ fallbackArg,
76
+ emitDeliveryAreaHelpers
75
77
  );
76
78
 
77
79
  return (
78
80
  `${GENERATED_FILE_BANNER}` +
79
- `import { ${providerClass}, ${coreImports} } from '@xndrjs/i18n';\n` +
80
- `import { dictionary } from '${dictionaryImport}';\n` +
81
- initialDictionaryImport +
81
+ `import { ${providerClass}, ${coreImports}, type OnMissingTranslation } from '@xndrjs/i18n';\n` +
82
+ schemaTypesImport +
82
83
  typesImportLine +
83
84
  `\n` +
84
85
  `export function ${factoryName}(\n` +
85
- ` initialDictionary: ${initialDictionaryType} = dictionary,\n` +
86
+ ` dictionary: ${dictionaryParamType},\n` +
87
+ ` options?: { onMissing?: OnMissingTranslation },\n` +
86
88
  `) {\n` +
87
- ` return new ${providerClass}<${providerTypeArgs}>(initialDictionary${providerOptions});\n` +
89
+ ` return new ${providerClass}<${providerTypeArgs}>(dictionary${providerOptions});\n` +
88
90
  `}\n` +
89
- projectLocalesBlock
91
+ projectionBlock
90
92
  );
91
93
  }
92
94
 
93
- function formatMultiProjectLocalesBlock(
95
+ function formatCoreImports(isSingle: boolean, emitDeliveryAreaHelpers: boolean): string {
96
+ if (isSingle) {
97
+ const imports = ["projectNamespaceLocalesCore"];
98
+ if (emitDeliveryAreaHelpers) {
99
+ imports.push("projectNamespaceForDeliveryAreaCore");
100
+ }
101
+ return imports.join(", ");
102
+ }
103
+
104
+ const imports = ["projectNamespaceLocalesCore", "projectDictionaryLocalesCore"];
105
+ if (emitDeliveryAreaHelpers) {
106
+ imports.push("projectNamespaceForDeliveryAreaCore", "projectDictionaryForDeliveryAreaCore");
107
+ }
108
+ return imports.join(", ");
109
+ }
110
+
111
+ function formatSingleProjectionBlock(
112
+ schemaTypeName: string,
113
+ localesParamType: string,
114
+ fallbackArg: string,
115
+ emitDeliveryAreaHelpers: boolean
116
+ ): string {
117
+ let block =
118
+ `\n` +
119
+ `export function projectDictionaryLocales(\n` +
120
+ ` dictionary: ${schemaTypeName},\n` +
121
+ ` locales: ${localesParamType},\n` +
122
+ `): ${schemaTypeName} {\n` +
123
+ ` return projectNamespaceLocalesCore(dictionary, locales${fallbackArg});\n` +
124
+ `}\n`;
125
+
126
+ if (emitDeliveryAreaHelpers) {
127
+ block +=
128
+ `\n` +
129
+ `export function projectDictionaryForDeliveryArea(\n` +
130
+ ` dictionary: ${schemaTypeName},\n` +
131
+ ` areaLocales: ${localesParamType},\n` +
132
+ `): ${schemaTypeName} {\n` +
133
+ ` return projectNamespaceForDeliveryAreaCore(dictionary, areaLocales${fallbackArg});\n` +
134
+ `}\n`;
135
+ }
136
+
137
+ return block;
138
+ }
139
+
140
+ function formatMultiProjectionBlock(
94
141
  schemaTypeName: string,
95
142
  namespaceNames: string[],
96
143
  localesParamType: string,
97
- projectLocalesFallbackArg: string
144
+ fallbackArg: string,
145
+ emitDeliveryAreaHelpers: boolean
98
146
  ): string {
99
- const overloads = namespaceNames
147
+ const namespaceLocaleOverloads = namespaceNames
100
148
  .map(
101
149
  (namespace) =>
102
150
  `export function projectNamespaceLocales(\n` +
@@ -106,22 +154,55 @@ function formatMultiProjectLocalesBlock(
106
154
  )
107
155
  .join("\n");
108
156
 
109
- return (
157
+ const namespaceDeliveryOverloads = emitDeliveryAreaHelpers
158
+ ? namespaceNames
159
+ .map(
160
+ (namespace) =>
161
+ `export function projectNamespaceForDeliveryArea(\n` +
162
+ ` dictionary: ${schemaTypeName}["${namespace}"],\n` +
163
+ ` areaLocales: ${localesParamType},\n` +
164
+ `): ${schemaTypeName}["${namespace}"];`
165
+ )
166
+ .join("\n")
167
+ : "";
168
+
169
+ let block =
110
170
  `\n` +
111
- `export function projectLocales(\n` +
171
+ `export function projectDictionaryLocales(\n` +
112
172
  ` dictionary: ${schemaTypeName},\n` +
113
173
  ` locales: ${localesParamType},\n` +
114
174
  `): ${schemaTypeName} {\n` +
115
- ` return projectNamespacesLocalesCore(dictionary, locales${projectLocalesFallbackArg});\n` +
175
+ ` return projectDictionaryLocalesCore(dictionary, locales${fallbackArg});\n` +
116
176
  `}\n` +
117
177
  `\n` +
118
- overloads +
178
+ namespaceLocaleOverloads +
119
179
  `\n` +
120
180
  `export function projectNamespaceLocales(\n` +
121
181
  ` dictionary: ${schemaTypeName}[keyof ${schemaTypeName}],\n` +
122
182
  ` locales: ${localesParamType},\n` +
123
183
  `): ${schemaTypeName}[keyof ${schemaTypeName}] {\n` +
124
- ` return projectLocalesCore(dictionary, locales${projectLocalesFallbackArg});\n` +
125
- `}\n`
126
- );
184
+ ` return projectNamespaceLocalesCore(dictionary, locales${fallbackArg});\n` +
185
+ `}\n`;
186
+
187
+ if (emitDeliveryAreaHelpers) {
188
+ block +=
189
+ `\n` +
190
+ `export function projectDictionaryForDeliveryArea(\n` +
191
+ ` dictionary: ${schemaTypeName},\n` +
192
+ ` areaLocales: ${localesParamType},\n` +
193
+ `): ${schemaTypeName} {\n` +
194
+ ` return projectDictionaryForDeliveryAreaCore(dictionary, areaLocales${fallbackArg});\n` +
195
+ `}\n` +
196
+ `\n` +
197
+ namespaceDeliveryOverloads +
198
+ `\n` +
199
+ `export function projectNamespaceForDeliveryArea(\n` +
200
+ ` dictionary: ${schemaTypeName}[keyof ${schemaTypeName}],\n` +
201
+ ` areaLocales: ${localesParamType},\n` +
202
+ `): ${schemaTypeName}[keyof ${schemaTypeName}] {\n` +
203
+ ` return projectNamespaceForDeliveryAreaCore(dictionary, areaLocales${fallbackArg});\n` +
204
+ `}\n`;
205
+ }
206
+
207
+ return block;
127
208
  }
@@ -0,0 +1,176 @@
1
+ import path from "node:path";
2
+ import { describe, expect, it } from "vitest";
3
+ import { formatNamespaceLoadersFile } from "./namespace-loaders-file.js";
4
+
5
+ const projectRoot = "/project";
6
+ const loadersOutputPath = path.join(projectRoot, "src/i18n/namespace-loaders.generated.ts");
7
+
8
+ describe("formatNamespaceLoadersFile", () => {
9
+ it("emits flat loaders in canonical mode", () => {
10
+ const output = formatNamespaceLoadersFile({
11
+ loadersOutputPath,
12
+ lazyEntries: [
13
+ {
14
+ namespace: "billing",
15
+ filePath: "src/i18n/translations/billing.json",
16
+ absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
17
+ },
18
+ ],
19
+ schemaTypeName: "AppSchema",
20
+ localeTypeName: "AppLocale",
21
+ typesModule: "i18n-types.generated",
22
+ importExtension: "none",
23
+ projectRoot,
24
+ delivery: "canonical",
25
+ });
26
+
27
+ expect(output).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
28
+ expect(output).toContain(
29
+ "billing: () => import('./translations/billing.json').then((m) => m.default),"
30
+ );
31
+ expect(output).not.toContain("AppLocale");
32
+ });
33
+
34
+ it("emits ns(locale) loaders in split mode", () => {
35
+ const output = formatNamespaceLoadersFile({
36
+ loadersOutputPath,
37
+ lazyEntries: [
38
+ {
39
+ namespace: "billing",
40
+ filePath: "src/i18n/translations/billing.json",
41
+ absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
42
+ },
43
+ {
44
+ namespace: "user",
45
+ filePath: "src/i18n/translations/user.json",
46
+ absolutePath: path.join(projectRoot, "src/i18n/translations/user.json"),
47
+ },
48
+ ],
49
+ schemaTypeName: "AppSchema",
50
+ localeTypeName: "AppLocale",
51
+ typesModule: "i18n-types.generated",
52
+ importExtension: "none",
53
+ projectRoot,
54
+ delivery: "split-by-locale",
55
+ requestLocales: ["en", "it", "de-CH"],
56
+ splitPathsByNamespace: {
57
+ billing: {
58
+ en: "src/i18n/generated/translations/billing.en.json",
59
+ it: "src/i18n/generated/translations/billing.it.json",
60
+ "de-CH": "src/i18n/generated/translations/billing.de-CH.json",
61
+ },
62
+ user: {
63
+ en: "src/i18n/generated/translations/user.en.json",
64
+ it: "src/i18n/generated/translations/user.it.json",
65
+ "de-CH": "src/i18n/generated/translations/user.de-CH.json",
66
+ },
67
+ },
68
+ });
69
+
70
+ expect(output).toContain("[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;");
71
+ expect(output).toContain("billing: (locale) => {");
72
+ expect(output).toContain('case "en":');
73
+ expect(output).toContain('case "it":');
74
+ expect(output).toContain('case "de-CH":');
75
+ expect(output).toContain(
76
+ "return import('./generated/translations/billing.en.json').then((m) => m.default);"
77
+ );
78
+ expect(output).toContain("user: (locale) => {");
79
+ expect(output).not.toContain("billing: {");
80
+ expect(output).not.toContain("import('./translations/billing.json')");
81
+ });
82
+
83
+ it("throws when a split path is missing", () => {
84
+ expect(() =>
85
+ formatNamespaceLoadersFile({
86
+ loadersOutputPath,
87
+ lazyEntries: [
88
+ {
89
+ namespace: "billing",
90
+ filePath: "src/i18n/translations/billing.json",
91
+ absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
92
+ },
93
+ ],
94
+ schemaTypeName: "AppSchema",
95
+ localeTypeName: "AppLocale",
96
+ typesModule: "i18n-types.generated",
97
+ importExtension: "none",
98
+ projectRoot,
99
+ delivery: "split-by-locale",
100
+ requestLocales: ["en", "it"],
101
+ splitPathsByNamespace: {
102
+ billing: {
103
+ en: "src/i18n/generated/translations/billing.en.json",
104
+ },
105
+ },
106
+ })
107
+ ).toThrow('[Codegen Error] Missing split path for namespace "billing", locale "it".');
108
+ });
109
+
110
+ it("emits ns(area) loaders in custom mode", () => {
111
+ const output = formatNamespaceLoadersFile({
112
+ loadersOutputPath,
113
+ lazyEntries: [
114
+ {
115
+ namespace: "billing",
116
+ filePath: "src/i18n/translations/billing.json",
117
+ absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
118
+ },
119
+ ],
120
+ schemaTypeName: "AppSchema",
121
+ localeTypeName: "AppLocale",
122
+ typesModule: "i18n-types.generated",
123
+ importExtension: "none",
124
+ projectRoot,
125
+ delivery: "custom",
126
+ deliveryAreaTypeName: "AppDeliveryArea",
127
+ deliveryAreaNames: ["eu", "us"],
128
+ splitPathsByNamespace: {
129
+ billing: {
130
+ eu: "src/i18n/generated/translations/billing.eu.json",
131
+ us: "src/i18n/generated/translations/billing.us.json",
132
+ },
133
+ },
134
+ });
135
+
136
+ expect(output).toContain(
137
+ "[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
138
+ );
139
+ expect(output).toContain("billing: (area) => {");
140
+ expect(output).toContain('case "eu":');
141
+ expect(output).toContain('case "us":');
142
+ expect(output).toContain(
143
+ "return import('./generated/translations/billing.eu.json').then((m) => m.default);"
144
+ );
145
+ expect(output).not.toContain("AppLocale");
146
+ expect(output).not.toContain("import('./translations/billing.json')");
147
+ });
148
+
149
+ it("throws when a custom delivery split path is missing", () => {
150
+ expect(() =>
151
+ formatNamespaceLoadersFile({
152
+ loadersOutputPath,
153
+ lazyEntries: [
154
+ {
155
+ namespace: "billing",
156
+ filePath: "src/i18n/translations/billing.json",
157
+ absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
158
+ },
159
+ ],
160
+ schemaTypeName: "AppSchema",
161
+ localeTypeName: "AppLocale",
162
+ typesModule: "i18n-types.generated",
163
+ importExtension: "none",
164
+ projectRoot,
165
+ delivery: "custom",
166
+ deliveryAreaTypeName: "AppDeliveryArea",
167
+ deliveryAreaNames: ["eu", "us"],
168
+ splitPathsByNamespace: {
169
+ billing: {
170
+ eu: "src/i18n/generated/translations/billing.eu.json",
171
+ },
172
+ },
173
+ })
174
+ ).toThrow('[Codegen Error] Missing split path for namespace "billing", area "us".');
175
+ });
176
+ });
@@ -1,19 +1,96 @@
1
+ import path from "node:path";
2
+ import type { DeliveryMode } from "../codegen-config-schema.js";
1
3
  import { GENERATED_FILE_BANNER, toImportPath, toRelativeModuleImport } from "../paths.js";
2
4
  import type { ImportExtension, NamespaceEntry } from "../types.js";
3
5
 
4
- export function formatNamespaceLoadersFile(
5
- loadersOutputPath: string,
6
- lazyEntries: (NamespaceEntry & { absolutePath: string })[],
7
- schemaTypeName: string,
8
- typesModule: string,
9
- dictionarySchemaModule: string,
10
- instanceModule: string,
11
- factoryName: string,
12
- importExtension: ImportExtension
6
+ /**
7
+ * Emits the generated `namespace-loaders.generated.ts` module: dynamic `import()`
8
+ * functions for lazy-loaded namespaces, shaped by delivery mode.
9
+ */
10
+ export interface NamespaceLoadersFileOptions {
11
+ loadersOutputPath: string;
12
+ lazyEntries: (NamespaceEntry & { absolutePath: string })[];
13
+ schemaTypeName: string;
14
+ localeTypeName: string;
15
+ typesModule: string;
16
+ importExtension: ImportExtension;
17
+ projectRoot: string;
18
+ delivery?: DeliveryMode;
19
+ splitPathsByNamespace?: Record<string, Record<string, string>>;
20
+ requestLocales?: readonly string[];
21
+ deliveryAreaTypeName?: string;
22
+ deliveryAreaNames?: readonly string[];
23
+ }
24
+
25
+ interface PartitionedNamespaceLoadersParams {
26
+ partitionKeys: readonly string[];
27
+ paramName: string;
28
+ paramTypeName: string;
29
+ }
30
+
31
+ /**
32
+ * Emits lazy loaders for split-by-locale and custom delivery.
33
+ * Each namespace gets a function that dynamic-imports the JSON for the given
34
+ * partition key (locale or area) via a switch.
35
+ */
36
+ function formatPartitionedNamespaceLoadersFile(
37
+ options: NamespaceLoadersFileOptions,
38
+ { partitionKeys, paramName, paramTypeName }: PartitionedNamespaceLoadersParams
13
39
  ): string {
14
- const dictionarySchemaImport = toRelativeModuleImport(dictionarySchemaModule, importExtension);
40
+ const {
41
+ loadersOutputPath,
42
+ lazyEntries,
43
+ schemaTypeName,
44
+ typesModule,
45
+ importExtension,
46
+ projectRoot,
47
+ splitPathsByNamespace = {},
48
+ } = options;
49
+
50
+ const typesImport = toRelativeModuleImport(typesModule, importExtension);
51
+ const loaderEntries = lazyEntries
52
+ .map((entry) => {
53
+ const switchCases = partitionKeys
54
+ .map((partitionKey) => {
55
+ const splitRelativePath = splitPathsByNamespace[entry.namespace]?.[partitionKey];
56
+ if (!splitRelativePath) {
57
+ throw new Error(
58
+ `[Codegen Error] Missing split path for namespace "${entry.namespace}", ${paramName} "${partitionKey}".`
59
+ );
60
+ }
61
+ const importPath = toImportPath(
62
+ loadersOutputPath,
63
+ path.resolve(projectRoot, splitRelativePath)
64
+ );
65
+ return (
66
+ ` case ${JSON.stringify(partitionKey)}:\n` +
67
+ ` return import('${importPath}.json').then((m) => m.default);`
68
+ );
69
+ })
70
+ .join("\n");
71
+
72
+ return (
73
+ ` ${entry.namespace}: (${paramName}) => {\n` +
74
+ ` switch (${paramName}) {\n${switchCases}\n` +
75
+ ` }\n` +
76
+ ` },`
77
+ );
78
+ })
79
+ .join("\n");
80
+
81
+ return (
82
+ `${GENERATED_FILE_BANNER}` +
83
+ `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName} } from '${typesImport}';\n\n` +
84
+ `export const namespaceLoaders: {\n` +
85
+ ` [K in LazyNamespace]: (${paramName}: ${paramTypeName}) => Promise<${schemaTypeName}[K]>;\n` +
86
+ `} = {\n${loaderEntries}\n};\n`
87
+ );
88
+ }
89
+
90
+ /** Emits lazy loaders for canonical delivery: one dynamic import per namespace. */
91
+ function formatCanonicalNamespaceLoadersFile(options: NamespaceLoadersFileOptions): string {
92
+ const { loadersOutputPath, lazyEntries, schemaTypeName, typesModule, importExtension } = options;
15
93
  const typesImport = toRelativeModuleImport(typesModule, importExtension);
16
- const instanceImport = toRelativeModuleImport(instanceModule, importExtension);
17
94
  const loaderEntries = lazyEntries
18
95
  .map((entry) => {
19
96
  const importPath = toImportPath(loadersOutputPath, entry.absolutePath);
@@ -23,26 +100,35 @@ export function formatNamespaceLoadersFile(
23
100
 
24
101
  return (
25
102
  `${GENERATED_FILE_BANNER}` +
26
- `import { ensureNamespacesLoadedImpl } from '@xndrjs/i18n';\n` +
27
- `import { validateExternalNamespace } from '${dictionarySchemaImport}';\n` +
28
- `import type { ${schemaTypeName}, LazyNamespace } from '${typesImport}';\n` +
29
- `import type { ${factoryName} } from '${instanceImport}';\n\n` +
30
- `export const namespaceLoaders: Record<\n` +
31
- ` LazyNamespace,\n` +
32
- ` () => Promise<${schemaTypeName}[LazyNamespace]>\n` +
33
- `> = {\n${loaderEntries}\n};\n\n` +
34
- `export async function ensureNamespacesLoaded(\n` +
35
- ` i18n: ReturnType<typeof ${factoryName}>,\n` +
36
- ` namespaces: LazyNamespace[],\n` +
37
- `): Promise<void> {\n` +
38
- ` return ensureNamespacesLoadedImpl<${schemaTypeName}, LazyNamespace>(\n` +
39
- ` {\n` +
40
- ` provider: i18n,\n` +
41
- ` resolveLoader: (namespace) => namespaceLoaders[namespace],\n` +
42
- ` validate: (namespace, raw) => validateExternalNamespace(namespace, raw),\n` +
43
- ` },\n` +
44
- ` namespaces,\n` +
45
- ` );\n` +
46
- `}\n`
103
+ `import type { ${schemaTypeName}, LazyNamespace } from '${typesImport}';\n\n` +
104
+ `export const namespaceLoaders: {\n` +
105
+ ` [K in LazyNamespace]: () => Promise<${schemaTypeName}[K]>;\n` +
106
+ `} = {\n${loaderEntries}\n};\n`
47
107
  );
48
108
  }
109
+
110
+ /**
111
+ * Builds `namespace-loaders.generated.ts` for the configured delivery mode:
112
+ * canonical, split-by-locale, or custom areas.
113
+ */
114
+ export function formatNamespaceLoadersFile(options: NamespaceLoadersFileOptions): string {
115
+ const delivery = options.delivery ?? "canonical";
116
+ if (delivery === "split-by-locale") {
117
+ return formatPartitionedNamespaceLoadersFile(options, {
118
+ partitionKeys: options.requestLocales ?? [],
119
+ paramName: "locale",
120
+ paramTypeName: options.localeTypeName,
121
+ });
122
+ }
123
+ if (delivery === "custom") {
124
+ if (!options.deliveryAreaTypeName) {
125
+ throw new Error("[Codegen Error] deliveryAreaTypeName is required for custom delivery.");
126
+ }
127
+ return formatPartitionedNamespaceLoadersFile(options, {
128
+ partitionKeys: options.deliveryAreaNames ?? [],
129
+ paramName: "area",
130
+ paramTypeName: options.deliveryAreaTypeName,
131
+ });
132
+ }
133
+ return formatCanonicalNamespaceLoadersFile(options);
134
+ }