@xndrjs/i18n 0.3.3 → 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -44
- package/dist/codegen/index.d.ts +85 -0
- package/dist/codegen/index.js +181 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/index.d.ts +34 -18
- package/dist/index.js +174 -145
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +86 -4
- package/dist/validation/index.js.map +1 -1
- package/package.json +6 -1
- package/src/IcuTranslationProviderMulti.test.ts +48 -1
- package/src/IcuTranslationProviderMulti.ts +41 -61
- package/src/IcuTranslationProviderSingle.test.ts +67 -0
- package/src/IcuTranslationProviderSingle.ts +27 -51
- package/src/audit/audit-dictionaries.ts +4 -1
- package/src/audit/run-audit.test.ts +35 -1
- package/src/audit/run-audit.ts +15 -7
- package/src/codegen/codegen-config-schema.ts +68 -1
- package/src/codegen/config.test.ts +174 -39
- package/src/codegen/config.ts +14 -5
- package/src/codegen/constants.ts +8 -0
- package/src/codegen/delivery-artifacts.test.ts +142 -0
- package/src/codegen/delivery-artifacts.ts +124 -0
- package/src/codegen/emit/dictionary-file.test.ts +190 -0
- package/src/codegen/emit/dictionary-file.ts +165 -2
- package/src/codegen/emit/dictionary-schema-file.ts +5 -0
- package/src/codegen/emit/instance-file.ts +119 -38
- package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
- package/src/codegen/emit/namespace-loaders-file.ts +118 -32
- package/src/codegen/emit/types-file.test.ts +114 -0
- package/src/codegen/emit/types-file.ts +48 -11
- package/src/codegen/generate-i18n-types.test.ts +765 -22
- package/src/codegen/generate-i18n-types.ts +111 -58
- package/src/codegen/icu-analysis.ts +9 -2
- package/src/codegen/locale-fallback.ts +19 -10
- package/src/codegen/locale-policy.ts +4 -0
- package/src/codegen/paths.ts +9 -5
- package/src/codegen/project-locales-set-namespace.test.ts +11 -9
- package/src/codegen/read-dictionary.test.ts +474 -2
- package/src/codegen/read-dictionary.ts +164 -15
- package/src/codegen/write-file-if-changed.test.ts +42 -0
- package/src/codegen/write-file-if-changed.ts +20 -0
- package/src/codegen-config/build-config.ts +34 -0
- package/src/codegen-config/codegen-config.test.ts +32 -0
- package/src/codegen-config/index.ts +21 -0
- package/src/codegen-config/write-config.ts +8 -0
- package/src/deep-freeze.test.ts +13 -0
- package/src/deep-freeze.ts +5 -0
- package/src/format-core.ts +91 -0
- package/src/index.ts +8 -5
- package/src/project-locales.test.ts +129 -10
- package/src/project-locales.ts +90 -3
- package/src/setup/setup-i18n.test.ts +1 -1
- package/src/setup/setup-i18n.ts +9 -32
- package/src/types.ts +19 -4
- package/src/validation/normalize.ts +4 -0
- package/dist/types-C1CpXVOJ.d.ts +0 -83
- package/src/ensure-namespace.integration.test.ts +0 -66
- package/src/ensure-namespace.test.ts +0 -125
- package/src/ensure-namespace.ts +0 -70
- /package/src/{setup → codegen-config}/type-names.ts +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
loadConfig,
|
|
5
|
+
resolveDeliveryOutputDir,
|
|
6
|
+
resolveLoadOnInit,
|
|
7
|
+
resolveNamespaces,
|
|
8
|
+
} from "./config.js";
|
|
4
9
|
import { formatDictionaryFile } from "./emit/dictionary-file.js";
|
|
5
10
|
import {
|
|
6
11
|
formatDictionarySchemaFile,
|
|
@@ -10,11 +15,18 @@ import { formatInstanceFile } from "./emit/instance-file.js";
|
|
|
10
15
|
import { formatNamespaceLoadersFile } from "./emit/namespace-loaders-file.js";
|
|
11
16
|
import { formatTypesFile } from "./emit/types-file.js";
|
|
12
17
|
import { analyzeDictionaries } from "./icu-analysis.js";
|
|
13
|
-
import {
|
|
18
|
+
import { getDeliveryArtifactsIssues } from "./delivery-artifacts.js";
|
|
19
|
+
import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
|
|
14
20
|
import { enrichLocaleFallback } from "./locale-policy.js";
|
|
15
|
-
import {
|
|
21
|
+
import { reportCodegenIssues, resolveImportExtension, toModuleBasename } from "./paths.js";
|
|
16
22
|
import { prepareDictionaryEntries } from "./read-dictionary.js";
|
|
23
|
+
import { writeFileIfChanged } from "./write-file-if-changed.js";
|
|
17
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Codegen CLI entry point. Pipeline:
|
|
27
|
+
* config → analyze (ICU + types input) → validate locale/delivery policy →
|
|
28
|
+
* prepare artifacts (YAML→JSON, split) → resolve lazy/eager → emit generated .ts files.
|
|
29
|
+
*/
|
|
18
30
|
function main() {
|
|
19
31
|
const configArgIndex = process.argv.indexOf("--config");
|
|
20
32
|
const configPath = path.resolve(
|
|
@@ -24,47 +36,29 @@ function main() {
|
|
|
24
36
|
const projectRoot = path.dirname(configPath);
|
|
25
37
|
|
|
26
38
|
if (!fs.existsSync(configPath)) {
|
|
27
|
-
|
|
39
|
+
throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
const config = loadConfig(configPath);
|
|
31
43
|
const sourceEntries = resolveNamespaces(config);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
let compiledFiles: string[];
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const prepared = prepareDictionaryEntries(projectRoot, sourceEntries, generatedDirRelative);
|
|
38
|
-
resolvedEntries = prepared.resolvedEntries;
|
|
39
|
-
compiledFiles = prepared.compiledFiles;
|
|
40
|
-
} catch (error) {
|
|
41
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
-
fail(message);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const entries = resolvedEntries;
|
|
44
|
+
const deliveryOutputRelative = resolveDeliveryOutputDir(config);
|
|
45
|
+
const delivery = config.delivery ?? "canonical";
|
|
46
46
|
const isSingle = Boolean(config.dictionary);
|
|
47
|
-
const { loadOnInitSet, lazyEntries, hasLazy } = resolveLoadOnInit(config, entries, isSingle);
|
|
48
|
-
|
|
49
|
-
if (hasLazy && !config.dictionarySchemaOutput) {
|
|
50
|
-
fail(
|
|
51
|
-
'[Codegen Error] Lazy namespaces require "dictionarySchemaOutput" for validateExternalNamespace.'
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const typesOutputPath = path.resolve(projectRoot, config.typesOutput);
|
|
56
|
-
const dictionaryOutputPath = path.resolve(projectRoot, config.dictionaryOutput);
|
|
57
|
-
const instanceOutputPath = path.resolve(projectRoot, config.instanceOutput);
|
|
58
47
|
|
|
59
|
-
const analysisResult = analyzeDictionaries(projectRoot,
|
|
48
|
+
const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
|
|
60
49
|
if (!analysisResult.ok) {
|
|
61
50
|
process.exit(1);
|
|
62
51
|
}
|
|
63
52
|
|
|
64
|
-
const { paramsByNamespace, argsSpecByNamespace, locales } =
|
|
53
|
+
const { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace } =
|
|
54
|
+
analysisResult.analysis;
|
|
65
55
|
|
|
66
|
-
if (config.localeFallback
|
|
67
|
-
|
|
56
|
+
if (config.localeFallback) {
|
|
57
|
+
const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
|
|
58
|
+
if (localeFallbackIssues.length > 0) {
|
|
59
|
+
reportCodegenIssues(localeFallbackIssues);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
const paramsTypeName = config.paramsTypeName;
|
|
@@ -74,14 +68,55 @@ function main() {
|
|
|
74
68
|
const localeFallbackTypeName = `${localeTypeName}Fallback`;
|
|
75
69
|
const factoryName = config.factoryName ?? "createI18n";
|
|
76
70
|
const importExtension = resolveImportExtension(config);
|
|
77
|
-
const typesModule = toModuleBasename(typesOutputPath);
|
|
78
71
|
|
|
79
72
|
const requestLocales = collectRequestLocales(locales, config.localeFallback);
|
|
80
|
-
const
|
|
73
|
+
const requestLocalesList = [...requestLocales].sort();
|
|
74
|
+
|
|
75
|
+
if (delivery === "custom" && config.deliveryArtifacts) {
|
|
76
|
+
const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
|
|
77
|
+
config.deliveryArtifacts,
|
|
78
|
+
requestLocales
|
|
79
|
+
);
|
|
80
|
+
if (deliveryArtifactsIssues.length > 0) {
|
|
81
|
+
reportCodegenIssues(deliveryArtifactsIssues);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
resolvedEntries: entries,
|
|
88
|
+
splitPathsByNamespace,
|
|
89
|
+
compiledFiles,
|
|
90
|
+
} = prepareDictionaryEntries(projectRoot, sourceEntries, deliveryOutputRelative, {
|
|
91
|
+
dictionariesByNamespace,
|
|
92
|
+
delivery,
|
|
93
|
+
localeFallback: config.localeFallback,
|
|
94
|
+
requestLocales: delivery === "split-by-locale" ? requestLocalesList : undefined,
|
|
95
|
+
deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : undefined,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const { loadOnInitSet, lazyEntries, hasLazy } = resolveLoadOnInit(config, entries, isSingle);
|
|
99
|
+
|
|
100
|
+
const typesOutputPath = path.resolve(projectRoot, config.typesOutput);
|
|
101
|
+
const dictionaryOutputPath = path.resolve(projectRoot, config.dictionaryOutput);
|
|
102
|
+
const instanceOutputPath = path.resolve(projectRoot, config.instanceOutput);
|
|
103
|
+
const typesModule = toModuleBasename(typesOutputPath);
|
|
104
|
+
|
|
105
|
+
const requestLocaleUnion = requestLocalesList
|
|
81
106
|
.sort()
|
|
82
107
|
.map((locale) => `'${locale}'`)
|
|
83
108
|
.join(" | ");
|
|
84
109
|
|
|
110
|
+
const deliveryAreaNames =
|
|
111
|
+
delivery === "custom" && config.deliveryArtifacts
|
|
112
|
+
? Object.keys(config.deliveryArtifacts).sort()
|
|
113
|
+
: undefined;
|
|
114
|
+
const deliveryAreaTypeName =
|
|
115
|
+
delivery === "custom" ? schemaTypeName.replace(/Schema$/, "DeliveryArea") : undefined;
|
|
116
|
+
const deliveryAreaUnion = deliveryAreaNames
|
|
117
|
+
? deliveryAreaNames.map((area) => `'${area}'`).join(" | ")
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
85
120
|
const eagerEntries = hasLazy
|
|
86
121
|
? entries.filter((entry) => loadOnInitSet.has(entry.namespace))
|
|
87
122
|
: entries;
|
|
@@ -103,6 +138,15 @@ function main() {
|
|
|
103
138
|
localeFallback: localeFallbackForEmit,
|
|
104
139
|
paramsByNamespace,
|
|
105
140
|
requestLocaleUnion,
|
|
141
|
+
...(deliveryAreaTypeName && deliveryAreaUnion
|
|
142
|
+
? {
|
|
143
|
+
deliveryAreaTypeName,
|
|
144
|
+
deliveryAreaUnion,
|
|
145
|
+
...(delivery === "custom" && config.deliveryArtifacts
|
|
146
|
+
? { deliveryArtifacts: config.deliveryArtifacts }
|
|
147
|
+
: {}),
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
106
150
|
hasLazy,
|
|
107
151
|
loadOnInitSet,
|
|
108
152
|
lazyEntries,
|
|
@@ -117,14 +161,20 @@ function main() {
|
|
|
117
161
|
dictionaryOutputPath,
|
|
118
162
|
typesOutputPath,
|
|
119
163
|
schemaTypeName,
|
|
164
|
+
localeTypeName,
|
|
120
165
|
importExtension,
|
|
166
|
+
delivery,
|
|
167
|
+
splitPathsByNamespace,
|
|
168
|
+
...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
|
|
169
|
+
...(delivery === "custom" && deliveryAreaTypeName && deliveryAreaNames
|
|
170
|
+
? { deliveryAreaTypeName, deliveryAreaNames }
|
|
171
|
+
: {}),
|
|
121
172
|
});
|
|
122
173
|
|
|
123
174
|
const instanceContent = formatInstanceFile({
|
|
124
175
|
isSingle,
|
|
125
176
|
hasLazy,
|
|
126
177
|
typesOutputPath,
|
|
127
|
-
dictionaryOutputPath,
|
|
128
178
|
paramsTypeName,
|
|
129
179
|
schemaTypeName,
|
|
130
180
|
localeTypeName,
|
|
@@ -134,16 +184,12 @@ function main() {
|
|
|
134
184
|
hasLocaleType: Boolean(requestLocaleUnion),
|
|
135
185
|
namespaceNames: entries.map((entry) => entry.namespace),
|
|
136
186
|
importExtension,
|
|
187
|
+
delivery,
|
|
137
188
|
});
|
|
138
189
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
fs.mkdirSync(path.dirname(dictionaryOutputPath), { recursive: true });
|
|
143
|
-
fs.writeFileSync(dictionaryOutputPath, dictionaryContent);
|
|
144
|
-
|
|
145
|
-
fs.mkdirSync(path.dirname(instanceOutputPath), { recursive: true });
|
|
146
|
-
fs.writeFileSync(instanceOutputPath, instanceContent);
|
|
190
|
+
writeFileIfChanged(typesOutputPath, typesContent);
|
|
191
|
+
writeFileIfChanged(dictionaryOutputPath, dictionaryContent);
|
|
192
|
+
writeFileIfChanged(instanceOutputPath, instanceContent);
|
|
147
193
|
|
|
148
194
|
const generatedFiles = [
|
|
149
195
|
path.relative(projectRoot, typesOutputPath),
|
|
@@ -162,8 +208,7 @@ function main() {
|
|
|
162
208
|
importExtension
|
|
163
209
|
);
|
|
164
210
|
|
|
165
|
-
|
|
166
|
-
fs.writeFileSync(dictionarySchemaOutputPath, dictionarySchemaContent);
|
|
211
|
+
writeFileIfChanged(dictionarySchemaOutputPath, dictionarySchemaContent);
|
|
167
212
|
generatedFiles.push(path.relative(projectRoot, dictionarySchemaOutputPath));
|
|
168
213
|
}
|
|
169
214
|
|
|
@@ -173,24 +218,27 @@ function main() {
|
|
|
173
218
|
config.namespaceLoadersOutput ??
|
|
174
219
|
path.join(path.dirname(config.instanceOutput), "namespace-loaders.generated.ts")
|
|
175
220
|
);
|
|
176
|
-
const dictionarySchemaOutputPath = path.resolve(projectRoot, config.dictionarySchemaOutput!);
|
|
177
221
|
const lazyEntriesWithPaths = lazyEntries.map((entry) => ({
|
|
178
222
|
...entry,
|
|
179
223
|
absolutePath: path.resolve(projectRoot, entry.filePath),
|
|
180
224
|
}));
|
|
181
|
-
const namespaceLoadersContent = formatNamespaceLoadersFile(
|
|
182
|
-
namespaceLoadersOutputPath,
|
|
183
|
-
lazyEntriesWithPaths,
|
|
225
|
+
const namespaceLoadersContent = formatNamespaceLoadersFile({
|
|
226
|
+
loadersOutputPath: namespaceLoadersOutputPath,
|
|
227
|
+
lazyEntries: lazyEntriesWithPaths,
|
|
184
228
|
schemaTypeName,
|
|
229
|
+
localeTypeName,
|
|
185
230
|
typesModule,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
231
|
+
importExtension,
|
|
232
|
+
projectRoot,
|
|
233
|
+
delivery,
|
|
234
|
+
splitPathsByNamespace,
|
|
235
|
+
...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
|
|
236
|
+
...(delivery === "custom" && deliveryAreaTypeName && deliveryAreaNames
|
|
237
|
+
? { deliveryAreaTypeName, deliveryAreaNames }
|
|
238
|
+
: {}),
|
|
239
|
+
});
|
|
191
240
|
|
|
192
|
-
|
|
193
|
-
fs.writeFileSync(namespaceLoadersOutputPath, namespaceLoadersContent);
|
|
241
|
+
writeFileIfChanged(namespaceLoadersOutputPath, namespaceLoadersContent);
|
|
194
242
|
generatedFiles.push(path.relative(projectRoot, namespaceLoadersOutputPath));
|
|
195
243
|
}
|
|
196
244
|
|
|
@@ -200,4 +248,9 @@ function main() {
|
|
|
200
248
|
}
|
|
201
249
|
}
|
|
202
250
|
|
|
203
|
-
|
|
251
|
+
try {
|
|
252
|
+
main();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
mergeVariableMetaAcrossLocales,
|
|
7
7
|
type VariableSpec,
|
|
8
8
|
} from "../icu/extract-variables.js";
|
|
9
|
-
import type { NamespaceEntry } from "./types.js";
|
|
9
|
+
import type { DictionaryJson, NamespaceEntry } from "./types.js";
|
|
10
10
|
import { readDictionaryFile } from "./read-dictionary.js";
|
|
11
11
|
|
|
12
12
|
export function paramsTypeForVariables(variables: VariableSpec): string {
|
|
@@ -26,8 +26,13 @@ export interface DictionaryAnalysis {
|
|
|
26
26
|
paramsByNamespace: Record<string, Record<string, string>>;
|
|
27
27
|
argsSpecByNamespace: Record<string, Record<string, VariableSpec>>;
|
|
28
28
|
locales: Set<string>;
|
|
29
|
+
dictionariesByNamespace: Record<string, DictionaryJson>;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Phase 1 of codegen: read source dictionaries, validate ICU syntax, infer param types.
|
|
34
|
+
* Output feeds type emission, optional `DICTIONARY_SPEC`, and `prepareDictionaryEntries`.
|
|
35
|
+
*/
|
|
31
36
|
export function analyzeDictionaries(
|
|
32
37
|
projectRoot: string,
|
|
33
38
|
entries: NamespaceEntry[]
|
|
@@ -35,6 +40,7 @@ export function analyzeDictionaries(
|
|
|
35
40
|
const paramsByNamespace: Record<string, Record<string, string>> = {};
|
|
36
41
|
const argsSpecByNamespace: Record<string, Record<string, VariableSpec>> = {};
|
|
37
42
|
const locales = new Set<string>();
|
|
43
|
+
const dictionariesByNamespace: Record<string, DictionaryJson> = {};
|
|
38
44
|
let hasErrors = false;
|
|
39
45
|
|
|
40
46
|
for (const entry of entries) {
|
|
@@ -49,6 +55,7 @@ export function analyzeDictionaries(
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
const dictionary = readDictionaryFile(absolutePath);
|
|
58
|
+
dictionariesByNamespace[entry.namespace] = dictionary;
|
|
52
59
|
paramsByNamespace[entry.namespace] = {};
|
|
53
60
|
argsSpecByNamespace[entry.namespace] = {};
|
|
54
61
|
|
|
@@ -98,6 +105,6 @@ export function analyzeDictionaries(
|
|
|
98
105
|
|
|
99
106
|
return {
|
|
100
107
|
ok: true,
|
|
101
|
-
analysis: { paramsByNamespace, argsSpecByNamespace, locales },
|
|
108
|
+
analysis: { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace },
|
|
102
109
|
};
|
|
103
110
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { validateLocaleFallback } from "../resolve-locale.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Union of locales found in dictionaries plus every locale referenced by `localeFallback`.
|
|
5
|
+
* Drives split-by-locale partitioning and the generated locale union type.
|
|
6
|
+
*/
|
|
3
7
|
export function collectRequestLocales(
|
|
4
8
|
dictionaryLocales: Set<string>,
|
|
5
9
|
fallback?: Record<string, string | null>
|
|
@@ -19,30 +23,35 @@ export function collectRequestLocales(
|
|
|
19
23
|
return all;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
export
|
|
26
|
+
export type LocaleFallbackIssue = {
|
|
27
|
+
path: (string | number)[];
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Pre-emit validation gate for `localeFallback` config against discovered dictionary locales. */
|
|
32
|
+
export function getCodegenLocaleFallbackIssues(
|
|
23
33
|
fallback: Record<string, string | null>,
|
|
24
34
|
dictionaryLocales: Set<string>
|
|
25
|
-
):
|
|
26
|
-
|
|
35
|
+
): LocaleFallbackIssue[] {
|
|
36
|
+
const issues: LocaleFallbackIssue[] = [];
|
|
27
37
|
|
|
28
38
|
try {
|
|
29
39
|
validateLocaleFallback(fallback);
|
|
30
40
|
} catch (error) {
|
|
31
41
|
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
-
|
|
33
|
-
hasErrors = true;
|
|
42
|
+
issues.push({ path: ["localeFallback"], message });
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
for (const [locale, target] of Object.entries(fallback)) {
|
|
37
46
|
if (target !== null && !(target in fallback) && !dictionaryLocales.has(target)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
issues.push({
|
|
48
|
+
path: ["localeFallback", locale],
|
|
49
|
+
message: `localeFallback: "${locale}" points to "${target}" which is not defined in the fallback map or dictionary locales`,
|
|
50
|
+
});
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
return
|
|
54
|
+
return issues;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
export function formatLocaleFallbackBlock(
|
|
@@ -7,6 +7,10 @@ export function buildRequiredLocales(
|
|
|
7
7
|
return [...collectRequestLocales(dictionaryLocales, configFallback)].sort();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Completes `localeFallback` with explicit `null` entries for every request locale,
|
|
12
|
+
* producing a stable map emitted into `i18n-types.generated.ts`.
|
|
13
|
+
*/
|
|
10
14
|
export function enrichLocaleFallback(
|
|
11
15
|
dictionaryLocales: Set<string>,
|
|
12
16
|
configFallback: Record<string, string | null>
|
package/src/codegen/paths.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { CodegenConfig } from "./codegen-config-schema.js";
|
|
|
6
6
|
export const GENERATED_FILE_BANNER = "// Automatically generated code. Do not edit manually.\n";
|
|
7
7
|
export const DEFAULT_IMPORT_EXTENSION: ImportExtension = "none";
|
|
8
8
|
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export function reportCodegenIssues(issues: readonly { message: string }[]): void {
|
|
10
|
+
for (const issue of issues) {
|
|
11
|
+
console.error(`[Codegen Error] ${issue.message}`);
|
|
12
|
+
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function toImportPath(fromFile: string, toFile: string): string {
|
|
@@ -27,7 +27,7 @@ export function resolveImportExtension(
|
|
|
27
27
|
): ImportExtension {
|
|
28
28
|
const extension = config.importExtension ?? DEFAULT_IMPORT_EXTENSION;
|
|
29
29
|
if (!SUPPORTED_IMPORT_EXTENSIONS.includes(extension)) {
|
|
30
|
-
|
|
30
|
+
throw new Error(
|
|
31
31
|
`[Codegen Error] importExtension must be "none", ".ts", or ".js", got ${JSON.stringify(extension)}.`
|
|
32
32
|
);
|
|
33
33
|
}
|
|
@@ -52,3 +52,7 @@ export function toImportIdentifier(namespace: string): string {
|
|
|
52
52
|
}
|
|
53
53
|
return `${safe}Ns`;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export function toLocaleObjectKey(locale: string): string {
|
|
57
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(locale) ? locale : JSON.stringify(locale);
|
|
58
|
+
}
|
|
@@ -95,7 +95,7 @@ function writeTscProject(tempDir: string) {
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
describe("codegen
|
|
98
|
+
describe("codegen projectDictionaryLocales + setNamespace", () => {
|
|
99
99
|
describe("generated instance output", () => {
|
|
100
100
|
let tempDir: string;
|
|
101
101
|
|
|
@@ -112,7 +112,7 @@ describe("codegen projectLocales + setNamespace", () => {
|
|
|
112
112
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
113
113
|
expect(factory).toContain('dictionary: AppSchema["billing"]');
|
|
114
114
|
expect(factory).toContain(
|
|
115
|
-
"
|
|
115
|
+
"projectDictionaryLocalesCore(dictionary, locales, LOCALE_FALLBACK)"
|
|
116
116
|
);
|
|
117
117
|
});
|
|
118
118
|
|
|
@@ -124,9 +124,10 @@ describe("codegen projectLocales + setNamespace", () => {
|
|
|
124
124
|
join(tempDir, "src/hydrate-billing.ts"),
|
|
125
125
|
[
|
|
126
126
|
`import { createI18n, projectNamespaceLocales } from "./i18n/instance.generated.js";`,
|
|
127
|
+
`import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
|
|
127
128
|
`import billingDictionary from "./i18n/translations/billing.json";`,
|
|
128
129
|
``,
|
|
129
|
-
`const i18n = createI18n();`,
|
|
130
|
+
`const i18n = createI18n(defaultDictionary);`,
|
|
130
131
|
`i18n.setNamespace("billing", projectNamespaceLocales(billingDictionary, ["en"]));`,
|
|
131
132
|
].join("\n")
|
|
132
133
|
);
|
|
@@ -137,25 +138,26 @@ describe("codegen projectLocales + setNamespace", () => {
|
|
|
137
138
|
expect(tsc.status, `${tsc.stdout}\n${tsc.stderr}`).toBe(0);
|
|
138
139
|
});
|
|
139
140
|
|
|
140
|
-
it("passes tsc when replacing the full schema with
|
|
141
|
+
it("passes tsc when replacing the full schema with projectDictionaryLocales before setAll", () => {
|
|
141
142
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
|
|
142
143
|
setupMultiCodegenFixture(tempDir);
|
|
143
144
|
|
|
144
145
|
writeFileSync(
|
|
145
146
|
join(tempDir, "src/hydrate-all.ts"),
|
|
146
147
|
[
|
|
147
|
-
`import { createI18n,
|
|
148
|
+
`import { createI18n, projectDictionaryLocales } from "./i18n/instance.generated.js";`,
|
|
149
|
+
`import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
|
|
148
150
|
`import type { AppSchema } from "./i18n/i18n-types.generated.js";`,
|
|
149
151
|
`import billingDictionary from "./i18n/translations/billing.json";`,
|
|
150
|
-
`import
|
|
152
|
+
`import defaultNs from "./i18n/translations/default.json";`,
|
|
151
153
|
``,
|
|
152
154
|
`const fullDictionary = {`,
|
|
153
|
-
` default:
|
|
155
|
+
` default: defaultNs,`,
|
|
154
156
|
` billing: billingDictionary,`,
|
|
155
157
|
`} satisfies AppSchema;`,
|
|
156
158
|
``,
|
|
157
|
-
`const i18n = createI18n();`,
|
|
158
|
-
`i18n.setAll(
|
|
159
|
+
`const i18n = createI18n(defaultDictionary);`,
|
|
160
|
+
`i18n.setAll(projectDictionaryLocales(fullDictionary, ["en"]));`,
|
|
159
161
|
].join("\n")
|
|
160
162
|
);
|
|
161
163
|
|