@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.
- package/README.md +193 -44
- package/dist/codegen/index.d.ts +85 -0
- package/dist/codegen/index.js +181 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/index.d.ts +34 -18
- package/dist/index.js +174 -145
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +86 -4
- package/dist/validation/index.js.map +1 -1
- package/package.json +6 -1
- package/src/IcuTranslationProviderMulti.test.ts +48 -1
- package/src/IcuTranslationProviderMulti.ts +41 -61
- package/src/IcuTranslationProviderSingle.test.ts +67 -0
- package/src/IcuTranslationProviderSingle.ts +27 -51
- package/src/audit/audit-dictionaries.ts +4 -1
- package/src/audit/run-audit.test.ts +35 -1
- package/src/audit/run-audit.ts +15 -7
- package/src/codegen/codegen-config-schema.ts +68 -1
- package/src/codegen/config.test.ts +174 -39
- package/src/codegen/config.ts +14 -5
- package/src/codegen/constants.ts +8 -0
- package/src/codegen/delivery-artifacts.test.ts +142 -0
- package/src/codegen/delivery-artifacts.ts +124 -0
- package/src/codegen/emit/dictionary-file.test.ts +190 -0
- package/src/codegen/emit/dictionary-file.ts +165 -2
- package/src/codegen/emit/dictionary-schema-file.ts +5 -0
- package/src/codegen/emit/instance-file.ts +119 -38
- package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
- package/src/codegen/emit/namespace-loaders-file.ts +118 -32
- package/src/codegen/emit/types-file.test.ts +114 -0
- package/src/codegen/emit/types-file.ts +48 -11
- package/src/codegen/generate-i18n-types.test.ts +765 -22
- package/src/codegen/generate-i18n-types.ts +111 -58
- package/src/codegen/icu-analysis.ts +9 -2
- package/src/codegen/locale-fallback.ts +19 -10
- package/src/codegen/locale-policy.ts +4 -0
- package/src/codegen/paths.ts +9 -5
- package/src/codegen/project-locales-set-namespace.test.ts +11 -9
- package/src/codegen/read-dictionary.test.ts +474 -2
- package/src/codegen/read-dictionary.ts +164 -15
- package/src/codegen/write-file-if-changed.test.ts +42 -0
- package/src/codegen/write-file-if-changed.ts +20 -0
- package/src/codegen-config/build-config.ts +34 -0
- package/src/codegen-config/codegen-config.test.ts +32 -0
- package/src/codegen-config/index.ts +21 -0
- package/src/codegen-config/write-config.ts +8 -0
- package/src/deep-freeze.test.ts +13 -0
- package/src/deep-freeze.ts +5 -0
- package/src/format-core.ts +91 -0
- package/src/index.ts +8 -5
- package/src/project-locales.test.ts +129 -10
- package/src/project-locales.ts +90 -3
- package/src/setup/setup-i18n.test.ts +1 -1
- package/src/setup/setup-i18n.ts +9 -32
- package/src/types.ts +19 -4
- package/src/validation/normalize.ts +4 -0
- package/dist/types-C1CpXVOJ.d.ts +0 -83
- package/src/ensure-namespace.integration.test.ts +0 -66
- package/src/ensure-namespace.test.ts +0 -125
- package/src/ensure-namespace.ts +0 -70
- /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
|
|
42
|
-
const
|
|
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
|
|
59
|
-
const coreImports = isSingle
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
86
|
+
` dictionary: ${dictionaryParamType},\n` +
|
|
87
|
+
` options?: { onMissing?: OnMissingTranslation },\n` +
|
|
86
88
|
`) {\n` +
|
|
87
|
-
` return new ${providerClass}<${providerTypeArgs}>(
|
|
89
|
+
` return new ${providerClass}<${providerTypeArgs}>(dictionary${providerOptions});\n` +
|
|
88
90
|
`}\n` +
|
|
89
|
-
|
|
91
|
+
projectionBlock
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
function
|
|
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
|
-
|
|
144
|
+
fallbackArg: string,
|
|
145
|
+
emitDeliveryAreaHelpers: boolean
|
|
98
146
|
): string {
|
|
99
|
-
const
|
|
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
|
-
|
|
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
|
|
171
|
+
`export function projectDictionaryLocales(\n` +
|
|
112
172
|
` dictionary: ${schemaTypeName},\n` +
|
|
113
173
|
` locales: ${localesParamType},\n` +
|
|
114
174
|
`): ${schemaTypeName} {\n` +
|
|
115
|
-
` return
|
|
175
|
+
` return projectDictionaryLocalesCore(dictionary, locales${fallbackArg});\n` +
|
|
116
176
|
`}\n` +
|
|
117
177
|
`\n` +
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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 {
|
|
27
|
-
`
|
|
28
|
-
`
|
|
29
|
-
`
|
|
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
|
+
}
|