@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.
- package/README.md +1 -2
- package/dist/cli/audit.js +739 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/codegen.js +1471 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/setup.js +148 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/codegen/index.js +43 -40
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +56 -10
- package/dist/index.js.map +1 -1
- package/package.json +6 -9
- package/bin/audit.mjs +0 -14
- package/bin/codegen.mjs +0 -17
- package/bin/setup.mjs +0 -14
- package/src/IcuTranslationProviderMulti.test.ts +0 -215
- package/src/IcuTranslationProviderMulti.ts +0 -176
- package/src/audit/audit-dictionaries.test.ts +0 -134
- package/src/audit/audit-dictionaries.ts +0 -178
- package/src/audit/run-audit.test.ts +0 -169
- package/src/audit/run-audit.ts +0 -94
- package/src/builder-load-registry.ts +0 -39
- package/src/builder-loaders.ts +0 -15
- package/src/builder-types.test.ts +0 -49
- package/src/builder-types.ts +0 -24
- package/src/builder.test.ts +0 -260
- package/src/builder.ts +0 -10
- package/src/codegen/codegen-config-schema.ts +0 -156
- package/src/codegen/config.test.ts +0 -231
- package/src/codegen/config.ts +0 -42
- package/src/codegen/constants.ts +0 -9
- package/src/codegen/delivery-artifacts.test.ts +0 -167
- package/src/codegen/delivery-artifacts.ts +0 -154
- package/src/codegen/dictionary-spec-contract.test.ts +0 -28
- package/src/codegen/dictionary-spec-contract.ts +0 -138
- package/src/codegen/emit/dictionary-schema-file.ts +0 -113
- package/src/codegen/emit/instance-file.test.ts +0 -71
- package/src/codegen/emit/instance-file.ts +0 -257
- package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
- package/src/codegen/emit/namespace-loaders-file.ts +0 -229
- package/src/codegen/emit/types-file.test.ts +0 -96
- package/src/codegen/emit/types-file.ts +0 -142
- package/src/codegen/fixtures/billing-namespace.json +0 -6
- package/src/codegen/generate-i18n-types.test.ts +0 -1137
- package/src/codegen/generate-i18n-types.ts +0 -20
- package/src/codegen/icu-analysis.ts +0 -110
- package/src/codegen/locale-fallback.ts +0 -73
- package/src/codegen/locale-policy.test.ts +0 -35
- package/src/codegen/locale-policy.ts +0 -30
- package/src/codegen/paths.ts +0 -44
- package/src/codegen/project-locales-set-namespace.test.ts +0 -134
- package/src/codegen/read-dictionary.test.ts +0 -594
- package/src/codegen/read-dictionary.ts +0 -260
- package/src/codegen/regenerate-namespaces.ts +0 -180
- package/src/codegen/run-codegen.test.ts +0 -230
- package/src/codegen/run-codegen.ts +0 -252
- package/src/codegen/types.ts +0 -10
- package/src/codegen/write-file-if-changed.test.ts +0 -42
- package/src/codegen/write-file-if-changed.ts +0 -20
- package/src/codegen-config/build-config.ts +0 -15
- package/src/codegen-config/codegen-config.test.ts +0 -36
- package/src/codegen-config/index.ts +0 -37
- package/src/codegen-config/type-names.ts +0 -20
- package/src/codegen-config/write-config.ts +0 -8
- package/src/engine.ts +0 -47
- package/src/fetch-artifact.ts +0 -16
- package/src/format-core.ts +0 -91
- package/src/i18n-handle.ts +0 -202
- package/src/icu/extract-variables.test.ts +0 -199
- package/src/icu/extract-variables.ts +0 -227
- package/src/icu/parse-template.ts +0 -31
- package/src/index.ts +0 -58
- package/src/project-locales.test.ts +0 -280
- package/src/project-locales.ts +0 -211
- package/src/resolve-locale.test.ts +0 -91
- package/src/resolve-locale.ts +0 -88
- package/src/scope-multi.ts +0 -105
- package/src/scope-types.ts +0 -35
- package/src/scope.test.ts +0 -172
- package/src/serialized-state.test.ts +0 -114
- package/src/serialized-state.ts +0 -28
- package/src/setup/setup-i18n.test.ts +0 -85
- package/src/setup/setup-i18n.ts +0 -156
- package/src/types.ts +0 -65
- package/src/validation/create-args-schema.ts +0 -76
- package/src/validation/create-normalized-schema.ts +0 -52
- package/src/validation/errors.ts +0 -43
- package/src/validation/index.ts +0 -202
- package/src/validation/normalize.ts +0 -299
- package/src/validation/to-dictionary.ts +0 -67
- package/src/validation/types.ts +0 -82
- package/src/validation/validate-normalized.ts +0 -133
- 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
|
-
});
|
package/src/audit/run-audit.ts
DELETED
|
@@ -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
|
-
}
|
package/src/builder-loaders.ts
DELETED
|
@@ -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
|
-
});
|
package/src/builder-types.ts
DELETED
|
@@ -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;
|
package/src/builder.test.ts
DELETED
|
@@ -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";
|