@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,88 +0,0 @@
1
- export type LocaleFallbackMap = Record<string, string | null>;
2
-
3
- export interface ResolvedLocaleTemplate {
4
- template: string;
5
- resolvedLocale: string;
6
- fallbackChain: readonly string[];
7
- }
8
-
9
- export function validateLocaleFallback(map: LocaleFallbackMap): void {
10
- for (const startLocale of Object.keys(map)) {
11
- const visited = new Set<string>();
12
- let current: string | undefined = startLocale;
13
-
14
- while (current !== undefined) {
15
- if (visited.has(current)) {
16
- throw new Error(`[i18n] Circular locale fallback detected involving "${current}"`);
17
- }
18
- visited.add(current);
19
-
20
- const next: string | null | undefined = map[current];
21
- if (next === undefined || next === null) {
22
- break;
23
- }
24
- current = next;
25
- }
26
- }
27
- }
28
-
29
- export function resolveLocaleTemplate(
30
- localeByKey: Record<string, string | undefined> | undefined,
31
- requestedLocale: string,
32
- fallbackMap?: LocaleFallbackMap
33
- ): ResolvedLocaleTemplate | undefined {
34
- const chain: string[] = [requestedLocale];
35
- let currentLocale = requestedLocale;
36
-
37
- while (true) {
38
- const template = localeByKey?.[currentLocale];
39
- if (template !== undefined) {
40
- return { template, resolvedLocale: currentLocale, fallbackChain: chain };
41
- }
42
-
43
- if (!fallbackMap) {
44
- return undefined;
45
- }
46
-
47
- const fallback = fallbackMap[currentLocale];
48
- if (fallback === undefined || fallback === null) {
49
- return undefined;
50
- }
51
-
52
- if (chain.includes(fallback)) {
53
- throw new Error(
54
- `[i18n] Circular locale fallback detected: ${[...chain, fallback].join(" → ")}`
55
- );
56
- }
57
-
58
- chain.push(fallback);
59
- currentLocale = fallback;
60
- }
61
- }
62
-
63
- export function formatLocaleFallbackChain(
64
- requestedLocale: string,
65
- fallbackMap?: LocaleFallbackMap
66
- ): string {
67
- if (!fallbackMap) {
68
- return requestedLocale;
69
- }
70
-
71
- const chain: string[] = [requestedLocale];
72
- let current = requestedLocale;
73
-
74
- while (true) {
75
- const fallback = fallbackMap[current];
76
- if (fallback === undefined || fallback === null) {
77
- break;
78
- }
79
- if (chain.includes(fallback)) {
80
- chain.push(fallback);
81
- break;
82
- }
83
- chain.push(fallback);
84
- current = fallback;
85
- }
86
-
87
- return chain.join(" → ");
88
- }
@@ -1,105 +0,0 @@
1
- import type { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
2
- import type { LocaleFallbackMap, LocaleOfMulti, MultiDictionary } from "./types.js";
3
- import type { MultiParams, ParamArgs } from "./scope-types.js";
4
-
5
- /** Type-safe translation scope for a multi-namespace schema subset. */
6
- export interface I18nScopeMulti<
7
- Schema extends MultiDictionary,
8
- Params,
9
- RequestLocales extends string = LocaleOfMulti<Schema>,
10
- > {
11
- t<
12
- const NS extends keyof Schema & keyof Params & string,
13
- const K extends keyof Schema[NS] & keyof Params[NS] & string,
14
- >(
15
- namespace: NS,
16
- key: K,
17
- locale: RequestLocales,
18
- ...params: ParamArgs<Params[NS][K]>
19
- ): string;
20
- forLocale<Locale extends RequestLocales>(
21
- locale: Locale
22
- ): I18nScopeMultiForLocale<Schema, Params, RequestLocales, Locale>;
23
- }
24
-
25
- /** Multi-namespace scope with a bound locale — `t(namespace, key)` without a locale argument. */
26
- export interface I18nScopeMultiForLocale<
27
- Schema extends MultiDictionary,
28
- Params,
29
- RequestLocales extends string,
30
- Locale extends RequestLocales,
31
- > {
32
- readonly locale: Locale;
33
- t<
34
- const NS extends keyof Schema & keyof Params & string,
35
- const K extends keyof Schema[NS] & keyof Params[NS] & string,
36
- >(
37
- namespace: NS,
38
- key: K,
39
- ...params: ParamArgs<Params[NS][K]>
40
- ): string;
41
- forLocale<Next extends RequestLocales>(
42
- locale: Next
43
- ): I18nScopeMultiForLocale<Schema, Params, RequestLocales, Next>;
44
- }
45
-
46
- export class I18nScopeMultiImpl<
47
- Schema extends MultiDictionary,
48
- Params extends MultiParams<Schema>,
49
- RequestLocales extends string,
50
- Fallback extends LocaleFallbackMap | undefined,
51
- > implements I18nScopeMulti<Schema, Params, RequestLocales> {
52
- constructor(
53
- private readonly engine: IcuTranslationProviderMulti<Schema, Params, RequestLocales, Fallback>
54
- ) {}
55
-
56
- t = <
57
- const NS extends keyof Schema & keyof Params & string,
58
- const K extends keyof Schema[NS] & keyof Params[NS] & string,
59
- >(
60
- namespace: NS,
61
- key: K,
62
- locale: RequestLocales,
63
- ...args: ParamArgs<Params[NS][K]>
64
- ): string => {
65
- const params = args[0] as Record<string, unknown> | undefined;
66
- return this.engine.getWithLocale(String(namespace), String(key), locale, params);
67
- };
68
-
69
- forLocale = <Locale extends RequestLocales>(
70
- locale: Locale
71
- ): I18nScopeMultiForLocaleImpl<Schema, Params, RequestLocales, Locale, Fallback> => {
72
- return new I18nScopeMultiForLocaleImpl(this.engine, locale);
73
- };
74
- }
75
-
76
- export class I18nScopeMultiForLocaleImpl<
77
- Schema extends MultiDictionary,
78
- Params extends MultiParams<Schema>,
79
- RequestLocales extends string,
80
- Locale extends RequestLocales,
81
- Fallback extends LocaleFallbackMap | undefined,
82
- > implements I18nScopeMultiForLocale<Schema, Params, RequestLocales, Locale> {
83
- constructor(
84
- private readonly engine: IcuTranslationProviderMulti<Schema, Params, RequestLocales, Fallback>,
85
- readonly locale: Locale
86
- ) {}
87
-
88
- t = <
89
- const NS extends keyof Schema & keyof Params & string,
90
- const K extends keyof Schema[NS] & keyof Params[NS] & string,
91
- >(
92
- namespace: NS,
93
- key: K,
94
- ...args: ParamArgs<Params[NS][K]>
95
- ): string => {
96
- const params = args[0] as Record<string, unknown> | undefined;
97
- return this.engine.getWithLocale(String(namespace), String(key), this.locale, params);
98
- };
99
-
100
- forLocale = <Next extends RequestLocales>(
101
- locale: Next
102
- ): I18nScopeMultiForLocaleImpl<Schema, Params, RequestLocales, Next, Fallback> => {
103
- return new I18nScopeMultiForLocaleImpl(this.engine, locale);
104
- };
105
- }
@@ -1,35 +0,0 @@
1
- import type { MultiDictionary } from "./types.js";
2
-
3
- /** ICU argument map for a multi-namespace schema. */
4
- export type MultiParams<Schema extends MultiDictionary> = {
5
- [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown };
6
- };
7
-
8
- /** Restrict a multi schema to a subset of namespaces. */
9
- export type SchemaForNamespaces<
10
- Schema extends MultiDictionary,
11
- NsList extends readonly (keyof Schema & string)[],
12
- > = Pick<Schema, NsList[number]>;
13
-
14
- /** Restrict multi ICU params to a subset of namespaces. */
15
- export type ParamsForNamespaces<
16
- Schema extends MultiDictionary,
17
- Params extends MultiParams<Schema>,
18
- NsList extends readonly (keyof Schema & string)[],
19
- > = Pick<Params, NsList[number]>;
20
-
21
- /** Rest args for `t()` — `never` params omit the third argument. */
22
- export type ParamArgs<P> = [P] extends [never] ? [] : [params: P];
23
-
24
- /**
25
- * Namespace-bound `t(key, …)` — locale already bound.
26
- * Call sites omit the namespace argument.
27
- */
28
- export type NamespaceBoundTranslateFn<
29
- Schema extends MultiDictionary,
30
- Params extends MultiParams<Schema>,
31
- NS extends keyof Schema & keyof Params & string,
32
- > = <const K extends keyof Schema[NS] & keyof Params[NS] & string>(
33
- key: K,
34
- ...params: ParamArgs<Params[NS][K]>
35
- ) => string;
package/src/scope.test.ts DELETED
@@ -1,172 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
3
- import type { LocaleOfMulti } from "./types.js";
4
-
5
- type MultiSchema = {
6
- default: {
7
- login_button: { en: string; it: string };
8
- welcome: { en: string };
9
- dashboard_status: { en: string; it: string };
10
- inbox_owner: { en: string; it: string };
11
- ranking_position: { en: string; it: string };
12
- };
13
- billing: {
14
- invoice_summary: { en: string; it: string };
15
- account_balance: { en: string; it: string };
16
- appointment_summary: { en: string; it: string };
17
- empty_label: { en: string };
18
- };
19
- };
20
-
21
- type MultiParams = {
22
- default: {
23
- login_button: never;
24
- welcome: { name: string };
25
- dashboard_status: { msgCount: number; chatCount: number };
26
- inbox_owner: { gender: string; name: string };
27
- ranking_position: { position: number };
28
- };
29
- billing: {
30
- invoice_summary: { count: number; name: string };
31
- account_balance: { amount: number };
32
- appointment_summary: { dueDate: Date | number; startTime: Date | number };
33
- empty_label: never;
34
- };
35
- };
36
-
37
- const multiDictionary: MultiSchema = {
38
- default: {
39
- login_button: { en: "Login", it: "Accedi" },
40
- welcome: { en: "Welcome {name}!" },
41
- dashboard_status: {
42
- en: "You have {msgCount, plural, one {1 message} other {{msgCount} messages}} in {chatCount, plural, one {one chat} other {{chatCount} chats}}",
43
- it: "Hai {msgCount, plural, one {1 messaggio} other {{msgCount} messaggi}} in {chatCount, plural, one {una chat} other {{chatCount} chat}}",
44
- },
45
- inbox_owner: {
46
- en: "{gender, select, female {{name} owns her inbox} male {{name} owns his inbox} other {{name} owns their inbox}}",
47
- it: "{gender, select, female {{name} gestisce la sua casella} male {{name} gestisce la sua casella} other {{name} gestisce la propria casella}}",
48
- },
49
- ranking_position: {
50
- en: "You finished {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}",
51
- it: "Hai concluso al {position, selectordinal, one {#°} other {#°}} posto",
52
- },
53
- },
54
- billing: {
55
- invoice_summary: {
56
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
57
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
58
- },
59
- account_balance: {
60
- en: "Balance: {amount, number, ::currency/EUR}",
61
- it: "Saldo: {amount, number, ::currency/EUR}",
62
- },
63
- appointment_summary: {
64
- en: "Due {dueDate, date, short} at {startTime, time, short}",
65
- it: "Scade il {dueDate, date, short} alle {startTime, time, short}",
66
- },
67
- empty_label: { en: "" },
68
- },
69
- };
70
-
71
- describe("I18nScope multi", () => {
72
- const engine = new IcuTranslationProviderMulti<MultiSchema, MultiParams>(multiDictionary);
73
-
74
- it("translates with t(namespace, key, locale)", () => {
75
- const view = engine.toScope({ namespaces: ["default", "billing"] });
76
-
77
- expect(view.t("default", "login_button", "it")).toBe("Accedi");
78
- expect(view.t("default", "welcome", "en", { name: "Ada" })).toBe("Welcome Ada!");
79
- expect(view.t("billing", "invoice_summary", "en", { count: 3, name: "Ada" })).toBe(
80
- "You have 3 invoices for Ada"
81
- );
82
- });
83
-
84
- it("restricts namespaces at the type level", () => {
85
- const billingOnly = engine.toScope({ namespaces: ["billing"] });
86
-
87
- expect(billingOnly.t("billing", "invoice_summary", "en", { count: 1, name: "Ada" })).toBe(
88
- "You have 1 invoice for Ada"
89
- );
90
- });
91
-
92
- it("binds locale via forLocale", () => {
93
- const view = engine.toScope({ namespaces: ["default", "billing"] });
94
- const it = view.forLocale("it");
95
-
96
- expect(it.locale).toBe("it");
97
- expect(it.t("default", "login_button")).toBe("Accedi");
98
- expect(it.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
99
- "Hai 2 fatture per Ada"
100
- );
101
- });
102
-
103
- it("binds locale via toScope({ namespaces, locale })", () => {
104
- const it = engine.toScope({ namespaces: ["billing"], locale: "it" });
105
- expect(it.locale).toBe("it");
106
- expect(it.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
107
- "Hai 2 fatture per Ada"
108
- );
109
- });
110
-
111
- it("supports destructuring t from locale-bound scopes", () => {
112
- const { t } = engine.toScope({ namespaces: ["billing"] }).forLocale("en");
113
-
114
- expect(t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
115
- "You have 1 invoice for Ada"
116
- );
117
- });
118
-
119
- it("reflects partial applyLoadMergeNamespace via a fresh toScope", () => {
120
- const partial = new IcuTranslationProviderMulti<MultiSchema, MultiParams>({
121
- billing: {
122
- invoice_summary: {
123
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
124
- },
125
- },
126
- });
127
-
128
- partial.applyLoadMergeNamespace("billing", {
129
- invoice_summary: {
130
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
131
- },
132
- });
133
-
134
- const view = partial.toScope({ namespaces: ["billing"], locale: "it" });
135
- expect(view.t("billing", "invoice_summary", { count: 2, name: "Bob" })).toBe(
136
- "Hai 2 fatture per Bob"
137
- );
138
- });
139
-
140
- it("degrades to onMissing for namespaces not in dictionary", () => {
141
- const partial = new IcuTranslationProviderMulti<MultiSchema, MultiParams>(
142
- { default: multiDictionary.default },
143
- { onMissing: "key" }
144
- );
145
- const view = partial.toScope({ namespaces: ["billing"], locale: "en" });
146
-
147
- expect(view.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
148
- "billing.invoice_summary"
149
- );
150
- });
151
-
152
- describe("locale fallback", () => {
153
- const fallbackMap = {
154
- en: null,
155
- "de-DE": "en",
156
- "de-CH": "de-DE",
157
- it: "en",
158
- } as const;
159
-
160
- const fallbackEngine = new IcuTranslationProviderMulti<
161
- MultiSchema,
162
- MultiParams,
163
- keyof typeof fallbackMap | LocaleOfMulti<MultiSchema>,
164
- typeof fallbackMap
165
- >(multiDictionary, { localeFallback: fallbackMap });
166
-
167
- it("resolves a locale through the fallback chain", () => {
168
- const view = fallbackEngine.toScope({ namespaces: ["default"] });
169
- expect(view.t("default", "login_button", "de-CH")).toBe("Login");
170
- });
171
- });
172
- });
@@ -1,114 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { createI18nHandle } from "./builder.js";
3
- import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
4
- import { normalizeI18nCreateInput } from "./serialized-state.js";
5
-
6
- type MultiSchema = {
7
- default: { greeting: { en: string; it: string } };
8
- billing: { invoice: { en: string; it: string } };
9
- };
10
- type MultiParams = {
11
- default: { greeting: never };
12
- billing: { invoice: never };
13
- };
14
-
15
- describe("serialize / createI18n({ state })", () => {
16
- it("normalizeI18nCreateInput defaults missing state to empty dictionary and [] resources", () => {
17
- expect(normalizeI18nCreateInput()).toEqual({
18
- dictionary: {},
19
- resources: [],
20
- });
21
- });
22
-
23
- it("normalizeI18nCreateInput defaults missing resources to []", () => {
24
- expect(normalizeI18nCreateInput({ dictionary: { greeting: { en: "Hi" } } })).toEqual({
25
- dictionary: { greeting: { en: "Hi" } },
26
- resources: [],
27
- });
28
- });
29
-
30
- it("serialize after load → fresh handle skips loaders for marked resources", async () => {
31
- const defaultLoader = vi.fn(async (locale: string) => ({
32
- greeting: { [locale]: locale === "it" ? "Ciao" : "Hello" },
33
- }));
34
- const billingLoader = vi.fn(async (locale: string) => ({
35
- invoice: { [locale]: locale === "it" ? "Fattura" : "Invoice" },
36
- }));
37
-
38
- const ssrEngine = new IcuTranslationProviderMulti<MultiSchema, MultiParams>({});
39
- const ssr = createI18nHandle(ssrEngine, {
40
- namespaceLoaders: {
41
- default: defaultLoader,
42
- billing: billingLoader,
43
- },
44
- });
45
-
46
- await ssr.load({ namespaces: ["default", "billing"], locale: "it" });
47
- expect(defaultLoader).toHaveBeenCalledTimes(1);
48
- expect(billingLoader).toHaveBeenCalledTimes(1);
49
-
50
- const serialized = ssr.serialize();
51
- expect(serialized.resources).toEqual(
52
- expect.arrayContaining([
53
- ["default", "it"],
54
- ["billing", "it"],
55
- ])
56
- );
57
- expect(serialized.dictionary.default?.greeting?.it).toBe("Ciao");
58
-
59
- const csrEngine = new IcuTranslationProviderMulti<MultiSchema, MultiParams>(
60
- serialized.dictionary
61
- );
62
- csrEngine.seedBuilderResources(serialized.resources);
63
- const csr = createI18nHandle(csrEngine, {
64
- namespaceLoaders: {
65
- default: defaultLoader,
66
- billing: billingLoader,
67
- },
68
- });
69
-
70
- expect(csrEngine.hasBuilderResourceLoaded("default", "it")).toBe(true);
71
- expect(csrEngine.hasBuilderResourceLoaded("billing", "it")).toBe(true);
72
-
73
- await csr.load({ namespaces: ["default", "billing"], locale: "it" });
74
- expect(defaultLoader).toHaveBeenCalledTimes(1);
75
- expect(billingLoader).toHaveBeenCalledTimes(1);
76
-
77
- const scope = csr.peek({ namespaces: ["default", "billing"], locale: "it" });
78
- expect(scope).not.toBeNull();
79
- expect(scope!.t("default", "greeting")).toBe("Ciao");
80
- expect(scope!.t("billing", "invoice")).toBe("Fattura");
81
- });
82
-
83
- it("peek returns scope after hydrate seed without invoking loaders", async () => {
84
- const defaultLoader = vi.fn(async (locale: string) => ({
85
- greeting: { [locale]: "Hello" },
86
- }));
87
- const billingLoader = vi.fn(async (locale: string) => ({
88
- invoice: { [locale]: "Invoice" },
89
- }));
90
-
91
- const ssrEngine = new IcuTranslationProviderMulti<MultiSchema, MultiParams>({});
92
- const ssr = createI18nHandle(ssrEngine, {
93
- namespaceLoaders: { default: defaultLoader, billing: billingLoader },
94
- });
95
- await ssr.load({ namespaces: ["default", "billing"], locale: "en" });
96
- const serialized = ssr.serialize();
97
-
98
- const csrEngine = new IcuTranslationProviderMulti<MultiSchema, MultiParams>(
99
- serialized.dictionary
100
- );
101
- csrEngine.seedBuilderResources(serialized.resources);
102
- const csr = createI18nHandle(csrEngine, {
103
- namespaceLoaders: { default: defaultLoader, billing: billingLoader },
104
- });
105
-
106
- const sync = csr.peek({ namespaces: ["default", "billing"], locale: "en" });
107
- expect(sync).not.toBeNull();
108
- expect(sync!.t("default", "greeting")).toBe("Hello");
109
- expect(defaultLoader).toHaveBeenCalledTimes(1);
110
- expect(billingLoader).toHaveBeenCalledTimes(1);
111
-
112
- expect(csr.peek({ namespaces: ["default"], locale: "it" })).toBeNull();
113
- });
114
- });
@@ -1,28 +0,0 @@
1
- import type { BuilderResourceEntry } from "./builder-load-registry.js";
2
-
3
- /**
4
- * Portable i18n instance state after `serialize()`: dictionary strings + which
5
- * builder resources are already loaded (namespace × partition).
6
- */
7
- export interface I18nSerializedState<Dictionary = Record<string, unknown>> {
8
- dictionary: Dictionary;
9
- resources: readonly BuilderResourceEntry[];
10
- }
11
-
12
- /**
13
- * Input nested under generated `createI18n({ state? })`. Omitted `state` defaults to an empty
14
- * dictionary (lazy cold start). `resources` may be omitted or `[]`.
15
- */
16
- export type I18nCreateInput<Dictionary = Record<string, unknown>> = {
17
- dictionary: Dictionary;
18
- resources?: readonly BuilderResourceEntry[];
19
- };
20
-
21
- export function normalizeI18nCreateInput<Dictionary>(
22
- value?: I18nCreateInput<Dictionary>
23
- ): I18nSerializedState<Dictionary> {
24
- return {
25
- dictionary: value?.dictionary ?? ({} as Dictionary),
26
- resources: value?.resources ?? [],
27
- };
28
- }
@@ -1,85 +0,0 @@
1
- import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, expect, it, afterEach } from "vitest";
5
- import { buildCodegenConfig, parseSetupArgs, runSetup } from "./setup-i18n.js";
6
- import { inferProjectName, typeNamesForProject } from "../codegen-config/type-names.js";
7
-
8
- describe("type-names", () => {
9
- it("derives PascalCase project names from directory names", () => {
10
- expect(inferProjectName("myapp")).toBe("Myapp");
11
- expect(inferProjectName("my-app")).toBe("MyApp");
12
- expect(inferProjectName("apps")).toBe("Apps");
13
- });
14
-
15
- it("builds type names from project prefix", () => {
16
- expect(typeNamesForProject("MyApp")).toEqual({
17
- paramsTypeName: "MyAppParams",
18
- schemaTypeName: "MyAppSchema",
19
- localeTypeName: "MyAppLocale",
20
- });
21
- });
22
- });
23
-
24
- describe("setup-i18n", () => {
25
- let tempDir: string;
26
-
27
- afterEach(() => {
28
- if (tempDir) {
29
- rmSync(tempDir, { recursive: true, force: true });
30
- }
31
- });
32
-
33
- it("accepts multi alias or positional targetDir", () => {
34
- expect(parseSetupArgs(["multi", "apps/myapp", "--project", "MyApp", "--force"])).toEqual({
35
- targetDir: "apps/myapp",
36
- project: "MyApp",
37
- force: true,
38
- });
39
- expect(parseSetupArgs([".", "--project", "MyApp"])).toEqual({
40
- targetDir: ".",
41
- project: "MyApp",
42
- force: false,
43
- });
44
- });
45
-
46
- it("scaffolds multi mode with projectName and codegenPath", () => {
47
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-setup-"));
48
- const result = runSetup({ targetDir: tempDir, project: "MyApp" });
49
-
50
- const config = JSON.parse(readFileSync(join(tempDir, "i18n/i18n.codegen.json"), "utf8"));
51
- expect(config.namespaces).toEqual({
52
- default: "translations/default.json",
53
- });
54
- expect(config.projectName).toBe("MyApp");
55
- expect(config.codegenPath).toBe("generated");
56
- expect(config).not.toHaveProperty("paramsTypeName");
57
- expect(existsSync(join(tempDir, "i18n/translations/default.json"))).toBe(true);
58
- expect(existsSync(join(tempDir, "i18n/index.ts"))).toBe(true);
59
- expect(existsSync(join(tempDir, "src"))).toBe(false);
60
- expect(result.created).toContain("i18n/i18n.codegen.json");
61
- });
62
-
63
- it("scaffolds under src/i18n when targetDir is src", () => {
64
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-setup-"));
65
- const srcDir = join(tempDir, "src");
66
- runSetup({ targetDir: srcDir, project: "MyApp" });
67
-
68
- expect(existsSync(join(srcDir, "i18n/i18n.codegen.json"))).toBe(true);
69
- expect(existsSync(join(tempDir, "i18n"))).toBe(false);
70
- });
71
-
72
- it("refuses to overwrite an existing config without --force", () => {
73
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-setup-"));
74
- runSetup({ targetDir: tempDir, project: "MyApp" });
75
-
76
- expect(() => runSetup({ targetDir: tempDir, project: "MyApp" })).toThrow("already exists");
77
- });
78
-
79
- it("buildCodegenConfig defaults to split-by-locale", () => {
80
- const config = buildCodegenConfig("MyApp");
81
- expect(config.delivery).toBe("split-by-locale");
82
- expect(config.codegenPath).toBe("generated");
83
- expect(config.projectName).toBe("MyApp");
84
- });
85
- });