@xndrjs/i18n 0.8.1 → 0.8.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/cli/audit.js +739 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/codegen.js +1471 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/setup.js +148 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/codegen/index.js +43 -40
- package/dist/codegen/index.js.map +1 -1
- package/package.json +6 -9
- package/bin/audit.mjs +0 -14
- package/bin/codegen.mjs +0 -17
- package/bin/setup.mjs +0 -14
- package/src/IcuTranslationProviderMulti.test.ts +0 -215
- package/src/IcuTranslationProviderMulti.ts +0 -188
- package/src/audit/audit-dictionaries.test.ts +0 -134
- package/src/audit/audit-dictionaries.ts +0 -178
- package/src/audit/run-audit.test.ts +0 -169
- package/src/audit/run-audit.ts +0 -94
- package/src/builder-load-registry.test.ts +0 -44
- package/src/builder-load-registry.ts +0 -78
- package/src/builder-loaders.ts +0 -15
- package/src/builder-types.test.ts +0 -57
- package/src/builder-types.ts +0 -24
- package/src/builder.test.ts +0 -303
- package/src/builder.ts +0 -15
- package/src/codegen/codegen-config-schema.ts +0 -156
- package/src/codegen/config.test.ts +0 -231
- package/src/codegen/config.ts +0 -42
- package/src/codegen/constants.ts +0 -9
- package/src/codegen/delivery-artifacts.test.ts +0 -167
- package/src/codegen/delivery-artifacts.ts +0 -154
- package/src/codegen/dictionary-spec-contract.test.ts +0 -28
- package/src/codegen/dictionary-spec-contract.ts +0 -138
- package/src/codegen/emit/dictionary-schema-file.ts +0 -113
- package/src/codegen/emit/instance-file.test.ts +0 -71
- package/src/codegen/emit/instance-file.ts +0 -257
- package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
- package/src/codegen/emit/namespace-loaders-file.ts +0 -229
- package/src/codegen/emit/types-file.test.ts +0 -96
- package/src/codegen/emit/types-file.ts +0 -142
- package/src/codegen/fixtures/billing-namespace.json +0 -6
- package/src/codegen/generate-i18n-types.test.ts +0 -1137
- package/src/codegen/generate-i18n-types.ts +0 -20
- package/src/codegen/icu-analysis.ts +0 -110
- package/src/codegen/locale-fallback.ts +0 -73
- package/src/codegen/locale-policy.test.ts +0 -35
- package/src/codegen/locale-policy.ts +0 -30
- package/src/codegen/paths.ts +0 -44
- package/src/codegen/project-locales-set-namespace.test.ts +0 -134
- package/src/codegen/read-dictionary.test.ts +0 -594
- package/src/codegen/read-dictionary.ts +0 -260
- package/src/codegen/regenerate-namespaces.ts +0 -180
- package/src/codegen/run-codegen.test.ts +0 -230
- package/src/codegen/run-codegen.ts +0 -252
- package/src/codegen/types.ts +0 -10
- package/src/codegen/write-file-if-changed.test.ts +0 -42
- package/src/codegen/write-file-if-changed.ts +0 -20
- package/src/codegen-config/build-config.ts +0 -15
- package/src/codegen-config/codegen-config.test.ts +0 -36
- package/src/codegen-config/index.ts +0 -37
- package/src/codegen-config/type-names.ts +0 -20
- package/src/codegen-config/write-config.ts +0 -8
- package/src/engine.ts +0 -47
- package/src/fetch-artifact.ts +0 -16
- package/src/format-core.ts +0 -91
- package/src/i18n-handle.ts +0 -216
- package/src/icu/extract-variables.test.ts +0 -199
- package/src/icu/extract-variables.ts +0 -227
- package/src/icu/parse-template.ts +0 -31
- package/src/index.ts +0 -63
- package/src/load-state.ts +0 -15
- package/src/project-locales.test.ts +0 -280
- package/src/project-locales.ts +0 -211
- package/src/resolve-locale.test.ts +0 -91
- package/src/resolve-locale.ts +0 -88
- package/src/scope-multi.ts +0 -105
- package/src/scope-types.ts +0 -35
- package/src/scope.test.ts +0 -172
- package/src/serialized-state.test.ts +0 -114
- package/src/serialized-state.ts +0 -28
- package/src/setup/setup-i18n.test.ts +0 -85
- package/src/setup/setup-i18n.ts +0 -156
- package/src/types.ts +0 -65
- package/src/validation/create-args-schema.ts +0 -76
- package/src/validation/create-normalized-schema.ts +0 -52
- package/src/validation/errors.ts +0 -43
- package/src/validation/index.ts +0 -202
- package/src/validation/normalize.ts +0 -299
- package/src/validation/to-dictionary.ts +0 -67
- package/src/validation/types.ts +0 -82
- package/src/validation/validate-normalized.ts +0 -133
- package/src/validation/validation.test.ts +0 -562
|
@@ -1,251 +0,0 @@
|
|
|
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 ns(locale) loaders in split 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
|
-
namespace: "default",
|
|
20
|
-
filePath: "src/i18n/translations/default.json",
|
|
21
|
-
absolutePath: path.join(projectRoot, "src/i18n/translations/default.json"),
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
namespace: "user",
|
|
25
|
-
filePath: "src/i18n/translations/user.json",
|
|
26
|
-
absolutePath: path.join(projectRoot, "src/i18n/translations/user.json"),
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
schemaTypeName: "AppSchema",
|
|
30
|
-
localeTypeName: "AppLocale",
|
|
31
|
-
typesModule: "i18n-types.generated",
|
|
32
|
-
importExtension: "none",
|
|
33
|
-
projectRoot,
|
|
34
|
-
delivery: "split-by-locale",
|
|
35
|
-
requestLocales: ["en", "it", "de-CH"],
|
|
36
|
-
splitPathsByNamespace: {
|
|
37
|
-
billing: {
|
|
38
|
-
en: "src/i18n/generated/translations/billing.en.json",
|
|
39
|
-
it: "src/i18n/generated/translations/billing.it.json",
|
|
40
|
-
"de-CH": "src/i18n/generated/translations/billing.de-CH.json",
|
|
41
|
-
},
|
|
42
|
-
default: {
|
|
43
|
-
en: "src/i18n/generated/translations/default.en.json",
|
|
44
|
-
it: "src/i18n/generated/translations/default.it.json",
|
|
45
|
-
"de-CH": "src/i18n/generated/translations/default.de-CH.json",
|
|
46
|
-
},
|
|
47
|
-
user: {
|
|
48
|
-
en: "src/i18n/generated/translations/user.en.json",
|
|
49
|
-
it: "src/i18n/generated/translations/user.it.json",
|
|
50
|
-
"de-CH": "src/i18n/generated/translations/user.de-CH.json",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(output).toContain(
|
|
56
|
-
"import type { AppSchema, LazyNamespace, AppLocale } from './i18n-types.generated';"
|
|
57
|
-
);
|
|
58
|
-
expect(output).toContain("[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;");
|
|
59
|
-
expect(output).toContain("billing: (locale) => {");
|
|
60
|
-
expect(output).toContain('case "en":');
|
|
61
|
-
expect(output).toContain('case "it":');
|
|
62
|
-
expect(output).toContain('case "de-CH":');
|
|
63
|
-
expect(output).toContain(
|
|
64
|
-
'throw new Error(`[i18n] No translation artifact for namespace "billing" and locale "${String(locale)}".`);'
|
|
65
|
-
);
|
|
66
|
-
expect(output).toContain(
|
|
67
|
-
"return import('./generated/translations/billing.en.json').then((m) => m.default);"
|
|
68
|
-
);
|
|
69
|
-
expect(output).toContain("user: (locale) => {");
|
|
70
|
-
expect(output).toContain(
|
|
71
|
-
'export const defaultLazyNamespaces = ["billing", "default", "user"] as const;'
|
|
72
|
-
);
|
|
73
|
-
expect(output.match(/from '\.\/i18n-types\.generated'/g)?.length).toBe(1);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("throws when a split path is missing", () => {
|
|
77
|
-
expect(() =>
|
|
78
|
-
formatNamespaceLoadersFile({
|
|
79
|
-
loadersOutputPath,
|
|
80
|
-
lazyEntries: [
|
|
81
|
-
{
|
|
82
|
-
namespace: "billing",
|
|
83
|
-
filePath: "src/i18n/translations/billing.json",
|
|
84
|
-
absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
schemaTypeName: "AppSchema",
|
|
88
|
-
localeTypeName: "AppLocale",
|
|
89
|
-
typesModule: "i18n-types.generated",
|
|
90
|
-
importExtension: "none",
|
|
91
|
-
projectRoot,
|
|
92
|
-
delivery: "split-by-locale",
|
|
93
|
-
requestLocales: ["en", "it"],
|
|
94
|
-
splitPathsByNamespace: {
|
|
95
|
-
billing: {
|
|
96
|
-
en: "src/i18n/generated/translations/billing.en.json",
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
).toThrow('[Codegen Error] Missing split path for namespace "billing", locale "it".');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("emits ns(area) loaders in custom mode", () => {
|
|
104
|
-
const output = formatNamespaceLoadersFile({
|
|
105
|
-
loadersOutputPath,
|
|
106
|
-
lazyEntries: [
|
|
107
|
-
{
|
|
108
|
-
namespace: "billing",
|
|
109
|
-
filePath: "src/i18n/translations/billing.json",
|
|
110
|
-
absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
namespace: "default",
|
|
114
|
-
filePath: "src/i18n/translations/default.json",
|
|
115
|
-
absolutePath: path.join(projectRoot, "src/i18n/translations/default.json"),
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
schemaTypeName: "AppSchema",
|
|
119
|
-
localeTypeName: "AppLocale",
|
|
120
|
-
typesModule: "i18n-types.generated",
|
|
121
|
-
importExtension: "none",
|
|
122
|
-
projectRoot,
|
|
123
|
-
delivery: "custom",
|
|
124
|
-
deliveryAreaTypeName: "AppDeliveryArea",
|
|
125
|
-
deliveryAreaNames: ["eu", "us"],
|
|
126
|
-
splitPathsByNamespace: {
|
|
127
|
-
billing: {
|
|
128
|
-
eu: "src/i18n/generated/translations/billing.eu.json",
|
|
129
|
-
us: "src/i18n/generated/translations/billing.us.json",
|
|
130
|
-
},
|
|
131
|
-
default: {
|
|
132
|
-
eu: "src/i18n/generated/translations/default.eu.json",
|
|
133
|
-
us: "src/i18n/generated/translations/default.us.json",
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(output).toContain(
|
|
139
|
-
"import type { AppSchema, LazyNamespace, AppDeliveryArea } from './i18n-types.generated';"
|
|
140
|
-
);
|
|
141
|
-
expect(output).toContain(
|
|
142
|
-
"[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
|
|
143
|
-
);
|
|
144
|
-
expect(output).toContain("billing: (area) => {");
|
|
145
|
-
expect(output).toContain('case "eu":');
|
|
146
|
-
expect(output).toContain('case "us":');
|
|
147
|
-
expect(output).toContain(
|
|
148
|
-
'throw new Error(`[i18n] No translation artifact for namespace "billing" and area "${String(area)}".`);'
|
|
149
|
-
);
|
|
150
|
-
expect(output).toContain(
|
|
151
|
-
"return import('./generated/translations/billing.eu.json').then((m) => m.default);"
|
|
152
|
-
);
|
|
153
|
-
expect(output).toContain(
|
|
154
|
-
'export const defaultLazyNamespaces = ["billing", "default"] as const;'
|
|
155
|
-
);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("throws when a custom delivery split path is missing", () => {
|
|
159
|
-
expect(() =>
|
|
160
|
-
formatNamespaceLoadersFile({
|
|
161
|
-
loadersOutputPath,
|
|
162
|
-
lazyEntries: [
|
|
163
|
-
{
|
|
164
|
-
namespace: "billing",
|
|
165
|
-
filePath: "src/i18n/translations/billing.json",
|
|
166
|
-
absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
schemaTypeName: "AppSchema",
|
|
170
|
-
localeTypeName: "AppLocale",
|
|
171
|
-
typesModule: "i18n-types.generated",
|
|
172
|
-
importExtension: "none",
|
|
173
|
-
projectRoot,
|
|
174
|
-
delivery: "custom",
|
|
175
|
-
deliveryAreaTypeName: "AppDeliveryArea",
|
|
176
|
-
deliveryAreaNames: ["eu", "us"],
|
|
177
|
-
splitPathsByNamespace: {
|
|
178
|
-
billing: {
|
|
179
|
-
eu: "src/i18n/generated/translations/billing.eu.json",
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
})
|
|
183
|
-
).toThrow('[Codegen Error] Missing split path for namespace "billing", area "us".');
|
|
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
|
-
});
|
|
251
|
-
});
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import type { DeliveryMode, LoaderStrategy } from "../codegen-config-schema.js";
|
|
3
|
-
import { GENERATED_FILE_BANNER, toImportPath, toRelativeModuleImport } from "../paths.js";
|
|
4
|
-
import type { ImportExtension, NamespaceEntry } from "../types.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Emits the generated `namespace-loaders.generated.ts` module: dynamic `import()`
|
|
8
|
-
* or injectable `fetchImpl(resourceId)` loaders, 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
|
-
loaderStrategy?: LoaderStrategy;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface PartitionedNamespaceLoadersParams {
|
|
27
|
-
partitionKeys: readonly string[];
|
|
28
|
-
paramName: string;
|
|
29
|
-
paramTypeName: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatDefaultNamespacesLiteral(lazyEntries: NamespaceEntry[]): string {
|
|
33
|
-
return [...lazyEntries]
|
|
34
|
-
.map((entry) => entry.namespace)
|
|
35
|
-
.sort()
|
|
36
|
-
.map((namespace) => JSON.stringify(namespace))
|
|
37
|
-
.join(", ");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function formatPartitionedTypesImport(
|
|
41
|
-
options: NamespaceLoadersFileOptions,
|
|
42
|
-
paramTypeName: string
|
|
43
|
-
): string {
|
|
44
|
-
const { schemaTypeName, typesModule, importExtension } = options;
|
|
45
|
-
return `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
46
|
-
}
|
|
47
|
-
|
|
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
|
-
}
|
|
56
|
-
|
|
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");
|
|
63
|
-
}
|
|
64
|
-
|
|
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
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function formatPartitionedNamespaceLoadersFile(
|
|
136
|
-
options: NamespaceLoadersFileOptions,
|
|
137
|
-
params: PartitionedNamespaceLoadersParams
|
|
138
|
-
): string {
|
|
139
|
-
const { partitionKeys, paramName, paramTypeName } = params;
|
|
140
|
-
const {
|
|
141
|
-
loadersOutputPath,
|
|
142
|
-
lazyEntries,
|
|
143
|
-
schemaTypeName,
|
|
144
|
-
projectRoot,
|
|
145
|
-
delivery,
|
|
146
|
-
splitPathsByNamespace = {},
|
|
147
|
-
} = options;
|
|
148
|
-
const loaderStrategy = options.loaderStrategy ?? "import";
|
|
149
|
-
|
|
150
|
-
const loaderValueType = `${schemaTypeName}[K]`;
|
|
151
|
-
const typesImport = formatPartitionedTypesImport(options, paramTypeName);
|
|
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,
|
|
168
|
-
})
|
|
169
|
-
)
|
|
170
|
-
.join("\n");
|
|
171
|
-
|
|
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
|
-
}
|
|
184
|
-
|
|
185
|
-
const loaderEntries = lazyEntries
|
|
186
|
-
.map((entry) =>
|
|
187
|
-
formatImportNamespaceLoaderEntry({
|
|
188
|
-
namespace: entry.namespace,
|
|
189
|
-
paramName,
|
|
190
|
-
partitionKeys,
|
|
191
|
-
splitPathsByNamespace,
|
|
192
|
-
loadersOutputPath,
|
|
193
|
-
projectRoot,
|
|
194
|
-
})
|
|
195
|
-
)
|
|
196
|
-
.join("\n");
|
|
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
|
-
|
|
201
|
-
return (
|
|
202
|
-
`${GENERATED_FILE_BANNER}` +
|
|
203
|
-
typesImport +
|
|
204
|
-
`export const namespaceLoaders: ${importLoadersType} = {\n${loaderEntries}\n};\n` +
|
|
205
|
-
`\n` +
|
|
206
|
-
`export const defaultLazyNamespaces = [${formatDefaultNamespacesLiteral(lazyEntries)}] as const;\n`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Builds `namespace-loaders.generated.ts` for split-by-locale or custom delivery areas.
|
|
212
|
-
*/
|
|
213
|
-
export function formatNamespaceLoadersFile(options: NamespaceLoadersFileOptions): string {
|
|
214
|
-
if (options.delivery === "split-by-locale") {
|
|
215
|
-
return formatPartitionedNamespaceLoadersFile(options, {
|
|
216
|
-
partitionKeys: options.requestLocales ?? [],
|
|
217
|
-
paramName: "locale",
|
|
218
|
-
paramTypeName: options.localeTypeName,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
if (!options.deliveryAreaTypeName) {
|
|
222
|
-
throw new Error("[Codegen Error] deliveryAreaTypeName is required for custom delivery.");
|
|
223
|
-
}
|
|
224
|
-
return formatPartitionedNamespaceLoadersFile(options, {
|
|
225
|
-
partitionKeys: options.deliveryAreaNames ?? [],
|
|
226
|
-
paramName: "area",
|
|
227
|
-
paramTypeName: options.deliveryAreaTypeName,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
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 and lazy aliases", () => {
|
|
10
|
-
const output = formatTypesFile({
|
|
11
|
-
entries: [
|
|
12
|
-
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
13
|
-
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
14
|
-
],
|
|
15
|
-
projectRoot,
|
|
16
|
-
typesOutputPath,
|
|
17
|
-
paramsTypeName: "AppParams",
|
|
18
|
-
schemaTypeName: "AppSchema",
|
|
19
|
-
localeTypeName: "AppLocale",
|
|
20
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
21
|
-
localeFallbackTypeName: "AppLocaleFallback",
|
|
22
|
-
paramsByNamespace: {
|
|
23
|
-
default: {
|
|
24
|
-
login_button: "never",
|
|
25
|
-
welcome: "{ name: string }",
|
|
26
|
-
},
|
|
27
|
-
billing: {
|
|
28
|
-
invoice_summary: "{ count: number }",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
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
|
-
});
|
|
37
|
-
|
|
38
|
-
expect(output).toContain("login_button: Partial<Record<AppLocale, string>>;");
|
|
39
|
-
expect(output).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
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>");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("emits delivery area type for custom delivery", () => {
|
|
47
|
-
const output = formatTypesFile({
|
|
48
|
-
entries: [
|
|
49
|
-
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
50
|
-
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
51
|
-
],
|
|
52
|
-
projectRoot,
|
|
53
|
-
typesOutputPath,
|
|
54
|
-
paramsTypeName: "AppParams",
|
|
55
|
-
schemaTypeName: "AppSchema",
|
|
56
|
-
localeTypeName: "AppLocale",
|
|
57
|
-
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
58
|
-
localeFallbackTypeName: "AppLocaleFallback",
|
|
59
|
-
localeFallback: {
|
|
60
|
-
"en-US": null,
|
|
61
|
-
it: "en-US",
|
|
62
|
-
fr: null,
|
|
63
|
-
},
|
|
64
|
-
paramsByNamespace: {
|
|
65
|
-
default: { some_key: "never" },
|
|
66
|
-
billing: { invoice_summary: "{ count: number }" },
|
|
67
|
-
},
|
|
68
|
-
requestLocales: ["en-US", "fr", "it"],
|
|
69
|
-
deliveryAreaTypeName: "AppDeliveryArea",
|
|
70
|
-
deliveryAreaNames: ["eu", "us"],
|
|
71
|
-
deliveryArtifacts: {
|
|
72
|
-
eu: ["it", "fr"],
|
|
73
|
-
us: ["en-US"],
|
|
74
|
-
},
|
|
75
|
-
lazyEntries: [
|
|
76
|
-
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
77
|
-
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
78
|
-
],
|
|
79
|
-
});
|
|
80
|
-
|
|
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];");
|
|
85
|
-
expect(output).toContain("export const DELIVERY_ARTIFACTS = {");
|
|
86
|
-
expect(output).toContain('"eu": ["fr", "it"] as const');
|
|
87
|
-
expect(output).toContain('"us": ["en-US"] as const');
|
|
88
|
-
expect(output).toContain("} as const satisfies Record<AppDeliveryArea, readonly AppLocale[]>;");
|
|
89
|
-
expect(output).toContain("export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;");
|
|
90
|
-
expect(output).toContain("export const LOCALE_DELIVERY_AREA = {");
|
|
91
|
-
expect(output).toContain('"it": "eu"');
|
|
92
|
-
expect(output).toContain('"en-US": "us"');
|
|
93
|
-
expect(output).toContain("} as const satisfies Record<AppLocale, AppDeliveryArea>;");
|
|
94
|
-
expect(output).toContain("some_key: Partial<Record<AppLocale, string>>;");
|
|
95
|
-
});
|
|
96
|
-
});
|