@xndrjs/i18n 0.3.3 → 0.5.0-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 (61) hide show
  1. package/README.md +193 -44
  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 -18
  6. package/dist/index.js +174 -145
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +86 -4
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +6 -1
  11. package/src/IcuTranslationProviderMulti.test.ts +48 -1
  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 +165 -2
  26. package/src/codegen/emit/dictionary-schema-file.ts +5 -0
  27. package/src/codegen/emit/instance-file.ts +119 -38
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
  29. package/src/codegen/emit/namespace-loaders-file.ts +118 -32
  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 +765 -22
  33. package/src/codegen/generate-i18n-types.ts +111 -58
  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 +11 -9
  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 -5
  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 +9 -32
  55. package/src/types.ts +19 -4
  56. package/src/validation/normalize.ts +4 -0
  57. package/dist/types-C1CpXVOJ.d.ts +0 -83
  58. package/src/ensure-namespace.integration.test.ts +0 -66
  59. package/src/ensure-namespace.test.ts +0 -125
  60. package/src/ensure-namespace.ts +0 -70
  61. /package/src/{setup → codegen-config}/type-names.ts +0 -0
@@ -1,125 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { ensureNamespacesLoadedImpl } from "./ensure-namespace.js";
3
- import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
4
- import type { ValidationResult } from "./validation/types.js";
5
-
6
- type TestSchema = {
7
- default: {
8
- hello: { en: string };
9
- };
10
- billing: {
11
- invoice: { en: string };
12
- };
13
- };
14
-
15
- type TestParams = {
16
- default: { hello: never };
17
- billing: { invoice: never };
18
- };
19
-
20
- const defaultNs: TestSchema["default"] = { hello: { en: "Hello" } };
21
- const billingNs: TestSchema["billing"] = { invoice: { en: "Invoice" } };
22
-
23
- function createProvider(initial: Partial<TestSchema> = { default: defaultNs }) {
24
- return new IcuTranslationProviderMulti<TestSchema, TestParams>(initial);
25
- }
26
-
27
- describe("ensureNamespacesLoadedImpl", () => {
28
- it("is a noop for an empty namespaces array", async () => {
29
- const provider = createProvider();
30
- const load = vi.fn();
31
-
32
- await ensureNamespacesLoadedImpl(
33
- {
34
- provider,
35
- resolveLoader: () => load,
36
- validate: () => ({ ok: true, data: billingNs }),
37
- },
38
- []
39
- );
40
-
41
- expect(load).not.toHaveBeenCalled();
42
- });
43
-
44
- it("is a noop when the namespace is already loaded", async () => {
45
- const provider = createProvider({ default: defaultNs, billing: billingNs });
46
- const load = vi.fn();
47
-
48
- await ensureNamespacesLoadedImpl(
49
- {
50
- provider,
51
- resolveLoader: () => load,
52
- validate: () => ({ ok: true, data: billingNs }),
53
- },
54
- ["billing"]
55
- );
56
-
57
- expect(load).not.toHaveBeenCalled();
58
- });
59
-
60
- it("loads multiple namespaces in parallel", async () => {
61
- const provider = createProvider();
62
- const loadBilling = vi.fn(async () => billingNs);
63
- const loadDefault = vi.fn(async () => defaultNs);
64
-
65
- await ensureNamespacesLoadedImpl(
66
- {
67
- provider,
68
- resolveLoader: (namespace) => (namespace === "billing" ? loadBilling : loadDefault),
69
- validate: (_namespace, raw) => ({ ok: true, data: raw as TestSchema["billing"] }),
70
- },
71
- ["billing"]
72
- );
73
-
74
- expect(loadBilling).toHaveBeenCalledTimes(1);
75
- expect(provider.hasNamespace("billing")).toBe(true);
76
- expect(provider.get("billing", "invoice", "en")).toBe("Invoice");
77
- });
78
-
79
- it("dedupes concurrent requests for the same namespace", async () => {
80
- const provider = createProvider();
81
- let resolveLoad!: () => void;
82
- const load = vi.fn(
83
- () =>
84
- new Promise<TestSchema["billing"]>((resolve) => {
85
- resolveLoad = () => resolve(billingNs);
86
- })
87
- );
88
-
89
- const options = {
90
- provider,
91
- resolveLoader: () => load,
92
- validate: () => ({ ok: true, data: billingNs }) as const,
93
- };
94
-
95
- const first = ensureNamespacesLoadedImpl(options, ["billing"]);
96
- const second = ensureNamespacesLoadedImpl(options, ["billing"]);
97
-
98
- resolveLoad();
99
- await Promise.all([first, second]);
100
-
101
- expect(load).toHaveBeenCalledTimes(1);
102
- expect(provider.hasNamespace("billing")).toBe(true);
103
- });
104
-
105
- it("propagates validation failures without marking the namespace loaded", async () => {
106
- const provider = createProvider();
107
- const failed: ValidationResult<TestSchema["billing"]> = {
108
- ok: false,
109
- issues: [{ kind: "invalid_input", message: "bad data" }],
110
- };
111
-
112
- await expect(
113
- ensureNamespacesLoadedImpl(
114
- {
115
- provider,
116
- resolveLoader: () => async () => ({ bad: true }),
117
- validate: () => failed,
118
- },
119
- ["billing"]
120
- )
121
- ).rejects.toThrow("bad data");
122
-
123
- expect(provider.hasNamespace("billing")).toBe(false);
124
- });
125
- });
@@ -1,70 +0,0 @@
1
- import type { MultiDictionary } from "./types.js";
2
- import type { ValidationResult } from "./validation/types.js";
3
- import { DictionaryValidationError } from "./validation/errors.js";
4
-
5
- export interface EnsureNamespacesLoadedOptions<
6
- Schema extends MultiDictionary,
7
- NS extends keyof Schema & string,
8
- > {
9
- provider: Pick<
10
- { hasNamespace(namespace: NS): boolean; setNamespace(namespace: NS, values: Schema[NS]): void },
11
- "hasNamespace" | "setNamespace"
12
- >;
13
- resolveLoader: (namespace: NS) => () => Promise<unknown>;
14
- validate: (namespace: NS, raw: unknown) => ValidationResult<Schema[NS]>;
15
- }
16
-
17
- const inFlightByProvider = new WeakMap<object, Map<string, Promise<void>>>();
18
-
19
- async function ensureOneNamespace<Schema extends MultiDictionary, NS extends keyof Schema & string>(
20
- options: EnsureNamespacesLoadedOptions<Schema, NS>,
21
- namespace: NS
22
- ): Promise<void> {
23
- if (options.provider.hasNamespace(namespace)) {
24
- return;
25
- }
26
-
27
- const providerKey = options.provider as object;
28
- let inFlight = inFlightByProvider.get(providerKey);
29
- if (!inFlight) {
30
- inFlight = new Map();
31
- inFlightByProvider.set(providerKey, inFlight);
32
- }
33
-
34
- const existing = inFlight.get(namespace);
35
- if (existing) {
36
- return existing;
37
- }
38
-
39
- const promise = (async () => {
40
- try {
41
- if (options.provider.hasNamespace(namespace)) {
42
- return;
43
- }
44
-
45
- const raw = await options.resolveLoader(namespace)();
46
- const result = options.validate(namespace, raw);
47
- if (!result.ok) {
48
- throw new DictionaryValidationError(result.issues);
49
- }
50
-
51
- options.provider.setNamespace(namespace, result.data);
52
- } finally {
53
- inFlight!.delete(namespace);
54
- }
55
- })();
56
-
57
- inFlight.set(namespace, promise);
58
- return promise;
59
- }
60
-
61
- export async function ensureNamespacesLoadedImpl<
62
- Schema extends MultiDictionary,
63
- NS extends keyof Schema & string,
64
- >(options: EnsureNamespacesLoadedOptions<Schema, NS>, namespaces: NS[]): Promise<void> {
65
- if (namespaces.length === 0) {
66
- return;
67
- }
68
-
69
- await Promise.all(namespaces.map((namespace) => ensureOneNamespace(options, namespace)));
70
- }
File without changes