@xndrjs/i18n 0.7.0 → 0.8.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 +67 -872
- package/dist/codegen/index.d.ts +117 -51
- package/dist/codegen/index.js +1585 -93
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +100 -234
- package/dist/index.js +74 -638
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.js.map +1 -1
- package/package.json +2 -2
- package/src/IcuTranslationProviderMulti.test.ts +18 -40
- package/src/IcuTranslationProviderMulti.ts +27 -37
- package/src/audit/audit-dictionaries.ts +1 -1
- package/src/audit/run-audit.test.ts +6 -7
- package/src/builder-load-registry.ts +26 -5
- package/src/builder-loaders.ts +8 -11
- package/src/builder-types.test.ts +11 -42
- package/src/builder.test.ts +82 -243
- package/src/builder.ts +8 -110
- package/src/codegen/codegen-config-schema.ts +82 -77
- package/src/codegen/config.test.ts +55 -143
- package/src/codegen/config.ts +10 -67
- package/src/codegen/dictionary-spec-contract.test.ts +28 -0
- package/src/codegen/dictionary-spec-contract.ts +138 -0
- package/src/codegen/emit/dictionary-schema-file.ts +7 -43
- package/src/codegen/emit/instance-file.test.ts +35 -52
- package/src/codegen/emit/instance-file.ts +170 -268
- package/src/codegen/emit/namespace-loaders-file.test.ts +72 -74
- package/src/codegen/emit/namespace-loaders-file.ts +147 -90
- package/src/codegen/emit/types-file.test.ts +20 -41
- package/src/codegen/emit/types-file.ts +48 -74
- package/src/codegen/generate-i18n-types.test.ts +163 -492
- package/src/codegen/generate-i18n-types.ts +7 -269
- package/src/codegen/paths.ts +0 -14
- package/src/codegen/project-locales-set-namespace.test.ts +44 -86
- package/src/codegen/read-dictionary.test.ts +27 -9
- package/src/codegen/read-dictionary.ts +18 -40
- package/src/codegen/regenerate-namespaces.ts +180 -0
- package/src/codegen/run-codegen.test.ts +230 -0
- package/src/codegen/run-codegen.ts +252 -0
- package/src/codegen/types.ts +0 -6
- package/src/codegen-config/build-config.ts +4 -23
- package/src/codegen-config/codegen-config.test.ts +10 -6
- package/src/codegen-config/index.ts +18 -3
- package/src/engine.ts +3 -27
- package/src/fetch-artifact.ts +16 -0
- package/src/i18n-handle.ts +202 -0
- package/src/index.ts +15 -34
- package/src/project-locales.ts +2 -2
- package/src/scope-multi.ts +1 -20
- package/src/scope-types.ts +14 -6
- package/src/scope.test.ts +3 -312
- package/src/serialized-state.test.ts +114 -0
- package/src/serialized-state.ts +28 -0
- package/src/setup/setup-i18n.test.ts +22 -33
- package/src/setup/setup-i18n.ts +22 -27
- package/src/types.ts +3 -3
- package/src/validation/index.ts +4 -0
- package/src/IcuTranslationProviderSingle.test.ts +0 -177
- package/src/IcuTranslationProviderSingle.ts +0 -114
- package/src/builder-multi.ts +0 -481
- package/src/codegen/emit/dictionary-file.test.ts +0 -215
- package/src/codegen/emit/dictionary-file.ts +0 -244
- package/src/deep-freeze.test.ts +0 -40
- package/src/deep-freeze.ts +0 -22
- package/src/patch-key.test.ts +0 -186
- package/src/patch-key.ts +0 -140
- package/src/scope-single.ts +0 -85
- package/src/single-builder.ts +0 -153
|
@@ -6,33 +6,6 @@ const projectRoot = "/project";
|
|
|
6
6
|
const loadersOutputPath = path.join(projectRoot, "src/i18n/namespace-loaders.generated.ts");
|
|
7
7
|
|
|
8
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
|
-
paramsTypeName: "AppParams",
|
|
21
|
-
localeTypeName: "AppLocale",
|
|
22
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
23
|
-
hasLocaleFallback: false,
|
|
24
|
-
typesModule: "i18n-types.generated",
|
|
25
|
-
importExtension: "none",
|
|
26
|
-
projectRoot,
|
|
27
|
-
delivery: "canonical",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
expect(output).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
|
|
31
|
-
expect(output).toContain(
|
|
32
|
-
"billing: () => import('./translations/billing.json').then((m) => m.default),"
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
9
|
it("emits ns(locale) loaders in split mode", () => {
|
|
37
10
|
const output = formatNamespaceLoadersFile({
|
|
38
11
|
loadersOutputPath,
|
|
@@ -54,14 +27,10 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
54
27
|
},
|
|
55
28
|
],
|
|
56
29
|
schemaTypeName: "AppSchema",
|
|
57
|
-
paramsTypeName: "AppParams",
|
|
58
30
|
localeTypeName: "AppLocale",
|
|
59
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
60
|
-
hasLocaleFallback: true,
|
|
61
31
|
typesModule: "i18n-types.generated",
|
|
62
32
|
importExtension: "none",
|
|
63
33
|
projectRoot,
|
|
64
|
-
isSingle: false,
|
|
65
34
|
delivery: "split-by-locale",
|
|
66
35
|
requestLocales: ["en", "it", "de-CH"],
|
|
67
36
|
splitPathsByNamespace: {
|
|
@@ -83,6 +52,9 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
83
52
|
},
|
|
84
53
|
});
|
|
85
54
|
|
|
55
|
+
expect(output).toContain(
|
|
56
|
+
"import type { AppSchema, LazyNamespace, AppLocale } from './i18n-types.generated';"
|
|
57
|
+
);
|
|
86
58
|
expect(output).toContain("[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;");
|
|
87
59
|
expect(output).toContain("billing: (locale) => {");
|
|
88
60
|
expect(output).toContain('case "en":');
|
|
@@ -101,39 +73,6 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
101
73
|
expect(output.match(/from '\.\/i18n-types\.generated'/g)?.length).toBe(1);
|
|
102
74
|
});
|
|
103
75
|
|
|
104
|
-
it("emits locale-scoped loaders for single mode in split delivery", () => {
|
|
105
|
-
const output = formatNamespaceLoadersFile({
|
|
106
|
-
loadersOutputPath,
|
|
107
|
-
lazyEntries: [
|
|
108
|
-
{
|
|
109
|
-
namespace: "default",
|
|
110
|
-
filePath: "src/i18n/translations/translations.json",
|
|
111
|
-
absolutePath: path.join(projectRoot, "src/i18n/translations/translations.json"),
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
schemaTypeName: "AppSchema",
|
|
115
|
-
paramsTypeName: "AppParams",
|
|
116
|
-
localeTypeName: "AppLocale",
|
|
117
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
118
|
-
hasLocaleFallback: true,
|
|
119
|
-
typesModule: "i18n-types.generated",
|
|
120
|
-
importExtension: "none",
|
|
121
|
-
projectRoot,
|
|
122
|
-
isSingle: true,
|
|
123
|
-
delivery: "split-by-locale",
|
|
124
|
-
requestLocales: ["en", "it"],
|
|
125
|
-
splitPathsByNamespace: {
|
|
126
|
-
default: {
|
|
127
|
-
en: "src/i18n/generated/translations/translations.en.json",
|
|
128
|
-
it: "src/i18n/generated/translations/translations.it.json",
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
expect(output).toContain("[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema>;");
|
|
134
|
-
expect(output).toContain('export const defaultLazyNamespaces = ["default"] as const;');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
76
|
it("throws when a split path is missing", () => {
|
|
138
77
|
expect(() =>
|
|
139
78
|
formatNamespaceLoadersFile({
|
|
@@ -146,10 +85,7 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
146
85
|
},
|
|
147
86
|
],
|
|
148
87
|
schemaTypeName: "AppSchema",
|
|
149
|
-
paramsTypeName: "AppParams",
|
|
150
88
|
localeTypeName: "AppLocale",
|
|
151
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
152
|
-
hasLocaleFallback: false,
|
|
153
89
|
typesModule: "i18n-types.generated",
|
|
154
90
|
importExtension: "none",
|
|
155
91
|
projectRoot,
|
|
@@ -180,14 +116,10 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
180
116
|
},
|
|
181
117
|
],
|
|
182
118
|
schemaTypeName: "AppSchema",
|
|
183
|
-
paramsTypeName: "AppParams",
|
|
184
119
|
localeTypeName: "AppLocale",
|
|
185
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
186
|
-
hasLocaleFallback: true,
|
|
187
120
|
typesModule: "i18n-types.generated",
|
|
188
121
|
importExtension: "none",
|
|
189
122
|
projectRoot,
|
|
190
|
-
isSingle: false,
|
|
191
123
|
delivery: "custom",
|
|
192
124
|
deliveryAreaTypeName: "AppDeliveryArea",
|
|
193
125
|
deliveryAreaNames: ["eu", "us"],
|
|
@@ -203,6 +135,9 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
203
135
|
},
|
|
204
136
|
});
|
|
205
137
|
|
|
138
|
+
expect(output).toContain(
|
|
139
|
+
"import type { AppSchema, LazyNamespace, AppDeliveryArea } from './i18n-types.generated';"
|
|
140
|
+
);
|
|
206
141
|
expect(output).toContain(
|
|
207
142
|
"[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
|
|
208
143
|
);
|
|
@@ -232,10 +167,7 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
232
167
|
},
|
|
233
168
|
],
|
|
234
169
|
schemaTypeName: "AppSchema",
|
|
235
|
-
paramsTypeName: "AppParams",
|
|
236
170
|
localeTypeName: "AppLocale",
|
|
237
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
238
|
-
hasLocaleFallback: false,
|
|
239
171
|
typesModule: "i18n-types.generated",
|
|
240
172
|
importExtension: "none",
|
|
241
173
|
projectRoot,
|
|
@@ -250,4 +182,70 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
250
182
|
})
|
|
251
183
|
).toThrow('[Codegen Error] Missing split path for namespace "billing", area "us".');
|
|
252
184
|
});
|
|
185
|
+
|
|
186
|
+
it("emits fetch loaders that pass a resource id to fetchImpl", () => {
|
|
187
|
+
const output = formatNamespaceLoadersFile({
|
|
188
|
+
loadersOutputPath,
|
|
189
|
+
lazyEntries: [
|
|
190
|
+
{
|
|
191
|
+
namespace: "billing",
|
|
192
|
+
filePath: "src/i18n/translations/billing.json",
|
|
193
|
+
absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
schemaTypeName: "AppSchema",
|
|
197
|
+
localeTypeName: "AppLocale",
|
|
198
|
+
typesModule: "i18n-types.generated",
|
|
199
|
+
importExtension: "none",
|
|
200
|
+
projectRoot,
|
|
201
|
+
delivery: "split-by-locale",
|
|
202
|
+
requestLocales: ["en", "it"],
|
|
203
|
+
loaderStrategy: "fetch",
|
|
204
|
+
splitPathsByNamespace: {
|
|
205
|
+
billing: {
|
|
206
|
+
en: "src/i18n/generated/translations/billing.en.json",
|
|
207
|
+
it: "src/i18n/generated/translations/billing.it.json",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(output).toContain("export function createNamespaceLoaders");
|
|
213
|
+
expect(output).toContain("import type { FetchArtifact }");
|
|
214
|
+
expect(output).toContain(
|
|
215
|
+
'fetchImpl({ locale, namespace: "billing" }) as Promise<AppSchema["billing"]>'
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("emits custom-delivery fetch loaders with locale + area", () => {
|
|
220
|
+
const output = formatNamespaceLoadersFile({
|
|
221
|
+
loadersOutputPath,
|
|
222
|
+
lazyEntries: [
|
|
223
|
+
{
|
|
224
|
+
namespace: "billing",
|
|
225
|
+
filePath: "src/i18n/translations/billing.json",
|
|
226
|
+
absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
schemaTypeName: "AppSchema",
|
|
230
|
+
localeTypeName: "AppLocale",
|
|
231
|
+
typesModule: "i18n-types.generated",
|
|
232
|
+
importExtension: "none",
|
|
233
|
+
projectRoot,
|
|
234
|
+
delivery: "custom",
|
|
235
|
+
deliveryAreaTypeName: "AppDeliveryArea",
|
|
236
|
+
deliveryAreaNames: ["eu", "us"],
|
|
237
|
+
loaderStrategy: "fetch",
|
|
238
|
+
splitPathsByNamespace: {
|
|
239
|
+
billing: {
|
|
240
|
+
eu: "src/i18n/generated/translations/billing.eu.json",
|
|
241
|
+
us: "src/i18n/generated/translations/billing.us.json",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(output).toContain("(area, { locale }) =>");
|
|
247
|
+
expect(output).toContain(
|
|
248
|
+
'fetchImpl({ locale, namespace: "billing", area }) as Promise<AppSchema["billing"]>'
|
|
249
|
+
);
|
|
250
|
+
});
|
|
253
251
|
});
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import type { DeliveryMode } from "../codegen-config-schema.js";
|
|
2
|
+
import type { DeliveryMode, LoaderStrategy } from "../codegen-config-schema.js";
|
|
3
3
|
import { GENERATED_FILE_BANNER, toImportPath, toRelativeModuleImport } from "../paths.js";
|
|
4
4
|
import type { ImportExtension, NamespaceEntry } from "../types.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Emits the generated `namespace-loaders.generated.ts` module: dynamic `import()`
|
|
8
|
-
*
|
|
8
|
+
* or injectable `fetchImpl(resourceId)` loaders, shaped by delivery mode.
|
|
9
9
|
*/
|
|
10
10
|
export interface NamespaceLoadersFileOptions {
|
|
11
11
|
loadersOutputPath: string;
|
|
12
12
|
lazyEntries: (NamespaceEntry & { absolutePath: string })[];
|
|
13
13
|
schemaTypeName: string;
|
|
14
|
-
paramsTypeName: string;
|
|
15
14
|
localeTypeName: string;
|
|
16
|
-
localeFallbackConstName: string;
|
|
17
|
-
hasLocaleFallback: boolean;
|
|
18
15
|
typesModule: string;
|
|
19
16
|
importExtension: ImportExtension;
|
|
20
17
|
projectRoot: string;
|
|
21
|
-
|
|
22
|
-
delivery?: DeliveryMode;
|
|
18
|
+
delivery: DeliveryMode;
|
|
23
19
|
splitPathsByNamespace?: Record<string, Record<string, string>>;
|
|
24
20
|
requestLocales?: readonly string[];
|
|
25
21
|
deliveryAreaTypeName?: string;
|
|
26
22
|
deliveryAreaNames?: readonly string[];
|
|
23
|
+
loaderStrategy?: LoaderStrategy;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
interface PartitionedNamespaceLoadersParams {
|
|
@@ -44,28 +41,97 @@ function formatPartitionedTypesImport(
|
|
|
44
41
|
options: NamespaceLoadersFileOptions,
|
|
45
42
|
paramTypeName: string
|
|
46
43
|
): string {
|
|
47
|
-
const { schemaTypeName,
|
|
48
|
-
|
|
44
|
+
const { schemaTypeName, typesModule, importExtension } = options;
|
|
45
|
+
return `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
46
|
+
}
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
function formatImportCase(
|
|
49
|
+
loadersOutputPath: string,
|
|
50
|
+
projectRoot: string,
|
|
51
|
+
splitRelativePath: string
|
|
52
|
+
): string {
|
|
53
|
+
const importPath = toImportPath(loadersOutputPath, path.resolve(projectRoot, splitRelativePath));
|
|
54
|
+
return `return import('${importPath}.json').then((m) => m.default);`;
|
|
55
|
+
}
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
function indentBlock(text: string, spaces: number): string {
|
|
58
|
+
const pad = " ".repeat(spaces);
|
|
59
|
+
return text
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((line) => (line.length === 0 ? line : `${pad}${line}`))
|
|
62
|
+
.join("\n");
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
function formatImportNamespaceLoaderEntry(options: {
|
|
66
|
+
namespace: string;
|
|
67
|
+
paramName: string;
|
|
68
|
+
partitionKeys: readonly string[];
|
|
69
|
+
splitPathsByNamespace: Record<string, Record<string, string>>;
|
|
70
|
+
loadersOutputPath: string;
|
|
71
|
+
projectRoot: string;
|
|
72
|
+
}): string {
|
|
73
|
+
const {
|
|
74
|
+
namespace,
|
|
75
|
+
paramName,
|
|
76
|
+
partitionKeys,
|
|
77
|
+
splitPathsByNamespace,
|
|
78
|
+
loadersOutputPath,
|
|
79
|
+
projectRoot,
|
|
80
|
+
} = options;
|
|
81
|
+
|
|
82
|
+
const caseBodies = partitionKeys.map((partitionKey) => {
|
|
83
|
+
const splitRelativePath = splitPathsByNamespace[namespace]?.[partitionKey];
|
|
84
|
+
if (!splitRelativePath) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[Codegen Error] Missing split path for namespace "${namespace}", ${paramName} "${partitionKey}".`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const body = formatImportCase(loadersOutputPath, projectRoot, splitRelativePath);
|
|
90
|
+
return [`case ${JSON.stringify(partitionKey)}:`, ` ${body}`].join("\n");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const switchBody = [
|
|
94
|
+
...caseBodies,
|
|
95
|
+
`default:`,
|
|
96
|
+
` throw new Error(\`[i18n] No translation artifact for namespace ${JSON.stringify(namespace)} and ${paramName} "\${String(${paramName})}".\`);`,
|
|
97
|
+
].join("\n");
|
|
98
|
+
|
|
99
|
+
const entry = [
|
|
100
|
+
`${namespace}: (${paramName}) => {`,
|
|
101
|
+
` switch (${paramName}) {`,
|
|
102
|
+
indentBlock(switchBody, 4),
|
|
103
|
+
` }`,
|
|
104
|
+
`},`,
|
|
105
|
+
].join("\n");
|
|
106
|
+
|
|
107
|
+
return indentBlock(entry, 2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Fetch loaders forward a resource id; URL mapping is left to `fetchImpl`. */
|
|
111
|
+
function formatFetchNamespaceLoaderEntry(options: {
|
|
112
|
+
namespace: string;
|
|
113
|
+
paramName: string;
|
|
114
|
+
schemaTypeName: string;
|
|
115
|
+
delivery: DeliveryMode;
|
|
116
|
+
}): string {
|
|
117
|
+
const { namespace, paramName, schemaTypeName, delivery } = options;
|
|
118
|
+
const nsLiteral = JSON.stringify(namespace);
|
|
119
|
+
const resourceId =
|
|
120
|
+
delivery === "custom"
|
|
121
|
+
? `{ locale, namespace: ${nsLiteral}, area }`
|
|
122
|
+
: `{ locale, namespace: ${nsLiteral} }`;
|
|
123
|
+
|
|
124
|
+
// Custom: partition is area; locale comes from load() context.
|
|
125
|
+
// Split-by-locale: partition is locale (second arg unused).
|
|
126
|
+
const signature = delivery === "custom" ? `(${paramName}, { locale })` : `(${paramName})`;
|
|
127
|
+
|
|
128
|
+
return indentBlock(
|
|
129
|
+
`${namespace}: ${signature} =>\n` +
|
|
130
|
+
` fetchImpl(${resourceId}) as Promise<${schemaTypeName}[${nsLiteral}]>,`,
|
|
131
|
+
4
|
|
62
132
|
);
|
|
63
133
|
}
|
|
64
134
|
|
|
65
|
-
/**
|
|
66
|
-
* Each namespace gets a function that dynamic-imports the JSON for the given
|
|
67
|
-
* partition key (locale or area) via a switch.
|
|
68
|
-
*/
|
|
69
135
|
function formatPartitionedNamespaceLoadersFile(
|
|
70
136
|
options: NamespaceLoadersFileOptions,
|
|
71
137
|
params: PartitionedNamespaceLoadersParams
|
|
@@ -76,97 +142,88 @@ function formatPartitionedNamespaceLoadersFile(
|
|
|
76
142
|
lazyEntries,
|
|
77
143
|
schemaTypeName,
|
|
78
144
|
projectRoot,
|
|
145
|
+
delivery,
|
|
79
146
|
splitPathsByNamespace = {},
|
|
80
|
-
isSingle = false,
|
|
81
147
|
} = options;
|
|
148
|
+
const loaderStrategy = options.loaderStrategy ?? "import";
|
|
82
149
|
|
|
83
|
-
const loaderValueType =
|
|
84
|
-
|
|
150
|
+
const loaderValueType = `${schemaTypeName}[K]`;
|
|
85
151
|
const typesImport = formatPartitionedTypesImport(options, paramTypeName);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
` return import('${importPath}.json').then((m) => m.default);`
|
|
103
|
-
);
|
|
152
|
+
|
|
153
|
+
if (loaderStrategy === "fetch") {
|
|
154
|
+
// Fetch loaders ignore partition-key switches: the resource id is enough for the app.
|
|
155
|
+
const fetchLoadersType = `{\n [K in LazyNamespace]: (${
|
|
156
|
+
delivery === "custom"
|
|
157
|
+
? `${paramName}: ${paramTypeName}, context: { locale: string }`
|
|
158
|
+
: `${paramName}: ${paramTypeName}`
|
|
159
|
+
}) => Promise<${loaderValueType}>;\n}`;
|
|
160
|
+
|
|
161
|
+
const loaderEntries = lazyEntries
|
|
162
|
+
.map((entry) =>
|
|
163
|
+
formatFetchNamespaceLoaderEntry({
|
|
164
|
+
namespace: entry.namespace,
|
|
165
|
+
paramName,
|
|
166
|
+
schemaTypeName,
|
|
167
|
+
delivery,
|
|
104
168
|
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
` ${entry.namespace}: (${paramName}) => {\n` +
|
|
109
|
-
` switch (${paramName}) {\n${switchCases}\n` +
|
|
110
|
-
`${formatSwitchDefaultCase(entry.namespace, paramName)}` +
|
|
111
|
-
` }\n` +
|
|
112
|
-
` },`
|
|
113
|
-
);
|
|
114
|
-
})
|
|
115
|
-
.join("\n");
|
|
169
|
+
)
|
|
170
|
+
.join("\n");
|
|
116
171
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
172
|
+
return (
|
|
173
|
+
`${GENERATED_FILE_BANNER}` +
|
|
174
|
+
`import type { FetchArtifact } from '@xndrjs/i18n';\n` +
|
|
175
|
+
typesImport +
|
|
176
|
+
`export type NamespaceLoaders = ${fetchLoadersType};\n\n` +
|
|
177
|
+
`/** Build loaders that resolve artifacts via the injected {@link FetchArtifact} (resource id only). */\n` +
|
|
178
|
+
`export function createNamespaceLoaders(fetchImpl: FetchArtifact): NamespaceLoaders {\n` +
|
|
179
|
+
` return {\n${loaderEntries}\n };\n` +
|
|
180
|
+
`}\n\n` +
|
|
181
|
+
`export const defaultLazyNamespaces = [${formatDefaultNamespacesLiteral(lazyEntries)}] as const;\n`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
127
184
|
|
|
128
|
-
/** Emits lazy loaders for canonical delivery: one dynamic import per namespace. */
|
|
129
|
-
function formatCanonicalNamespaceLoadersFile(options: NamespaceLoadersFileOptions): string {
|
|
130
|
-
const { loadersOutputPath, lazyEntries, schemaTypeName, typesModule, importExtension } = options;
|
|
131
|
-
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
132
185
|
const loaderEntries = lazyEntries
|
|
133
|
-
.map((entry) =>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
186
|
+
.map((entry) =>
|
|
187
|
+
formatImportNamespaceLoaderEntry({
|
|
188
|
+
namespace: entry.namespace,
|
|
189
|
+
paramName,
|
|
190
|
+
partitionKeys,
|
|
191
|
+
splitPathsByNamespace,
|
|
192
|
+
loadersOutputPath,
|
|
193
|
+
projectRoot,
|
|
194
|
+
})
|
|
195
|
+
)
|
|
137
196
|
.join("\n");
|
|
138
197
|
|
|
198
|
+
// Import loaders only use the partition key; context is accepted for a uniform NamespaceLoader shape.
|
|
199
|
+
const importLoadersType = `{\n [K in LazyNamespace]: (${paramName}: ${paramTypeName}) => Promise<${loaderValueType}>;\n}`;
|
|
200
|
+
|
|
139
201
|
return (
|
|
140
202
|
`${GENERATED_FILE_BANNER}` +
|
|
141
|
-
|
|
142
|
-
`export const namespaceLoaders: {\n` +
|
|
143
|
-
|
|
144
|
-
`
|
|
203
|
+
typesImport +
|
|
204
|
+
`export const namespaceLoaders: ${importLoadersType} = {\n${loaderEntries}\n};\n` +
|
|
205
|
+
`\n` +
|
|
206
|
+
`export const defaultLazyNamespaces = [${formatDefaultNamespacesLiteral(lazyEntries)}] as const;\n`
|
|
145
207
|
);
|
|
146
208
|
}
|
|
147
209
|
|
|
148
210
|
/**
|
|
149
|
-
* Builds `namespace-loaders.generated.ts` for
|
|
150
|
-
* canonical, split-by-locale, or custom areas.
|
|
211
|
+
* Builds `namespace-loaders.generated.ts` for split-by-locale or custom delivery areas.
|
|
151
212
|
*/
|
|
152
213
|
export function formatNamespaceLoadersFile(options: NamespaceLoadersFileOptions): string {
|
|
153
|
-
|
|
154
|
-
if (delivery === "split-by-locale") {
|
|
214
|
+
if (options.delivery === "split-by-locale") {
|
|
155
215
|
return formatPartitionedNamespaceLoadersFile(options, {
|
|
156
216
|
partitionKeys: options.requestLocales ?? [],
|
|
157
217
|
paramName: "locale",
|
|
158
218
|
paramTypeName: options.localeTypeName,
|
|
159
219
|
});
|
|
160
220
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
throw new Error("[Codegen Error] deliveryAreaTypeName is required for custom delivery.");
|
|
164
|
-
}
|
|
165
|
-
return formatPartitionedNamespaceLoadersFile(options, {
|
|
166
|
-
partitionKeys: options.deliveryAreaNames ?? [],
|
|
167
|
-
paramName: "area",
|
|
168
|
-
paramTypeName: options.deliveryAreaTypeName,
|
|
169
|
-
});
|
|
221
|
+
if (!options.deliveryAreaTypeName) {
|
|
222
|
+
throw new Error("[Codegen Error] deliveryAreaTypeName is required for custom delivery.");
|
|
170
223
|
}
|
|
171
|
-
return
|
|
224
|
+
return formatPartitionedNamespaceLoadersFile(options, {
|
|
225
|
+
partitionKeys: options.deliveryAreaNames ?? [],
|
|
226
|
+
paramName: "area",
|
|
227
|
+
paramTypeName: options.deliveryAreaTypeName,
|
|
228
|
+
});
|
|
172
229
|
}
|
|
@@ -6,9 +6,8 @@ const projectRoot = "/project";
|
|
|
6
6
|
const typesOutputPath = path.join(projectRoot, "src/i18n/generated/i18n-types.generated.ts");
|
|
7
7
|
|
|
8
8
|
describe("formatTypesFile", () => {
|
|
9
|
-
it("emits explicit Partial schema types
|
|
9
|
+
it("emits explicit Partial schema types and lazy aliases", () => {
|
|
10
10
|
const output = formatTypesFile({
|
|
11
|
-
isSingle: false,
|
|
12
11
|
entries: [
|
|
13
12
|
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
14
13
|
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
@@ -29,46 +28,23 @@ describe("formatTypesFile", () => {
|
|
|
29
28
|
invoice_summary: "{ count: number }",
|
|
30
29
|
},
|
|
31
30
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
requestLocales: ["en", "it"],
|
|
32
|
+
lazyEntries: [
|
|
33
|
+
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
34
|
+
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
35
|
+
],
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
expect(output).toContain("login_button: Partial<Record<AppLocale, string>>;");
|
|
39
39
|
expect(output).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
isSingle: true,
|
|
45
|
-
entries: [{ namespace: "translations", filePath: "src/i18n/translations/translations.yaml" }],
|
|
46
|
-
projectRoot,
|
|
47
|
-
typesOutputPath,
|
|
48
|
-
paramsTypeName: "AppParams",
|
|
49
|
-
schemaTypeName: "AppSchema",
|
|
50
|
-
localeTypeName: "AppLocale",
|
|
51
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
52
|
-
localeFallbackTypeName: "AppLocaleFallback",
|
|
53
|
-
paramsByNamespace: {
|
|
54
|
-
translations: {
|
|
55
|
-
welcome: "{ name: string }",
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
requestLocaleUnion: "'en' | 'it'",
|
|
59
|
-
hasLazy: false,
|
|
60
|
-
loadOnInitSet: new Set(),
|
|
61
|
-
lazyEntries: [],
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(output).toContain(
|
|
65
|
-
"export type AppSchema = {\n welcome: Partial<Record<AppLocale, string>>;\n};"
|
|
66
|
-
);
|
|
40
|
+
expect(output).toContain('export const AppLocales = ["en", "it"] as const;');
|
|
41
|
+
expect(output).toContain("export type AppLocale = (typeof AppLocales)[number];");
|
|
42
|
+
expect(output).toContain("export type LazyNamespace = 'default' | 'billing'");
|
|
43
|
+
expect(output).toContain("export type InitialSchema = Record<string, never>");
|
|
67
44
|
});
|
|
68
45
|
|
|
69
46
|
it("emits delivery area type for custom delivery", () => {
|
|
70
47
|
const output = formatTypesFile({
|
|
71
|
-
isSingle: false,
|
|
72
48
|
entries: [
|
|
73
49
|
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
74
50
|
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
@@ -89,20 +65,23 @@ describe("formatTypesFile", () => {
|
|
|
89
65
|
default: { some_key: "never" },
|
|
90
66
|
billing: { invoice_summary: "{ count: number }" },
|
|
91
67
|
},
|
|
92
|
-
|
|
68
|
+
requestLocales: ["en-US", "fr", "it"],
|
|
93
69
|
deliveryAreaTypeName: "AppDeliveryArea",
|
|
94
|
-
|
|
70
|
+
deliveryAreaNames: ["eu", "us"],
|
|
95
71
|
deliveryArtifacts: {
|
|
96
72
|
eu: ["it", "fr"],
|
|
97
73
|
us: ["en-US"],
|
|
98
74
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
75
|
+
lazyEntries: [
|
|
76
|
+
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
77
|
+
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
78
|
+
],
|
|
102
79
|
});
|
|
103
80
|
|
|
104
|
-
expect(output).toContain(
|
|
105
|
-
expect(output).toContain("export type
|
|
81
|
+
expect(output).toContain('export const AppLocales = ["en-US", "fr", "it"] as const;');
|
|
82
|
+
expect(output).toContain("export type AppLocale = (typeof AppLocales)[number];");
|
|
83
|
+
expect(output).toContain('export const AppDeliveryAreas = ["eu", "us"] as const;');
|
|
84
|
+
expect(output).toContain("export type AppDeliveryArea = (typeof AppDeliveryAreas)[number];");
|
|
106
85
|
expect(output).toContain("export const DELIVERY_ARTIFACTS = {");
|
|
107
86
|
expect(output).toContain('"eu": ["fr", "it"] as const');
|
|
108
87
|
expect(output).toContain('"us": ["en-US"] as const');
|