@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,169 +0,0 @@
1
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { spawnSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { dirname } from "node:path";
7
- import { afterEach, describe, expect, it, vi } from "vitest";
8
- import { parseAuditArgs, runAuditCli } from "./run-audit.js";
9
-
10
- const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
11
- const auditBin = join(packageRoot, "bin/audit.mjs");
12
-
13
- describe("parseAuditArgs", () => {
14
- it("defaults to report-only mode without --fail-on", () => {
15
- expect(parseAuditArgs([])).toEqual({
16
- configPath: join(process.cwd(), "i18n/i18n.codegen.json"),
17
- outPath: undefined,
18
- failOn: undefined,
19
- treatEmptyAsMissing: true,
20
- });
21
- });
22
-
23
- it("parses fail-on and allow-empty", () => {
24
- const parsed = parseAuditArgs([
25
- "--config",
26
- "cfg.json",
27
- "--fail-on",
28
- "effective",
29
- "--allow-empty",
30
- ]);
31
- expect(parsed.failOn).toBe("effective");
32
- expect(parsed.treatEmptyAsMissing).toBe(false);
33
- expect(parsed.configPath).toBe(join(process.cwd(), "cfg.json"));
34
- });
35
- });
36
-
37
- describe("runAuditCli", () => {
38
- let tempDir = "";
39
-
40
- afterEach(() => {
41
- if (tempDir) {
42
- rmSync(tempDir, { recursive: true, force: true });
43
- tempDir = "";
44
- }
45
- });
46
-
47
- function writeFixture(dictionary: Record<string, Record<string, string>>) {
48
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-audit-"));
49
- mkdirSync(join(tempDir, "i18n/translations"), { recursive: true });
50
- writeFileSync(join(tempDir, "i18n/translations/default.json"), JSON.stringify(dictionary));
51
- writeFileSync(
52
- join(tempDir, "i18n/i18n.codegen.json"),
53
- JSON.stringify({
54
- projectName: "App",
55
- namespaces: {
56
- default: "translations/default.json",
57
- },
58
- codegenPath: "generated",
59
- localeFallback: {
60
- en: null,
61
- it: "en",
62
- },
63
- })
64
- );
65
- }
66
-
67
- it("exits 0 without --fail-on even when effective gaps exist", async () => {
68
- writeFixture({
69
- welcome: { en: "Welcome" },
70
- });
71
-
72
- const previousCwd = process.cwd();
73
- process.chdir(tempDir);
74
- try {
75
- const exitCode = await runAuditCli(["--config", "i18n/i18n.codegen.json"]);
76
- expect(exitCode).toBe(0);
77
- } finally {
78
- process.chdir(previousCwd);
79
- }
80
- });
81
-
82
- it("exits 1 with --fail-on effective when effective gaps exist", async () => {
83
- writeFixture({
84
- login_button: { it: "Accedi" },
85
- });
86
-
87
- const previousCwd = process.cwd();
88
- process.chdir(tempDir);
89
- try {
90
- const exitCode = await runAuditCli([
91
- "--config",
92
- "i18n/i18n.codegen.json",
93
- "--fail-on",
94
- "effective",
95
- ]);
96
- expect(exitCode).toBe(1);
97
- } finally {
98
- process.chdir(previousCwd);
99
- }
100
- });
101
-
102
- it("writes JSON report to --out", async () => {
103
- writeFixture({
104
- login_button: { en: "Login", it: "Accedi" },
105
- });
106
-
107
- const outPath = join(tempDir, "audit.json");
108
- const previousCwd = process.cwd();
109
- process.chdir(tempDir);
110
- try {
111
- const exitCode = await runAuditCli(["--config", "i18n/i18n.codegen.json", "--out", outPath]);
112
- expect(exitCode).toBe(0);
113
- const report = JSON.parse(readFileSync(outPath, "utf8"));
114
- expect(report.requiredLocales).toEqual(["en", "it"]);
115
- expect(report.missingEffectiveByLocale.default.en).toEqual([]);
116
- } finally {
117
- process.chdir(previousCwd);
118
- }
119
- });
120
-
121
- it("exits 2 with an error message when the config is invalid", async () => {
122
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-audit-"));
123
- mkdirSync(join(tempDir, "i18n"), { recursive: true });
124
- writeFileSync(
125
- join(tempDir, "i18n/i18n.codegen.json"),
126
- JSON.stringify({ namespaces: { default: "translations/default.json" } })
127
- );
128
-
129
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => void 0);
130
- const previousCwd = process.cwd();
131
- process.chdir(tempDir);
132
- try {
133
- const exitCode = await runAuditCli(["--config", "i18n/i18n.codegen.json"]);
134
- expect(exitCode).toBe(2);
135
- expect(String(errorSpy.mock.calls[0]?.[0])).toContain("Invalid i18n.codegen.json");
136
- } finally {
137
- process.chdir(previousCwd);
138
- errorSpy.mockRestore();
139
- }
140
- });
141
-
142
- it("exits 2 with an error message when --fail-on has an invalid value", async () => {
143
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => void 0);
144
- try {
145
- const exitCode = await runAuditCli(["--fail-on", "bogus"]);
146
- expect(exitCode).toBe(2);
147
- expect(String(errorSpy.mock.calls[0]?.[0])).toContain(
148
- "[Audit Error] --fail-on must be one of: effective, direct, any"
149
- );
150
- } finally {
151
- errorSpy.mockRestore();
152
- }
153
- });
154
-
155
- it("runs through the bin wrapper", () => {
156
- writeFixture({
157
- login_button: { en: "Login" },
158
- });
159
-
160
- const result = spawnSync("node", [auditBin, "--config", "i18n/i18n.codegen.json"], {
161
- cwd: tempDir,
162
- encoding: "utf8",
163
- });
164
-
165
- expect(result.status).toBe(0);
166
- const report = JSON.parse(result.stdout);
167
- expect(report.requiredLocales).toEqual(["en", "it"]);
168
- });
169
- });
@@ -1,94 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import type { CodegenConfig } from "../codegen/codegen-config-schema.js";
4
- import { loadConfig } from "../codegen/config.js";
5
- import { type FailOnCriterion, runAuditFromConfig } from "./audit-dictionaries.js";
6
-
7
- const FAIL_ON_VALUES = new Set<FailOnCriterion>(["effective", "direct", "any"]);
8
-
9
- export interface AuditCliOptions {
10
- configPath: string;
11
- outPath?: string;
12
- failOn?: FailOnCriterion;
13
- treatEmptyAsMissing: boolean;
14
- }
15
-
16
- export function parseAuditArgs(argv: string[]): AuditCliOptions {
17
- const configArgIndex = argv.indexOf("--config");
18
- const outArgIndex = argv.indexOf("--out");
19
- const failOnArgIndex = argv.indexOf("--fail-on");
20
-
21
- const configPath = path.resolve(
22
- process.cwd(),
23
- configArgIndex >= 0 ? argv[configArgIndex + 1]! : "i18n/i18n.codegen.json"
24
- );
25
-
26
- const outPath =
27
- outArgIndex >= 0 ? path.resolve(process.cwd(), argv[outArgIndex + 1]!) : undefined;
28
-
29
- let failOn: FailOnCriterion | undefined;
30
- if (failOnArgIndex >= 0) {
31
- const value = argv[failOnArgIndex + 1];
32
- if (!value || !FAIL_ON_VALUES.has(value as FailOnCriterion)) {
33
- throw new Error(`[Audit Error] --fail-on must be one of: effective, direct, any`);
34
- }
35
- failOn = value as FailOnCriterion;
36
- }
37
-
38
- const treatEmptyAsMissing = !argv.includes("--allow-empty");
39
-
40
- return {
41
- configPath,
42
- treatEmptyAsMissing,
43
- ...(outPath !== undefined ? { outPath } : {}),
44
- ...(failOn !== undefined ? { failOn } : {}),
45
- };
46
- }
47
-
48
- export async function runAuditCli(argv = process.argv.slice(2)): Promise<number> {
49
- let options: AuditCliOptions;
50
- let config: CodegenConfig;
51
-
52
- try {
53
- options = parseAuditArgs(argv);
54
-
55
- if (!fs.existsSync(options.configPath)) {
56
- console.error(`[Audit Error] Config file not found: ${options.configPath}`);
57
- return 2;
58
- }
59
-
60
- config = loadConfig(options.configPath);
61
- } catch (error) {
62
- console.error(error instanceof Error ? error.message : String(error));
63
- return 2;
64
- }
65
-
66
- const projectRoot = path.dirname(options.configPath);
67
-
68
- const { report, exitCode } = await runAuditFromConfig({
69
- projectRoot,
70
- config,
71
- treatEmptyAsMissing: options.treatEmptyAsMissing,
72
- ...(options.failOn !== undefined ? { failOn: options.failOn } : {}),
73
- });
74
-
75
- const json = `${JSON.stringify(report, null, 2)}\n`;
76
-
77
- if (options.outPath) {
78
- fs.mkdirSync(path.dirname(options.outPath), { recursive: true });
79
- fs.writeFileSync(options.outPath, json);
80
- } else {
81
- process.stdout.write(json);
82
- }
83
-
84
- return exitCode;
85
- }
86
-
87
- async function main(): Promise<void> {
88
- const exitCode = await runAuditCli();
89
- process.exit(exitCode);
90
- }
91
-
92
- if (process.argv[1]?.endsWith("run-audit.ts")) {
93
- void main();
94
- }
@@ -1,39 +0,0 @@
1
- /** Stable key for a lazy-loaded builder resource (namespace + locale or delivery area). */
2
- export function formatBuilderResourceKey(namespace: string, partition: string): string {
3
- return `${namespace}\0${partition}`;
4
- }
5
-
6
- /** Serialized builder resource: namespace + partition (locale / delivery area). */
7
- export type BuilderResourceEntry = readonly [namespace: string, partition: string];
8
-
9
- /**
10
- * Dedup cache for lazy-loaded builder resources (`namespace` × partition).
11
- * A repeated `load()` for the same resource skips the loader (avoids re-fetch / re-merge).
12
- */
13
- export class BuilderLoadRegistry {
14
- private readonly loaded = new Set<string>();
15
-
16
- has(namespace: string, partition: string): boolean {
17
- return this.loaded.has(formatBuilderResourceKey(namespace, partition));
18
- }
19
-
20
- mark(namespace: string, partition: string): void {
21
- this.loaded.add(formatBuilderResourceKey(namespace, partition));
22
- }
23
-
24
- /** Snapshot of loaded resources as `[namespace, partition]` tuples. */
25
- entries(): BuilderResourceEntry[] {
26
- const out: BuilderResourceEntry[] = [];
27
- for (const key of this.loaded) {
28
- const sep = key.indexOf("\0");
29
- out.push([key.slice(0, sep), key.slice(sep + 1)]);
30
- }
31
- return out;
32
- }
33
-
34
- seed(resources: readonly BuilderResourceEntry[]): void {
35
- for (const [namespace, partition] of resources) {
36
- this.mark(namespace, partition);
37
- }
38
- }
39
- }
@@ -1,15 +0,0 @@
1
- /** Split-by-locale or custom-area loader — partition key is locale or area name. */
2
- export type PartitionedLoader<T> = (
3
- partition: string,
4
- context: { locale: string }
5
- ) => Promise<T> | T;
6
-
7
- export type NamespaceLoader<T> = PartitionedLoader<T>;
8
-
9
- export async function invokeNamespaceLoader<T>(
10
- loader: NamespaceLoader<T>,
11
- partition: string,
12
- context: { locale: string }
13
- ): Promise<T> {
14
- return await loader(partition, context);
15
- }
@@ -1,49 +0,0 @@
1
- import { describe, expectTypeOf, it } from "vitest";
2
- import { createI18nHandle } from "./builder.js";
3
- import type { I18nHandle } from "./i18n-handle.js";
4
- import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
5
- import type { LocalesForDeliveryArea, LocalesForDeliveryAreaOrAll } from "./builder-types.js";
6
- import type { PartialMultiDictionary } from "./types.js";
7
-
8
- type TypestateSchema = {
9
- billing: { invoice_summary: { en: string; it: string } };
10
- };
11
-
12
- type TypestateParams = {
13
- billing: { invoice_summary: { count: number } };
14
- };
15
-
16
- describe("builder-types", () => {
17
- type ProjectLocale = "en" | "en-US" | "it" | "fr";
18
- type DeliveryArea = "eu" | "amer";
19
- type Artifacts = {
20
- eu: readonly ["en", "it", "fr"];
21
- amer: readonly ["en-US"];
22
- };
23
-
24
- it("LocalesForDeliveryArea indexes artifact locales", () => {
25
- expectTypeOf<LocalesForDeliveryArea<Artifacts, "eu">>().toEqualTypeOf<"en" | "it" | "fr">();
26
- expectTypeOf<LocalesForDeliveryArea<Artifacts, "amer">>().toEqualTypeOf<"en-US">();
27
- });
28
-
29
- it("LocalesForDeliveryAreaOrAll narrows only when a delivery map is configured", () => {
30
- expectTypeOf<
31
- LocalesForDeliveryAreaOrAll<ProjectLocale, DeliveryArea, Artifacts, "eu">
32
- >().toEqualTypeOf<"en" | "it" | "fr">();
33
- expectTypeOf<
34
- LocalesForDeliveryAreaOrAll<ProjectLocale, never, Record<string, never>, "eu">
35
- >().toEqualTypeOf<ProjectLocale>();
36
- });
37
- });
38
-
39
- describe("I18nHandle typestate", () => {
40
- it("exposes load / peek / serialize", () => {
41
- const engine = new IcuTranslationProviderMulti<TypestateSchema, TypestateParams>({});
42
- const handle = createI18nHandle(engine);
43
- expectTypeOf(handle).toMatchTypeOf<I18nHandle<TypestateSchema, TypestateParams>>();
44
- expectTypeOf(handle.serialize).returns.toEqualTypeOf<{
45
- dictionary: PartialMultiDictionary<TypestateSchema>;
46
- resources: readonly (readonly [string, string])[];
47
- }>();
48
- });
49
- });
@@ -1,24 +0,0 @@
1
- /** Locales served by a delivery area, from a codegen `DELIVERY_ARTIFACTS`-shaped map. */
2
- export type LocalesForDeliveryArea<
3
- Artifacts extends Record<string, readonly string[]>,
4
- Area extends keyof Artifacts & string,
5
- > = Artifacts[Area][number];
6
-
7
- /** Map from delivery area to locale lists; empty when custom delivery is not configured. */
8
- export type DeliveryArtifactsMap<RequestLocales extends string, DeliveryArea extends string> = [
9
- DeliveryArea,
10
- ] extends [never]
11
- ? Record<string, never>
12
- : Record<DeliveryArea, readonly RequestLocales[]>;
13
-
14
- /** When a delivery-area map is configured, resolve partition locales; otherwise keep the full pool. */
15
- export type LocalesForDeliveryAreaOrAll<
16
- RequestLocales extends string,
17
- DeliveryArea extends string,
18
- DeliveryArtifacts extends DeliveryArtifactsMap<RequestLocales, DeliveryArea>,
19
- Area extends string,
20
- > = [DeliveryArea] extends [never]
21
- ? RequestLocales
22
- : Area extends DeliveryArea
23
- ? DeliveryArtifacts[Area][number]
24
- : never;
@@ -1,260 +0,0 @@
1
- import { describe, expect, expectTypeOf, it, vi } from "vitest";
2
- import { createI18nHandle } from "./builder.js";
3
- import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
4
-
5
- type MultiSchema = {
6
- default: {
7
- login_button: { en: string; it: string };
8
- welcome: { en: string };
9
- };
10
- billing: {
11
- invoice_summary: { en: string; it: string };
12
- };
13
- };
14
-
15
- type TestMultiParams = {
16
- default: {
17
- login_button: never;
18
- welcome: { name: string };
19
- };
20
- billing: {
21
- invoice_summary: { count: number; name: string };
22
- };
23
- };
24
-
25
- const billingIt = {
26
- invoice_summary: {
27
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
28
- },
29
- };
30
-
31
- const billingEn = {
32
- invoice_summary: {
33
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
34
- },
35
- };
36
-
37
- const defaultIt = {
38
- login_button: { it: "Accedi" },
39
- welcome: { it: "Benvenuto {name}!" },
40
- };
41
-
42
- describe("I18nHandle", () => {
43
- it("load() invokes mock loaders, merges into the engine, and returns a bound view", async () => {
44
- const billingLoader = vi.fn(async (locale: string) => {
45
- return locale === "it" ? billingIt : billingEn;
46
- });
47
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
48
- const handle = createI18nHandle(engine, {
49
- namespaceLoaders: {
50
- billing: billingLoader,
51
- },
52
- });
53
-
54
- const view = await handle.load({ namespaces: ["billing"], locale: "it" });
55
-
56
- expect(billingLoader).toHaveBeenCalledWith("it", { locale: "it" });
57
- expect(view.locale).toBe("it");
58
- expect(view.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
59
- "Hai 2 fatture per Ada"
60
- );
61
- });
62
-
63
- it("loads multiple namespaces in one call", async () => {
64
- const billingLoader = vi.fn(async (locale: string) =>
65
- locale === "it" ? billingIt : billingEn
66
- );
67
- const defaultLoader = vi.fn(async () => defaultIt);
68
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
69
-
70
- const view = await createI18nHandle(engine, {
71
- namespaceLoaders: {
72
- billing: billingLoader,
73
- default: defaultLoader,
74
- },
75
- }).load({ namespaces: ["billing", "default"], locale: "it" });
76
-
77
- expect(billingLoader).toHaveBeenCalledWith("it", { locale: "it" });
78
- expect(defaultLoader).toHaveBeenCalledWith("it", { locale: "it" });
79
- expect(view.t("default", "login_button")).toBe("Accedi");
80
- expect(view.t("billing", "invoice_summary", { count: 1, name: "Bob" })).toBe(
81
- "Hai 1 fattura per Bob"
82
- );
83
- });
84
-
85
- it("accumulates engine data across repeated load() calls on the same handle", async () => {
86
- const billingLoader = vi.fn(async (locale: string) =>
87
- locale === "it" ? billingIt : billingEn
88
- );
89
- const defaultLoader = vi.fn(async () => defaultIt);
90
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
91
- const handle = createI18nHandle(engine, {
92
- namespaceLoaders: {
93
- billing: billingLoader,
94
- default: defaultLoader,
95
- },
96
- });
97
-
98
- const billingView = await handle.load({ namespaces: ["billing"], locale: "it" });
99
- const defaultView = await handle.load({ namespaces: ["default"], locale: "it" });
100
-
101
- expect(billingLoader).toHaveBeenCalledTimes(1);
102
- expect(defaultLoader).toHaveBeenCalledTimes(1);
103
- expect(billingView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
104
- "Hai 1 fattura per Ada"
105
- );
106
- expect(defaultView.t("default", "login_button")).toBe("Accedi");
107
- expect(engine.getAll().billing?.invoice_summary?.it).toBe(billingIt.invoice_summary.it);
108
- expect(engine.getAll().default?.login_button?.it).toBe("Accedi");
109
- });
110
-
111
- it("returns a view scoped to namespaces declared at load time", async () => {
112
- const billingLoader = vi.fn(async (locale: string) =>
113
- locale === "it" ? billingIt : billingEn
114
- );
115
- const defaultLoader = vi.fn(async () => defaultIt);
116
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
117
- const handle = createI18nHandle(engine, {
118
- namespaceLoaders: {
119
- billing: billingLoader,
120
- default: defaultLoader,
121
- },
122
- });
123
-
124
- const billingView = await handle.load({ namespaces: ["billing"], locale: "it" });
125
- await handle.load({ namespaces: ["default"], locale: "it" });
126
-
127
- expect(billingView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
128
- "Hai 1 fattura per Ada"
129
- );
130
- // billingView is typed for the billing namespace only; default is excluded at compile time.
131
- });
132
-
133
- it("maps locale to delivery partition via partitionForLocale", async () => {
134
- const billingLoader = vi.fn(async (area: string) => {
135
- return area === "eu" ? billingIt : billingEn;
136
- });
137
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
138
-
139
- const view = await createI18nHandle(engine, {
140
- namespaceLoaders: {
141
- billing: billingLoader,
142
- },
143
- partitionForLocale: (locale) => (locale === "it" ? "eu" : "us"),
144
- }).load({ namespaces: ["billing"], locale: "it" });
145
-
146
- expect(billingLoader).toHaveBeenCalledWith("eu", { locale: "it" });
147
- expect(view.locale).toBe("it");
148
- expect(view.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
149
- "Hai 2 fatture per Ada"
150
- );
151
- expectTypeOf(view.t).toBeCallableWith("billing", "invoice_summary", {
152
- count: 2,
153
- name: "Ada",
154
- });
155
- });
156
-
157
- it("loads partitioned namespace loaders with locale", async () => {
158
- const billingLoader = vi.fn(async (locale: string) =>
159
- locale === "it"
160
- ? {
161
- invoice_summary: {
162
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
163
- },
164
- }
165
- : {
166
- invoice_summary: {
167
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
168
- },
169
- }
170
- );
171
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
172
-
173
- const view = await createI18nHandle(engine, {
174
- namespaceLoaders: {
175
- billing: billingLoader,
176
- },
177
- }).load({ namespaces: ["billing"], locale: "en" });
178
-
179
- expect(billingLoader).toHaveBeenCalledTimes(1);
180
- expect(billingLoader.mock.calls[0]).toEqual(["en", { locale: "en" }]);
181
- expect(view.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
182
- "You have 1 invoice for Ada"
183
- );
184
- });
185
-
186
- it("rejects an empty namespaces array", async () => {
187
- const billingLoader = vi.fn(async () => billingEn);
188
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
189
- const handle = createI18nHandle(engine, {
190
- namespaceLoaders: {
191
- billing: billingLoader,
192
- },
193
- });
194
-
195
- await expect(
196
- handle.load({ namespaces: [] as unknown as ["billing"], locale: "en" })
197
- ).rejects.toThrow("load() requires a non-empty namespaces array");
198
- });
199
-
200
- it("skips a repeated load for the same namespace partition", async () => {
201
- const billingLoader = vi.fn(async (locale: string) =>
202
- locale === "it" ? billingIt : billingEn
203
- );
204
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
205
- const handle = createI18nHandle(engine, {
206
- namespaceLoaders: { billing: billingLoader },
207
- });
208
-
209
- await handle.load({ namespaces: ["billing"], locale: "it" });
210
- await handle.load({ namespaces: ["billing"], locale: "it" });
211
-
212
- expect(billingLoader).toHaveBeenCalledTimes(1);
213
- });
214
-
215
- it("returns a destructuring-safe locale-bound scope from load()", async () => {
216
- const billingLoader = vi.fn(async () => billingEn);
217
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
218
- const { t } = await createI18nHandle(engine, {
219
- namespaceLoaders: { billing: billingLoader },
220
- }).load({ namespaces: ["billing"], locale: "en" });
221
-
222
- expect(t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
223
- "You have 1 invoice for Ada"
224
- );
225
- });
226
-
227
- it("still loads a different partition for the same namespace", async () => {
228
- const billingLoader = vi.fn(async (locale: string) =>
229
- locale === "it" ? billingIt : billingEn
230
- );
231
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
232
- const handle = createI18nHandle(engine, {
233
- namespaceLoaders: { billing: billingLoader },
234
- });
235
-
236
- await handle.load({ namespaces: ["billing"], locale: "it" });
237
- await handle.load({ namespaces: ["billing"], locale: "en" });
238
-
239
- expect(billingLoader).toHaveBeenCalledTimes(2);
240
- expect(billingLoader).toHaveBeenNthCalledWith(1, "it", { locale: "it" });
241
- expect(billingLoader).toHaveBeenNthCalledWith(2, "en", { locale: "en" });
242
- });
243
-
244
- it("peek() returns null until resources are loaded, then a sync scope", async () => {
245
- const billingLoader = vi.fn(async () => billingEn);
246
- const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
247
- const handle = createI18nHandle(engine, {
248
- namespaceLoaders: { billing: billingLoader },
249
- });
250
-
251
- expect(handle.peek({ namespaces: ["billing"], locale: "en" })).toBeNull();
252
-
253
- await handle.load({ namespaces: ["billing"], locale: "en" });
254
- const sync = handle.peek({ namespaces: ["billing"], locale: "en" });
255
- expect(sync).not.toBeNull();
256
- expect(sync!.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
257
- "You have 1 invoice for Ada"
258
- );
259
- });
260
- });
package/src/builder.ts DELETED
@@ -1,10 +0,0 @@
1
- export type { NamespaceLoader, PartitionedLoader } from "./builder-loaders.js";
2
- export { invokeNamespaceLoader } from "./builder-loaders.js";
3
- export type {
4
- I18nHandle,
5
- I18nHandleOptions,
6
- LoadNamespacesInput,
7
- PartitionForLocale,
8
- ScopeForLocale,
9
- } from "./i18n-handle.js";
10
- export { createI18nHandle } from "./i18n-handle.js";