@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.
Files changed (93) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/audit.js +739 -0
  3. package/dist/cli/audit.js.map +1 -0
  4. package/dist/cli/codegen.js +1471 -0
  5. package/dist/cli/codegen.js.map +1 -0
  6. package/dist/cli/setup.js +148 -0
  7. package/dist/cli/setup.js.map +1 -0
  8. package/dist/codegen/index.js +43 -40
  9. package/dist/codegen/index.js.map +1 -1
  10. package/package.json +6 -9
  11. package/bin/audit.mjs +0 -14
  12. package/bin/codegen.mjs +0 -17
  13. package/bin/setup.mjs +0 -14
  14. package/src/IcuTranslationProviderMulti.test.ts +0 -215
  15. package/src/IcuTranslationProviderMulti.ts +0 -188
  16. package/src/audit/audit-dictionaries.test.ts +0 -134
  17. package/src/audit/audit-dictionaries.ts +0 -178
  18. package/src/audit/run-audit.test.ts +0 -169
  19. package/src/audit/run-audit.ts +0 -94
  20. package/src/builder-load-registry.test.ts +0 -44
  21. package/src/builder-load-registry.ts +0 -78
  22. package/src/builder-loaders.ts +0 -15
  23. package/src/builder-types.test.ts +0 -57
  24. package/src/builder-types.ts +0 -24
  25. package/src/builder.test.ts +0 -303
  26. package/src/builder.ts +0 -15
  27. package/src/codegen/codegen-config-schema.ts +0 -156
  28. package/src/codegen/config.test.ts +0 -231
  29. package/src/codegen/config.ts +0 -42
  30. package/src/codegen/constants.ts +0 -9
  31. package/src/codegen/delivery-artifacts.test.ts +0 -167
  32. package/src/codegen/delivery-artifacts.ts +0 -154
  33. package/src/codegen/dictionary-spec-contract.test.ts +0 -28
  34. package/src/codegen/dictionary-spec-contract.ts +0 -138
  35. package/src/codegen/emit/dictionary-schema-file.ts +0 -113
  36. package/src/codegen/emit/instance-file.test.ts +0 -71
  37. package/src/codegen/emit/instance-file.ts +0 -257
  38. package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
  39. package/src/codegen/emit/namespace-loaders-file.ts +0 -229
  40. package/src/codegen/emit/types-file.test.ts +0 -96
  41. package/src/codegen/emit/types-file.ts +0 -142
  42. package/src/codegen/fixtures/billing-namespace.json +0 -6
  43. package/src/codegen/generate-i18n-types.test.ts +0 -1137
  44. package/src/codegen/generate-i18n-types.ts +0 -20
  45. package/src/codegen/icu-analysis.ts +0 -110
  46. package/src/codegen/locale-fallback.ts +0 -73
  47. package/src/codegen/locale-policy.test.ts +0 -35
  48. package/src/codegen/locale-policy.ts +0 -30
  49. package/src/codegen/paths.ts +0 -44
  50. package/src/codegen/project-locales-set-namespace.test.ts +0 -134
  51. package/src/codegen/read-dictionary.test.ts +0 -594
  52. package/src/codegen/read-dictionary.ts +0 -260
  53. package/src/codegen/regenerate-namespaces.ts +0 -180
  54. package/src/codegen/run-codegen.test.ts +0 -230
  55. package/src/codegen/run-codegen.ts +0 -252
  56. package/src/codegen/types.ts +0 -10
  57. package/src/codegen/write-file-if-changed.test.ts +0 -42
  58. package/src/codegen/write-file-if-changed.ts +0 -20
  59. package/src/codegen-config/build-config.ts +0 -15
  60. package/src/codegen-config/codegen-config.test.ts +0 -36
  61. package/src/codegen-config/index.ts +0 -37
  62. package/src/codegen-config/type-names.ts +0 -20
  63. package/src/codegen-config/write-config.ts +0 -8
  64. package/src/engine.ts +0 -47
  65. package/src/fetch-artifact.ts +0 -16
  66. package/src/format-core.ts +0 -91
  67. package/src/i18n-handle.ts +0 -216
  68. package/src/icu/extract-variables.test.ts +0 -199
  69. package/src/icu/extract-variables.ts +0 -227
  70. package/src/icu/parse-template.ts +0 -31
  71. package/src/index.ts +0 -63
  72. package/src/load-state.ts +0 -15
  73. package/src/project-locales.test.ts +0 -280
  74. package/src/project-locales.ts +0 -211
  75. package/src/resolve-locale.test.ts +0 -91
  76. package/src/resolve-locale.ts +0 -88
  77. package/src/scope-multi.ts +0 -105
  78. package/src/scope-types.ts +0 -35
  79. package/src/scope.test.ts +0 -172
  80. package/src/serialized-state.test.ts +0 -114
  81. package/src/serialized-state.ts +0 -28
  82. package/src/setup/setup-i18n.test.ts +0 -85
  83. package/src/setup/setup-i18n.ts +0 -156
  84. package/src/types.ts +0 -65
  85. package/src/validation/create-args-schema.ts +0 -76
  86. package/src/validation/create-normalized-schema.ts +0 -52
  87. package/src/validation/errors.ts +0 -43
  88. package/src/validation/index.ts +0 -202
  89. package/src/validation/normalize.ts +0 -299
  90. package/src/validation/to-dictionary.ts +0 -67
  91. package/src/validation/types.ts +0 -82
  92. package/src/validation/validate-normalized.ts +0 -133
  93. 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
- });