@xndrjs/i18n 0.8.1 → 0.8.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/cli/audit.js +739 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/codegen.js +1471 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/setup.js +148 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/codegen/index.js +43 -40
- package/dist/codegen/index.js.map +1 -1
- package/package.json +6 -9
- package/bin/audit.mjs +0 -14
- package/bin/codegen.mjs +0 -17
- package/bin/setup.mjs +0 -14
- package/src/IcuTranslationProviderMulti.test.ts +0 -215
- package/src/IcuTranslationProviderMulti.ts +0 -188
- package/src/audit/audit-dictionaries.test.ts +0 -134
- package/src/audit/audit-dictionaries.ts +0 -178
- package/src/audit/run-audit.test.ts +0 -169
- package/src/audit/run-audit.ts +0 -94
- package/src/builder-load-registry.test.ts +0 -44
- package/src/builder-load-registry.ts +0 -78
- package/src/builder-loaders.ts +0 -15
- package/src/builder-types.test.ts +0 -57
- package/src/builder-types.ts +0 -24
- package/src/builder.test.ts +0 -303
- package/src/builder.ts +0 -15
- package/src/codegen/codegen-config-schema.ts +0 -156
- package/src/codegen/config.test.ts +0 -231
- package/src/codegen/config.ts +0 -42
- package/src/codegen/constants.ts +0 -9
- package/src/codegen/delivery-artifacts.test.ts +0 -167
- package/src/codegen/delivery-artifacts.ts +0 -154
- package/src/codegen/dictionary-spec-contract.test.ts +0 -28
- package/src/codegen/dictionary-spec-contract.ts +0 -138
- package/src/codegen/emit/dictionary-schema-file.ts +0 -113
- package/src/codegen/emit/instance-file.test.ts +0 -71
- package/src/codegen/emit/instance-file.ts +0 -257
- package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
- package/src/codegen/emit/namespace-loaders-file.ts +0 -229
- package/src/codegen/emit/types-file.test.ts +0 -96
- package/src/codegen/emit/types-file.ts +0 -142
- package/src/codegen/fixtures/billing-namespace.json +0 -6
- package/src/codegen/generate-i18n-types.test.ts +0 -1137
- package/src/codegen/generate-i18n-types.ts +0 -20
- package/src/codegen/icu-analysis.ts +0 -110
- package/src/codegen/locale-fallback.ts +0 -73
- package/src/codegen/locale-policy.test.ts +0 -35
- package/src/codegen/locale-policy.ts +0 -30
- package/src/codegen/paths.ts +0 -44
- package/src/codegen/project-locales-set-namespace.test.ts +0 -134
- package/src/codegen/read-dictionary.test.ts +0 -594
- package/src/codegen/read-dictionary.ts +0 -260
- package/src/codegen/regenerate-namespaces.ts +0 -180
- package/src/codegen/run-codegen.test.ts +0 -230
- package/src/codegen/run-codegen.ts +0 -252
- package/src/codegen/types.ts +0 -10
- package/src/codegen/write-file-if-changed.test.ts +0 -42
- package/src/codegen/write-file-if-changed.ts +0 -20
- package/src/codegen-config/build-config.ts +0 -15
- package/src/codegen-config/codegen-config.test.ts +0 -36
- package/src/codegen-config/index.ts +0 -37
- package/src/codegen-config/type-names.ts +0 -20
- package/src/codegen-config/write-config.ts +0 -8
- package/src/engine.ts +0 -47
- package/src/fetch-artifact.ts +0 -16
- package/src/format-core.ts +0 -91
- package/src/i18n-handle.ts +0 -216
- package/src/icu/extract-variables.test.ts +0 -199
- package/src/icu/extract-variables.ts +0 -227
- package/src/icu/parse-template.ts +0 -31
- package/src/index.ts +0 -63
- package/src/load-state.ts +0 -15
- package/src/project-locales.test.ts +0 -280
- package/src/project-locales.ts +0 -211
- package/src/resolve-locale.test.ts +0 -91
- package/src/resolve-locale.ts +0 -88
- package/src/scope-multi.ts +0 -105
- package/src/scope-types.ts +0 -35
- package/src/scope.test.ts +0 -172
- package/src/serialized-state.test.ts +0 -114
- package/src/serialized-state.ts +0 -28
- package/src/setup/setup-i18n.test.ts +0 -85
- package/src/setup/setup-i18n.ts +0 -156
- package/src/types.ts +0 -65
- package/src/validation/create-args-schema.ts +0 -76
- package/src/validation/create-normalized-schema.ts +0 -52
- package/src/validation/errors.ts +0 -43
- package/src/validation/index.ts +0 -202
- package/src/validation/normalize.ts +0 -299
- package/src/validation/to-dictionary.ts +0 -67
- package/src/validation/types.ts +0 -82
- package/src/validation/validate-normalized.ts +0 -133
- package/src/validation/validation.test.ts +0 -562
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { parse as parseYaml } from "yaml";
|
|
4
|
-
import { IDENTIFIER_NAME_PATTERN, IDENTIFIER_NAME_REQUIREMENT } from "./constants.js";
|
|
5
|
-
import type { DeliveryMode } from "./codegen-config-schema.js";
|
|
6
|
-
import type { DeliveryArtifactsMap } from "./delivery-artifacts.js";
|
|
7
|
-
import {
|
|
8
|
-
projectNamespaceForDeliveryAreaCore,
|
|
9
|
-
projectNamespaceLocalesCore,
|
|
10
|
-
} from "../project-locales.js";
|
|
11
|
-
import type { LocaleFallbackMap } from "../types.js";
|
|
12
|
-
import type { DictionaryJson, NamespaceEntry } from "./types.js";
|
|
13
|
-
import { writeFileIfChanged } from "./write-file-if-changed.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Dictionary I/O for codegen and audit: read/validate source files (`readDictionaryFile`)
|
|
17
|
-
* and write compiled or split JSON artifacts (`prepareDictionaryEntries`).
|
|
18
|
-
*/
|
|
19
|
-
export const SUPPORTED_DICTIONARY_EXTENSIONS = [".json", ".yaml", ".yml"] as const;
|
|
20
|
-
export type DictionaryFormat = "json" | "yaml";
|
|
21
|
-
|
|
22
|
-
export function getDictionaryFormat(filePath: string): DictionaryFormat | null {
|
|
23
|
-
const extension = path.extname(filePath).toLowerCase();
|
|
24
|
-
if (extension === ".json") {
|
|
25
|
-
return "json";
|
|
26
|
-
}
|
|
27
|
-
if (extension === ".yaml" || extension === ".yml") {
|
|
28
|
-
return "yaml";
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function resolveCompiledJsonPath(sourcePath: string, generatedDirRelative: string): string {
|
|
34
|
-
const baseName = `${path.basename(sourcePath, path.extname(sourcePath))}.json`;
|
|
35
|
-
return path.join(generatedDirRelative, "translations", baseName).replace(/\\/g, "/");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function resolveSplitJsonPath(
|
|
39
|
-
sourcePath: string,
|
|
40
|
-
locale: string,
|
|
41
|
-
generatedDirRelative: string
|
|
42
|
-
): string {
|
|
43
|
-
const baseName = path.basename(sourcePath, path.extname(sourcePath));
|
|
44
|
-
return path
|
|
45
|
-
.join(generatedDirRelative, "translations", `${baseName}.${locale}.json`)
|
|
46
|
-
.replace(/\\/g, "/");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function resolveAreaJsonPath(
|
|
50
|
-
sourcePath: string,
|
|
51
|
-
area: string,
|
|
52
|
-
generatedDirRelative: string
|
|
53
|
-
): string {
|
|
54
|
-
const baseName = path.basename(sourcePath, path.extname(sourcePath));
|
|
55
|
-
return path
|
|
56
|
-
.join(generatedDirRelative, "translations", `${baseName}.${area}.json`)
|
|
57
|
-
.replace(/\\/g, "/");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Projects a canonical dictionary into per-locale slices for split-by-locale delivery. */
|
|
61
|
-
export function splitDictionaryByLocale(
|
|
62
|
-
dictionary: DictionaryJson,
|
|
63
|
-
locales: readonly string[],
|
|
64
|
-
localeFallback?: LocaleFallbackMap
|
|
65
|
-
): Record<string, DictionaryJson> {
|
|
66
|
-
const byLocale: Record<string, DictionaryJson> = {};
|
|
67
|
-
|
|
68
|
-
for (const locale of locales) {
|
|
69
|
-
byLocale[locale] = projectNamespaceLocalesCore(dictionary, [locale], localeFallback);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return byLocale;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Projects a canonical dictionary into per-area slices for custom delivery. */
|
|
76
|
-
export function splitDictionaryByDeliveryArea(
|
|
77
|
-
dictionary: DictionaryJson,
|
|
78
|
-
deliveryArtifacts: DeliveryArtifactsMap,
|
|
79
|
-
localeFallback?: LocaleFallbackMap
|
|
80
|
-
): Record<string, DictionaryJson> {
|
|
81
|
-
const byArea: Record<string, DictionaryJson> = {};
|
|
82
|
-
|
|
83
|
-
for (const [area, areaLocales] of Object.entries(deliveryArtifacts)) {
|
|
84
|
-
byArea[area] = projectNamespaceForDeliveryAreaCore(dictionary, areaLocales, localeFallback);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return byArea;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function assertDictionaryShape(value: unknown, context: string): asserts value is DictionaryJson {
|
|
91
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
92
|
-
throw new Error(`[Codegen Error] ${context} must be a plain object.`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
for (const [key, localesByKey] of Object.entries(value)) {
|
|
96
|
-
if (!IDENTIFIER_NAME_PATTERN.test(key)) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`[Codegen Error] ${context}: invalid key "${key}" (${IDENTIFIER_NAME_REQUIREMENT}).`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (localesByKey === null || typeof localesByKey !== "object" || Array.isArray(localesByKey)) {
|
|
103
|
-
throw new Error(`[Codegen Error] ${context}: key "${key}" must map locales to strings.`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
for (const [locale, template] of Object.entries(localesByKey)) {
|
|
107
|
-
if (typeof template !== "string") {
|
|
108
|
-
throw new Error(
|
|
109
|
-
`[Codegen Error] ${context}: key "${key}", locale "${locale}" must be a string.`
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Reads and shape-validates a dictionary file from disk (JSON or YAML). */
|
|
117
|
-
export function readDictionaryFile(absolutePath: string): DictionaryJson {
|
|
118
|
-
const format = getDictionaryFormat(absolutePath);
|
|
119
|
-
if (!format) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
`[Codegen Error] Unsupported dictionary format "${path.extname(absolutePath)}" for ${absolutePath}. Use .json, .yaml, or .yml.`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const source = fs.readFileSync(absolutePath, "utf8");
|
|
126
|
-
let parsed: unknown;
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
parsed = format === "json" ? JSON.parse(source) : parseYaml(source);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
-
throw new Error(`[Codegen Error] Failed to parse dictionary ${absolutePath}: ${message}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
assertDictionaryShape(parsed, `Dictionary file ${absolutePath}`);
|
|
136
|
-
return parsed;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function writeCompiledJson(absoluteJsonPath: string, dictionary: DictionaryJson): boolean {
|
|
140
|
-
return writeFileIfChanged(absoluteJsonPath, `${JSON.stringify(dictionary, null, 2)}\n`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export interface PrepareDictionaryEntriesOptions {
|
|
144
|
-
dictionariesByNamespace: Record<string, DictionaryJson>;
|
|
145
|
-
delivery: DeliveryMode;
|
|
146
|
-
localeFallback?: LocaleFallbackMap | undefined;
|
|
147
|
-
requestLocales?: readonly string[] | undefined;
|
|
148
|
-
deliveryArtifacts?: DeliveryArtifactsMap | undefined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface PrepareDictionariesResult {
|
|
152
|
-
resolvedEntries: NamespaceEntry[];
|
|
153
|
-
splitPathsByNamespace: Record<string, Record<string, string>>;
|
|
154
|
-
compiledFiles: string[];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Phase 2 of codegen: materialize JSON artifacts on disk (locale/area split).
|
|
159
|
-
* Returns `splitPathsByNamespace` consumed by namespace-loader emitters.
|
|
160
|
-
*/
|
|
161
|
-
export function prepareDictionaryEntries(
|
|
162
|
-
projectRoot: string,
|
|
163
|
-
entries: NamespaceEntry[],
|
|
164
|
-
generatedDirRelative: string,
|
|
165
|
-
options: PrepareDictionaryEntriesOptions
|
|
166
|
-
): PrepareDictionariesResult {
|
|
167
|
-
const delivery = options.delivery;
|
|
168
|
-
const { dictionariesByNamespace, localeFallback, requestLocales, deliveryArtifacts } = options;
|
|
169
|
-
const resolvedEntries: NamespaceEntry[] = [];
|
|
170
|
-
const splitPathsByNamespace: Record<string, Record<string, string>> = {};
|
|
171
|
-
const compiledFiles: string[] = [];
|
|
172
|
-
|
|
173
|
-
if (delivery === "split-by-locale" && (!requestLocales || requestLocales.length === 0)) {
|
|
174
|
-
throw new Error(
|
|
175
|
-
"[Codegen Error] split-by-locale delivery requires at least one request locale."
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
delivery === "custom" &&
|
|
181
|
-
(!deliveryArtifacts || Object.keys(deliveryArtifacts).length === 0)
|
|
182
|
-
) {
|
|
183
|
-
throw new Error("[Codegen Error] custom delivery requires deliveryArtifacts.");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
for (const entry of entries) {
|
|
187
|
-
const sourceAbsolutePath = path.resolve(projectRoot, entry.filePath);
|
|
188
|
-
const format = getDictionaryFormat(entry.filePath);
|
|
189
|
-
|
|
190
|
-
if (!format) {
|
|
191
|
-
throw new Error(
|
|
192
|
-
`[Codegen Error] Namespace "${entry.namespace}" uses unsupported dictionary extension in "${entry.filePath}". Use .json, .yaml, or .yml.`
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const dictionary = dictionariesByNamespace[entry.namespace];
|
|
197
|
-
if (!dictionary) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
`[Codegen Error] Missing parsed dictionary for namespace "${entry.namespace}".`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (delivery === "split-by-locale") {
|
|
204
|
-
const splitPaths: Record<string, string> = {};
|
|
205
|
-
const dictionariesByLocale = splitDictionaryByLocale(
|
|
206
|
-
dictionary,
|
|
207
|
-
requestLocales!,
|
|
208
|
-
localeFallback
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
for (const locale of requestLocales!) {
|
|
212
|
-
const splitRelativePath = resolveSplitJsonPath(
|
|
213
|
-
entry.filePath,
|
|
214
|
-
locale,
|
|
215
|
-
generatedDirRelative
|
|
216
|
-
);
|
|
217
|
-
const splitAbsolutePath = path.resolve(projectRoot, splitRelativePath);
|
|
218
|
-
const wroteFile = writeCompiledJson(splitAbsolutePath, dictionariesByLocale[locale]!);
|
|
219
|
-
|
|
220
|
-
if (wroteFile) {
|
|
221
|
-
compiledFiles.push(
|
|
222
|
-
`${path.relative(projectRoot, sourceAbsolutePath)} → ${splitRelativePath}`
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
splitPaths[locale] = splitRelativePath;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
splitPathsByNamespace[entry.namespace] = splitPaths;
|
|
230
|
-
resolvedEntries.push(entry);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const splitPaths: Record<string, string> = {};
|
|
235
|
-
const dictionariesByArea = splitDictionaryByDeliveryArea(
|
|
236
|
-
dictionary,
|
|
237
|
-
deliveryArtifacts!,
|
|
238
|
-
localeFallback
|
|
239
|
-
);
|
|
240
|
-
|
|
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]!);
|
|
245
|
-
|
|
246
|
-
if (wroteFile) {
|
|
247
|
-
compiledFiles.push(
|
|
248
|
-
`${path.relative(projectRoot, sourceAbsolutePath)} → ${areaRelativePath}`
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
splitPaths[area] = areaRelativePath;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
splitPathsByNamespace[entry.namespace] = splitPaths;
|
|
256
|
-
resolvedEntries.push(entry);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return { resolvedEntries, splitPathsByNamespace, compiledFiles };
|
|
260
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { CodegenConfig } from "./codegen-config-schema.js";
|
|
4
|
-
import {
|
|
5
|
-
loadConfig,
|
|
6
|
-
resolveArtifactsPath,
|
|
7
|
-
resolveCodegenPaths,
|
|
8
|
-
resolveNamespaces,
|
|
9
|
-
} from "./config.js";
|
|
10
|
-
import { getDeliveryArtifactsIssues } from "./delivery-artifacts.js";
|
|
11
|
-
import {
|
|
12
|
-
buildDictionarySpecFromAnalysis,
|
|
13
|
-
loadDictionarySpecFromSchemaFile,
|
|
14
|
-
namespaceContractsMatch,
|
|
15
|
-
} from "./dictionary-spec-contract.js";
|
|
16
|
-
import { analyzeDictionaries } from "./icu-analysis.js";
|
|
17
|
-
import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
|
|
18
|
-
import { reportCodegenIssues } from "./paths.js";
|
|
19
|
-
import { prepareDictionaryEntries } from "./read-dictionary.js";
|
|
20
|
-
import type { DictionaryJson } from "./types.js";
|
|
21
|
-
|
|
22
|
-
export interface RegenerateNamespacesInput {
|
|
23
|
-
/** Namespaces whose delivery JSON should be refreshed from authoring sources. */
|
|
24
|
-
namespaces: readonly string[];
|
|
25
|
-
configPath?: string;
|
|
26
|
-
config?: CodegenConfig;
|
|
27
|
-
projectRoot?: string;
|
|
28
|
-
/** When false, skip console.log summaries (default true). */
|
|
29
|
-
log?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface RegenerateNamespacesResult {
|
|
33
|
-
projectRoot: string;
|
|
34
|
-
compiledFiles: string[];
|
|
35
|
-
splitPathsByNamespace: Record<string, Record<string, string>>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resolveRegenerateInput(input: RegenerateNamespacesInput): {
|
|
39
|
-
projectRoot: string;
|
|
40
|
-
config: CodegenConfig;
|
|
41
|
-
namespaces: readonly string[];
|
|
42
|
-
log: boolean;
|
|
43
|
-
} {
|
|
44
|
-
const log = input.log !== false;
|
|
45
|
-
const namespaces = input.namespaces;
|
|
46
|
-
|
|
47
|
-
if (input.config !== undefined) {
|
|
48
|
-
return {
|
|
49
|
-
projectRoot: path.resolve(input.projectRoot ?? process.cwd()),
|
|
50
|
-
config: input.config,
|
|
51
|
-
namespaces,
|
|
52
|
-
log,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const configPath = path.resolve(process.cwd(), input.configPath ?? "i18n/i18n.codegen.json");
|
|
57
|
-
if (!fs.existsSync(configPath)) {
|
|
58
|
-
throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
projectRoot: path.dirname(configPath),
|
|
62
|
-
config: loadConfig(configPath),
|
|
63
|
-
namespaces,
|
|
64
|
-
log,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Content-only refresh: re-materialize delivery JSON for selected namespaces from
|
|
70
|
-
* current authoring files, without rewriting generated TypeScript.
|
|
71
|
-
*
|
|
72
|
-
* Must not change the ICU key/param contract established by a prior {@link runCodegen}.
|
|
73
|
-
* If authoring changed keys or ICU args, this throws — run {@link runCodegen} and ship a release.
|
|
74
|
-
*
|
|
75
|
-
* Authoring updates are out of scope for this library (CMS or editors write those files).
|
|
76
|
-
* End-to-end without app rebuild requires `loaderStrategy: "fetch"`.
|
|
77
|
-
*/
|
|
78
|
-
export function regenerateNamespaces(input: RegenerateNamespacesInput): RegenerateNamespacesResult {
|
|
79
|
-
const { projectRoot, config, namespaces, log } = resolveRegenerateInput(input);
|
|
80
|
-
|
|
81
|
-
if (namespaces.length === 0) {
|
|
82
|
-
throw new Error("[Codegen Error] regenerateNamespaces requires a non-empty namespaces list.");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const sourceEntries = resolveNamespaces(config);
|
|
86
|
-
const known = new Set(sourceEntries.map((entry) => entry.namespace));
|
|
87
|
-
const unknown = namespaces.filter((namespace) => !known.has(namespace));
|
|
88
|
-
if (unknown.length > 0) {
|
|
89
|
-
throw new Error(
|
|
90
|
-
`[Codegen Error] Unknown namespace(s) for regenerateNamespaces: ${unknown.join(", ")}.`
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const requested = new Set(namespaces);
|
|
95
|
-
const selectedEntries = sourceEntries.filter((entry) => requested.has(entry.namespace));
|
|
96
|
-
|
|
97
|
-
const paths = resolveCodegenPaths(config);
|
|
98
|
-
const schemaPath = path.resolve(projectRoot, paths.dictionarySchemaOutput);
|
|
99
|
-
const established = loadDictionarySpecFromSchemaFile(schemaPath);
|
|
100
|
-
|
|
101
|
-
const artifactsPathRelative = resolveArtifactsPath(config);
|
|
102
|
-
const delivery = config.delivery ?? "split-by-locale";
|
|
103
|
-
|
|
104
|
-
// Analyze all namespaces so locale unions / deliveryArtifacts stay consistent with full codegen.
|
|
105
|
-
const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
|
|
106
|
-
if (!analysisResult.ok) {
|
|
107
|
-
throw new Error(
|
|
108
|
-
"[Codegen Error] Dictionary ICU analysis failed while regenerating namespaces."
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const {
|
|
113
|
-
argsSpecByNamespace,
|
|
114
|
-
locales,
|
|
115
|
-
dictionariesByNamespace: sourceDictionaries,
|
|
116
|
-
} = analysisResult.analysis;
|
|
117
|
-
|
|
118
|
-
const current = buildDictionarySpecFromAnalysis(selectedEntries, argsSpecByNamespace);
|
|
119
|
-
if (!namespaceContractsMatch(namespaces, current, established)) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
"[Codegen Error] Namespace ICU contract changed (keys or params). Contract change requires runCodegen."
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (config.localeFallback) {
|
|
126
|
-
const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
|
|
127
|
-
if (localeFallbackIssues.length > 0) {
|
|
128
|
-
reportCodegenIssues(localeFallbackIssues);
|
|
129
|
-
throw new Error("[Codegen Error] Invalid localeFallback configuration.");
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const requestLocales = collectRequestLocales(locales, config.localeFallback);
|
|
134
|
-
const requestLocalesList = [...requestLocales].sort();
|
|
135
|
-
|
|
136
|
-
if (delivery === "custom" && config.deliveryArtifacts) {
|
|
137
|
-
const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
|
|
138
|
-
config.deliveryArtifacts,
|
|
139
|
-
requestLocales
|
|
140
|
-
);
|
|
141
|
-
if (deliveryArtifactsIssues.length > 0) {
|
|
142
|
-
reportCodegenIssues(deliveryArtifactsIssues);
|
|
143
|
-
throw new Error("[Codegen Error] Invalid deliveryArtifacts configuration.");
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const dictionariesByNamespace: Record<string, DictionaryJson> = {};
|
|
148
|
-
for (const entry of selectedEntries) {
|
|
149
|
-
const dictionary = sourceDictionaries[entry.namespace];
|
|
150
|
-
if (!dictionary) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`[Codegen Error] Missing parsed dictionary for namespace "${entry.namespace}".`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
dictionariesByNamespace[entry.namespace] = dictionary;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const { splitPathsByNamespace, compiledFiles } = prepareDictionaryEntries(
|
|
159
|
-
projectRoot,
|
|
160
|
-
selectedEntries,
|
|
161
|
-
artifactsPathRelative,
|
|
162
|
-
{
|
|
163
|
-
dictionariesByNamespace,
|
|
164
|
-
delivery,
|
|
165
|
-
localeFallback: config.localeFallback,
|
|
166
|
-
requestLocales: delivery === "split-by-locale" ? requestLocalesList : undefined,
|
|
167
|
-
deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : undefined,
|
|
168
|
-
}
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
if (log) {
|
|
172
|
-
if (compiledFiles.length > 0) {
|
|
173
|
-
console.log(`✅ Regenerated delivery artifacts: ${compiledFiles.join(", ")}`);
|
|
174
|
-
} else {
|
|
175
|
-
console.log("✅ Regenerated delivery artifacts: unchanged");
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return { projectRoot, compiledFiles, splitPathsByNamespace };
|
|
180
|
-
}
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { regenerateNamespaces } from "./regenerate-namespaces.js";
|
|
6
|
-
import { runCodegen } from "./run-codegen.js";
|
|
7
|
-
|
|
8
|
-
function setupProject() {
|
|
9
|
-
const projectRoot = mkdtempSync(join(tmpdir(), "xndrjs-i18n-a5-"));
|
|
10
|
-
mkdirSync(join(projectRoot, "translations"), { recursive: true });
|
|
11
|
-
mkdirSync(join(projectRoot, "generated"), { recursive: true });
|
|
12
|
-
|
|
13
|
-
writeFileSync(
|
|
14
|
-
join(projectRoot, "translations/default.json"),
|
|
15
|
-
JSON.stringify(
|
|
16
|
-
{
|
|
17
|
-
welcome: { en: "Hello {name}!", it: "Ciao {name}!" },
|
|
18
|
-
login: { en: "Login", it: "Accedi" },
|
|
19
|
-
},
|
|
20
|
-
null,
|
|
21
|
-
2
|
|
22
|
-
)
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
writeFileSync(
|
|
26
|
-
join(projectRoot, "translations/billing.json"),
|
|
27
|
-
JSON.stringify(
|
|
28
|
-
{
|
|
29
|
-
invoice_summary: {
|
|
30
|
-
en: "You have {count} invoices",
|
|
31
|
-
it: "Hai {count} fatture",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
null,
|
|
35
|
-
2
|
|
36
|
-
)
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
writeFileSync(
|
|
40
|
-
join(projectRoot, "i18n.codegen.json"),
|
|
41
|
-
JSON.stringify(
|
|
42
|
-
{
|
|
43
|
-
projectName: "App",
|
|
44
|
-
namespaces: {
|
|
45
|
-
default: "translations/default.json",
|
|
46
|
-
billing: "translations/billing.json",
|
|
47
|
-
},
|
|
48
|
-
codegenPath: "generated",
|
|
49
|
-
delivery: "split-by-locale",
|
|
50
|
-
artifactsPath: "generated",
|
|
51
|
-
},
|
|
52
|
-
null,
|
|
53
|
-
2
|
|
54
|
-
)
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
return projectRoot;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
describe("runCodegen + regenerateNamespaces", () => {
|
|
61
|
-
let projectRoot: string;
|
|
62
|
-
|
|
63
|
-
afterEach(() => {
|
|
64
|
-
if (projectRoot) {
|
|
65
|
-
rmSync(projectRoot, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("runCodegen emits types, loaders, and delivery JSON", () => {
|
|
70
|
-
projectRoot = setupProject();
|
|
71
|
-
const result = runCodegen({
|
|
72
|
-
configPath: join(projectRoot, "i18n.codegen.json"),
|
|
73
|
-
log: false,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(result.generatedFiles).toEqual(
|
|
77
|
-
expect.arrayContaining([
|
|
78
|
-
"generated/i18n-types.generated.ts",
|
|
79
|
-
"generated/instance.generated.ts",
|
|
80
|
-
"generated/namespace-loaders.generated.ts",
|
|
81
|
-
"generated/dictionary-schema.generated.ts",
|
|
82
|
-
])
|
|
83
|
-
);
|
|
84
|
-
expect(
|
|
85
|
-
readFileSync(join(projectRoot, "generated/translations/default.en.json"), "utf8")
|
|
86
|
-
).toContain("Hello {name}!");
|
|
87
|
-
expect(
|
|
88
|
-
readFileSync(join(projectRoot, "generated/namespace-loaders.generated.ts"), "utf8")
|
|
89
|
-
).toContain("import('./translations/default.en.json')");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("runCodegen emits fetch loaders when configured", () => {
|
|
93
|
-
projectRoot = setupProject();
|
|
94
|
-
writeFileSync(
|
|
95
|
-
join(projectRoot, "i18n.codegen.json"),
|
|
96
|
-
JSON.stringify(
|
|
97
|
-
{
|
|
98
|
-
projectName: "App",
|
|
99
|
-
namespaces: {
|
|
100
|
-
default: "translations/default.json",
|
|
101
|
-
billing: "translations/billing.json",
|
|
102
|
-
},
|
|
103
|
-
codegenPath: "generated",
|
|
104
|
-
delivery: "split-by-locale",
|
|
105
|
-
artifactsPath: "generated",
|
|
106
|
-
loaderStrategy: "fetch",
|
|
107
|
-
},
|
|
108
|
-
null,
|
|
109
|
-
2
|
|
110
|
-
)
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
|
|
114
|
-
const loaders = readFileSync(
|
|
115
|
-
join(projectRoot, "generated/namespace-loaders.generated.ts"),
|
|
116
|
-
"utf8"
|
|
117
|
-
);
|
|
118
|
-
expect(loaders).toContain('fetchImpl({ locale, namespace: "default" })');
|
|
119
|
-
expect(loaders).toContain("createNamespaceLoaders");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("regenerateNamespaces refreshes selected delivery JSON without rewriting types", () => {
|
|
123
|
-
projectRoot = setupProject();
|
|
124
|
-
runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
|
|
125
|
-
|
|
126
|
-
const typesBefore = readFileSync(
|
|
127
|
-
join(projectRoot, "generated/i18n-types.generated.ts"),
|
|
128
|
-
"utf8"
|
|
129
|
-
);
|
|
130
|
-
const instanceBefore = readFileSync(
|
|
131
|
-
join(projectRoot, "generated/instance.generated.ts"),
|
|
132
|
-
"utf8"
|
|
133
|
-
);
|
|
134
|
-
const billingBefore = readFileSync(
|
|
135
|
-
join(projectRoot, "generated/translations/billing.en.json"),
|
|
136
|
-
"utf8"
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
writeFileSync(
|
|
140
|
-
join(projectRoot, "translations/default.json"),
|
|
141
|
-
JSON.stringify(
|
|
142
|
-
{
|
|
143
|
-
welcome: { en: "Hi {name}!", it: "Ciao {name}!" },
|
|
144
|
-
login: { en: "Login", it: "Accedi" },
|
|
145
|
-
},
|
|
146
|
-
null,
|
|
147
|
-
2
|
|
148
|
-
)
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
regenerateNamespaces({
|
|
152
|
-
configPath: join(projectRoot, "i18n.codegen.json"),
|
|
153
|
-
namespaces: ["default"],
|
|
154
|
-
log: false,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const en = JSON.parse(
|
|
158
|
-
readFileSync(join(projectRoot, "generated/translations/default.en.json"), "utf8")
|
|
159
|
-
);
|
|
160
|
-
expect(en.welcome.en).toBe("Hi {name}!");
|
|
161
|
-
expect(en.login.en).toBe("Login");
|
|
162
|
-
expect(readFileSync(join(projectRoot, "generated/translations/billing.en.json"), "utf8")).toBe(
|
|
163
|
-
billingBefore
|
|
164
|
-
);
|
|
165
|
-
expect(readFileSync(join(projectRoot, "generated/i18n-types.generated.ts"), "utf8")).toBe(
|
|
166
|
-
typesBefore
|
|
167
|
-
);
|
|
168
|
-
expect(readFileSync(join(projectRoot, "generated/instance.generated.ts"), "utf8")).toBe(
|
|
169
|
-
instanceBefore
|
|
170
|
-
);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("regenerateNamespaces rejects authoring that changes ICU args", () => {
|
|
174
|
-
projectRoot = setupProject();
|
|
175
|
-
runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
|
|
176
|
-
|
|
177
|
-
writeFileSync(
|
|
178
|
-
join(projectRoot, "translations/default.json"),
|
|
179
|
-
JSON.stringify(
|
|
180
|
-
{
|
|
181
|
-
welcome: { en: "Hello {name} {extra}!", it: "Ciao {name} {extra}!" },
|
|
182
|
-
login: { en: "Login", it: "Accedi" },
|
|
183
|
-
},
|
|
184
|
-
null,
|
|
185
|
-
2
|
|
186
|
-
)
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
expect(() =>
|
|
190
|
-
regenerateNamespaces({
|
|
191
|
-
configPath: join(projectRoot, "i18n.codegen.json"),
|
|
192
|
-
namespaces: ["default"],
|
|
193
|
-
log: false,
|
|
194
|
-
})
|
|
195
|
-
).toThrow(/Contract change requires runCodegen/);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("regenerateNamespaces rejects empty or unknown namespaces", () => {
|
|
199
|
-
projectRoot = setupProject();
|
|
200
|
-
runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
|
|
201
|
-
|
|
202
|
-
expect(() =>
|
|
203
|
-
regenerateNamespaces({
|
|
204
|
-
configPath: join(projectRoot, "i18n.codegen.json"),
|
|
205
|
-
namespaces: [],
|
|
206
|
-
log: false,
|
|
207
|
-
})
|
|
208
|
-
).toThrow(/non-empty namespaces/);
|
|
209
|
-
|
|
210
|
-
expect(() =>
|
|
211
|
-
regenerateNamespaces({
|
|
212
|
-
configPath: join(projectRoot, "i18n.codegen.json"),
|
|
213
|
-
namespaces: ["missing"],
|
|
214
|
-
log: false,
|
|
215
|
-
})
|
|
216
|
-
).toThrow(/Unknown namespace/);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("regenerateNamespaces requires a prior runCodegen schema", () => {
|
|
220
|
-
projectRoot = setupProject();
|
|
221
|
-
|
|
222
|
-
expect(() =>
|
|
223
|
-
regenerateNamespaces({
|
|
224
|
-
configPath: join(projectRoot, "i18n.codegen.json"),
|
|
225
|
-
namespaces: ["default"],
|
|
226
|
-
log: false,
|
|
227
|
-
})
|
|
228
|
-
).toThrow(/Run runCodegen first/);
|
|
229
|
-
});
|
|
230
|
-
});
|