@xndrjs/i18n 0.6.0 → 0.7.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 +154 -134
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +276 -61
- package/dist/index.js +687 -126
- 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 +115 -215
- package/src/IcuTranslationProviderMulti.ts +107 -96
- package/src/IcuTranslationProviderSingle.test.ts +36 -294
- package/src/IcuTranslationProviderSingle.ts +55 -72
- 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 +35 -10
- package/src/codegen/emit/namespace-loaders-file.ts +11 -61
- 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 +47 -7
- package/src/patch-key.test.ts +186 -0
- package/src/patch-key.ts +140 -0
- package/src/project-locales.test.ts +76 -0
- package/src/project-locales.ts +58 -1
- 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
|
@@ -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,16 +95,45 @@ 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: I18nMultiInstance,");
|
|
104
98
|
expect(output).toContain(
|
|
105
|
-
'
|
|
99
|
+
'export const defaultLazyNamespaces = ["billing", "default", "user"] as const;'
|
|
106
100
|
);
|
|
107
101
|
expect(output.match(/from '\.\/i18n-types\.generated'/g)?.length).toBe(1);
|
|
108
102
|
});
|
|
109
103
|
|
|
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
|
+
|
|
110
137
|
it("throws when a split path is missing", () => {
|
|
111
138
|
expect(() =>
|
|
112
139
|
formatNamespaceLoadersFile({
|
|
@@ -188,10 +215,8 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
188
215
|
expect(output).toContain(
|
|
189
216
|
"return import('./generated/translations/billing.eu.json').then((m) => m.default);"
|
|
190
217
|
);
|
|
191
|
-
expect(output).not.toContain("import('./translations/billing.json')");
|
|
192
|
-
expect(output).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
193
218
|
expect(output).toContain(
|
|
194
|
-
'
|
|
219
|
+
'export const defaultLazyNamespaces = ["billing", "default"] as const;'
|
|
195
220
|
);
|
|
196
221
|
});
|
|
197
222
|
|
|
@@ -30,23 +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 {
|
|
38
|
-
schemaTypeName,
|
|
39
|
-
paramsTypeName,
|
|
40
|
-
localeTypeName,
|
|
41
|
-
localeFallbackConstName,
|
|
42
|
-
hasLocaleFallback,
|
|
43
|
-
} = options;
|
|
44
|
-
|
|
45
|
-
const typeArgs = hasLocaleFallback
|
|
46
|
-
? `${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, typeof ${localeFallbackConstName}`
|
|
47
|
-
: `${schemaTypeName}, ${paramsTypeName}`;
|
|
48
|
-
|
|
49
|
-
return `IcuTranslationProviderMulti<${typeArgs}>`;
|
|
50
33
|
}
|
|
51
34
|
|
|
52
35
|
function formatDefaultNamespacesLiteral(lazyEntries: NamespaceEntry[]): string {
|
|
@@ -57,54 +40,19 @@ function formatDefaultNamespacesLiteral(lazyEntries: NamespaceEntry[]): string {
|
|
|
57
40
|
.join(", ");
|
|
58
41
|
}
|
|
59
42
|
|
|
60
|
-
function formatLoadNamespacesHelper(
|
|
61
|
-
options: NamespaceLoadersFileOptions,
|
|
62
|
-
{ paramName, paramTypeName, loadNamespacesFunctionName }: PartitionedNamespaceLoadersParams
|
|
63
|
-
): string {
|
|
64
|
-
const { lazyEntries } = options;
|
|
65
|
-
const defaultNamespaces = formatDefaultNamespacesLiteral(lazyEntries);
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
`\ntype I18nMultiInstance = ${formatI18nMultiInstanceType(options)};\n\n` +
|
|
69
|
-
`export async function ${loadNamespacesFunctionName}(\n` +
|
|
70
|
-
` i18n: I18nMultiInstance,\n` +
|
|
71
|
-
` ${paramName}: ${paramTypeName},\n` +
|
|
72
|
-
` namespaces: readonly LazyNamespace[] = [${defaultNamespaces}] as const,\n` +
|
|
73
|
-
`): Promise<void> {\n` +
|
|
74
|
-
` await Promise.all(\n` +
|
|
75
|
-
` namespaces.map(async (namespace) => {\n` +
|
|
76
|
-
` i18n.setNamespace(namespace, await namespaceLoaders[namespace](${paramName}));\n` +
|
|
77
|
-
` }),\n` +
|
|
78
|
-
` );\n` +
|
|
79
|
-
`}\n`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
43
|
function formatPartitionedTypesImport(
|
|
84
44
|
options: NamespaceLoadersFileOptions,
|
|
85
45
|
paramTypeName: string
|
|
86
46
|
): string {
|
|
87
|
-
const {
|
|
88
|
-
|
|
89
|
-
paramsTypeName,
|
|
90
|
-
localeTypeName,
|
|
91
|
-
localeFallbackConstName,
|
|
92
|
-
hasLocaleFallback,
|
|
93
|
-
typesModule,
|
|
94
|
-
importExtension,
|
|
95
|
-
isSingle,
|
|
96
|
-
} = options;
|
|
47
|
+
const { schemaTypeName, paramsTypeName, localeTypeName, typesModule, importExtension, isSingle } =
|
|
48
|
+
options;
|
|
97
49
|
|
|
98
50
|
if (isSingle) {
|
|
99
|
-
return `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
51
|
+
return `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName}, ${localeTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
100
52
|
}
|
|
101
53
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
`import { IcuTranslationProviderMulti } from '@xndrjs/i18n';\n` +
|
|
106
|
-
`import { ${fallbackImport}type ${localeTypeName}, type ${paramsTypeName}, type ${schemaTypeName}, type LazyNamespace${paramTypeImport} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`
|
|
107
|
-
);
|
|
54
|
+
const paramTypeImport = paramTypeName === localeTypeName ? "" : `, ${paramTypeName}`;
|
|
55
|
+
return `import type { ${localeTypeName}, ${paramsTypeName}, ${schemaTypeName}, LazyNamespace${paramTypeImport} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
108
56
|
}
|
|
109
57
|
|
|
110
58
|
function formatSwitchDefaultCase(namespace: string, paramName: string): string {
|
|
@@ -129,8 +77,11 @@ function formatPartitionedNamespaceLoadersFile(
|
|
|
129
77
|
schemaTypeName,
|
|
130
78
|
projectRoot,
|
|
131
79
|
splitPathsByNamespace = {},
|
|
80
|
+
isSingle = false,
|
|
132
81
|
} = options;
|
|
133
82
|
|
|
83
|
+
const loaderValueType = isSingle ? schemaTypeName : `${schemaTypeName}[K]`;
|
|
84
|
+
|
|
134
85
|
const typesImport = formatPartitionedTypesImport(options, paramTypeName);
|
|
135
86
|
const loaderEntries = lazyEntries
|
|
136
87
|
.map((entry) => {
|
|
@@ -167,9 +118,10 @@ function formatPartitionedNamespaceLoadersFile(
|
|
|
167
118
|
`${GENERATED_FILE_BANNER}` +
|
|
168
119
|
typesImport +
|
|
169
120
|
`export const namespaceLoaders: {\n` +
|
|
170
|
-
` [K in LazyNamespace]: (${paramName}: ${paramTypeName}) => Promise<${
|
|
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", () => {
|