@xndrjs/i18n 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -872
- package/dist/codegen/index.d.ts +117 -51
- package/dist/codegen/index.js +1585 -93
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +100 -234
- package/dist/index.js +74 -638
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.js.map +1 -1
- package/package.json +2 -2
- package/src/IcuTranslationProviderMulti.test.ts +18 -40
- package/src/IcuTranslationProviderMulti.ts +27 -37
- package/src/audit/audit-dictionaries.ts +1 -1
- package/src/audit/run-audit.test.ts +6 -7
- package/src/builder-load-registry.ts +26 -5
- package/src/builder-loaders.ts +8 -11
- package/src/builder-types.test.ts +11 -42
- package/src/builder.test.ts +82 -243
- package/src/builder.ts +8 -110
- package/src/codegen/codegen-config-schema.ts +82 -77
- package/src/codegen/config.test.ts +55 -143
- package/src/codegen/config.ts +10 -67
- package/src/codegen/dictionary-spec-contract.test.ts +28 -0
- package/src/codegen/dictionary-spec-contract.ts +138 -0
- package/src/codegen/emit/dictionary-schema-file.ts +7 -43
- package/src/codegen/emit/instance-file.test.ts +35 -52
- package/src/codegen/emit/instance-file.ts +170 -268
- package/src/codegen/emit/namespace-loaders-file.test.ts +72 -74
- package/src/codegen/emit/namespace-loaders-file.ts +147 -90
- package/src/codegen/emit/types-file.test.ts +20 -41
- package/src/codegen/emit/types-file.ts +48 -74
- package/src/codegen/generate-i18n-types.test.ts +163 -492
- package/src/codegen/generate-i18n-types.ts +7 -269
- package/src/codegen/paths.ts +0 -14
- package/src/codegen/project-locales-set-namespace.test.ts +44 -86
- package/src/codegen/read-dictionary.test.ts +27 -9
- package/src/codegen/read-dictionary.ts +18 -40
- package/src/codegen/regenerate-namespaces.ts +180 -0
- package/src/codegen/run-codegen.test.ts +230 -0
- package/src/codegen/run-codegen.ts +252 -0
- package/src/codegen/types.ts +0 -6
- package/src/codegen-config/build-config.ts +4 -23
- package/src/codegen-config/codegen-config.test.ts +10 -6
- package/src/codegen-config/index.ts +18 -3
- package/src/engine.ts +3 -27
- package/src/fetch-artifact.ts +16 -0
- package/src/i18n-handle.ts +202 -0
- package/src/index.ts +15 -34
- package/src/project-locales.ts +2 -2
- package/src/scope-multi.ts +1 -20
- package/src/scope-types.ts +14 -6
- package/src/scope.test.ts +3 -312
- package/src/serialized-state.test.ts +114 -0
- package/src/serialized-state.ts +28 -0
- package/src/setup/setup-i18n.test.ts +22 -33
- package/src/setup/setup-i18n.ts +22 -27
- package/src/types.ts +3 -3
- package/src/validation/index.ts +4 -0
- package/src/IcuTranslationProviderSingle.test.ts +0 -177
- package/src/IcuTranslationProviderSingle.ts +0 -114
- package/src/builder-multi.ts +0 -481
- package/src/codegen/emit/dictionary-file.test.ts +0 -215
- package/src/codegen/emit/dictionary-file.ts +0 -244
- package/src/deep-freeze.test.ts +0 -40
- package/src/deep-freeze.ts +0 -22
- package/src/patch-key.test.ts +0 -186
- package/src/patch-key.ts +0 -140
- package/src/scope-single.ts +0 -85
- package/src/single-builder.ts +0 -153
|
@@ -1,277 +1,15 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
loadConfig,
|
|
5
|
-
resolveDeliveryOutputDir,
|
|
6
|
-
resolveDictionaryOutputPath,
|
|
7
|
-
resolveLoadOnInit,
|
|
8
|
-
resolveNamespaces,
|
|
9
|
-
} from "./config.js";
|
|
10
|
-
import { formatDictionaryFile } from "./emit/dictionary-file.js";
|
|
11
|
-
import {
|
|
12
|
-
formatDictionarySchemaFile,
|
|
13
|
-
formatDictionarySpecBlock,
|
|
14
|
-
} from "./emit/dictionary-schema-file.js";
|
|
15
|
-
import { formatInstanceFile } from "./emit/instance-file.js";
|
|
16
|
-
import { formatNamespaceLoadersFile } from "./emit/namespace-loaders-file.js";
|
|
17
|
-
import { formatTypesFile } from "./emit/types-file.js";
|
|
18
|
-
import { analyzeDictionaries } from "./icu-analysis.js";
|
|
19
|
-
import { getDeliveryArtifactsIssues, getDeliveryArtifactsTypeName } from "./delivery-artifacts.js";
|
|
20
|
-
import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
|
|
21
|
-
import { enrichLocaleFallback } from "./locale-policy.js";
|
|
22
|
-
import { reportCodegenIssues, resolveImportExtension, toModuleBasename } from "./paths.js";
|
|
23
|
-
import { prepareDictionaryEntries } from "./read-dictionary.js";
|
|
24
|
-
import { writeFileIfChanged } from "./write-file-if-changed.js";
|
|
25
|
-
|
|
26
1
|
/**
|
|
27
|
-
* Codegen CLI entry point.
|
|
28
|
-
* config → analyze (ICU + types input) → validate locale/delivery policy →
|
|
29
|
-
* prepare artifacts (YAML→JSON, split) → resolve lazy/eager → emit generated .ts files.
|
|
2
|
+
* Codegen CLI entry point. Thin wrapper around {@link runCodegen}.
|
|
30
3
|
*/
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { runCodegen } from "./run-codegen.js";
|
|
6
|
+
|
|
31
7
|
function main() {
|
|
32
8
|
const configArgIndex = process.argv.indexOf("--config");
|
|
33
|
-
const configPath =
|
|
34
|
-
process.
|
|
35
|
-
configArgIndex >= 0 ? process.argv[configArgIndex + 1]! : "i18n/i18n.codegen.json"
|
|
36
|
-
);
|
|
37
|
-
const projectRoot = path.dirname(configPath);
|
|
38
|
-
|
|
39
|
-
if (!fs.existsSync(configPath)) {
|
|
40
|
-
throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const config = loadConfig(configPath);
|
|
44
|
-
const sourceEntries = resolveNamespaces(config);
|
|
45
|
-
const deliveryOutputRelative = resolveDeliveryOutputDir(config);
|
|
46
|
-
const delivery = config.delivery ?? "canonical";
|
|
47
|
-
const isSingle = Boolean(config.dictionary);
|
|
48
|
-
|
|
49
|
-
const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
|
|
50
|
-
if (!analysisResult.ok) {
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace } =
|
|
55
|
-
analysisResult.analysis;
|
|
56
|
-
|
|
57
|
-
if (config.localeFallback) {
|
|
58
|
-
const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
|
|
59
|
-
if (localeFallbackIssues.length > 0) {
|
|
60
|
-
reportCodegenIssues(localeFallbackIssues);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const paramsTypeName = config.paramsTypeName;
|
|
66
|
-
const schemaTypeName = config.schemaTypeName;
|
|
67
|
-
const localeTypeName = config.localeTypeName ?? schemaTypeName.replace(/Schema$/, "Locale");
|
|
68
|
-
const localeFallbackConstName = config.localeFallbackConstName ?? "LOCALE_FALLBACK";
|
|
69
|
-
const localeFallbackTypeName = `${localeTypeName}Fallback`;
|
|
70
|
-
const factoryName = config.factoryName ?? "createI18n";
|
|
71
|
-
const importExtension = resolveImportExtension(config);
|
|
72
|
-
|
|
73
|
-
const requestLocales = collectRequestLocales(locales, config.localeFallback);
|
|
74
|
-
const requestLocalesList = [...requestLocales].sort();
|
|
75
|
-
|
|
76
|
-
if (delivery === "custom" && config.deliveryArtifacts) {
|
|
77
|
-
const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
|
|
78
|
-
config.deliveryArtifacts,
|
|
79
|
-
requestLocales
|
|
80
|
-
);
|
|
81
|
-
if (deliveryArtifactsIssues.length > 0) {
|
|
82
|
-
reportCodegenIssues(deliveryArtifactsIssues);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const {
|
|
88
|
-
resolvedEntries: entries,
|
|
89
|
-
splitPathsByNamespace,
|
|
90
|
-
compiledFiles,
|
|
91
|
-
} = prepareDictionaryEntries(projectRoot, sourceEntries, deliveryOutputRelative, {
|
|
92
|
-
dictionariesByNamespace,
|
|
93
|
-
delivery,
|
|
94
|
-
localeFallback: config.localeFallback,
|
|
95
|
-
requestLocales: delivery === "split-by-locale" ? requestLocalesList : undefined,
|
|
96
|
-
deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : undefined,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const { loadOnInitSet, lazyEntries, hasLazy } = resolveLoadOnInit(config, entries, isSingle);
|
|
100
|
-
|
|
101
|
-
const typesOutputPath = path.resolve(projectRoot, config.typesOutput);
|
|
102
|
-
const dictionaryOutputPath = path.resolve(projectRoot, resolveDictionaryOutputPath(config));
|
|
103
|
-
const instanceOutputPath = path.resolve(projectRoot, config.instanceOutput);
|
|
104
|
-
const typesModule = toModuleBasename(typesOutputPath);
|
|
105
|
-
|
|
106
|
-
const requestLocaleUnion = requestLocalesList
|
|
107
|
-
.sort()
|
|
108
|
-
.map((locale) => `'${locale}'`)
|
|
109
|
-
.join(" | ");
|
|
110
|
-
|
|
111
|
-
const deliveryAreaNames =
|
|
112
|
-
delivery === "custom" && config.deliveryArtifacts
|
|
113
|
-
? Object.keys(config.deliveryArtifacts).sort()
|
|
114
|
-
: undefined;
|
|
115
|
-
const deliveryAreaTypeName =
|
|
116
|
-
delivery === "custom" ? schemaTypeName.replace(/Schema$/, "DeliveryArea") : undefined;
|
|
117
|
-
const deliveryAreaUnion = deliveryAreaNames
|
|
118
|
-
? deliveryAreaNames.map((area) => `'${area}'`).join(" | ")
|
|
119
|
-
: undefined;
|
|
120
|
-
|
|
121
|
-
const eagerEntries = hasLazy
|
|
122
|
-
? entries.filter((entry) => loadOnInitSet.has(entry.namespace))
|
|
123
|
-
: entries;
|
|
124
|
-
|
|
125
|
-
const localeFallbackForEmit = config.localeFallback
|
|
126
|
-
? enrichLocaleFallback(locales, config.localeFallback)
|
|
127
|
-
: undefined;
|
|
128
|
-
|
|
129
|
-
const typesContent = formatTypesFile({
|
|
130
|
-
isSingle,
|
|
131
|
-
entries,
|
|
132
|
-
projectRoot,
|
|
133
|
-
typesOutputPath,
|
|
134
|
-
paramsTypeName,
|
|
135
|
-
schemaTypeName,
|
|
136
|
-
localeTypeName,
|
|
137
|
-
localeFallbackConstName,
|
|
138
|
-
localeFallbackTypeName,
|
|
139
|
-
localeFallback: localeFallbackForEmit,
|
|
140
|
-
paramsByNamespace,
|
|
141
|
-
requestLocaleUnion,
|
|
142
|
-
...(deliveryAreaTypeName && deliveryAreaUnion
|
|
143
|
-
? {
|
|
144
|
-
deliveryAreaTypeName,
|
|
145
|
-
deliveryAreaUnion,
|
|
146
|
-
...(delivery === "custom" && config.deliveryArtifacts
|
|
147
|
-
? { deliveryArtifacts: config.deliveryArtifacts }
|
|
148
|
-
: {}),
|
|
149
|
-
}
|
|
150
|
-
: {}),
|
|
151
|
-
hasLazy,
|
|
152
|
-
loadOnInitSet,
|
|
153
|
-
lazyEntries,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const dictionaryContent = formatDictionaryFile({
|
|
157
|
-
isSingle,
|
|
158
|
-
hasLazy,
|
|
159
|
-
entries,
|
|
160
|
-
eagerEntries,
|
|
161
|
-
projectRoot,
|
|
162
|
-
dictionaryOutputPath,
|
|
163
|
-
typesOutputPath,
|
|
164
|
-
schemaTypeName,
|
|
165
|
-
localeTypeName,
|
|
166
|
-
importExtension,
|
|
167
|
-
delivery,
|
|
168
|
-
splitPathsByNamespace,
|
|
169
|
-
...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
|
|
170
|
-
...(delivery === "custom" && deliveryAreaTypeName && deliveryAreaNames
|
|
171
|
-
? { deliveryAreaTypeName, deliveryAreaNames }
|
|
172
|
-
: {}),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const instanceContent = formatInstanceFile({
|
|
176
|
-
isSingle,
|
|
177
|
-
hasLazy,
|
|
178
|
-
typesOutputPath,
|
|
179
|
-
...(hasLazy
|
|
180
|
-
? {
|
|
181
|
-
namespaceLoadersOutputPath: path.resolve(
|
|
182
|
-
projectRoot,
|
|
183
|
-
config.namespaceLoadersOutput ??
|
|
184
|
-
path.join(path.dirname(config.instanceOutput), "namespace-loaders.generated.ts")
|
|
185
|
-
),
|
|
186
|
-
}
|
|
187
|
-
: {}),
|
|
188
|
-
paramsTypeName,
|
|
189
|
-
schemaTypeName,
|
|
190
|
-
localeTypeName,
|
|
191
|
-
localeFallbackConstName,
|
|
192
|
-
factoryName,
|
|
193
|
-
hasLocaleFallback: Boolean(config.localeFallback),
|
|
194
|
-
hasLocaleType: Boolean(requestLocaleUnion),
|
|
195
|
-
namespaceNames: entries.map((entry) => entry.namespace),
|
|
196
|
-
importExtension,
|
|
197
|
-
delivery,
|
|
198
|
-
...(delivery === "custom" && deliveryAreaTypeName
|
|
199
|
-
? {
|
|
200
|
-
deliveryAreaTypeName,
|
|
201
|
-
deliveryArtifactsTypeName: getDeliveryArtifactsTypeName(deliveryAreaTypeName),
|
|
202
|
-
}
|
|
203
|
-
: {}),
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
writeFileIfChanged(typesOutputPath, typesContent);
|
|
207
|
-
|
|
208
|
-
const generatedFiles = [
|
|
209
|
-
path.relative(projectRoot, typesOutputPath),
|
|
210
|
-
path.relative(projectRoot, instanceOutputPath),
|
|
211
|
-
];
|
|
212
|
-
|
|
213
|
-
if (dictionaryContent !== null) {
|
|
214
|
-
writeFileIfChanged(dictionaryOutputPath, dictionaryContent);
|
|
215
|
-
generatedFiles.splice(1, 0, path.relative(projectRoot, dictionaryOutputPath));
|
|
216
|
-
} else if (fs.existsSync(dictionaryOutputPath)) {
|
|
217
|
-
fs.unlinkSync(dictionaryOutputPath);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
writeFileIfChanged(instanceOutputPath, instanceContent);
|
|
221
|
-
|
|
222
|
-
if (config.dictionarySchemaOutput) {
|
|
223
|
-
const dictionarySchemaOutputPath = path.resolve(projectRoot, config.dictionarySchemaOutput);
|
|
224
|
-
const dictionarySpecBlock = formatDictionarySpecBlock(isSingle, entries, argsSpecByNamespace);
|
|
225
|
-
const dictionarySchemaContent = formatDictionarySchemaFile(
|
|
226
|
-
schemaTypeName,
|
|
227
|
-
typesModule,
|
|
228
|
-
isSingle,
|
|
229
|
-
dictionarySpecBlock,
|
|
230
|
-
importExtension
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
writeFileIfChanged(dictionarySchemaOutputPath, dictionarySchemaContent);
|
|
234
|
-
generatedFiles.push(path.relative(projectRoot, dictionarySchemaOutputPath));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (hasLazy) {
|
|
238
|
-
const namespaceLoadersOutputPath = path.resolve(
|
|
239
|
-
projectRoot,
|
|
240
|
-
config.namespaceLoadersOutput ??
|
|
241
|
-
path.join(path.dirname(config.instanceOutput), "namespace-loaders.generated.ts")
|
|
242
|
-
);
|
|
243
|
-
const lazyEntriesWithPaths = lazyEntries.map((entry) => ({
|
|
244
|
-
...entry,
|
|
245
|
-
absolutePath: path.resolve(projectRoot, entry.filePath),
|
|
246
|
-
}));
|
|
247
|
-
const namespaceLoadersContent = formatNamespaceLoadersFile({
|
|
248
|
-
loadersOutputPath: namespaceLoadersOutputPath,
|
|
249
|
-
lazyEntries: lazyEntriesWithPaths,
|
|
250
|
-
schemaTypeName,
|
|
251
|
-
paramsTypeName,
|
|
252
|
-
localeTypeName,
|
|
253
|
-
localeFallbackConstName,
|
|
254
|
-
hasLocaleFallback: Boolean(config.localeFallback),
|
|
255
|
-
typesModule,
|
|
256
|
-
importExtension,
|
|
257
|
-
projectRoot,
|
|
258
|
-
isSingle,
|
|
259
|
-
delivery,
|
|
260
|
-
splitPathsByNamespace,
|
|
261
|
-
...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
|
|
262
|
-
...(delivery === "custom" && deliveryAreaTypeName && deliveryAreaNames
|
|
263
|
-
? { deliveryAreaTypeName, deliveryAreaNames }
|
|
264
|
-
: {}),
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
writeFileIfChanged(namespaceLoadersOutputPath, namespaceLoadersContent);
|
|
268
|
-
generatedFiles.push(path.relative(projectRoot, namespaceLoadersOutputPath));
|
|
269
|
-
}
|
|
9
|
+
const configPath =
|
|
10
|
+
configArgIndex >= 0 ? process.argv[configArgIndex + 1]! : "i18n/i18n.codegen.json";
|
|
270
11
|
|
|
271
|
-
|
|
272
|
-
if (compiledFiles.length > 0) {
|
|
273
|
-
console.log(`✅ Compiled: ${compiledFiles.join(", ")}`);
|
|
274
|
-
}
|
|
12
|
+
runCodegen(path.resolve(process.cwd(), configPath));
|
|
275
13
|
}
|
|
276
14
|
|
|
277
15
|
try {
|
package/src/codegen/paths.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { ImportExtension } from "./types.js";
|
|
3
|
-
import { SUPPORTED_IMPORT_EXTENSIONS } from "./constants.js";
|
|
4
|
-
import { CodegenConfig } from "./codegen-config-schema.js";
|
|
5
3
|
|
|
6
4
|
export const GENERATED_FILE_BANNER = "// Automatically generated code. Do not edit manually.\n";
|
|
7
5
|
export const DEFAULT_IMPORT_EXTENSION: ImportExtension = "none";
|
|
@@ -22,18 +20,6 @@ export function toModuleBasename(filePath: string): string {
|
|
|
22
20
|
return path.basename(filePath).replace(/\.ts$/, "");
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
export function resolveImportExtension(
|
|
26
|
-
config: Pick<CodegenConfig, "importExtension">
|
|
27
|
-
): ImportExtension {
|
|
28
|
-
const extension = config.importExtension ?? DEFAULT_IMPORT_EXTENSION;
|
|
29
|
-
if (!SUPPORTED_IMPORT_EXTENSIONS.includes(extension)) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`[Codegen Error] importExtension must be "none", ".ts", or ".js", got ${JSON.stringify(extension)}.`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
return extension;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
23
|
export function importExtensionSuffix(importExtension: ImportExtension): string {
|
|
38
24
|
return importExtension === "none" ? "" : importExtension;
|
|
39
25
|
}
|
|
@@ -3,14 +3,11 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { afterEach, describe, expect,
|
|
7
|
-
import billingDictionary from "./fixtures/billing-namespace.json";
|
|
6
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
8
7
|
|
|
9
8
|
const codegenScript = fileURLToPath(new URL("./generate-i18n-types.ts", import.meta.url));
|
|
10
9
|
const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
11
10
|
|
|
12
|
-
type BillingNamespace = typeof billingDictionary;
|
|
13
|
-
|
|
14
11
|
function runCodegen(cwd: string) {
|
|
15
12
|
return spawnSync("tsx", [codegenScript, "--config", "i18n.codegen.json"], {
|
|
16
13
|
cwd,
|
|
@@ -49,15 +46,12 @@ function setupMultiCodegenFixture(tempDir: string) {
|
|
|
49
46
|
writeFileSync(
|
|
50
47
|
join(tempDir, "i18n.codegen.json"),
|
|
51
48
|
JSON.stringify({
|
|
49
|
+
projectName: "App",
|
|
52
50
|
namespaces: {
|
|
53
51
|
default: "src/i18n/translations/default.json",
|
|
54
52
|
billing: "src/i18n/translations/billing.json",
|
|
55
53
|
},
|
|
56
|
-
|
|
57
|
-
dictionaryOutput: "src/i18n/dictionary.generated.ts",
|
|
58
|
-
instanceOutput: "src/i18n/instance.generated.ts",
|
|
59
|
-
paramsTypeName: "AppParams",
|
|
60
|
-
schemaTypeName: "AppSchema",
|
|
54
|
+
codegenPath: "src/i18n",
|
|
61
55
|
localeFallback: {
|
|
62
56
|
en: null,
|
|
63
57
|
it: "en",
|
|
@@ -85,6 +79,7 @@ function writeTscProject(tempDir: string) {
|
|
|
85
79
|
skipLibCheck: true,
|
|
86
80
|
paths: {
|
|
87
81
|
"@xndrjs/i18n": [join(packageRoot, "src/index.ts")],
|
|
82
|
+
"@xndrjs/i18n/validation": [join(packageRoot, "src/validation/index.ts")],
|
|
88
83
|
},
|
|
89
84
|
},
|
|
90
85
|
include: ["src/**/*.ts"],
|
|
@@ -95,82 +90,45 @@ function writeTscProject(tempDir: string) {
|
|
|
95
90
|
);
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
describe("codegen
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
[
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
` billing: async () => billingEn,`,
|
|
139
|
-
` },`,
|
|
140
|
-
` })`,
|
|
141
|
-
` .withNamespaces(["billing"])`,
|
|
142
|
-
` .withLocale("en")`,
|
|
143
|
-
` .load();`,
|
|
144
|
-
``,
|
|
145
|
-
` view.set(`,
|
|
146
|
-
` "billing",`,
|
|
147
|
-
` "invoice_summary",`,
|
|
148
|
-
` "You have {count, plural, one {1 invoice} other {{count} invoices}}"`,
|
|
149
|
-
` );`,
|
|
150
|
-
` return view.t("billing", "invoice_summary", { count: 1 });`,
|
|
151
|
-
`}`,
|
|
152
|
-
``,
|
|
153
|
-
`export { hydrateBilling };`,
|
|
154
|
-
].join("\n")
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
writeTscProject(tempDir);
|
|
158
|
-
|
|
159
|
-
const tsc = runTsc(join(tempDir, "tsconfig.json"));
|
|
160
|
-
expect(tsc.status, `${tsc.stdout}\n${tsc.stderr}`).toBe(0);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("types projectNamespaceLocales result as the namespace schema", () => {
|
|
164
|
-
type AppSchema = {
|
|
165
|
-
billing: BillingNamespace;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
type ProjectNamespaceLocalesBilling = (
|
|
169
|
-
dictionary: AppSchema["billing"],
|
|
170
|
-
locales: readonly ("en" | "it")[]
|
|
171
|
-
) => AppSchema["billing"];
|
|
172
|
-
|
|
173
|
-
expectTypeOf<ReturnType<ProjectNamespaceLocalesBilling>>().toEqualTypeOf<BillingNamespace>();
|
|
174
|
-
});
|
|
93
|
+
describe("codegen instance + load", () => {
|
|
94
|
+
let tempDir: string;
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
if (tempDir) {
|
|
98
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("emits createI18n wired to namespaceLoaders", () => {
|
|
103
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
|
|
104
|
+
setupMultiCodegenFixture(tempDir);
|
|
105
|
+
|
|
106
|
+
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
107
|
+
expect(factory).toContain("export function createI18n(");
|
|
108
|
+
expect(factory).toContain("namespaceLoaders");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("passes tsc when loading via generated namespaceLoaders", () => {
|
|
112
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
|
|
113
|
+
setupMultiCodegenFixture(tempDir);
|
|
114
|
+
|
|
115
|
+
writeFileSync(
|
|
116
|
+
join(tempDir, "src/hydrate-billing.ts"),
|
|
117
|
+
[
|
|
118
|
+
`import { createI18n } from "./i18n/instance.generated.js";`,
|
|
119
|
+
``,
|
|
120
|
+
`async function hydrateBilling() {`,
|
|
121
|
+
` const { t } = await createI18n().load({ namespaces: ["billing"], locale: "en" });`,
|
|
122
|
+
` return t("billing", "invoice_summary", { count: 1 });`,
|
|
123
|
+
`}`,
|
|
124
|
+
``,
|
|
125
|
+
`export { hydrateBilling };`,
|
|
126
|
+
].join("\n")
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
writeTscProject(tempDir);
|
|
130
|
+
|
|
131
|
+
const tsc = runTsc(join(tempDir, "tsconfig.json"));
|
|
132
|
+
expect(tsc.status, `${tsc.stdout}\n${tsc.stderr}`).toBe(0);
|
|
175
133
|
});
|
|
176
134
|
});
|
|
@@ -105,7 +105,7 @@ describe("read-dictionary", () => {
|
|
|
105
105
|
expect(() => readDictionaryFile(jsonPath)).not.toThrow();
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
it("compiles yaml sources
|
|
108
|
+
it("compiles yaml sources into per-locale split files", () => {
|
|
109
109
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
|
|
110
110
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
111
111
|
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
@@ -121,6 +121,8 @@ describe("read-dictionary", () => {
|
|
|
121
121
|
[{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" }],
|
|
122
122
|
"src/i18n/generated",
|
|
123
123
|
{
|
|
124
|
+
delivery: "split-by-locale",
|
|
125
|
+
requestLocales: ["en"],
|
|
124
126
|
dictionariesByNamespace: {
|
|
125
127
|
billing: readDictionaryFile(join(tempDir, "src/i18n/translations/billing.yaml")),
|
|
126
128
|
},
|
|
@@ -128,20 +130,24 @@ describe("read-dictionary", () => {
|
|
|
128
130
|
);
|
|
129
131
|
|
|
130
132
|
expect(result.resolvedEntries).toEqual([
|
|
131
|
-
{ namespace: "billing", filePath: "src/i18n/
|
|
133
|
+
{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
|
|
132
134
|
]);
|
|
133
|
-
expect(result.splitPathsByNamespace).toEqual({
|
|
135
|
+
expect(result.splitPathsByNamespace).toEqual({
|
|
136
|
+
billing: {
|
|
137
|
+
en: "src/i18n/generated/translations/billing.en.json",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
134
140
|
expect(result.compiledFiles).toEqual([
|
|
135
|
-
"src/i18n/translations/billing.yaml → src/i18n/generated/translations/billing.json",
|
|
141
|
+
"src/i18n/translations/billing.yaml → src/i18n/generated/translations/billing.en.json",
|
|
136
142
|
]);
|
|
137
143
|
|
|
138
144
|
const compiled = JSON.parse(
|
|
139
|
-
readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
|
|
145
|
+
readFileSync(join(tempDir, "src/i18n/generated/translations/billing.en.json"), "utf8")
|
|
140
146
|
) as Record<string, Record<string, string>>;
|
|
141
147
|
expect(compiled.invoice_summary?.en).toBe("You have {count} invoices");
|
|
142
148
|
});
|
|
143
149
|
|
|
144
|
-
it("
|
|
150
|
+
it("splits json sources into per-locale files", () => {
|
|
145
151
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
|
|
146
152
|
mkdirSync(join(tempDir, "translations"), { recursive: true });
|
|
147
153
|
writeFileSync(
|
|
@@ -154,6 +160,8 @@ describe("read-dictionary", () => {
|
|
|
154
160
|
[{ namespace: "default", filePath: "translations/default.json" }],
|
|
155
161
|
"generated",
|
|
156
162
|
{
|
|
163
|
+
delivery: "split-by-locale",
|
|
164
|
+
requestLocales: ["en"],
|
|
157
165
|
dictionariesByNamespace: {
|
|
158
166
|
default: readDictionaryFile(join(tempDir, "translations/default.json")),
|
|
159
167
|
},
|
|
@@ -163,8 +171,14 @@ describe("read-dictionary", () => {
|
|
|
163
171
|
expect(result.resolvedEntries).toEqual([
|
|
164
172
|
{ namespace: "default", filePath: "translations/default.json" },
|
|
165
173
|
]);
|
|
166
|
-
expect(result.splitPathsByNamespace).toEqual({
|
|
167
|
-
|
|
174
|
+
expect(result.splitPathsByNamespace).toEqual({
|
|
175
|
+
default: {
|
|
176
|
+
en: "generated/translations/default.en.json",
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
expect(result.compiledFiles).toEqual([
|
|
180
|
+
"translations/default.json → generated/translations/default.en.json",
|
|
181
|
+
]);
|
|
168
182
|
});
|
|
169
183
|
|
|
170
184
|
it("splits multiple namespaces into per-locale files", () => {
|
|
@@ -405,7 +419,11 @@ describe("read-dictionary", () => {
|
|
|
405
419
|
tempDir,
|
|
406
420
|
[{ namespace: "default", filePath: "translations/default.json" }],
|
|
407
421
|
"generated",
|
|
408
|
-
{
|
|
422
|
+
{
|
|
423
|
+
delivery: "split-by-locale",
|
|
424
|
+
requestLocales: ["en"],
|
|
425
|
+
dictionariesByNamespace: {},
|
|
426
|
+
}
|
|
409
427
|
)
|
|
410
428
|
).toThrow('[Codegen Error] Missing parsed dictionary for namespace "default".');
|
|
411
429
|
});
|
|
@@ -142,7 +142,7 @@ function writeCompiledJson(absoluteJsonPath: string, dictionary: DictionaryJson)
|
|
|
142
142
|
|
|
143
143
|
export interface PrepareDictionaryEntriesOptions {
|
|
144
144
|
dictionariesByNamespace: Record<string, DictionaryJson>;
|
|
145
|
-
delivery
|
|
145
|
+
delivery: DeliveryMode;
|
|
146
146
|
localeFallback?: LocaleFallbackMap | undefined;
|
|
147
147
|
requestLocales?: readonly string[] | undefined;
|
|
148
148
|
deliveryArtifacts?: DeliveryArtifactsMap | undefined;
|
|
@@ -155,8 +155,8 @@ export interface PrepareDictionariesResult {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
* Phase 2 of codegen: materialize JSON artifacts on disk (
|
|
159
|
-
* Returns `splitPathsByNamespace` consumed by
|
|
158
|
+
* Phase 2 of codegen: materialize JSON artifacts on disk (locale/area split).
|
|
159
|
+
* Returns `splitPathsByNamespace` consumed by namespace-loader emitters.
|
|
160
160
|
*/
|
|
161
161
|
export function prepareDictionaryEntries(
|
|
162
162
|
projectRoot: string,
|
|
@@ -164,7 +164,7 @@ export function prepareDictionaryEntries(
|
|
|
164
164
|
generatedDirRelative: string,
|
|
165
165
|
options: PrepareDictionaryEntriesOptions
|
|
166
166
|
): PrepareDictionariesResult {
|
|
167
|
-
const delivery = options.delivery
|
|
167
|
+
const delivery = options.delivery;
|
|
168
168
|
const { dictionariesByNamespace, localeFallback, requestLocales, deliveryArtifacts } = options;
|
|
169
169
|
const resolvedEntries: NamespaceEntry[] = [];
|
|
170
170
|
const splitPathsByNamespace: Record<string, Record<string, string>> = {};
|
|
@@ -231,50 +231,28 @@ export function prepareDictionaryEntries(
|
|
|
231
231
|
continue;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
for (const area of Object.keys(deliveryArtifacts!).sort()) {
|
|
243
|
-
const areaRelativePath = resolveAreaJsonPath(entry.filePath, area, generatedDirRelative);
|
|
244
|
-
const areaAbsolutePath = path.resolve(projectRoot, areaRelativePath);
|
|
245
|
-
const wroteFile = writeCompiledJson(areaAbsolutePath, dictionariesByArea[area]!);
|
|
246
|
-
|
|
247
|
-
if (wroteFile) {
|
|
248
|
-
compiledFiles.push(
|
|
249
|
-
`${path.relative(projectRoot, sourceAbsolutePath)} → ${areaRelativePath}`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
splitPaths[area] = areaRelativePath;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
splitPathsByNamespace[entry.namespace] = splitPaths;
|
|
257
|
-
resolvedEntries.push(entry);
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
234
|
+
const splitPaths: Record<string, string> = {};
|
|
235
|
+
const dictionariesByArea = splitDictionaryByDeliveryArea(
|
|
236
|
+
dictionary,
|
|
237
|
+
deliveryArtifacts!,
|
|
238
|
+
localeFallback
|
|
239
|
+
);
|
|
260
240
|
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const wroteFile = writeCompiledJson(
|
|
241
|
+
for (const area of Object.keys(deliveryArtifacts!).sort()) {
|
|
242
|
+
const areaRelativePath = resolveAreaJsonPath(entry.filePath, area, generatedDirRelative);
|
|
243
|
+
const areaAbsolutePath = path.resolve(projectRoot, areaRelativePath);
|
|
244
|
+
const wroteFile = writeCompiledJson(areaAbsolutePath, dictionariesByArea[area]!);
|
|
265
245
|
|
|
266
246
|
if (wroteFile) {
|
|
267
247
|
compiledFiles.push(
|
|
268
|
-
`${path.relative(projectRoot, sourceAbsolutePath)} → ${
|
|
248
|
+
`${path.relative(projectRoot, sourceAbsolutePath)} → ${areaRelativePath}`
|
|
269
249
|
);
|
|
270
250
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
filePath: compiledRelativePath,
|
|
274
|
-
});
|
|
275
|
-
continue;
|
|
251
|
+
|
|
252
|
+
splitPaths[area] = areaRelativePath;
|
|
276
253
|
}
|
|
277
254
|
|
|
255
|
+
splitPathsByNamespace[entry.namespace] = splitPaths;
|
|
278
256
|
resolvedEntries.push(entry);
|
|
279
257
|
}
|
|
280
258
|
|