@xndrjs/i18n 0.8.0 → 0.8.2-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.
Files changed (94) 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/dist/index.d.ts +20 -1
  11. package/dist/index.js +56 -10
  12. package/dist/index.js.map +1 -1
  13. package/package.json +6 -9
  14. package/bin/audit.mjs +0 -14
  15. package/bin/codegen.mjs +0 -17
  16. package/bin/setup.mjs +0 -14
  17. package/src/IcuTranslationProviderMulti.test.ts +0 -215
  18. package/src/IcuTranslationProviderMulti.ts +0 -176
  19. package/src/audit/audit-dictionaries.test.ts +0 -134
  20. package/src/audit/audit-dictionaries.ts +0 -178
  21. package/src/audit/run-audit.test.ts +0 -169
  22. package/src/audit/run-audit.ts +0 -94
  23. package/src/builder-load-registry.ts +0 -39
  24. package/src/builder-loaders.ts +0 -15
  25. package/src/builder-types.test.ts +0 -49
  26. package/src/builder-types.ts +0 -24
  27. package/src/builder.test.ts +0 -260
  28. package/src/builder.ts +0 -10
  29. package/src/codegen/codegen-config-schema.ts +0 -156
  30. package/src/codegen/config.test.ts +0 -231
  31. package/src/codegen/config.ts +0 -42
  32. package/src/codegen/constants.ts +0 -9
  33. package/src/codegen/delivery-artifacts.test.ts +0 -167
  34. package/src/codegen/delivery-artifacts.ts +0 -154
  35. package/src/codegen/dictionary-spec-contract.test.ts +0 -28
  36. package/src/codegen/dictionary-spec-contract.ts +0 -138
  37. package/src/codegen/emit/dictionary-schema-file.ts +0 -113
  38. package/src/codegen/emit/instance-file.test.ts +0 -71
  39. package/src/codegen/emit/instance-file.ts +0 -257
  40. package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
  41. package/src/codegen/emit/namespace-loaders-file.ts +0 -229
  42. package/src/codegen/emit/types-file.test.ts +0 -96
  43. package/src/codegen/emit/types-file.ts +0 -142
  44. package/src/codegen/fixtures/billing-namespace.json +0 -6
  45. package/src/codegen/generate-i18n-types.test.ts +0 -1137
  46. package/src/codegen/generate-i18n-types.ts +0 -20
  47. package/src/codegen/icu-analysis.ts +0 -110
  48. package/src/codegen/locale-fallback.ts +0 -73
  49. package/src/codegen/locale-policy.test.ts +0 -35
  50. package/src/codegen/locale-policy.ts +0 -30
  51. package/src/codegen/paths.ts +0 -44
  52. package/src/codegen/project-locales-set-namespace.test.ts +0 -134
  53. package/src/codegen/read-dictionary.test.ts +0 -594
  54. package/src/codegen/read-dictionary.ts +0 -260
  55. package/src/codegen/regenerate-namespaces.ts +0 -180
  56. package/src/codegen/run-codegen.test.ts +0 -230
  57. package/src/codegen/run-codegen.ts +0 -252
  58. package/src/codegen/types.ts +0 -10
  59. package/src/codegen/write-file-if-changed.test.ts +0 -42
  60. package/src/codegen/write-file-if-changed.ts +0 -20
  61. package/src/codegen-config/build-config.ts +0 -15
  62. package/src/codegen-config/codegen-config.test.ts +0 -36
  63. package/src/codegen-config/index.ts +0 -37
  64. package/src/codegen-config/type-names.ts +0 -20
  65. package/src/codegen-config/write-config.ts +0 -8
  66. package/src/engine.ts +0 -47
  67. package/src/fetch-artifact.ts +0 -16
  68. package/src/format-core.ts +0 -91
  69. package/src/i18n-handle.ts +0 -202
  70. package/src/icu/extract-variables.test.ts +0 -199
  71. package/src/icu/extract-variables.ts +0 -227
  72. package/src/icu/parse-template.ts +0 -31
  73. package/src/index.ts +0 -58
  74. package/src/project-locales.test.ts +0 -280
  75. package/src/project-locales.ts +0 -211
  76. package/src/resolve-locale.test.ts +0 -91
  77. package/src/resolve-locale.ts +0 -88
  78. package/src/scope-multi.ts +0 -105
  79. package/src/scope-types.ts +0 -35
  80. package/src/scope.test.ts +0 -172
  81. package/src/serialized-state.test.ts +0 -114
  82. package/src/serialized-state.ts +0 -28
  83. package/src/setup/setup-i18n.test.ts +0 -85
  84. package/src/setup/setup-i18n.ts +0 -156
  85. package/src/types.ts +0 -65
  86. package/src/validation/create-args-schema.ts +0 -76
  87. package/src/validation/create-normalized-schema.ts +0 -52
  88. package/src/validation/errors.ts +0 -43
  89. package/src/validation/index.ts +0 -202
  90. package/src/validation/normalize.ts +0 -299
  91. package/src/validation/to-dictionary.ts +0 -67
  92. package/src/validation/types.ts +0 -82
  93. package/src/validation/validate-normalized.ts +0 -133
  94. package/src/validation/validation.test.ts +0 -562
@@ -1,252 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import {
4
- loadConfig,
5
- resolveArtifactsPath,
6
- resolveCodegenPaths,
7
- resolveNamespaces,
8
- } from "./config.js";
9
- import type { CodegenConfig } from "./codegen-config-schema.js";
10
- import {
11
- formatDictionarySchemaFile,
12
- formatDictionarySpecBlock,
13
- } from "./emit/dictionary-schema-file.js";
14
- import { formatInstanceFile } from "./emit/instance-file.js";
15
- import { formatNamespaceLoadersFile } from "./emit/namespace-loaders-file.js";
16
- import { formatTypesFile } from "./emit/types-file.js";
17
- import { analyzeDictionaries } from "./icu-analysis.js";
18
- import { getDeliveryArtifactsIssues } from "./delivery-artifacts.js";
19
- import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
20
- import { enrichLocaleFallback } from "./locale-policy.js";
21
- import { DEFAULT_IMPORT_EXTENSION, reportCodegenIssues, toModuleBasename } from "./paths.js";
22
- import { prepareDictionaryEntries } from "./read-dictionary.js";
23
- import { writeFileIfChanged } from "./write-file-if-changed.js";
24
-
25
- export type RunCodegenInput =
26
- | string
27
- | {
28
- configPath?: string;
29
- config?: CodegenConfig;
30
- projectRoot?: string;
31
- /** When false, skip console.log summaries (default true). */
32
- log?: boolean;
33
- };
34
-
35
- export interface RunCodegenResult {
36
- projectRoot: string;
37
- generatedFiles: string[];
38
- compiledFiles: string[];
39
- }
40
-
41
- function resolveRunCodegenInput(input?: RunCodegenInput): {
42
- projectRoot: string;
43
- config: CodegenConfig;
44
- log: boolean;
45
- } {
46
- if (typeof input === "string" || input === undefined) {
47
- const configPath = path.resolve(
48
- process.cwd(),
49
- typeof input === "string" ? input : "i18n/i18n.codegen.json"
50
- );
51
- if (!fs.existsSync(configPath)) {
52
- throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
53
- }
54
- return {
55
- projectRoot: path.dirname(configPath),
56
- config: loadConfig(configPath),
57
- log: true,
58
- };
59
- }
60
-
61
- const log = input.log !== false;
62
-
63
- if (input.config !== undefined) {
64
- const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
65
- return { projectRoot, config: input.config, log };
66
- }
67
-
68
- const configPath = path.resolve(process.cwd(), input.configPath ?? "i18n/i18n.codegen.json");
69
- if (!fs.existsSync(configPath)) {
70
- throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
71
- }
72
- return {
73
- projectRoot: path.dirname(configPath),
74
- config: loadConfig(configPath),
75
- log,
76
- };
77
- }
78
-
79
- /**
80
- * Full codegen pipeline: types + instance + loaders + delivery JSON (+ schema).
81
- * Run at build time or when the *contract* changes (keys, params, namespaces, locales).
82
- * Content-only refreshes (same ICU contract) should use {@link regenerateNamespaces} instead.
83
- */
84
- export function runCodegen(input?: RunCodegenInput): RunCodegenResult {
85
- const { projectRoot, config, log } = resolveRunCodegenInput(input);
86
- const sourceEntries = resolveNamespaces(config);
87
- const artifactsPathRelative = resolveArtifactsPath(config);
88
- const delivery = config.delivery ?? "split-by-locale";
89
- const loaderStrategy = config.loaderStrategy ?? "import";
90
-
91
- const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
92
- if (!analysisResult.ok) {
93
- throw new Error("[Codegen Error] Dictionary ICU analysis failed. See messages above.");
94
- }
95
-
96
- const { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace } =
97
- analysisResult.analysis;
98
-
99
- if (config.localeFallback) {
100
- const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
101
- if (localeFallbackIssues.length > 0) {
102
- reportCodegenIssues(localeFallbackIssues);
103
- throw new Error("[Codegen Error] Invalid localeFallback configuration.");
104
- }
105
- }
106
-
107
- const paths = resolveCodegenPaths(config);
108
- const {
109
- paramsTypeName,
110
- schemaTypeName,
111
- localeTypeName,
112
- localeFallbackConstName,
113
- factoryName,
114
- deliveryAreaTypeName: resolvedDeliveryAreaTypeName,
115
- } = paths;
116
- const localeFallbackTypeName = `${localeTypeName}Fallback`;
117
- const importExtension = DEFAULT_IMPORT_EXTENSION;
118
-
119
- const requestLocales = collectRequestLocales(locales, config.localeFallback);
120
- const requestLocalesList = [...requestLocales].sort();
121
-
122
- if (delivery === "custom" && config.deliveryArtifacts) {
123
- const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
124
- config.deliveryArtifacts,
125
- requestLocales
126
- );
127
- if (deliveryArtifactsIssues.length > 0) {
128
- reportCodegenIssues(deliveryArtifactsIssues);
129
- throw new Error("[Codegen Error] Invalid deliveryArtifacts configuration.");
130
- }
131
- }
132
-
133
- const {
134
- resolvedEntries: entries,
135
- splitPathsByNamespace,
136
- compiledFiles,
137
- } = prepareDictionaryEntries(projectRoot, sourceEntries, artifactsPathRelative, {
138
- dictionariesByNamespace,
139
- delivery,
140
- localeFallback: config.localeFallback,
141
- requestLocales: delivery === "split-by-locale" ? requestLocalesList : undefined,
142
- deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : undefined,
143
- });
144
-
145
- const typesOutputPath = path.resolve(projectRoot, paths.typesOutput);
146
- const instanceOutputPath = path.resolve(projectRoot, paths.instanceOutput);
147
- const namespaceLoadersOutputPath = path.resolve(projectRoot, paths.namespaceLoadersOutput);
148
- const dictionarySchemaOutputPath = path.resolve(projectRoot, paths.dictionarySchemaOutput);
149
- const typesModule = toModuleBasename(typesOutputPath);
150
-
151
- const deliveryAreaNames =
152
- delivery === "custom" && config.deliveryArtifacts
153
- ? Object.keys(config.deliveryArtifacts).sort()
154
- : undefined;
155
- const deliveryAreaTypeName = delivery === "custom" ? resolvedDeliveryAreaTypeName : undefined;
156
-
157
- const localeFallbackForEmit = config.localeFallback
158
- ? enrichLocaleFallback(locales, config.localeFallback)
159
- : undefined;
160
-
161
- const typesContent = formatTypesFile({
162
- entries,
163
- projectRoot,
164
- typesOutputPath,
165
- paramsTypeName,
166
- schemaTypeName,
167
- localeTypeName,
168
- localeFallbackConstName,
169
- localeFallbackTypeName,
170
- localeFallback: localeFallbackForEmit,
171
- paramsByNamespace,
172
- requestLocales: requestLocalesList,
173
- ...(deliveryAreaTypeName && deliveryAreaNames
174
- ? {
175
- deliveryAreaTypeName,
176
- deliveryAreaNames,
177
- ...(delivery === "custom" && config.deliveryArtifacts
178
- ? { deliveryArtifacts: config.deliveryArtifacts }
179
- : {}),
180
- }
181
- : {}),
182
- lazyEntries: entries,
183
- });
184
-
185
- const instanceContent = formatInstanceFile({
186
- typesOutputPath,
187
- namespaceLoadersOutputPath,
188
- paramsTypeName,
189
- schemaTypeName,
190
- localeTypeName,
191
- localeFallbackConstName,
192
- factoryName,
193
- hasLocaleFallback: Boolean(config.localeFallback),
194
- hasLocaleType: requestLocalesList.length > 0,
195
- importExtension,
196
- delivery,
197
- loaderStrategy,
198
- ...(delivery === "custom" ? { localeDeliveryAreaConstName: "LOCALE_DELIVERY_AREA" } : {}),
199
- });
200
-
201
- writeFileIfChanged(typesOutputPath, typesContent);
202
-
203
- const generatedFiles = [
204
- path.relative(projectRoot, typesOutputPath),
205
- path.relative(projectRoot, instanceOutputPath),
206
- ];
207
-
208
- writeFileIfChanged(instanceOutputPath, instanceContent);
209
-
210
- const dictionarySpecBlock = formatDictionarySpecBlock(entries, argsSpecByNamespace);
211
- const dictionarySchemaContent = formatDictionarySchemaFile(
212
- schemaTypeName,
213
- typesModule,
214
- dictionarySpecBlock,
215
- importExtension
216
- );
217
- writeFileIfChanged(dictionarySchemaOutputPath, dictionarySchemaContent);
218
- generatedFiles.push(path.relative(projectRoot, dictionarySchemaOutputPath));
219
-
220
- const lazyEntriesWithPaths = entries.map((entry) => ({
221
- ...entry,
222
- absolutePath: path.resolve(projectRoot, entry.filePath),
223
- }));
224
- const namespaceLoadersContent = formatNamespaceLoadersFile({
225
- loadersOutputPath: namespaceLoadersOutputPath,
226
- lazyEntries: lazyEntriesWithPaths,
227
- schemaTypeName,
228
- localeTypeName,
229
- typesModule,
230
- importExtension,
231
- projectRoot,
232
- delivery,
233
- splitPathsByNamespace,
234
- loaderStrategy,
235
- ...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
236
- ...(delivery === "custom" && deliveryAreaTypeName && deliveryAreaNames
237
- ? { deliveryAreaTypeName, deliveryAreaNames }
238
- : {}),
239
- });
240
-
241
- writeFileIfChanged(namespaceLoadersOutputPath, namespaceLoadersContent);
242
- generatedFiles.push(path.relative(projectRoot, namespaceLoadersOutputPath));
243
-
244
- if (log) {
245
- console.log(`✅ Generated: ${generatedFiles.join(", ")}`);
246
- if (compiledFiles.length > 0) {
247
- console.log(`✅ Compiled: ${compiledFiles.join(", ")}`);
248
- }
249
- }
250
-
251
- return { projectRoot, generatedFiles, compiledFiles };
252
- }
@@ -1,10 +0,0 @@
1
- import type { SUPPORTED_IMPORT_EXTENSIONS } from "./constants.js";
2
-
3
- export type ImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];
4
-
5
- export interface NamespaceEntry {
6
- namespace: string;
7
- filePath: string;
8
- }
9
-
10
- export type DictionaryJson = Record<string, Record<string, string>>;
@@ -1,42 +0,0 @@
1
- import { mkdtempSync, readFileSync, rmSync, statSync, 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 { writeFileIfChanged } from "./write-file-if-changed.js";
6
-
7
- describe("writeFileIfChanged", () => {
8
- let tempDir: string;
9
-
10
- afterEach(() => {
11
- if (tempDir) {
12
- rmSync(tempDir, { recursive: true, force: true });
13
- }
14
- });
15
-
16
- it("creates missing files together with their parent directories", () => {
17
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-write-"));
18
- const filePath = join(tempDir, "nested/dir/output.ts");
19
-
20
- expect(writeFileIfChanged(filePath, "export {};\n")).toBe(true);
21
- expect(readFileSync(filePath, "utf8")).toBe("export {};\n");
22
- });
23
-
24
- it("skips the write when content is unchanged", () => {
25
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-write-"));
26
- const filePath = join(tempDir, "output.ts");
27
- writeFileSync(filePath, "export {};\n");
28
- const mtimeBefore = statSync(filePath).mtimeMs;
29
-
30
- expect(writeFileIfChanged(filePath, "export {};\n")).toBe(false);
31
- expect(statSync(filePath).mtimeMs).toBe(mtimeBefore);
32
- });
33
-
34
- it("overwrites the file when content differs", () => {
35
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-write-"));
36
- const filePath = join(tempDir, "output.ts");
37
- writeFileSync(filePath, "export const a = 1;\n");
38
-
39
- expect(writeFileIfChanged(filePath, "export const a = 2;\n")).toBe(true);
40
- expect(readFileSync(filePath, "utf8")).toBe("export const a = 2;\n");
41
- });
42
- });
@@ -1,20 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- /**
5
- * Writes `content` to `absolutePath` only when it differs from the current
6
- * file content, keeping mtimes stable for downstream watchers/bundlers.
7
- * Returns true when the file was actually written.
8
- */
9
- export function writeFileIfChanged(absolutePath: string, content: string): boolean {
10
- if (fs.existsSync(absolutePath)) {
11
- const currentContent = fs.readFileSync(absolutePath, "utf8");
12
- if (currentContent === content) {
13
- return false;
14
- }
15
- }
16
-
17
- fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
18
- fs.writeFileSync(absolutePath, content);
19
- return true;
20
- }
@@ -1,15 +0,0 @@
1
- import type { CodegenConfigInput } from "../codegen/codegen-config-schema.js";
2
-
3
- const GENERATED_DIR = "generated";
4
- const TRANSLATIONS_DIR = "translations";
5
-
6
- export function buildCodegenConfig(project: string): CodegenConfigInput {
7
- return {
8
- projectName: project,
9
- namespaces: {
10
- default: `${TRANSLATIONS_DIR}/default.json`,
11
- },
12
- delivery: "split-by-locale",
13
- codegenPath: GENERATED_DIR,
14
- };
15
- }
@@ -1,36 +0,0 @@
1
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, expect, it, afterEach } from "vitest";
5
- import { buildCodegenConfig, type CodegenConfigInput, writeCodegenConfig } from "./index.js";
6
-
7
- describe("codegen-config", () => {
8
- let tempDir: string;
9
-
10
- afterEach(() => {
11
- if (tempDir) {
12
- rmSync(tempDir, { recursive: true, force: true });
13
- }
14
- });
15
-
16
- it("buildCodegenConfig returns a valid multi-namespace input shape", () => {
17
- const config = buildCodegenConfig("MyApp");
18
- expect(config.namespaces).toEqual({
19
- default: "translations/default.json",
20
- });
21
- expect(config.projectName).toBe("MyApp");
22
- expect(config.codegenPath).toBe("generated");
23
- expect(config).not.toHaveProperty("dictionary");
24
- expect(config).not.toHaveProperty("paramsTypeName");
25
- });
26
-
27
- it("writeCodegenConfig writes formatted JSON", () => {
28
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-config-"));
29
- const configPath = join(tempDir, "i18n/i18n.codegen.json");
30
- const config: CodegenConfigInput = buildCodegenConfig("Demo");
31
-
32
- writeCodegenConfig(configPath, config);
33
-
34
- expect(readFileSync(configPath, "utf8")).toBe(`${JSON.stringify(config, null, 2)}\n`);
35
- });
36
- });
@@ -1,37 +0,0 @@
1
- export type {
2
- CodegenConfig,
3
- CodegenConfigInput,
4
- DeliveryMode,
5
- LoaderStrategy,
6
- ResolvedCodegenPaths,
7
- } from "../codegen/codegen-config-schema.js";
8
- export {
9
- codegenConfigKeys,
10
- DELIVERY_MODES,
11
- LOADER_STRATEGIES,
12
- GENERATED_BASENAMES,
13
- DEFAULT_FACTORY_NAME,
14
- DEFAULT_LOCALE_FALLBACK_CONST_NAME,
15
- formatCodegenConfigIssues,
16
- resolveCodegenPaths,
17
- resolveArtifactsPath,
18
- } from "../codegen/codegen-config-schema.js";
19
- export { loadConfig, resolveNamespaces } from "../codegen/config.js";
20
-
21
- export type { DeliveryArtifactsMap } from "../codegen/delivery-artifacts.js";
22
-
23
- export {
24
- SUPPORTED_IMPORT_EXTENSIONS,
25
- type SupportedImportExtension,
26
- } from "../codegen/constants.js";
27
-
28
- export { buildCodegenConfig } from "./build-config.js";
29
- export { inferProjectName, typeNamesForProject } from "./type-names.js";
30
- export { writeCodegenConfig } from "./write-config.js";
31
-
32
- export { runCodegen, type RunCodegenInput, type RunCodegenResult } from "../codegen/run-codegen.js";
33
- export {
34
- regenerateNamespaces,
35
- type RegenerateNamespacesInput,
36
- type RegenerateNamespacesResult,
37
- } from "../codegen/regenerate-namespaces.js";
@@ -1,20 +0,0 @@
1
- export function inferProjectName(dirName: string): string {
2
- const parts = dirName.split(/[-_]+/).filter(Boolean);
3
- if (parts.length === 0) {
4
- return "App";
5
- }
6
-
7
- return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
8
- }
9
-
10
- export function typeNamesForProject(project: string): {
11
- paramsTypeName: string;
12
- schemaTypeName: string;
13
- localeTypeName: string;
14
- } {
15
- return {
16
- paramsTypeName: `${project}Params`,
17
- schemaTypeName: `${project}Schema`,
18
- localeTypeName: `${project}Locale`,
19
- };
20
- }
@@ -1,8 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import type { CodegenConfigInput } from "../codegen/codegen-config-schema.js";
4
-
5
- export function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void {
6
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
7
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
8
- }
package/src/engine.ts DELETED
@@ -1,47 +0,0 @@
1
- import type { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
2
- import type {
3
- LocaleFallbackMap,
4
- LocaleOfMulti,
5
- MultiDictionary,
6
- PartialMultiDictionary,
7
- } from "./types.js";
8
- import type { I18nScopeMulti, I18nScopeMultiForLocale } from "./scope-multi.js";
9
- import type { MultiParams, ParamsForNamespaces, SchemaForNamespaces } from "./scope-types.js";
10
-
11
- /** Mutable translation engine for a multi-namespace dictionary. */
12
- export interface I18nEngineMulti<
13
- Schema extends MultiDictionary,
14
- Params extends MultiParams<Schema>,
15
- RequestLocales extends string = LocaleOfMulti<Schema>,
16
- > {
17
- readonly __i18nEngineMode: "multi";
18
- /** Loaded namespaces only — may be empty at cold start. */
19
- getAll(): PartialMultiDictionary<Schema, RequestLocales>;
20
- toScope<NsList extends readonly (keyof Schema & string)[]>(options: {
21
- namespaces: NsList;
22
- }): I18nScopeMulti<
23
- SchemaForNamespaces<Schema, NsList>,
24
- ParamsForNamespaces<Schema, Params, NsList>,
25
- RequestLocales
26
- >;
27
- toScope<
28
- NsList extends readonly (keyof Schema & string)[],
29
- Locale extends RequestLocales,
30
- >(options: {
31
- namespaces: NsList;
32
- locale: Locale;
33
- }): I18nScopeMultiForLocale<
34
- SchemaForNamespaces<Schema, NsList>,
35
- ParamsForNamespaces<Schema, Params, NsList>,
36
- RequestLocales,
37
- Locale
38
- >;
39
- }
40
-
41
- /** Alias for the default multi-namespace engine implementation. */
42
- export type I18nEngineMultiImpl<
43
- Schema extends MultiDictionary,
44
- Params extends MultiParams<Schema>,
45
- RequestLocales extends string = LocaleOfMulti<Schema>,
46
- Fallback extends LocaleFallbackMap | undefined = undefined,
47
- > = IcuTranslationProviderMulti<Schema, Params, RequestLocales, Fallback>;
@@ -1,16 +0,0 @@
1
- /**
2
- * Identifies a delivery JSON artifact. Codegen passes this to {@link FetchArtifact};
3
- * URL / filesystem mapping is an application concern.
4
- */
5
- export type DeliveryResourceId = {
6
- locale: string;
7
- namespace: string;
8
- /** Present for `delivery: "custom"` (partition key is the area). */
9
- area?: string;
10
- };
11
-
12
- /**
13
- * Loads a translation JSON artifact by resource id.
14
- * Must return the parsed JSON value (not a raw Response).
15
- */
16
- export type FetchArtifact = (id: DeliveryResourceId) => Promise<unknown>;
@@ -1,91 +0,0 @@
1
- import { IntlMessageFormat } from "intl-messageformat";
2
- import { formatLocaleFallbackChain, resolveLocaleTemplate } from "./resolve-locale.js";
3
- import type { LocaleCache, LocaleFallbackMap, OnMissingTranslation } from "./types.js";
4
-
5
- export type FormatCoreContext = {
6
- key: string;
7
- namespace?: string | undefined;
8
- locale: string;
9
- localeFallback?: LocaleFallbackMap | undefined;
10
- onMissing: OnMissingTranslation;
11
- };
12
-
13
- export type ResolveAndFormatOptions = {
14
- localeByKey: Record<string, string | undefined> | undefined;
15
- locale: string;
16
- params?: Record<string, unknown> | undefined;
17
- getCache: (resolvedLocale: string) => LocaleCache;
18
- context: FormatCoreContext;
19
- };
20
-
21
- function formatErrorLabel(context: Pick<FormatCoreContext, "key" | "namespace">): string {
22
- if (context.namespace !== undefined) {
23
- return `namespace "${context.namespace}", key "${context.key}"`;
24
- }
25
- return `key "${context.key}"`;
26
- }
27
-
28
- function formatMissingError(context: FormatCoreContext): string {
29
- const chain = formatLocaleFallbackChain(context.locale, context.localeFallback);
30
- if (context.namespace !== undefined) {
31
- return `[i18n] Missing key or locale: namespace "${context.namespace}", key "${context.key}" [${context.locale}] (fallback chain: ${chain})`;
32
- }
33
- return `[i18n] Missing key or locale: "${context.key}" [${context.locale}] (fallback chain: ${chain})`;
34
- }
35
-
36
- function resolveMissing(context: FormatCoreContext): string {
37
- const chain = formatLocaleFallbackChain(context.locale, context.localeFallback);
38
- if (context.onMissing === "key") {
39
- if (context.namespace !== undefined) {
40
- return `${context.namespace}.${context.key}`;
41
- }
42
- return context.key;
43
- }
44
- if (typeof context.onMissing === "function") {
45
- return context.onMissing({
46
- ...(context.namespace !== undefined ? { namespace: context.namespace } : {}),
47
- key: context.key,
48
- locale: context.locale,
49
- fallbackChain: chain,
50
- });
51
- }
52
- throw new Error(formatMissingError(context));
53
- }
54
-
55
- /**
56
- * Shared runtime path for both ICU providers: resolve locale (with fallback) →
57
- * compile template (cached) → format with params; handles missing keys via `onMissing`.
58
- */
59
- export function resolveAndFormat(options: ResolveAndFormatOptions): string {
60
- const { localeByKey, locale, params, getCache, context } = options;
61
- const resolved = resolveLocaleTemplate(localeByKey, locale, context.localeFallback);
62
-
63
- if (!resolved) {
64
- return resolveMissing(context);
65
- }
66
-
67
- const { template, resolvedLocale } = resolved;
68
- const cache = getCache(resolvedLocale);
69
- const cacheKey = context.key;
70
- const label = formatErrorLabel(context);
71
-
72
- if (!cache[cacheKey]) {
73
- try {
74
- cache[cacheKey] = new IntlMessageFormat(template, resolvedLocale);
75
- } catch (err) {
76
- const message = err instanceof Error ? err.message : String(err);
77
- throw new Error(
78
- `[i18n ICU Syntax Error] Dictionary error for ${label} [${resolvedLocale}]: ${message}`
79
- );
80
- }
81
- }
82
-
83
- try {
84
- return cache[cacheKey].format(params) as string;
85
- } catch (err) {
86
- const message = err instanceof Error ? err.message : String(err);
87
- throw new Error(
88
- `[i18n Formatting Error] Invalid or missing parameters for ${label} [${resolvedLocale}]: ${message}`
89
- );
90
- }
91
- }