@xndrjs/i18n 0.6.1 → 0.7.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 +154 -141
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +268 -69
- package/dist/index.js +572 -65
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +23 -7
- package/dist/validation/index.js +196 -89
- package/dist/validation/index.js.map +1 -1
- package/package.json +1 -1
- package/src/IcuTranslationProviderMulti.test.ts +80 -234
- package/src/IcuTranslationProviderMulti.ts +101 -112
- package/src/IcuTranslationProviderSingle.test.ts +30 -309
- package/src/IcuTranslationProviderSingle.ts +42 -75
- package/src/builder-load-registry.ts +18 -0
- package/src/builder-loaders.ts +18 -0
- package/src/builder-multi.ts +481 -0
- package/src/builder-types.test.ts +80 -0
- package/src/builder-types.ts +24 -0
- package/src/builder.test.ts +421 -0
- package/src/builder.ts +112 -0
- package/src/codegen/delivery-artifacts.test.ts +2 -1
- package/src/codegen/delivery-artifacts.ts +2 -1
- package/src/codegen/emit/dictionary-file.test.ts +0 -7
- package/src/codegen/emit/dictionary-schema-file.ts +55 -42
- package/src/codegen/emit/instance-file.test.ts +88 -0
- package/src/codegen/emit/instance-file.ts +164 -17
- package/src/codegen/emit/namespace-loaders-file.test.ts +4 -18
- package/src/codegen/emit/namespace-loaders-file.ts +4 -54
- package/src/codegen/emit/types-file.test.ts +0 -2
- package/src/codegen/generate-i18n-types.test.ts +8 -303
- package/src/codegen/generate-i18n-types.ts +16 -1
- package/src/codegen/project-locales-set-namespace.test.ts +25 -32
- package/src/engine.ts +71 -0
- package/src/index.ts +45 -7
- package/src/patch-key.test.ts +186 -0
- package/src/patch-key.ts +140 -0
- package/src/project-locales.test.ts +16 -6
- package/src/project-locales.ts +17 -6
- package/src/scope-multi.ts +124 -0
- package/src/scope-single.ts +85 -0
- package/src/scope-types.ts +27 -0
- package/src/scope.test.ts +481 -0
- package/src/single-builder.ts +153 -0
- package/src/types.ts +16 -0
- package/src/validation/create-normalized-schema.ts +1 -1
- package/src/validation/errors.ts +2 -0
- package/src/validation/index.ts +135 -27
- package/src/validation/normalize.ts +176 -46
- package/src/validation/types.ts +4 -0
- package/src/validation/validate-normalized.ts +43 -1
- package/src/validation/validation.test.ts +200 -7
|
@@ -27,7 +27,6 @@ describe("formatDictionaryFile", () => {
|
|
|
27
27
|
|
|
28
28
|
expect(output).toContain("export const defaultDictionary: InitialSchema");
|
|
29
29
|
expect(output).toContain("defaultNs");
|
|
30
|
-
expect(output).not.toContain("defaultDictionaryFor");
|
|
31
30
|
});
|
|
32
31
|
|
|
33
32
|
it("emits an empty module when all namespaces are lazy in split mode (multi)", () => {
|
|
@@ -100,8 +99,6 @@ describe("formatDictionaryFile", () => {
|
|
|
100
99
|
"export function defaultDictionaryFor(locale: AppLocale): InitialSchema"
|
|
101
100
|
);
|
|
102
101
|
expect(output).toContain("return {\n default: defaultByLocale[locale],\n };");
|
|
103
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
104
|
-
expect(output).not.toContain("billingEn");
|
|
105
102
|
});
|
|
106
103
|
|
|
107
104
|
it("emits defaultDictionaryFor with per-locale imports in split mode (single)", () => {
|
|
@@ -136,7 +133,6 @@ describe("formatDictionaryFile", () => {
|
|
|
136
133
|
);
|
|
137
134
|
expect(output).toContain("export function defaultDictionaryFor(locale: AppLocale): AppSchema");
|
|
138
135
|
expect(output).toContain("return translationsByLocale[locale];");
|
|
139
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
140
136
|
});
|
|
141
137
|
|
|
142
138
|
it("emits defaultDictionaryFor with per-area imports in custom mode (multi)", () => {
|
|
@@ -178,8 +174,6 @@ describe("formatDictionaryFile", () => {
|
|
|
178
174
|
"export function defaultDictionaryFor(area: AppDeliveryArea): InitialSchema"
|
|
179
175
|
);
|
|
180
176
|
expect(output).toContain("return {\n default: defaultByArea[area],\n };");
|
|
181
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
182
|
-
expect(output).not.toContain("billingEu");
|
|
183
177
|
});
|
|
184
178
|
|
|
185
179
|
it("emits defaultDictionaryFor with per-area imports in custom mode (single)", () => {
|
|
@@ -217,6 +211,5 @@ describe("formatDictionaryFile", () => {
|
|
|
217
211
|
"export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
|
|
218
212
|
);
|
|
219
213
|
expect(output).toContain("return translationsByArea[area];");
|
|
220
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
221
214
|
});
|
|
222
215
|
});
|
|
@@ -79,58 +79,71 @@ export function formatDictionarySchemaFile(
|
|
|
79
79
|
importExtension: ImportExtension
|
|
80
80
|
): string {
|
|
81
81
|
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
82
|
-
const
|
|
83
|
-
?
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
const partialImport = isSingle
|
|
83
|
+
? ` validateExternalDictionaryPartial as validateExternalDictionaryPartialCore,\n` +
|
|
84
|
+
` validateExternalKey as validateExternalKeyCore,\n`
|
|
85
|
+
: ` validateExternalDictionaryPartial as validateExternalDictionaryPartialCore,\n` +
|
|
86
|
+
` validateExternalNamespacePartial as validateExternalNamespacePartialCore,\n` +
|
|
87
|
+
` validateExternalKey as validateExternalKeyCore,\n`;
|
|
88
|
+
|
|
89
|
+
const singleKeyValidator =
|
|
90
|
+
`export function validateExternalKey<K extends keyof ${schemaTypeName}>(\n` +
|
|
91
|
+
` key: K,\n` +
|
|
92
|
+
` input: unknown,\n` +
|
|
93
|
+
`): ValidationResult<Pick<${schemaTypeName}, K>> {\n` +
|
|
94
|
+
` return validateExternalKeyCore<Pick<${schemaTypeName}, K>>(\n` +
|
|
95
|
+
` key as string,\n` +
|
|
96
|
+
` input,\n` +
|
|
97
|
+
` DICTIONARY_SPEC,\n` +
|
|
98
|
+
` );\n` +
|
|
99
|
+
`}\n\n`;
|
|
100
|
+
|
|
101
|
+
const multiValidators =
|
|
102
|
+
`export function validateExternalNamespacePartial<NS extends keyof ${schemaTypeName}>(\n` +
|
|
103
|
+
` namespace: NS,\n` +
|
|
104
|
+
` input: unknown,\n` +
|
|
105
|
+
`): ValidationResult<Partial<${schemaTypeName}[NS]>> {\n` +
|
|
106
|
+
` return validateExternalNamespacePartialCore<Partial<${schemaTypeName}[NS]>>(\n` +
|
|
107
|
+
` namespace as string,\n` +
|
|
108
|
+
` input,\n` +
|
|
109
|
+
` DICTIONARY_SPEC,\n` +
|
|
110
|
+
` );\n` +
|
|
111
|
+
`}\n\n` +
|
|
112
|
+
`export function validateExternalKey<\n` +
|
|
113
|
+
` NS extends keyof ${schemaTypeName},\n` +
|
|
114
|
+
` K extends keyof ${schemaTypeName}[NS],\n` +
|
|
115
|
+
`>(\n` +
|
|
116
|
+
` namespace: NS,\n` +
|
|
117
|
+
` key: K,\n` +
|
|
118
|
+
` input: unknown,\n` +
|
|
119
|
+
`): ValidationResult<Pick<${schemaTypeName}[NS], K>> {\n` +
|
|
120
|
+
` return validateExternalKeyCore<Pick<${schemaTypeName}[NS], K>>(\n` +
|
|
121
|
+
` namespace as string,\n` +
|
|
122
|
+
` key as string,\n` +
|
|
123
|
+
` input,\n` +
|
|
124
|
+
` DICTIONARY_SPEC,\n` +
|
|
125
|
+
` );\n` +
|
|
126
|
+
`}\n\n`;
|
|
127
|
+
|
|
128
|
+
const keyValidator = isSingle ? singleKeyValidator : multiValidators;
|
|
97
129
|
|
|
98
130
|
return (
|
|
99
131
|
`${GENERATED_FILE_BANNER}` +
|
|
100
132
|
`import {\n` +
|
|
101
|
-
|
|
102
|
-
` validateNormalizedDictionary,\n` +
|
|
103
|
-
namespaceImport +
|
|
104
|
-
` toDictionary,\n` +
|
|
133
|
+
partialImport +
|
|
105
134
|
` type DictionarySpec,\n` +
|
|
106
|
-
` type NormalizedDictionary,\n` +
|
|
107
135
|
` type ValidationResult,\n` +
|
|
108
136
|
`} from '@xndrjs/i18n/validation';\n` +
|
|
109
137
|
`import type { ${schemaTypeName} } from '${typesImport}';\n\n` +
|
|
110
138
|
`${dictionarySpecBlock}\n` +
|
|
111
|
-
`export function
|
|
112
|
-
` input: unknown,\n` +
|
|
113
|
-
`): ValidationResult<NormalizedDictionary> {\n` +
|
|
114
|
-
` return normalizeDictionary(input, DICTIONARY_SPEC);\n` +
|
|
115
|
-
`}\n\n` +
|
|
116
|
-
`export function validateNormalizedExternalDictionary(\n` +
|
|
117
|
-
` normalized: NormalizedDictionary,\n` +
|
|
118
|
-
`): ValidationResult<NormalizedDictionary> {\n` +
|
|
119
|
-
` return validateNormalizedDictionary(normalized, DICTIONARY_SPEC);\n` +
|
|
120
|
-
`}\n\n` +
|
|
121
|
-
`export function validateExternalDictionary(\n` +
|
|
139
|
+
`export function validateExternalDictionaryPartial(\n` +
|
|
122
140
|
` input: unknown,\n` +
|
|
123
|
-
`): ValidationResult<${schemaTypeName}
|
|
124
|
-
`
|
|
125
|
-
`
|
|
126
|
-
`
|
|
127
|
-
`
|
|
128
|
-
` const step2 = validateNormalizedExternalDictionary(step1.data);\n` +
|
|
129
|
-
` if (!step2.ok) {\n` +
|
|
130
|
-
` return step2;\n` +
|
|
131
|
-
` }\n\n` +
|
|
132
|
-
` return { ok: true, data: toDictionary(step2.data) as ${schemaTypeName} };\n` +
|
|
141
|
+
`): ValidationResult<Partial<${schemaTypeName}>> {\n` +
|
|
142
|
+
` return validateExternalDictionaryPartialCore<Partial<${schemaTypeName}>>(\n` +
|
|
143
|
+
` input,\n` +
|
|
144
|
+
` DICTIONARY_SPEC,\n` +
|
|
145
|
+
` );\n` +
|
|
133
146
|
`}\n\n` +
|
|
134
|
-
|
|
147
|
+
keyValidator
|
|
135
148
|
);
|
|
136
149
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatInstanceFile, type InstanceFileOptions } from "./instance-file.js";
|
|
3
|
+
|
|
4
|
+
function baseOptions(overrides: Partial<InstanceFileOptions> = {}): InstanceFileOptions {
|
|
5
|
+
return {
|
|
6
|
+
isSingle: false,
|
|
7
|
+
hasLazy: false,
|
|
8
|
+
typesOutputPath: "src/i18n/i18n-types.generated.ts",
|
|
9
|
+
paramsTypeName: "AppParams",
|
|
10
|
+
schemaTypeName: "AppSchema",
|
|
11
|
+
localeTypeName: "AppLocale",
|
|
12
|
+
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
13
|
+
factoryName: "createI18n",
|
|
14
|
+
hasLocaleFallback: true,
|
|
15
|
+
hasLocaleType: true,
|
|
16
|
+
namespaceNames: ["default", "billing"],
|
|
17
|
+
importExtension: "none",
|
|
18
|
+
delivery: "canonical",
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function packageImportBlock(output: string): string {
|
|
24
|
+
const match = output.match(/import \{([\s\S]*?)\} from '@xndrjs\/i18n'/);
|
|
25
|
+
return match?.[1] ?? "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("formatInstanceFile package imports", () => {
|
|
29
|
+
it("emits only single-scope symbols for single eager projects", () => {
|
|
30
|
+
const imports = packageImportBlock(formatInstanceFile(baseOptions({ isSingle: true })));
|
|
31
|
+
|
|
32
|
+
expect(imports).toContain("IcuTranslationProviderSingle");
|
|
33
|
+
expect(imports).toContain("projectNamespaceLocalesCore");
|
|
34
|
+
expect(imports).toContain("type I18nScopeSingle");
|
|
35
|
+
expect(imports).toContain("type OnMissingTranslation");
|
|
36
|
+
expect(imports).not.toContain("createI18nBuilder");
|
|
37
|
+
expect(imports).not.toContain("createI18nMultiBuilder");
|
|
38
|
+
expect(imports).not.toContain("I18nBuilderMultiInitial");
|
|
39
|
+
expect(imports).not.toContain("I18nBuilderMultiOptions");
|
|
40
|
+
expect(imports).not.toContain("I18nBuilderMultiPartitioned");
|
|
41
|
+
expect(imports).not.toContain("I18nScopeMulti");
|
|
42
|
+
expect(imports).not.toContain("projectDictionaryLocalesCore");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("emits multi eager scope symbols without builder imports", () => {
|
|
46
|
+
const imports = packageImportBlock(formatInstanceFile(baseOptions()));
|
|
47
|
+
|
|
48
|
+
expect(imports).toContain("IcuTranslationProviderMulti");
|
|
49
|
+
expect(imports).toContain("projectDictionaryLocalesCore");
|
|
50
|
+
expect(imports).toContain("type I18nScopeMulti");
|
|
51
|
+
expect(imports).not.toContain("createI18nMultiBuilder");
|
|
52
|
+
expect(imports).not.toContain("I18nBuilderMultiInitial");
|
|
53
|
+
expect(imports).not.toContain("I18nScopeSingle");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("emits lazy multi builder symbols instead of I18nScopeMulti", () => {
|
|
57
|
+
const imports = packageImportBlock(
|
|
58
|
+
formatInstanceFile(
|
|
59
|
+
baseOptions({
|
|
60
|
+
hasLazy: true,
|
|
61
|
+
namespaceLoadersOutputPath: "src/i18n/namespace-loaders.generated.ts",
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(imports).toContain("createI18nMultiBuilder");
|
|
67
|
+
expect(imports).toContain("type I18nBuilderMultiInitial");
|
|
68
|
+
expect(imports).toContain("type I18nBuilderMultiOptions");
|
|
69
|
+
expect(imports).not.toContain("type I18nScopeMulti");
|
|
70
|
+
expect(imports).not.toContain("createI18nBuilder");
|
|
71
|
+
expect(imports).not.toContain("I18nBuilderMultiPartitioned");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("includes delivery-area projection helpers when delivery is custom", () => {
|
|
75
|
+
const imports = packageImportBlock(
|
|
76
|
+
formatInstanceFile(
|
|
77
|
+
baseOptions({
|
|
78
|
+
delivery: "custom",
|
|
79
|
+
deliveryAreaTypeName: "AppDeliveryArea",
|
|
80
|
+
deliveryArtifactsTypeName: "AppDeliveryArtifacts",
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(imports).toContain("projectNamespaceForDeliveryAreaCore");
|
|
86
|
+
expect(imports).toContain("projectDictionaryForDeliveryAreaCore");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -10,6 +10,7 @@ export interface InstanceFileOptions {
|
|
|
10
10
|
isSingle: boolean;
|
|
11
11
|
hasLazy: boolean;
|
|
12
12
|
typesOutputPath: string;
|
|
13
|
+
namespaceLoadersOutputPath?: string;
|
|
13
14
|
paramsTypeName: string;
|
|
14
15
|
schemaTypeName: string;
|
|
15
16
|
localeTypeName: string;
|
|
@@ -20,6 +21,8 @@ export interface InstanceFileOptions {
|
|
|
20
21
|
namespaceNames: string[];
|
|
21
22
|
importExtension: ImportExtension;
|
|
22
23
|
delivery: DeliveryMode;
|
|
24
|
+
deliveryAreaTypeName?: string;
|
|
25
|
+
deliveryArtifactsTypeName?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
@@ -27,6 +30,7 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
27
30
|
isSingle,
|
|
28
31
|
hasLazy,
|
|
29
32
|
typesOutputPath,
|
|
33
|
+
namespaceLoadersOutputPath,
|
|
30
34
|
paramsTypeName,
|
|
31
35
|
schemaTypeName,
|
|
32
36
|
localeTypeName,
|
|
@@ -37,16 +41,28 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
37
41
|
namespaceNames,
|
|
38
42
|
importExtension,
|
|
39
43
|
delivery,
|
|
44
|
+
deliveryAreaTypeName,
|
|
45
|
+
deliveryArtifactsTypeName,
|
|
40
46
|
} = options;
|
|
41
47
|
|
|
42
48
|
const emitDeliveryAreaHelpers = delivery === "custom";
|
|
43
49
|
const providerClass = isSingle ? "IcuTranslationProviderSingle" : "IcuTranslationProviderMulti";
|
|
44
50
|
const typesModule = toModuleBasename(typesOutputPath);
|
|
45
51
|
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
52
|
+
const loadersModule = namespaceLoadersOutputPath
|
|
53
|
+
? toModuleBasename(namespaceLoadersOutputPath)
|
|
54
|
+
: null;
|
|
55
|
+
const loadersImport =
|
|
56
|
+
loadersModule && namespaceLoadersOutputPath
|
|
57
|
+
? toRelativeModuleImport(loadersModule, importExtension)
|
|
58
|
+
: null;
|
|
46
59
|
const dictionaryParamType = hasLazy ? "InitialSchema" : schemaTypeName;
|
|
47
|
-
const schemaTypesImport =
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
const schemaTypesImport =
|
|
61
|
+
hasLazy && delivery === "custom" && deliveryAreaTypeName && deliveryArtifactsTypeName
|
|
62
|
+
? `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema, ${deliveryAreaTypeName}, ${deliveryArtifactsTypeName} } from '${typesImport}';\n`
|
|
63
|
+
: hasLazy
|
|
64
|
+
? `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema } from '${typesImport}';\n`
|
|
65
|
+
: `import type { ${paramsTypeName}, ${schemaTypeName} } from '${typesImport}';\n`;
|
|
50
66
|
const providerTypeArgs = hasLocaleFallback
|
|
51
67
|
? `${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, typeof ${localeFallbackConstName}`
|
|
52
68
|
: `${schemaTypeName}, ${paramsTypeName}`;
|
|
@@ -60,7 +76,12 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
60
76
|
: "";
|
|
61
77
|
const localesParamType = hasLocaleType ? `readonly ${localeTypeName}[]` : "readonly string[]";
|
|
62
78
|
const fallbackArg = hasLocaleFallback ? `, ${localeFallbackConstName}` : "";
|
|
63
|
-
const
|
|
79
|
+
const packageImports = formatPackageImports(
|
|
80
|
+
providerClass,
|
|
81
|
+
isSingle,
|
|
82
|
+
hasLazy,
|
|
83
|
+
emitDeliveryAreaHelpers
|
|
84
|
+
);
|
|
64
85
|
const projectionBlock = isSingle
|
|
65
86
|
? formatSingleProjectionBlock(
|
|
66
87
|
schemaTypeName,
|
|
@@ -76,36 +97,162 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
76
97
|
emitDeliveryAreaHelpers
|
|
77
98
|
);
|
|
78
99
|
|
|
100
|
+
const createFactoryBlock = formatCreateI18nFactory({
|
|
101
|
+
isSingle,
|
|
102
|
+
hasLazy,
|
|
103
|
+
hasLocaleFallback,
|
|
104
|
+
providerClass,
|
|
105
|
+
providerTypeArgs,
|
|
106
|
+
providerOptions,
|
|
107
|
+
factoryName,
|
|
108
|
+
dictionaryParamType,
|
|
109
|
+
schemaTypeName,
|
|
110
|
+
paramsTypeName,
|
|
111
|
+
localeTypeName,
|
|
112
|
+
namespaceNames,
|
|
113
|
+
loadersImport,
|
|
114
|
+
delivery,
|
|
115
|
+
...(deliveryAreaTypeName !== undefined ? { deliveryAreaTypeName } : {}),
|
|
116
|
+
...(deliveryArtifactsTypeName !== undefined ? { deliveryArtifactsTypeName } : {}),
|
|
117
|
+
});
|
|
118
|
+
|
|
79
119
|
return (
|
|
80
120
|
`${GENERATED_FILE_BANNER}` +
|
|
81
|
-
`import {
|
|
121
|
+
`import {\n` +
|
|
122
|
+
` ${packageImports},\n` +
|
|
123
|
+
`} from '@xndrjs/i18n';\n` +
|
|
82
124
|
schemaTypesImport +
|
|
83
125
|
typesImportLine +
|
|
126
|
+
(hasLazy && loadersImport ? `import { namespaceLoaders } from '${loadersImport}';\n` : "") +
|
|
84
127
|
`\n` +
|
|
128
|
+
createFactoryBlock +
|
|
129
|
+
projectionBlock
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatCreateI18nFactory(options: {
|
|
134
|
+
isSingle: boolean;
|
|
135
|
+
hasLazy: boolean;
|
|
136
|
+
hasLocaleFallback: boolean;
|
|
137
|
+
providerClass: string;
|
|
138
|
+
providerTypeArgs: string;
|
|
139
|
+
providerOptions: string;
|
|
140
|
+
factoryName: string;
|
|
141
|
+
dictionaryParamType: string;
|
|
142
|
+
schemaTypeName: string;
|
|
143
|
+
paramsTypeName: string;
|
|
144
|
+
localeTypeName: string;
|
|
145
|
+
namespaceNames: string[];
|
|
146
|
+
loadersImport: string | null;
|
|
147
|
+
delivery: DeliveryMode;
|
|
148
|
+
deliveryAreaTypeName?: string;
|
|
149
|
+
deliveryArtifactsTypeName?: string;
|
|
150
|
+
}): string {
|
|
151
|
+
const {
|
|
152
|
+
isSingle,
|
|
153
|
+
hasLazy,
|
|
154
|
+
providerClass,
|
|
155
|
+
providerTypeArgs,
|
|
156
|
+
providerOptions,
|
|
157
|
+
factoryName,
|
|
158
|
+
dictionaryParamType,
|
|
159
|
+
schemaTypeName,
|
|
160
|
+
paramsTypeName,
|
|
161
|
+
localeTypeName,
|
|
162
|
+
namespaceNames,
|
|
163
|
+
loadersImport,
|
|
164
|
+
delivery,
|
|
165
|
+
deliveryAreaTypeName,
|
|
166
|
+
deliveryArtifactsTypeName,
|
|
167
|
+
} = options;
|
|
168
|
+
|
|
169
|
+
if (isSingle) {
|
|
170
|
+
return (
|
|
171
|
+
`export function ${factoryName}(\n` +
|
|
172
|
+
` dictionary: ${dictionaryParamType},\n` +
|
|
173
|
+
` options?: { onMissing?: OnMissingTranslation },\n` +
|
|
174
|
+
`): I18nScopeSingle<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}> {\n` +
|
|
175
|
+
` return new ${providerClass}<${providerTypeArgs}>(dictionary${providerOptions}).toScope();\n` +
|
|
176
|
+
`}\n`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!hasLazy) {
|
|
181
|
+
const namespacesLiteral = `[${namespaceNames
|
|
182
|
+
.slice()
|
|
183
|
+
.sort()
|
|
184
|
+
.map((ns) => JSON.stringify(ns))
|
|
185
|
+
.join(", ")}] as const`;
|
|
186
|
+
return (
|
|
187
|
+
`export function ${factoryName}(\n` +
|
|
188
|
+
` dictionary: ${dictionaryParamType},\n` +
|
|
189
|
+
` options?: { onMissing?: OnMissingTranslation },\n` +
|
|
190
|
+
`): I18nScopeMulti<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}> {\n` +
|
|
191
|
+
` const engine = new ${providerClass}<${providerTypeArgs}>(dictionary${providerOptions});\n` +
|
|
192
|
+
` return engine.toScope({ namespaces: ${namespacesLiteral} });\n` +
|
|
193
|
+
`}\n`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!loadersImport) {
|
|
198
|
+
throw new Error("[Codegen Error] namespaceLoadersOutputPath is required when hasLazy is true.");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const builderReturnType =
|
|
202
|
+
delivery === "custom" && deliveryAreaTypeName && deliveryArtifactsTypeName
|
|
203
|
+
? `I18nBuilderMultiInitial<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, ${localeTypeName}, ${deliveryAreaTypeName}, ${deliveryArtifactsTypeName}>`
|
|
204
|
+
: `I18nBuilderMultiInitial<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}>`;
|
|
205
|
+
|
|
206
|
+
const multiBuilderTypeArgs =
|
|
207
|
+
delivery === "custom" && deliveryAreaTypeName && deliveryArtifactsTypeName
|
|
208
|
+
? `<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, ${deliveryAreaTypeName}, ${deliveryArtifactsTypeName}>`
|
|
209
|
+
: `<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}>`;
|
|
210
|
+
|
|
211
|
+
return (
|
|
85
212
|
`export function ${factoryName}(\n` +
|
|
86
213
|
` dictionary: ${dictionaryParamType},\n` +
|
|
87
214
|
` options?: { onMissing?: OnMissingTranslation },\n` +
|
|
88
|
-
`) {\n` +
|
|
89
|
-
`
|
|
90
|
-
`}\n` +
|
|
91
|
-
|
|
215
|
+
`): ${builderReturnType} {\n` +
|
|
216
|
+
` const engine = new ${providerClass}<${providerTypeArgs}>(dictionary${providerOptions});\n` +
|
|
217
|
+
` return createI18nMultiBuilder${multiBuilderTypeArgs}(engine, {\n` +
|
|
218
|
+
` namespaceLoaders,\n` +
|
|
219
|
+
` } as I18nBuilderMultiOptions<${schemaTypeName}, ${localeTypeName}>);\n` +
|
|
220
|
+
`}\n`
|
|
92
221
|
);
|
|
93
222
|
}
|
|
94
223
|
|
|
95
|
-
function
|
|
224
|
+
function formatPackageImports(
|
|
225
|
+
providerClass: string,
|
|
226
|
+
isSingle: boolean,
|
|
227
|
+
hasLazy: boolean,
|
|
228
|
+
emitDeliveryAreaHelpers: boolean
|
|
229
|
+
): string {
|
|
230
|
+
const imports = [providerClass];
|
|
231
|
+
|
|
96
232
|
if (isSingle) {
|
|
97
|
-
|
|
233
|
+
imports.push("projectNamespaceLocalesCore");
|
|
98
234
|
if (emitDeliveryAreaHelpers) {
|
|
99
235
|
imports.push("projectNamespaceForDeliveryAreaCore");
|
|
100
236
|
}
|
|
101
|
-
|
|
237
|
+
imports.push("type I18nScopeSingle");
|
|
238
|
+
} else {
|
|
239
|
+
imports.push("projectNamespaceLocalesCore", "projectDictionaryLocalesCore");
|
|
240
|
+
if (emitDeliveryAreaHelpers) {
|
|
241
|
+
imports.push("projectNamespaceForDeliveryAreaCore", "projectDictionaryForDeliveryAreaCore");
|
|
242
|
+
}
|
|
243
|
+
if (hasLazy) {
|
|
244
|
+
imports.push(
|
|
245
|
+
"createI18nMultiBuilder",
|
|
246
|
+
"type I18nBuilderMultiInitial",
|
|
247
|
+
"type I18nBuilderMultiOptions"
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
imports.push("type I18nScopeMulti");
|
|
251
|
+
}
|
|
102
252
|
}
|
|
103
253
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
imports.push("projectNamespaceForDeliveryAreaCore", "projectDictionaryForDeliveryAreaCore");
|
|
107
|
-
}
|
|
108
|
-
return imports.join(", ");
|
|
254
|
+
imports.push("type OnMissingTranslation");
|
|
255
|
+
return imports.join(",\n ");
|
|
109
256
|
}
|
|
110
257
|
|
|
111
258
|
function formatSingleProjectionBlock(
|
|
@@ -31,8 +31,6 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
31
31
|
expect(output).toContain(
|
|
32
32
|
"billing: () => import('./translations/billing.json').then((m) => m.default),"
|
|
33
33
|
);
|
|
34
|
-
expect(output).not.toContain("AppLocale");
|
|
35
|
-
expect(output).not.toContain("ensureNamespacesLoadedForLocale");
|
|
36
34
|
});
|
|
37
35
|
|
|
38
36
|
it("emits ns(locale) loaders in split mode", () => {
|
|
@@ -97,18 +95,13 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
97
95
|
"return import('./generated/translations/billing.en.json').then((m) => m.default);"
|
|
98
96
|
);
|
|
99
97
|
expect(output).toContain("user: (locale) => {");
|
|
100
|
-
expect(output).not.toContain("billing: {");
|
|
101
|
-
expect(output).not.toContain("import('./translations/billing.json')");
|
|
102
|
-
expect(output).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
103
|
-
expect(output).toContain("i18n.mergeNamespace(namespace,");
|
|
104
|
-
expect(output).toContain("i18n: I18nMultiInstance,");
|
|
105
98
|
expect(output).toContain(
|
|
106
|
-
'
|
|
99
|
+
'export const defaultLazyNamespaces = ["billing", "default", "user"] as const;'
|
|
107
100
|
);
|
|
108
101
|
expect(output.match(/from '\.\/i18n-types\.generated'/g)?.length).toBe(1);
|
|
109
102
|
});
|
|
110
103
|
|
|
111
|
-
it("emits
|
|
104
|
+
it("emits locale-scoped loaders for single mode in split delivery", () => {
|
|
112
105
|
const output = formatNamespaceLoadersFile({
|
|
113
106
|
loadersOutputPath,
|
|
114
107
|
lazyEntries: [
|
|
@@ -138,11 +131,7 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
138
131
|
});
|
|
139
132
|
|
|
140
133
|
expect(output).toContain("[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema>;");
|
|
141
|
-
expect(output).toContain(
|
|
142
|
-
expect(output).toContain("type I18nSingleInstance = TranslationProviderSingle<");
|
|
143
|
-
expect(output).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
144
|
-
expect(output).toContain("i18n.mergeAll(await namespaceLoaders[namespace](locale));");
|
|
145
|
-
expect(output).not.toContain("mergeNamespace");
|
|
134
|
+
expect(output).toContain('export const defaultLazyNamespaces = ["default"] as const;');
|
|
146
135
|
});
|
|
147
136
|
|
|
148
137
|
it("throws when a split path is missing", () => {
|
|
@@ -226,11 +215,8 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
226
215
|
expect(output).toContain(
|
|
227
216
|
"return import('./generated/translations/billing.eu.json').then((m) => m.default);"
|
|
228
217
|
);
|
|
229
|
-
expect(output).not.toContain("import('./translations/billing.json')");
|
|
230
|
-
expect(output).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
231
|
-
expect(output).toContain("i18n.mergeNamespace(namespace,");
|
|
232
218
|
expect(output).toContain(
|
|
233
|
-
'
|
|
219
|
+
'export const defaultLazyNamespaces = ["billing", "default"] as const;'
|
|
234
220
|
);
|
|
235
221
|
});
|
|
236
222
|
|
|
@@ -30,49 +30,6 @@ interface PartitionedNamespaceLoadersParams {
|
|
|
30
30
|
partitionKeys: readonly string[];
|
|
31
31
|
paramName: string;
|
|
32
32
|
paramTypeName: string;
|
|
33
|
-
loadNamespacesFunctionName: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function formatI18nMultiInstanceType(options: NamespaceLoadersFileOptions): string {
|
|
37
|
-
const { schemaTypeName, paramsTypeName, localeTypeName } = options;
|
|
38
|
-
|
|
39
|
-
return `TranslationProviderMulti<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}>`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function formatI18nSingleInstanceType(options: NamespaceLoadersFileOptions): string {
|
|
43
|
-
const { schemaTypeName, paramsTypeName, localeTypeName } = options;
|
|
44
|
-
|
|
45
|
-
return `TranslationProviderSingle<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}>`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function formatLoadNamespacesHelper(
|
|
49
|
-
options: NamespaceLoadersFileOptions,
|
|
50
|
-
{ paramName, paramTypeName, loadNamespacesFunctionName }: PartitionedNamespaceLoadersParams
|
|
51
|
-
): string {
|
|
52
|
-
const { lazyEntries, isSingle } = options;
|
|
53
|
-
const defaultNamespaces = formatDefaultNamespacesLiteral(lazyEntries);
|
|
54
|
-
const i18nInstanceType = isSingle
|
|
55
|
-
? formatI18nSingleInstanceType(options)
|
|
56
|
-
: formatI18nMultiInstanceType(options);
|
|
57
|
-
const mergeCall = isSingle
|
|
58
|
-
? `i18n.mergeAll(await namespaceLoaders[namespace](${paramName}))`
|
|
59
|
-
: `i18n.mergeNamespace(namespace, await namespaceLoaders[namespace](${paramName}))`;
|
|
60
|
-
const i18nTypeAlias = isSingle ? "I18nSingleInstance" : "I18nMultiInstance";
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
`\ntype ${i18nTypeAlias} = ${i18nInstanceType};\n\n` +
|
|
64
|
-
`export async function ${loadNamespacesFunctionName}(\n` +
|
|
65
|
-
` i18n: ${i18nTypeAlias},\n` +
|
|
66
|
-
` ${paramName}: ${paramTypeName},\n` +
|
|
67
|
-
` namespaces: readonly LazyNamespace[] = [${defaultNamespaces}] as const,\n` +
|
|
68
|
-
`): Promise<void> {\n` +
|
|
69
|
-
` await Promise.all(\n` +
|
|
70
|
-
` namespaces.map(async (namespace) => {\n` +
|
|
71
|
-
` ${mergeCall};\n` +
|
|
72
|
-
` }),\n` +
|
|
73
|
-
` );\n` +
|
|
74
|
-
`}\n`
|
|
75
|
-
);
|
|
76
33
|
}
|
|
77
34
|
|
|
78
35
|
function formatDefaultNamespacesLiteral(lazyEntries: NamespaceEntry[]): string {
|
|
@@ -91,17 +48,11 @@ function formatPartitionedTypesImport(
|
|
|
91
48
|
options;
|
|
92
49
|
|
|
93
50
|
if (isSingle) {
|
|
94
|
-
return (
|
|
95
|
-
`import type { TranslationProviderSingle } from '@xndrjs/i18n';\n` +
|
|
96
|
-
`import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName}, ${localeTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`
|
|
97
|
-
);
|
|
51
|
+
return `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName}, ${localeTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
98
52
|
}
|
|
99
53
|
|
|
100
54
|
const paramTypeImport = paramTypeName === localeTypeName ? "" : `, ${paramTypeName}`;
|
|
101
|
-
return (
|
|
102
|
-
`import type { TranslationProviderMulti } from '@xndrjs/i18n';\n` +
|
|
103
|
-
`import type { ${localeTypeName}, ${paramsTypeName}, ${schemaTypeName}, LazyNamespace${paramTypeImport} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`
|
|
104
|
-
);
|
|
55
|
+
return `import type { ${localeTypeName}, ${paramsTypeName}, ${schemaTypeName}, LazyNamespace${paramTypeImport} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
105
56
|
}
|
|
106
57
|
|
|
107
58
|
function formatSwitchDefaultCase(namespace: string, paramName: string): string {
|
|
@@ -169,7 +120,8 @@ function formatPartitionedNamespaceLoadersFile(
|
|
|
169
120
|
`export const namespaceLoaders: {\n` +
|
|
170
121
|
` [K in LazyNamespace]: (${paramName}: ${paramTypeName}) => Promise<${loaderValueType}>;\n` +
|
|
171
122
|
`} = {\n${loaderEntries}\n};\n` +
|
|
172
|
-
|
|
123
|
+
`\n` +
|
|
124
|
+
`export const defaultLazyNamespaces = [${formatDefaultNamespacesLiteral(lazyEntries)}] as const;\n`
|
|
173
125
|
);
|
|
174
126
|
}
|
|
175
127
|
|
|
@@ -204,7 +156,6 @@ export function formatNamespaceLoadersFile(options: NamespaceLoadersFileOptions)
|
|
|
204
156
|
partitionKeys: options.requestLocales ?? [],
|
|
205
157
|
paramName: "locale",
|
|
206
158
|
paramTypeName: options.localeTypeName,
|
|
207
|
-
loadNamespacesFunctionName: "ensureNamespacesLoadedForLocale",
|
|
208
159
|
});
|
|
209
160
|
}
|
|
210
161
|
if (delivery === "custom") {
|
|
@@ -215,7 +166,6 @@ export function formatNamespaceLoadersFile(options: NamespaceLoadersFileOptions)
|
|
|
215
166
|
partitionKeys: options.deliveryAreaNames ?? [],
|
|
216
167
|
paramName: "area",
|
|
217
168
|
paramTypeName: options.deliveryAreaTypeName,
|
|
218
|
-
loadNamespacesFunctionName: "ensureNamespacesLoadedForArea",
|
|
219
169
|
});
|
|
220
170
|
}
|
|
221
171
|
return formatCanonicalNamespaceLoadersFile(options);
|
|
@@ -37,7 +37,6 @@ describe("formatTypesFile", () => {
|
|
|
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
|
-
expect(output).not.toContain("typeof import");
|
|
41
40
|
});
|
|
42
41
|
|
|
43
42
|
it("emits explicit Partial schema types for single-file mode", () => {
|
|
@@ -65,7 +64,6 @@ describe("formatTypesFile", () => {
|
|
|
65
64
|
expect(output).toContain(
|
|
66
65
|
"export type AppSchema = {\n welcome: Partial<Record<AppLocale, string>>;\n};"
|
|
67
66
|
);
|
|
68
|
-
expect(output).not.toContain("typeof import");
|
|
69
67
|
});
|
|
70
68
|
|
|
71
69
|
it("emits delivery area type for custom delivery", () => {
|