@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,154 +0,0 @@
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
- }
125
-
126
- export function getDeliveryArtifactsTypeName(deliveryAreaTypeName: string): string {
127
- return deliveryAreaTypeName.endsWith("DeliveryArea")
128
- ? deliveryAreaTypeName.replace(/DeliveryArea$/, "DeliveryArtifacts")
129
- : `${deliveryAreaTypeName}Artifacts`;
130
- }
131
-
132
- export function formatDeliveryArtifactsBlock(
133
- deliveryArtifacts: DeliveryArtifactsMap,
134
- constName: string,
135
- localeTypeName: string,
136
- deliveryAreaTypeName: string
137
- ): string {
138
- const deliveryArtifactsTypeName = getDeliveryArtifactsTypeName(deliveryAreaTypeName);
139
- const areaEntries = getDeliveryAreaNames(deliveryArtifacts)
140
- .map((area) => {
141
- const locales = [...deliveryArtifacts[area]!]
142
- .sort()
143
- .map((locale) => JSON.stringify(locale))
144
- .join(", ");
145
- return ` ${JSON.stringify(area)}: [${locales}] as const,`;
146
- })
147
- .join("\n");
148
-
149
- return (
150
- `export const ${constName} = {\n${areaEntries}\n} as const satisfies Record<${deliveryAreaTypeName}, readonly ${localeTypeName}[]>;\n\n` +
151
- `export type ${deliveryArtifactsTypeName} = typeof ${constName};\n\n` +
152
- `export type LocalesForDeliveryArea<A extends ${deliveryAreaTypeName}> = ${deliveryArtifactsTypeName}[A][number];\n\n`
153
- );
154
- }
@@ -1,28 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- buildDictionarySpecFromAnalysis,
4
- namespaceContractsMatch,
5
- } from "./dictionary-spec-contract.js";
6
-
7
- describe("dictionary-spec-contract", () => {
8
- it("namespaceContractsMatch compares keys and VariableSpec", () => {
9
- const entries = [{ namespace: "default", filePath: "translations/default.json" }];
10
- const current = buildDictionarySpecFromAnalysis(entries, {
11
- default: { welcome: { name: "string" }, login: {} },
12
- });
13
- const established = buildDictionarySpecFromAnalysis(entries, {
14
- default: { welcome: { name: "string" }, login: {} },
15
- });
16
- expect(namespaceContractsMatch(["default"], current, established)).toBe(true);
17
-
18
- const changedArgs = buildDictionarySpecFromAnalysis(entries, {
19
- default: { welcome: { name: "string", extra: "string" }, login: {} },
20
- });
21
- expect(namespaceContractsMatch(["default"], changedArgs, established)).toBe(false);
22
-
23
- const changedKeys = buildDictionarySpecFromAnalysis(entries, {
24
- default: { welcome: { name: "string" } },
25
- });
26
- expect(namespaceContractsMatch(["default"], changedKeys, established)).toBe(false);
27
- });
28
- });
@@ -1,138 +0,0 @@
1
- import fs from "node:fs";
2
- import type { VariableSpec } from "../icu/extract-variables.js";
3
- import type { DictionarySpec } from "../validation/types.js";
4
- import type { NamespaceEntry } from "./types.js";
5
-
6
- /** Builds a multi-mode {@link DictionarySpec} from ICU analysis for the given entries. */
7
- export function buildDictionarySpecFromAnalysis(
8
- entries: NamespaceEntry[],
9
- argsSpecByNamespace: Record<string, Record<string, VariableSpec>>
10
- ): Extract<DictionarySpec, { mode: "multi" }> {
11
- const requiredKeys: Record<string, readonly string[]> = {};
12
- const argsByKey: Record<string, Record<string, VariableSpec>> = {};
13
-
14
- for (const entry of entries) {
15
- const argsByKeyForNs = argsSpecByNamespace[entry.namespace] ?? {};
16
- requiredKeys[entry.namespace] = Object.keys(argsByKeyForNs).sort();
17
- argsByKey[entry.namespace] = argsByKeyForNs;
18
- }
19
-
20
- return {
21
- mode: "multi",
22
- requiredKeys,
23
- argsByKey,
24
- };
25
- }
26
-
27
- /**
28
- * Loads `DICTIONARY_SPEC` from a previously generated `dictionary-schema.generated.ts`.
29
- * Sync parse of the emitter format from {@link formatDictionarySpecBlock} (no TS runtime import).
30
- */
31
- export function loadDictionarySpecFromSchemaFile(
32
- absoluteSchemaPath: string
33
- ): Extract<DictionarySpec, { mode: "multi" }> {
34
- if (!fs.existsSync(absoluteSchemaPath)) {
35
- throw new Error(
36
- `[Codegen Error] Missing dictionary schema at ${absoluteSchemaPath}. Run runCodegen first.`
37
- );
38
- }
39
-
40
- const source = fs.readFileSync(absoluteSchemaPath, "utf8");
41
- const match = source.match(
42
- /export const DICTIONARY_SPEC = (\{[\s\S]*?\n\}) satisfies DictionarySpec;/
43
- );
44
- if (!match?.[1]) {
45
- throw new Error(
46
- `[Codegen Error] Could not parse DICTIONARY_SPEC from ${absoluteSchemaPath}. Re-run runCodegen.`
47
- );
48
- }
49
-
50
- const cleaned = match[1].replace(/ as const/g, "");
51
- let parsed: unknown;
52
- try {
53
- parsed = new Function(`"use strict"; return (${cleaned});`)();
54
- } catch (error) {
55
- const message = error instanceof Error ? error.message : String(error);
56
- throw new Error(
57
- `[Codegen Error] Failed to evaluate DICTIONARY_SPEC from ${absoluteSchemaPath}: ${message}`
58
- );
59
- }
60
-
61
- if (
62
- !parsed ||
63
- typeof parsed !== "object" ||
64
- (parsed as DictionarySpec).mode !== "multi" ||
65
- !("requiredKeys" in parsed) ||
66
- !("argsByKey" in parsed)
67
- ) {
68
- throw new Error(
69
- `[Codegen Error] DICTIONARY_SPEC in ${absoluteSchemaPath} is not a multi-mode DictionarySpec.`
70
- );
71
- }
72
-
73
- return parsed as Extract<DictionarySpec, { mode: "multi" }>;
74
- }
75
-
76
- function sortedKeys(keys: readonly string[]): string[] {
77
- return [...keys].sort();
78
- }
79
-
80
- function variableSpecsEqual(a: VariableSpec, b: VariableSpec): boolean {
81
- const aKeys = Object.keys(a).sort();
82
- const bKeys = Object.keys(b).sort();
83
- if (aKeys.length !== bKeys.length) {
84
- return false;
85
- }
86
- for (let i = 0; i < aKeys.length; i++) {
87
- const key = aKeys[i]!;
88
- if (key !== bKeys[i] || a[key] !== b[key]) {
89
- return false;
90
- }
91
- }
92
- return true;
93
- }
94
-
95
- function namespaceArgsEqual(
96
- a: Readonly<Record<string, VariableSpec>> | undefined,
97
- b: Readonly<Record<string, VariableSpec>> | undefined
98
- ): boolean {
99
- const aKeys = Object.keys(a ?? {}).sort();
100
- const bKeys = Object.keys(b ?? {}).sort();
101
- if (aKeys.length !== bKeys.length) {
102
- return false;
103
- }
104
- for (const key of aKeys) {
105
- if (!bKeys.includes(key)) {
106
- return false;
107
- }
108
- if (!variableSpecsEqual(a![key]!, b![key]!)) {
109
- return false;
110
- }
111
- }
112
- return true;
113
- }
114
-
115
- /**
116
- * Returns true when `current` matches `established` for every namespace in `namespaces`
117
- * (same keys + same ICU VariableSpec per key).
118
- */
119
- export function namespaceContractsMatch(
120
- namespaces: readonly string[],
121
- current: Extract<DictionarySpec, { mode: "multi" }>,
122
- established: Extract<DictionarySpec, { mode: "multi" }>
123
- ): boolean {
124
- for (const namespace of namespaces) {
125
- const currentKeys = sortedKeys(current.requiredKeys[namespace] ?? []);
126
- const establishedKeys = sortedKeys(established.requiredKeys[namespace] ?? []);
127
- if (
128
- currentKeys.length !== establishedKeys.length ||
129
- currentKeys.some((key, index) => key !== establishedKeys[index])
130
- ) {
131
- return false;
132
- }
133
- if (!namespaceArgsEqual(current.argsByKey[namespace], established.argsByKey[namespace])) {
134
- return false;
135
- }
136
- }
137
- return true;
138
- }
@@ -1,113 +0,0 @@
1
- import type { VariableSpec } from "../../icu/extract-variables.js";
2
- import { GENERATED_FILE_BANNER, toRelativeModuleImport } from "../paths.js";
3
- import type { ImportExtension, NamespaceEntry } from "../types.js";
4
-
5
- function formatVariableSpecObject(spec: VariableSpec): string {
6
- const entries = Object.entries(spec);
7
- if (entries.length === 0) {
8
- return "{}";
9
- }
10
-
11
- const lines = entries.map(
12
- ([name, type]) => ` ${JSON.stringify(name)}: ${JSON.stringify(type)},`
13
- );
14
- return `{\n${lines.join("\n")}\n }`;
15
- }
16
-
17
- /** Builds the `DICTIONARY_SPEC` constant from ICU analysis output (keys + variable args). */
18
- export function formatDictionarySpecBlock(
19
- entries: NamespaceEntry[],
20
- argsSpecByNamespace: Record<string, Record<string, VariableSpec>>
21
- ): string {
22
- const requiredKeysLines = entries
23
- .map((entry) => {
24
- const keys = Object.keys(argsSpecByNamespace[entry.namespace] ?? {})
25
- .map((key) => JSON.stringify(key))
26
- .join(", ");
27
- return ` ${JSON.stringify(entry.namespace)}: [${keys}] as const,`;
28
- })
29
- .join("\n");
30
-
31
- const argsByKeyLines = entries
32
- .map((entry) => {
33
- const argsByKey = argsSpecByNamespace[entry.namespace] ?? {};
34
- const keyLines = Object.entries(argsByKey)
35
- .map(([key, spec]) => ` ${JSON.stringify(key)}: ${formatVariableSpecObject(spec)},`)
36
- .join("\n");
37
- return ` ${JSON.stringify(entry.namespace)}: {\n${keyLines}\n },`;
38
- })
39
- .join("\n");
40
-
41
- return (
42
- `export const DICTIONARY_SPEC = {\n` +
43
- ` mode: 'multi' as const,\n` +
44
- ` requiredKeys: {\n${requiredKeysLines}\n },\n` +
45
- ` argsByKey: {\n${argsByKeyLines}\n },\n` +
46
- `} satisfies DictionarySpec;\n`
47
- );
48
- }
49
-
50
- /**
51
- * Emits the optional dictionary-schema module: `DICTIONARY_SPEC` plus thin wrappers
52
- * around `@xndrjs/i18n/validation` for validating external payloads before
53
- * updating authoring / `regenerateNamespaces` (e.g. CMS → authoring JSON), not for runtime key patches.
54
- */
55
- export function formatDictionarySchemaFile(
56
- schemaTypeName: string,
57
- typesModule: string,
58
- dictionarySpecBlock: string,
59
- importExtension: ImportExtension
60
- ): string {
61
- const typesImport = toRelativeModuleImport(typesModule, importExtension);
62
- const partialImport =
63
- ` validateExternalDictionaryPartial as validateExternalDictionaryPartialCore,\n` +
64
- ` validateExternalNamespacePartial as validateExternalNamespacePartialCore,\n` +
65
- ` validateExternalKey as validateExternalKeyCore,\n`;
66
-
67
- const multiValidators =
68
- `export function validateExternalNamespacePartial<NS extends keyof ${schemaTypeName}>(\n` +
69
- ` namespace: NS,\n` +
70
- ` input: unknown,\n` +
71
- `): ValidationResult<Partial<${schemaTypeName}[NS]>> {\n` +
72
- ` return validateExternalNamespacePartialCore<Partial<${schemaTypeName}[NS]>>(\n` +
73
- ` namespace as string,\n` +
74
- ` input,\n` +
75
- ` DICTIONARY_SPEC,\n` +
76
- ` );\n` +
77
- `}\n\n` +
78
- `export function validateExternalKey<\n` +
79
- ` NS extends keyof ${schemaTypeName},\n` +
80
- ` K extends keyof ${schemaTypeName}[NS],\n` +
81
- `>(\n` +
82
- ` namespace: NS,\n` +
83
- ` key: K,\n` +
84
- ` input: unknown,\n` +
85
- `): ValidationResult<Pick<${schemaTypeName}[NS], K>> {\n` +
86
- ` return validateExternalKeyCore<Pick<${schemaTypeName}[NS], K>>(\n` +
87
- ` namespace as string,\n` +
88
- ` key as string,\n` +
89
- ` input,\n` +
90
- ` DICTIONARY_SPEC,\n` +
91
- ` );\n` +
92
- `}\n\n`;
93
-
94
- return (
95
- `${GENERATED_FILE_BANNER}` +
96
- `import {\n` +
97
- partialImport +
98
- ` type DictionarySpec,\n` +
99
- ` type ValidationResult,\n` +
100
- `} from '@xndrjs/i18n/validation';\n` +
101
- `import type { ${schemaTypeName} } from '${typesImport}';\n\n` +
102
- `${dictionarySpecBlock}\n` +
103
- `export function validateExternalDictionaryPartial(\n` +
104
- ` input: unknown,\n` +
105
- `): ValidationResult<Partial<${schemaTypeName}>> {\n` +
106
- ` return validateExternalDictionaryPartialCore<Partial<${schemaTypeName}>>(\n` +
107
- ` input,\n` +
108
- ` DICTIONARY_SPEC,\n` +
109
- ` );\n` +
110
- `}\n\n` +
111
- multiValidators
112
- );
113
- }
@@ -1,71 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { formatInstanceFile, type InstanceFileOptions } from "./instance-file.js";
3
-
4
- function baseOptions(overrides: Partial<InstanceFileOptions> = {}): InstanceFileOptions {
5
- return {
6
- typesOutputPath: "src/i18n/i18n-types.generated.ts",
7
- namespaceLoadersOutputPath: "src/i18n/namespace-loaders.generated.ts",
8
- paramsTypeName: "AppParams",
9
- schemaTypeName: "AppSchema",
10
- localeTypeName: "AppLocale",
11
- localeFallbackConstName: "LOCALE_FALLBACK",
12
- factoryName: "createI18n",
13
- hasLocaleFallback: true,
14
- hasLocaleType: true,
15
- importExtension: "none",
16
- delivery: "split-by-locale",
17
- ...overrides,
18
- };
19
- }
20
-
21
- function packageImportBlock(output: string): string {
22
- const match = output.match(/import \{([\s\S]*?)\} from '@xndrjs\/i18n'/);
23
- return match?.[1] ?? "";
24
- }
25
-
26
- describe("formatInstanceFile package imports", () => {
27
- it("emits lazy multi handle symbols", () => {
28
- const output = formatInstanceFile(baseOptions());
29
- const imports = packageImportBlock(output);
30
-
31
- expect(imports).toContain("IcuTranslationProviderMulti");
32
- expect(imports).toContain("createI18nHandle");
33
- expect(imports).toContain("type I18nHandle");
34
- expect(imports).toContain("type I18nHandleOptions");
35
- expect(output).toContain("partitionForLocale: (locale) => locale");
36
- expect(output).toContain("I18nHandle<AppSchema, AppParams, AppLocale>");
37
- expect(output).toContain(
38
- "options?: { state?: { dictionary: InitialSchema; resources?: readonly (readonly [string, string])[] }; onMissing?: OnMissingTranslation }"
39
- );
40
- expect(output).toContain("const { state, ...providerOptions } = options ?? {}");
41
- });
42
-
43
- it("injects LOCALE_DELIVERY_AREA for custom delivery", () => {
44
- const output = formatInstanceFile(
45
- baseOptions({
46
- delivery: "custom",
47
- localeDeliveryAreaConstName: "LOCALE_DELIVERY_AREA",
48
- })
49
- );
50
-
51
- expect(output).toContain("LOCALE_DELIVERY_AREA");
52
- expect(output).toContain("partitionForLocale: (locale) => LOCALE_DELIVERY_AREA[locale]");
53
- });
54
-
55
- it("wires createNamespaceLoaders + required fetchImpl in options bag", () => {
56
- const output = formatInstanceFile(
57
- baseOptions({
58
- loaderStrategy: "fetch",
59
- })
60
- );
61
-
62
- expect(output).toContain("type FetchArtifact");
63
- expect(output).toContain("import { createNamespaceLoaders }");
64
- expect(output).toContain(
65
- "options: { fetchImpl: FetchArtifact; state?: { dictionary: InitialSchema; resources?: readonly (readonly [string, string])[] }; onMissing?: OnMissingTranslation }"
66
- );
67
- expect(output).toContain("const { fetchImpl, state, ...providerOptions } = options;");
68
- expect(output).toContain("createNamespaceLoaders(fetchImpl)");
69
- expect(output).not.toContain("import { namespaceLoaders }");
70
- });
71
- });