@xndrjs/i18n 0.4.0 → 0.5.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 (57) hide show
  1. package/README.md +156 -29
  2. package/dist/codegen/index.d.ts +85 -0
  3. package/dist/codegen/index.js +181 -0
  4. package/dist/codegen/index.js.map +1 -0
  5. package/dist/index.d.ts +34 -5
  6. package/dist/index.js +175 -74
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +4 -0
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +6 -1
  11. package/src/IcuTranslationProviderMulti.test.ts +47 -0
  12. package/src/IcuTranslationProviderMulti.ts +41 -61
  13. package/src/IcuTranslationProviderSingle.test.ts +67 -0
  14. package/src/IcuTranslationProviderSingle.ts +27 -51
  15. package/src/audit/audit-dictionaries.ts +4 -1
  16. package/src/audit/run-audit.test.ts +35 -1
  17. package/src/audit/run-audit.ts +15 -7
  18. package/src/codegen/codegen-config-schema.ts +68 -1
  19. package/src/codegen/config.test.ts +174 -39
  20. package/src/codegen/config.ts +14 -5
  21. package/src/codegen/constants.ts +8 -0
  22. package/src/codegen/delivery-artifacts.test.ts +142 -0
  23. package/src/codegen/delivery-artifacts.ts +124 -0
  24. package/src/codegen/emit/dictionary-file.test.ts +190 -0
  25. package/src/codegen/emit/dictionary-file.ts +163 -0
  26. package/src/codegen/emit/dictionary-schema-file.ts +5 -0
  27. package/src/codegen/emit/instance-file.ts +114 -28
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
  29. package/src/codegen/emit/namespace-loaders-file.ts +114 -6
  30. package/src/codegen/emit/types-file.test.ts +114 -0
  31. package/src/codegen/emit/types-file.ts +48 -11
  32. package/src/codegen/generate-i18n-types.test.ts +754 -16
  33. package/src/codegen/generate-i18n-types.ts +111 -47
  34. package/src/codegen/icu-analysis.ts +9 -2
  35. package/src/codegen/locale-fallback.ts +19 -10
  36. package/src/codegen/locale-policy.ts +4 -0
  37. package/src/codegen/paths.ts +9 -5
  38. package/src/codegen/project-locales-set-namespace.test.ts +5 -5
  39. package/src/codegen/read-dictionary.test.ts +474 -2
  40. package/src/codegen/read-dictionary.ts +164 -15
  41. package/src/codegen/write-file-if-changed.test.ts +42 -0
  42. package/src/codegen/write-file-if-changed.ts +20 -0
  43. package/src/codegen-config/build-config.ts +34 -0
  44. package/src/codegen-config/codegen-config.test.ts +32 -0
  45. package/src/codegen-config/index.ts +21 -0
  46. package/src/codegen-config/write-config.ts +8 -0
  47. package/src/deep-freeze.test.ts +13 -0
  48. package/src/deep-freeze.ts +5 -0
  49. package/src/format-core.ts +91 -0
  50. package/src/index.ts +8 -2
  51. package/src/project-locales.test.ts +129 -10
  52. package/src/project-locales.ts +90 -3
  53. package/src/setup/setup-i18n.test.ts +1 -1
  54. package/src/setup/setup-i18n.ts +6 -30
  55. package/src/types.ts +19 -4
  56. package/src/validation/normalize.ts +4 -0
  57. /package/src/{setup → codegen-config}/type-names.ts +0 -0
@@ -0,0 +1,124 @@
1
+ /** Valid TypeScript identifier / filename segment for a delivery area id. */
2
+ export const DELIVERY_AREA_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
3
+
4
+ export type DeliveryArtifactsMap = Record<string, readonly string[]>;
5
+
6
+ export type DeliveryArtifactsIssue = {
7
+ path: (string | number)[];
8
+ message: string;
9
+ };
10
+
11
+ export function getDeliveryArtifactsStructureIssues(
12
+ deliveryArtifacts: DeliveryArtifactsMap
13
+ ): DeliveryArtifactsIssue[] {
14
+ const issues: DeliveryArtifactsIssue[] = [];
15
+
16
+ for (const [area, locales] of Object.entries(deliveryArtifacts)) {
17
+ if (!DELIVERY_AREA_NAME_PATTERN.test(area)) {
18
+ issues.push({
19
+ path: ["deliveryArtifacts", area],
20
+ message: `Invalid delivery area name "${area}": must match ${DELIVERY_AREA_NAME_PATTERN.source}`,
21
+ });
22
+ }
23
+
24
+ if (locales.length === 0) {
25
+ issues.push({
26
+ path: ["deliveryArtifacts", area],
27
+ message: `Delivery area "${area}" must include at least one locale.`,
28
+ });
29
+ }
30
+ }
31
+
32
+ const localeToArea = new Map<string, string>();
33
+ for (const [area, locales] of Object.entries(deliveryArtifacts)) {
34
+ for (const locale of locales) {
35
+ const previousArea = localeToArea.get(locale);
36
+ if (previousArea) {
37
+ issues.push({
38
+ path: ["deliveryArtifacts", area],
39
+ message: `Locale "${locale}" appears in both "${previousArea}" and "${area}".`,
40
+ });
41
+ } else {
42
+ localeToArea.set(locale, area);
43
+ }
44
+ }
45
+ }
46
+
47
+ return issues;
48
+ }
49
+
50
+ export function getDeliveryArtifactsPartitionIssues(
51
+ deliveryArtifacts: DeliveryArtifactsMap,
52
+ requestLocales: ReadonlySet<string>
53
+ ): DeliveryArtifactsIssue[] {
54
+ const issues: DeliveryArtifactsIssue[] = [];
55
+ const artifactLocales = new Set<string>();
56
+
57
+ for (const locales of Object.values(deliveryArtifacts)) {
58
+ for (const locale of locales) {
59
+ artifactLocales.add(locale);
60
+ }
61
+ }
62
+
63
+ const missing = [...requestLocales].filter((locale) => !artifactLocales.has(locale)).sort();
64
+ if (missing.length > 0) {
65
+ issues.push({
66
+ path: ["deliveryArtifacts"],
67
+ message: `deliveryArtifacts is missing locales required by dictionaries and localeFallback: ${missing.join(", ")}`,
68
+ });
69
+ }
70
+
71
+ const excess = [...artifactLocales].filter((locale) => !requestLocales.has(locale)).sort();
72
+ if (excess.length > 0) {
73
+ issues.push({
74
+ path: ["deliveryArtifacts"],
75
+ message: `deliveryArtifacts includes locales not required by dictionaries and localeFallback: ${excess.join(", ")}`,
76
+ });
77
+ }
78
+
79
+ return issues;
80
+ }
81
+
82
+ /** Pre-emit validation gate for `deliveryArtifacts` in custom delivery mode. */
83
+ export function getDeliveryArtifactsIssues(
84
+ deliveryArtifacts: DeliveryArtifactsMap,
85
+ requestLocales: ReadonlySet<string>
86
+ ): DeliveryArtifactsIssue[] {
87
+ return [
88
+ ...getDeliveryArtifactsStructureIssues(deliveryArtifacts),
89
+ ...getDeliveryArtifactsPartitionIssues(deliveryArtifacts, requestLocales),
90
+ ];
91
+ }
92
+
93
+ export function getDeliveryAreaNames(deliveryArtifacts: DeliveryArtifactsMap): string[] {
94
+ return Object.keys(deliveryArtifacts).sort();
95
+ }
96
+
97
+ export function getLocaleDeliveryAreaMap(
98
+ deliveryArtifacts: DeliveryArtifactsMap
99
+ ): Record<string, string> {
100
+ const localeToArea: Record<string, string> = {};
101
+
102
+ for (const [area, locales] of Object.entries(deliveryArtifacts)) {
103
+ for (const locale of locales) {
104
+ localeToArea[locale] = area;
105
+ }
106
+ }
107
+
108
+ return localeToArea;
109
+ }
110
+
111
+ export function formatLocaleDeliveryAreaBlock(
112
+ deliveryArtifacts: DeliveryArtifactsMap,
113
+ constName: string,
114
+ localeTypeName: string,
115
+ deliveryAreaTypeName: string
116
+ ): string {
117
+ const localeToArea = getLocaleDeliveryAreaMap(deliveryArtifacts);
118
+ const lines = Object.keys(localeToArea)
119
+ .sort()
120
+ .map((locale) => ` ${JSON.stringify(locale)}: ${JSON.stringify(localeToArea[locale])},`)
121
+ .join("\n");
122
+
123
+ return `export const ${constName} = {\n${lines}\n} as const satisfies Record<${localeTypeName}, ${deliveryAreaTypeName}>;\n\n`;
124
+ }
@@ -0,0 +1,190 @@
1
+ import path from "node:path";
2
+ import { describe, expect, it } from "vitest";
3
+ import { formatDictionaryFile } from "./dictionary-file.js";
4
+
5
+ const projectRoot = "/project";
6
+ const dictionaryOutputPath = path.join(projectRoot, "src/i18n/dictionary.generated.ts");
7
+ const typesOutputPath = path.join(projectRoot, "src/i18n/i18n-types.generated.ts");
8
+
9
+ describe("formatDictionaryFile", () => {
10
+ it("emits defaultDictionary in canonical mode", () => {
11
+ const output = formatDictionaryFile({
12
+ isSingle: false,
13
+ hasLazy: true,
14
+ entries: [
15
+ { namespace: "default", filePath: "src/i18n/translations/default.json" },
16
+ { namespace: "billing", filePath: "src/i18n/translations/billing.json" },
17
+ ],
18
+ eagerEntries: [{ namespace: "default", filePath: "src/i18n/translations/default.json" }],
19
+ projectRoot,
20
+ dictionaryOutputPath,
21
+ typesOutputPath,
22
+ schemaTypeName: "AppSchema",
23
+ localeTypeName: "AppLocale",
24
+ importExtension: "none",
25
+ delivery: "canonical",
26
+ });
27
+
28
+ expect(output).toContain("export const defaultDictionary: InitialSchema");
29
+ expect(output).toContain("defaultNs");
30
+ expect(output).not.toContain("defaultDictionaryFor");
31
+ });
32
+
33
+ it("emits defaultDictionaryFor with per-locale imports in split mode (multi)", () => {
34
+ const output = formatDictionaryFile({
35
+ isSingle: false,
36
+ hasLazy: true,
37
+ entries: [
38
+ { namespace: "default", filePath: "src/i18n/translations/default.json" },
39
+ { namespace: "billing", filePath: "src/i18n/translations/billing.json" },
40
+ ],
41
+ eagerEntries: [{ namespace: "default", filePath: "src/i18n/translations/default.json" }],
42
+ projectRoot,
43
+ dictionaryOutputPath,
44
+ typesOutputPath,
45
+ schemaTypeName: "AppSchema",
46
+ localeTypeName: "AppLocale",
47
+ importExtension: "none",
48
+ delivery: "split-by-locale",
49
+ requestLocales: ["en", "it"],
50
+ splitPathsByNamespace: {
51
+ default: {
52
+ en: "src/i18n/generated/translations/default.en.json",
53
+ it: "src/i18n/generated/translations/default.it.json",
54
+ },
55
+ billing: {
56
+ en: "src/i18n/generated/translations/billing.en.json",
57
+ it: "src/i18n/generated/translations/billing.it.json",
58
+ },
59
+ },
60
+ });
61
+
62
+ expect(output).toContain("import defaultEn from './generated/translations/default.en.json';");
63
+ expect(output).toContain("import defaultIt from './generated/translations/default.it.json';");
64
+ expect(output).toContain(
65
+ "const defaultByLocale = {\n en: defaultEn,\n it: defaultIt,\n} as const;"
66
+ );
67
+ expect(output).toContain(
68
+ "export function defaultDictionaryFor(locale: AppLocale): InitialSchema"
69
+ );
70
+ expect(output).toContain("return {\n default: defaultByLocale[locale],\n };");
71
+ expect(output).not.toContain("export const defaultDictionary");
72
+ expect(output).not.toContain("billingEn");
73
+ });
74
+
75
+ it("emits defaultDictionaryFor with per-locale imports in split mode (single)", () => {
76
+ const output = formatDictionaryFile({
77
+ isSingle: true,
78
+ hasLazy: false,
79
+ entries: [{ namespace: "translations", filePath: "src/i18n/translations/translations.json" }],
80
+ eagerEntries: [
81
+ { namespace: "translations", filePath: "src/i18n/translations/translations.json" },
82
+ ],
83
+ projectRoot,
84
+ dictionaryOutputPath,
85
+ typesOutputPath,
86
+ schemaTypeName: "AppSchema",
87
+ localeTypeName: "AppLocale",
88
+ importExtension: "none",
89
+ delivery: "split-by-locale",
90
+ requestLocales: ["en", "it"],
91
+ splitPathsByNamespace: {
92
+ translations: {
93
+ en: "src/i18n/generated/translations/translations.en.json",
94
+ it: "src/i18n/generated/translations/translations.it.json",
95
+ },
96
+ },
97
+ });
98
+
99
+ expect(output).toContain(
100
+ "import translationsEn from './generated/translations/translations.en.json';"
101
+ );
102
+ expect(output).toContain(
103
+ "import translationsIt from './generated/translations/translations.it.json';"
104
+ );
105
+ expect(output).toContain("export function defaultDictionaryFor(locale: AppLocale): AppSchema");
106
+ expect(output).toContain("return translationsByLocale[locale];");
107
+ expect(output).not.toContain("export const defaultDictionary");
108
+ });
109
+
110
+ it("emits defaultDictionaryFor with per-area imports in custom mode (multi)", () => {
111
+ const output = formatDictionaryFile({
112
+ isSingle: false,
113
+ hasLazy: true,
114
+ entries: [
115
+ { namespace: "default", filePath: "src/i18n/translations/default.json" },
116
+ { namespace: "billing", filePath: "src/i18n/translations/billing.json" },
117
+ ],
118
+ eagerEntries: [{ namespace: "default", filePath: "src/i18n/translations/default.json" }],
119
+ projectRoot,
120
+ dictionaryOutputPath,
121
+ typesOutputPath,
122
+ schemaTypeName: "AppSchema",
123
+ localeTypeName: "AppLocale",
124
+ importExtension: "none",
125
+ delivery: "custom",
126
+ deliveryAreaTypeName: "AppDeliveryArea",
127
+ deliveryAreaNames: ["eu", "us"],
128
+ splitPathsByNamespace: {
129
+ default: {
130
+ eu: "src/i18n/generated/translations/default.eu.json",
131
+ us: "src/i18n/generated/translations/default.us.json",
132
+ },
133
+ billing: {
134
+ eu: "src/i18n/generated/translations/billing.eu.json",
135
+ us: "src/i18n/generated/translations/billing.us.json",
136
+ },
137
+ },
138
+ });
139
+
140
+ expect(output).toContain("import defaultEu from './generated/translations/default.eu.json';");
141
+ expect(output).toContain("import defaultUs from './generated/translations/default.us.json';");
142
+ expect(output).toContain(
143
+ "const defaultByArea = {\n eu: defaultEu,\n us: defaultUs,\n} as const;"
144
+ );
145
+ expect(output).toContain(
146
+ "export function defaultDictionaryFor(area: AppDeliveryArea): InitialSchema"
147
+ );
148
+ expect(output).toContain("return {\n default: defaultByArea[area],\n };");
149
+ expect(output).not.toContain("export const defaultDictionary");
150
+ expect(output).not.toContain("billingEu");
151
+ });
152
+
153
+ it("emits defaultDictionaryFor with per-area imports in custom mode (single)", () => {
154
+ const output = formatDictionaryFile({
155
+ isSingle: true,
156
+ hasLazy: false,
157
+ entries: [{ namespace: "translations", filePath: "src/i18n/translations/translations.json" }],
158
+ eagerEntries: [
159
+ { namespace: "translations", filePath: "src/i18n/translations/translations.json" },
160
+ ],
161
+ projectRoot,
162
+ dictionaryOutputPath,
163
+ typesOutputPath,
164
+ schemaTypeName: "AppSchema",
165
+ localeTypeName: "AppLocale",
166
+ importExtension: "none",
167
+ delivery: "custom",
168
+ deliveryAreaTypeName: "AppDeliveryArea",
169
+ deliveryAreaNames: ["eu", "us"],
170
+ splitPathsByNamespace: {
171
+ translations: {
172
+ eu: "src/i18n/generated/translations/translations.eu.json",
173
+ us: "src/i18n/generated/translations/translations.us.json",
174
+ },
175
+ },
176
+ });
177
+
178
+ expect(output).toContain(
179
+ "import translationsEu from './generated/translations/translations.eu.json';"
180
+ );
181
+ expect(output).toContain(
182
+ "import translationsUs from './generated/translations/translations.us.json';"
183
+ );
184
+ expect(output).toContain(
185
+ "export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
186
+ );
187
+ expect(output).toContain("return translationsByArea[area];");
188
+ expect(output).not.toContain("export const defaultDictionary");
189
+ });
190
+ });
@@ -3,11 +3,17 @@ import {
3
3
  GENERATED_FILE_BANNER,
4
4
  toImportIdentifier,
5
5
  toImportPath,
6
+ toLocaleObjectKey,
6
7
  toModuleBasename,
7
8
  toRelativeModuleImport,
8
9
  } from "../paths.js";
10
+ import type { DeliveryMode } from "../codegen-config-schema.js";
9
11
  import type { ImportExtension, NamespaceEntry } from "../types.js";
10
12
 
13
+ /**
14
+ * Emits the generated `dictionary.generated.ts` module: eager translation imports
15
+ * wired into `defaultDictionary` or `defaultDictionaryFor`, depending on delivery mode.
16
+ */
11
17
  export interface DictionaryFileOptions {
12
18
  isSingle: boolean;
13
19
  hasLazy: boolean;
@@ -17,10 +23,167 @@ export interface DictionaryFileOptions {
17
23
  dictionaryOutputPath: string;
18
24
  typesOutputPath: string;
19
25
  schemaTypeName: string;
26
+ localeTypeName: string;
20
27
  importExtension: ImportExtension;
28
+ delivery?: DeliveryMode;
29
+ splitPathsByNamespace?: Record<string, Record<string, string>>;
30
+ requestLocales?: readonly string[];
31
+ deliveryAreaTypeName?: string;
32
+ deliveryAreaNames?: readonly string[];
21
33
  }
22
34
 
35
+ function toArtifactIdentifierSuffix(artifactKey: string): string {
36
+ return artifactKey
37
+ .split(/[^a-zA-Z0-9]+/)
38
+ .filter(Boolean)
39
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
40
+ .join("");
41
+ }
42
+
43
+ function toSplitImportIdentifier(namespace: string, artifactKey: string): string {
44
+ const namespaceId = toImportIdentifier(namespace).replace(/Ns$/, "");
45
+ return `${namespaceId}${toArtifactIdentifierSuffix(artifactKey)}`;
46
+ }
47
+
48
+ function toNamespaceBaseIdentifier(namespace: string): string {
49
+ return toImportIdentifier(namespace).replace(/Ns$/, "");
50
+ }
51
+
52
+ interface PartitionedDictionaryParams {
53
+ partitionKeys: readonly string[];
54
+ paramName: string;
55
+ paramTypeName: string;
56
+ }
57
+
58
+ function toPartitionSuffix(paramName: string): string {
59
+ return `By${paramName.charAt(0).toUpperCase()}${paramName.slice(1)}`;
60
+ }
61
+
62
+ /**
63
+ * Emits dictionary source for split-by-locale and custom delivery.
64
+ * Imports one JSON file per namespace × partition key (locale or area) and
65
+ * exposes `defaultDictionaryFor(partition)` that selects the matching slice.
66
+ */
67
+ function formatPartitionedDictionaryFile(
68
+ options: DictionaryFileOptions,
69
+ { partitionKeys, paramName, paramTypeName }: PartitionedDictionaryParams
70
+ ): string {
71
+ const {
72
+ isSingle,
73
+ hasLazy,
74
+ eagerEntries,
75
+ projectRoot,
76
+ dictionaryOutputPath,
77
+ typesOutputPath,
78
+ schemaTypeName,
79
+ importExtension,
80
+ splitPathsByNamespace = {},
81
+ } = options;
82
+
83
+ const partitionSuffix = toPartitionSuffix(paramName);
84
+ const typesModule = toModuleBasename(typesOutputPath);
85
+ const typesImport = toRelativeModuleImport(typesModule, importExtension);
86
+ const dictionaryTypeName = hasLazy ? "InitialSchema" : schemaTypeName;
87
+ const schemaTypeImport = hasLazy ? `, ${schemaTypeName}` : "";
88
+
89
+ const imports = eagerEntries
90
+ .flatMap((entry) =>
91
+ partitionKeys.map((partitionKey) => {
92
+ const splitRelativePath = splitPathsByNamespace[entry.namespace]?.[partitionKey];
93
+ if (!splitRelativePath) {
94
+ throw new Error(
95
+ `[Codegen Error] Missing split path for namespace "${entry.namespace}", ${paramName} "${partitionKey}".`
96
+ );
97
+ }
98
+ const importPath = toImportPath(
99
+ dictionaryOutputPath,
100
+ path.resolve(projectRoot, splitRelativePath)
101
+ );
102
+ const importId = toSplitImportIdentifier(entry.namespace, partitionKey);
103
+ return `import ${importId} from '${importPath}.json';`;
104
+ })
105
+ )
106
+ .join("\n");
107
+
108
+ if (isSingle) {
109
+ const entry = eagerEntries[0]!;
110
+ const baseId = toNamespaceBaseIdentifier(entry.namespace);
111
+ const partitionEntries = partitionKeys
112
+ .map(
113
+ (partitionKey) =>
114
+ ` ${toLocaleObjectKey(partitionKey)}: ${toSplitImportIdentifier(entry.namespace, partitionKey)},`
115
+ )
116
+ .join("\n");
117
+
118
+ return (
119
+ `${GENERATED_FILE_BANNER}` +
120
+ `${imports}\n` +
121
+ `import type { ${schemaTypeName}, ${paramTypeName} } from '${typesImport}';\n\n` +
122
+ `const ${baseId}${partitionSuffix} = {\n${partitionEntries}\n} as const;\n\n` +
123
+ `export function defaultDictionaryFor(${paramName}: ${paramTypeName}): ${schemaTypeName} {\n` +
124
+ ` return ${baseId}${partitionSuffix}[${paramName}];\n` +
125
+ `}\n`
126
+ );
127
+ }
128
+
129
+ const byPartitionBlocks = eagerEntries
130
+ .map((entry) => {
131
+ const baseId = toNamespaceBaseIdentifier(entry.namespace);
132
+ const partitionEntries = partitionKeys
133
+ .map(
134
+ (partitionKey) =>
135
+ ` ${toLocaleObjectKey(partitionKey)}: ${toSplitImportIdentifier(entry.namespace, partitionKey)},`
136
+ )
137
+ .join("\n");
138
+ return `const ${baseId}${partitionSuffix} = {\n${partitionEntries}\n} as const;`;
139
+ })
140
+ .join("\n\n");
141
+
142
+ const objectEntries = eagerEntries
143
+ .map((entry) => {
144
+ const baseId = toNamespaceBaseIdentifier(entry.namespace);
145
+ return ` ${entry.namespace}: ${baseId}${partitionSuffix}[${paramName}],`;
146
+ })
147
+ .join("\n");
148
+
149
+ const importsBlock = imports.length > 0 ? `${imports}\n` : "";
150
+ const byPartitionBlock = byPartitionBlocks.length > 0 ? `${byPartitionBlocks}\n\n` : "";
151
+
152
+ return (
153
+ `${GENERATED_FILE_BANNER}` +
154
+ `${importsBlock}` +
155
+ `import type { ${dictionaryTypeName}, ${paramTypeName}${schemaTypeImport} } from '${typesImport}';\n\n` +
156
+ `${byPartitionBlock}` +
157
+ `export function defaultDictionaryFor(${paramName}: ${paramTypeName}): ${dictionaryTypeName} {\n` +
158
+ ` return {\n${objectEntries}\n };\n` +
159
+ `}\n`
160
+ );
161
+ }
162
+
163
+ /**
164
+ * Builds `dictionary.generated.ts` for the configured delivery mode:
165
+ * canonical (single bundled JSON per namespace), split-by-locale, or custom areas.
166
+ */
23
167
  export function formatDictionaryFile(options: DictionaryFileOptions): string {
168
+ const delivery = options.delivery ?? "canonical";
169
+ if (delivery === "split-by-locale") {
170
+ return formatPartitionedDictionaryFile(options, {
171
+ partitionKeys: options.requestLocales ?? [],
172
+ paramName: "locale",
173
+ paramTypeName: options.localeTypeName,
174
+ });
175
+ }
176
+ if (delivery === "custom") {
177
+ if (!options.deliveryAreaTypeName) {
178
+ throw new Error("[Codegen Error] deliveryAreaTypeName is required for custom delivery.");
179
+ }
180
+ return formatPartitionedDictionaryFile(options, {
181
+ partitionKeys: options.deliveryAreaNames ?? [],
182
+ paramName: "area",
183
+ paramTypeName: options.deliveryAreaTypeName,
184
+ });
185
+ }
186
+
24
187
  const {
25
188
  isSingle,
26
189
  hasLazy,
@@ -14,6 +14,7 @@ function formatVariableSpecObject(spec: VariableSpec): string {
14
14
  return `{\n${lines.join("\n")}\n }`;
15
15
  }
16
16
 
17
+ /** Builds the `DICTIONARY_SPEC` constant from ICU analysis output (keys + variable args). */
17
18
  export function formatDictionarySpecBlock(
18
19
  isSingle: boolean,
19
20
  entries: NamespaceEntry[],
@@ -66,6 +67,10 @@ export function formatDictionarySpecBlock(
66
67
  );
67
68
  }
68
69
 
70
+ /**
71
+ * Emits the optional dictionary-schema module: `DICTIONARY_SPEC` plus thin wrappers
72
+ * around `@xndrjs/i18n/validation` for external dictionary ingestion.
73
+ */
69
74
  export function formatDictionarySchemaFile(
70
75
  schemaTypeName: string,
71
76
  typesModule: string,