@xndrjs/i18n 0.6.1 → 0.7.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 (51) hide show
  1. package/README.md +154 -141
  2. package/dist/codegen/index.js.map +1 -1
  3. package/dist/index.d.ts +268 -69
  4. package/dist/index.js +572 -65
  5. package/dist/index.js.map +1 -1
  6. package/dist/validation/index.d.ts +23 -7
  7. package/dist/validation/index.js +196 -89
  8. package/dist/validation/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/IcuTranslationProviderMulti.test.ts +80 -234
  11. package/src/IcuTranslationProviderMulti.ts +101 -112
  12. package/src/IcuTranslationProviderSingle.test.ts +30 -309
  13. package/src/IcuTranslationProviderSingle.ts +42 -75
  14. package/src/builder-load-registry.ts +18 -0
  15. package/src/builder-loaders.ts +18 -0
  16. package/src/builder-multi.ts +481 -0
  17. package/src/builder-types.test.ts +80 -0
  18. package/src/builder-types.ts +24 -0
  19. package/src/builder.test.ts +421 -0
  20. package/src/builder.ts +112 -0
  21. package/src/codegen/delivery-artifacts.test.ts +2 -1
  22. package/src/codegen/delivery-artifacts.ts +2 -1
  23. package/src/codegen/emit/dictionary-file.test.ts +0 -7
  24. package/src/codegen/emit/dictionary-schema-file.ts +55 -42
  25. package/src/codegen/emit/instance-file.test.ts +88 -0
  26. package/src/codegen/emit/instance-file.ts +164 -17
  27. package/src/codegen/emit/namespace-loaders-file.test.ts +4 -18
  28. package/src/codegen/emit/namespace-loaders-file.ts +4 -54
  29. package/src/codegen/emit/types-file.test.ts +0 -2
  30. package/src/codegen/generate-i18n-types.test.ts +8 -303
  31. package/src/codegen/generate-i18n-types.ts +16 -1
  32. package/src/codegen/project-locales-set-namespace.test.ts +25 -32
  33. package/src/engine.ts +71 -0
  34. package/src/index.ts +45 -7
  35. package/src/patch-key.test.ts +186 -0
  36. package/src/patch-key.ts +140 -0
  37. package/src/project-locales.test.ts +16 -6
  38. package/src/project-locales.ts +17 -6
  39. package/src/scope-multi.ts +124 -0
  40. package/src/scope-single.ts +85 -0
  41. package/src/scope-types.ts +27 -0
  42. package/src/scope.test.ts +481 -0
  43. package/src/single-builder.ts +153 -0
  44. package/src/types.ts +16 -0
  45. package/src/validation/create-normalized-schema.ts +1 -1
  46. package/src/validation/errors.ts +2 -0
  47. package/src/validation/index.ts +135 -27
  48. package/src/validation/normalize.ts +176 -46
  49. package/src/validation/types.ts +4 -0
  50. package/src/validation/validate-normalized.ts +43 -1
  51. package/src/validation/validation.test.ts +200 -7
package/src/engine.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
2
+ import type { IcuTranslationProviderSingle } from "./IcuTranslationProviderSingle.js";
3
+ import type {
4
+ KeyDictionary,
5
+ LocaleFallbackMap,
6
+ LocaleOfMulti,
7
+ LocaleOfSingle,
8
+ MultiDictionary,
9
+ } from "./types.js";
10
+ import type { I18nScopeMulti, I18nScopeMultiForLocale } from "./scope-multi.js";
11
+ import type { I18nScopeSingle, I18nScopeSingleForLocale } from "./scope-single.js";
12
+ import type { MultiParams, ParamsForNamespaces, SchemaForNamespaces } from "./scope-types.js";
13
+
14
+ /** Mutable translation engine for a single-namespace dictionary. */
15
+ export interface I18nEngineSingle<
16
+ Schema extends KeyDictionary,
17
+ Params extends { [K in keyof Schema]: unknown },
18
+ RequestLocales extends string = LocaleOfSingle<Schema>,
19
+ > {
20
+ readonly __i18nEngineMode: "single";
21
+ getAll(): Schema;
22
+ toScope(): I18nScopeSingle<Schema, Params, RequestLocales>;
23
+ toScope<Locale extends RequestLocales>(options: {
24
+ locale: Locale;
25
+ }): I18nScopeSingleForLocale<Schema, Params, Locale>;
26
+ }
27
+
28
+ /** Mutable translation engine for a multi-namespace dictionary. */
29
+ export interface I18nEngineMulti<
30
+ Schema extends MultiDictionary,
31
+ Params extends MultiParams<Schema>,
32
+ RequestLocales extends string = LocaleOfMulti<Schema>,
33
+ > {
34
+ readonly __i18nEngineMode: "multi";
35
+ getAll(): Schema;
36
+ toScope<NsList extends readonly (keyof Schema & string)[]>(options: {
37
+ namespaces: NsList;
38
+ }): I18nScopeMulti<
39
+ SchemaForNamespaces<Schema, NsList>,
40
+ ParamsForNamespaces<Schema, Params, NsList>,
41
+ RequestLocales
42
+ >;
43
+ toScope<
44
+ NsList extends readonly (keyof Schema & string)[],
45
+ Locale extends RequestLocales,
46
+ >(options: {
47
+ namespaces: NsList;
48
+ locale: Locale;
49
+ }): I18nScopeMultiForLocale<
50
+ SchemaForNamespaces<Schema, NsList>,
51
+ ParamsForNamespaces<Schema, Params, NsList>,
52
+ RequestLocales,
53
+ Locale
54
+ >;
55
+ }
56
+
57
+ /** Alias for the default single-namespace engine implementation. */
58
+ export type I18nEngineSingleImpl<
59
+ Schema extends KeyDictionary,
60
+ Params extends { [K in keyof Schema]: unknown },
61
+ RequestLocales extends string = LocaleOfSingle<Schema>,
62
+ Fallback extends LocaleFallbackMap | undefined = undefined,
63
+ > = IcuTranslationProviderSingle<Schema, Params, RequestLocales, Fallback>;
64
+
65
+ /** Alias for the default multi-namespace engine implementation. */
66
+ export type I18nEngineMultiImpl<
67
+ Schema extends MultiDictionary,
68
+ Params extends MultiParams<Schema>,
69
+ RequestLocales extends string = LocaleOfMulti<Schema>,
70
+ Fallback extends LocaleFallbackMap | undefined = undefined,
71
+ > = IcuTranslationProviderMulti<Schema, Params, RequestLocales, Fallback>;
package/src/index.ts CHANGED
@@ -1,15 +1,51 @@
1
1
  export type {
2
- TranslationProviderSingle,
3
- TranslationProviderSingleForLocale,
4
- } from "./IcuTranslationProviderSingle.js";
5
- export { IcuTranslationProviderSingle } from "./IcuTranslationProviderSingle.js";
2
+ DeliveryArtifactsMap,
3
+ LocalesForDeliveryArea,
4
+ LocalesForDeliveryAreaOrAll,
5
+ } from "./builder-types.js";
6
+
7
+ export type {
8
+ I18nEngineMulti,
9
+ I18nEngineMultiImpl,
10
+ I18nEngineSingle,
11
+ I18nEngineSingleImpl,
12
+ } from "./engine.js";
6
13
 
7
14
  export type {
8
- TranslationProviderMulti,
9
- TranslationProviderMultiForLocale,
10
- } from "./IcuTranslationProviderMulti.js";
15
+ CanonicalLoader,
16
+ I18nBuilderMulti,
17
+ I18nBuilderMultiForLocale,
18
+ I18nBuilderMultiInitial,
19
+ I18nBuilderMultiOptions,
20
+ I18nBuilderMultiPartitioned,
21
+ I18nBuilderMultiReady,
22
+ I18nBuilderSingle,
23
+ I18nBuilderSingleForLocale,
24
+ I18nBuilderSingleOptions,
25
+ NamespaceLoader,
26
+ PartitionedLoader,
27
+ } from "./builder.js";
28
+ export {
29
+ createI18nBuilder,
30
+ createI18nMultiBuilder,
31
+ createI18nSingleBuilder,
32
+ invokeNamespaceLoader,
33
+ } from "./builder.js";
34
+
35
+ export { IcuTranslationProviderSingle } from "./IcuTranslationProviderSingle.js";
36
+
11
37
  export { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
12
38
 
39
+ export type { I18nScopeMulti, I18nScopeMultiForLocale } from "./scope-multi.js";
40
+ export type { I18nScopeSingle, I18nScopeSingleForLocale } from "./scope-single.js";
41
+
42
+ export type {
43
+ MultiParams,
44
+ ParamsForNamespaces,
45
+ SchemaForNamespaces,
46
+ SingleParams,
47
+ } from "./scope-types.js";
48
+
13
49
  export type {
14
50
  IcuTranslationProviderOptions,
15
51
  KeyDictionary,
@@ -19,6 +55,8 @@ export type {
19
55
  LocaleOfSingle,
20
56
  MissingTranslationContext,
21
57
  MultiDictionary,
58
+ PartialKeyDictionary,
59
+ PartialMultiDictionary,
22
60
  OnMissingTranslation,
23
61
  } from "./types.js";
24
62
 
@@ -0,0 +1,186 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ assertPatchKeyMulti,
4
+ assertPatchKeySingle,
5
+ preloadKeyMulti,
6
+ preloadKeySingle,
7
+ recordPreloadedKeysMulti,
8
+ recordPreloadedKeysSingle,
9
+ seedPreloadedKeysMulti,
10
+ seedPreloadedKeysSingle,
11
+ } from "./patch-key.js";
12
+
13
+ describe("preload key helpers", () => {
14
+ it("formats single preload keys as key:locale", () => {
15
+ expect(preloadKeySingle("welcome", "en")).toBe("welcome:en");
16
+ });
17
+
18
+ it("formats multi preload keys as namespace:key:locale", () => {
19
+ expect(preloadKeyMulti("billing", "invoice_summary", "it")).toBe("billing:invoice_summary:it");
20
+ });
21
+ });
22
+
23
+ describe("seedPreloadedKeysSingle", () => {
24
+ it("seeds every key/locale pair from the initial dictionary", () => {
25
+ const preloadedKeys = new Set<string>();
26
+ seedPreloadedKeysSingle(
27
+ {
28
+ login_button: { en: "Login", it: "Accedi" },
29
+ welcome: { en: "Welcome {name}!" },
30
+ },
31
+ preloadedKeys
32
+ );
33
+
34
+ expect(preloadedKeys).toEqual(new Set(["login_button:en", "login_button:it", "welcome:en"]));
35
+ });
36
+ });
37
+
38
+ describe("seedPreloadedKeysMulti", () => {
39
+ it("seeds every namespace/key/locale triple from the initial dictionary", () => {
40
+ const preloadedKeys = new Set<string>();
41
+ seedPreloadedKeysMulti(
42
+ {
43
+ default: { login_button: { en: "Login" } },
44
+ billing: { invoice_summary: { en: "One", it: "Uno" } },
45
+ },
46
+ preloadedKeys
47
+ );
48
+
49
+ expect(preloadedKeys).toEqual(
50
+ new Set([
51
+ "default:login_button:en",
52
+ "billing:invoice_summary:en",
53
+ "billing:invoice_summary:it",
54
+ ])
55
+ );
56
+ });
57
+ });
58
+
59
+ describe("recordPreloadedKeysSingle", () => {
60
+ it("records locales from a load merge payload", () => {
61
+ const preloadedKeys = new Set<string>();
62
+ recordPreloadedKeysSingle(
63
+ {
64
+ welcome: { it: "Benvenuto {name}!" },
65
+ },
66
+ preloadedKeys
67
+ );
68
+
69
+ expect(preloadedKeys).toEqual(new Set(["welcome:it"]));
70
+ });
71
+ });
72
+
73
+ describe("recordPreloadedKeysMulti", () => {
74
+ it("records locales from a namespace load merge payload", () => {
75
+ const preloadedKeys = new Set<string>();
76
+ recordPreloadedKeysMulti(
77
+ "billing",
78
+ {
79
+ invoice_summary: { it: "Hai {count} fatture" },
80
+ },
81
+ preloadedKeys
82
+ );
83
+
84
+ expect(preloadedKeys).toEqual(new Set(["billing:invoice_summary:it"]));
85
+ });
86
+ });
87
+
88
+ describe("assertPatchKeySingle", () => {
89
+ const preloadedKeys = new Set(["welcome:en", "welcome:it", "invoice_count:en"]);
90
+
91
+ it("accepts a valid patch for a preloaded key", () => {
92
+ expect(() =>
93
+ assertPatchKeySingle("welcome", "en", "Hello {name}!", preloadedKeys, {
94
+ en: "Welcome {name}!",
95
+ it: "Benvenuto {name}!",
96
+ })
97
+ ).not.toThrow();
98
+ });
99
+
100
+ it("rejects a key that was not preloaded", () => {
101
+ expect(() =>
102
+ assertPatchKeySingle("welcome", "fr", "Bonjour {name}!", preloadedKeys, {
103
+ en: "Welcome {name}!",
104
+ })
105
+ ).toThrow("[i18n] Key not preloaded: welcome (fr)");
106
+ });
107
+
108
+ it("rejects invalid ICU syntax", () => {
109
+ expect(() =>
110
+ assertPatchKeySingle("welcome", "en", "Hi {name", preloadedKeys, {
111
+ en: "Welcome {name}!",
112
+ })
113
+ ).toThrow("[i18n] ICU syntax error on patch:");
114
+ });
115
+
116
+ it("rejects incompatible ICU args against other locales for the same key", () => {
117
+ expect(() =>
118
+ assertPatchKeySingle(
119
+ "invoice_count",
120
+ "en",
121
+ "{count, select, other {{count}}}",
122
+ preloadedKeys,
123
+ {
124
+ en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
125
+ it: "Hai {count, plural, one {1 fattura} other {{count} fatture}}",
126
+ }
127
+ )
128
+ ).toThrow("[i18n] ICU args mismatch on patch:");
129
+ });
130
+ });
131
+
132
+ describe("assertPatchKeyMulti", () => {
133
+ const preloadedKeys = new Set([
134
+ "billing:invoice_summary:en",
135
+ "billing:invoice_summary:it",
136
+ "default:login_button:en",
137
+ ]);
138
+
139
+ it("accepts a valid patch for a preloaded namespace key", () => {
140
+ expect(() =>
141
+ assertPatchKeyMulti(
142
+ "billing",
143
+ "invoice_summary",
144
+ "en",
145
+ "You have {count, plural, one {1 bill} other {{count} bills}} for {name}",
146
+ preloadedKeys,
147
+ {
148
+ en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
149
+ it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
150
+ }
151
+ )
152
+ ).not.toThrow();
153
+ });
154
+
155
+ it("rejects a namespace key that was not preloaded", () => {
156
+ expect(() =>
157
+ assertPatchKeyMulti("billing", "invoice_summary", "fr", "Facture {count}", preloadedKeys, {
158
+ en: "You have {count} invoices",
159
+ })
160
+ ).toThrow("[i18n] Key not preloaded: billing.invoice_summary (fr)");
161
+ });
162
+
163
+ it("rejects invalid ICU syntax", () => {
164
+ expect(() =>
165
+ assertPatchKeyMulti("default", "login_button", "en", "Sign {in", preloadedKeys, {
166
+ en: "Login",
167
+ })
168
+ ).toThrow("[i18n] ICU syntax error on patch:");
169
+ });
170
+
171
+ it("rejects incompatible ICU args against other locales for the same key", () => {
172
+ expect(() =>
173
+ assertPatchKeyMulti(
174
+ "billing",
175
+ "invoice_summary",
176
+ "en",
177
+ "{count, select, other {{count}}}",
178
+ preloadedKeys,
179
+ {
180
+ en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
181
+ it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
182
+ }
183
+ )
184
+ ).toThrow("[i18n] ICU args mismatch on patch:");
185
+ });
186
+ });
@@ -0,0 +1,140 @@
1
+ import { mergeVariableMetaAcrossLocales, type VariableMetaSpec } from "./icu/extract-variables.js";
2
+ import { parseTemplate } from "./icu/parse-template.js";
3
+ import type { KeyDictionary, PartialKeyDictionary } from "./types.js";
4
+
5
+ export function preloadKeySingle(key: string, locale: string): string {
6
+ return `${key}:${locale}`;
7
+ }
8
+
9
+ export function preloadKeyMulti(namespace: string, key: string, locale: string): string {
10
+ return `${namespace}:${key}:${locale}`;
11
+ }
12
+
13
+ export function seedPreloadedKeysSingle(
14
+ dictionary: KeyDictionary,
15
+ preloadedKeys: Set<string>
16
+ ): void {
17
+ for (const [key, localeByKey] of Object.entries(dictionary)) {
18
+ if (localeByKey === undefined || typeof localeByKey !== "object") {
19
+ continue;
20
+ }
21
+ for (const locale of Object.keys(localeByKey)) {
22
+ preloadedKeys.add(preloadKeySingle(key, locale));
23
+ }
24
+ }
25
+ }
26
+
27
+ export function seedPreloadedKeysMulti(
28
+ dictionary: Record<string, KeyDictionary>,
29
+ preloadedKeys: Set<string>
30
+ ): void {
31
+ for (const [namespace, keyDictionary] of Object.entries(dictionary)) {
32
+ if (keyDictionary === undefined || typeof keyDictionary !== "object") {
33
+ continue;
34
+ }
35
+ for (const [key, localeByKey] of Object.entries(keyDictionary)) {
36
+ if (localeByKey === undefined || typeof localeByKey !== "object") {
37
+ continue;
38
+ }
39
+ for (const locale of Object.keys(localeByKey)) {
40
+ preloadedKeys.add(preloadKeyMulti(namespace, key, locale));
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ export function recordPreloadedKeysSingle(
47
+ partial: PartialKeyDictionary<KeyDictionary, string>,
48
+ preloadedKeys: Set<string>
49
+ ): void {
50
+ for (const [key, incomingLocales] of Object.entries(partial)) {
51
+ if (incomingLocales === undefined || typeof incomingLocales !== "object") {
52
+ continue;
53
+ }
54
+ for (const locale of Object.keys(incomingLocales)) {
55
+ preloadedKeys.add(preloadKeySingle(key, locale));
56
+ }
57
+ }
58
+ }
59
+
60
+ export function recordPreloadedKeysMulti(
61
+ namespace: string,
62
+ partial: PartialKeyDictionary<KeyDictionary, string>,
63
+ preloadedKeys: Set<string>
64
+ ): void {
65
+ for (const [key, incomingLocales] of Object.entries(partial)) {
66
+ if (incomingLocales === undefined || typeof incomingLocales !== "object") {
67
+ continue;
68
+ }
69
+ for (const locale of Object.keys(incomingLocales)) {
70
+ preloadedKeys.add(preloadKeyMulti(namespace, key, locale));
71
+ }
72
+ }
73
+ }
74
+
75
+ function collectLocaleMetasForKey(
76
+ localeByKey: Record<string, string>,
77
+ patchedLocale: string,
78
+ patchedMeta: VariableMetaSpec
79
+ ): VariableMetaSpec[] {
80
+ const localeMetas: VariableMetaSpec[] = [patchedMeta];
81
+
82
+ for (const [locale, template] of Object.entries(localeByKey)) {
83
+ if (locale === patchedLocale) {
84
+ continue;
85
+ }
86
+ const parsed = parseTemplate(template);
87
+ if (parsed.ok) {
88
+ localeMetas.push(parsed.meta);
89
+ }
90
+ }
91
+
92
+ return localeMetas;
93
+ }
94
+
95
+ export function assertPatchKeySingle(
96
+ key: string,
97
+ locale: string,
98
+ template: string,
99
+ preloadedKeys: Set<string>,
100
+ localeByKey: Record<string, string> | undefined
101
+ ): void {
102
+ if (!preloadedKeys.has(preloadKeySingle(key, locale))) {
103
+ throw new Error(`[i18n] Key not preloaded: ${key} (${locale})`);
104
+ }
105
+
106
+ const parsed = parseTemplate(template);
107
+ if (!parsed.ok) {
108
+ throw new Error(`[i18n] ICU syntax error on patch: ${parsed.message}`);
109
+ }
110
+
111
+ const localeMetas = collectLocaleMetasForKey(localeByKey ?? {}, locale, parsed.meta);
112
+ const merged = mergeVariableMetaAcrossLocales(localeMetas);
113
+ if (!merged.ok) {
114
+ throw new Error(`[i18n] ICU args mismatch on patch: ${merged.message}`);
115
+ }
116
+ }
117
+
118
+ export function assertPatchKeyMulti(
119
+ namespace: string,
120
+ key: string,
121
+ locale: string,
122
+ template: string,
123
+ preloadedKeys: Set<string>,
124
+ localeByKey: Record<string, string> | undefined
125
+ ): void {
126
+ if (!preloadedKeys.has(preloadKeyMulti(namespace, key, locale))) {
127
+ throw new Error(`[i18n] Key not preloaded: ${namespace}.${key} (${locale})`);
128
+ }
129
+
130
+ const parsed = parseTemplate(template);
131
+ if (!parsed.ok) {
132
+ throw new Error(`[i18n] ICU syntax error on patch: ${parsed.message}`);
133
+ }
134
+
135
+ const localeMetas = collectLocaleMetasForKey(localeByKey ?? {}, locale, parsed.meta);
136
+ const merged = mergeVariableMetaAcrossLocales(localeMetas);
137
+ if (!merged.ok) {
138
+ throw new Error(`[i18n] ICU args mismatch on patch: ${merged.message}`);
139
+ }
140
+ }
@@ -206,21 +206,23 @@ describe("projectDictionaryLocalesCore", () => {
206
206
  });
207
207
 
208
208
  describe("mergeNamespaceLocalesCore", () => {
209
+ type TestDictionary = {
210
+ welcome: Partial<Record<"en" | "it", string>>;
211
+ login_button: Partial<Record<"en" | "it", string>>;
212
+ };
213
+
209
214
  it("merges locale maps per key without dropping existing locales", () => {
210
- const existing = {
215
+ const existing: TestDictionary = {
211
216
  welcome: { en: "Welcome {name}!" },
212
217
  login_button: { en: "Login", it: "Accedi" },
213
218
  };
214
219
  const incoming = {
215
220
  welcome: { it: "Benvenuto {name}!" },
216
- invoice_summary: { en: "{count} invoices" },
217
221
  };
218
222
 
219
- // @ts-expect-error missing key in incoming
220
223
  expect(mergeNamespaceLocalesCore(existing, incoming)).toEqual({
221
224
  welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
222
225
  login_button: { en: "Login", it: "Accedi" },
223
- invoice_summary: { en: "{count} invoices" },
224
226
  });
225
227
  });
226
228
 
@@ -239,8 +241,17 @@ describe("mergeNamespaceLocalesCore", () => {
239
241
  });
240
242
 
241
243
  describe("mergeDictionaryLocalesCore", () => {
244
+ type TestMultiDictionary = {
245
+ default: {
246
+ welcome: Partial<Record<"en" | "it", string>>;
247
+ };
248
+ billing: {
249
+ invoice_summary: Partial<Record<"en" | "it", string>>;
250
+ };
251
+ };
252
+
242
253
  it("merges each namespace without dropping existing locales", () => {
243
- const existing = {
254
+ const existing: TestMultiDictionary = {
244
255
  default: {
245
256
  welcome: { en: "Welcome {name}!" },
246
257
  },
@@ -257,7 +268,6 @@ describe("mergeDictionaryLocalesCore", () => {
257
268
  },
258
269
  };
259
270
 
260
- // @ts-expect-error missing locale
261
271
  expect(mergeDictionaryLocalesCore(existing, incoming)).toEqual({
262
272
  default: {
263
273
  welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
@@ -1,5 +1,13 @@
1
1
  import { resolveLocaleTemplate } from "./resolve-locale.js";
2
- import type { KeyDictionary, LocaleFallbackMap, MultiDictionary } from "./types.js";
2
+ import type {
3
+ KeyDictionary,
4
+ LocaleFallbackMap,
5
+ LocaleOfMulti,
6
+ LocaleOfSingle,
7
+ MultiDictionary,
8
+ PartialKeyDictionary,
9
+ PartialMultiDictionary,
10
+ } from "./types.js";
3
11
 
4
12
  function classifyAreaLocales(
5
13
  areaLocales: readonly string[],
@@ -157,7 +165,10 @@ export function projectDictionaryLocalesCore<T extends MultiDictionary>(
157
165
  * Merges locale entries per translation key without dropping keys or locales
158
166
  * already present in `existing`. Incoming locales overwrite the same locale on conflict.
159
167
  */
160
- export function mergeNamespaceLocalesCore<T extends KeyDictionary>(existing: T, incoming: T): T {
168
+ export function mergeNamespaceLocalesCore<
169
+ T extends KeyDictionary,
170
+ Locales extends string = LocaleOfSingle<T>,
171
+ >(existing: T, incoming: PartialKeyDictionary<T, Locales>): T {
161
172
  const merged = structuredClone(existing);
162
173
 
163
174
  for (const [key, incomingLocales] of Object.entries(incoming)) {
@@ -178,10 +189,10 @@ export function mergeNamespaceLocalesCore<T extends KeyDictionary>(existing: T,
178
189
  * Merges locale entries per key in every namespace present in `incoming`.
179
190
  * Namespaces and keys not in `incoming` are preserved from `existing`.
180
191
  */
181
- export function mergeDictionaryLocalesCore<T extends MultiDictionary>(
182
- existing: T,
183
- incoming: Partial<T>
184
- ): T {
192
+ export function mergeDictionaryLocalesCore<
193
+ T extends MultiDictionary,
194
+ Locales extends string = LocaleOfMulti<T>,
195
+ >(existing: T, incoming: PartialMultiDictionary<T, Locales>): T {
185
196
  const merged = structuredClone(existing) as T;
186
197
 
187
198
  for (const namespace of Object.keys(incoming) as (keyof T & string)[]) {
@@ -0,0 +1,124 @@
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)` and `set(...)` 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
+ set<
42
+ const NS extends keyof Schema & keyof Params & string,
43
+ const K extends keyof Schema[NS] & keyof Params[NS] & string,
44
+ >(
45
+ namespace: NS,
46
+ key: K,
47
+ template: string
48
+ ): void;
49
+ forLocale<Next extends RequestLocales>(
50
+ locale: Next
51
+ ): I18nScopeMultiForLocale<Schema, Params, RequestLocales, Next>;
52
+ }
53
+
54
+ export class I18nScopeMultiImpl<
55
+ Schema extends MultiDictionary,
56
+ Params extends MultiParams<Schema>,
57
+ RequestLocales extends string,
58
+ Fallback extends LocaleFallbackMap | undefined,
59
+ > implements I18nScopeMulti<Schema, Params, RequestLocales> {
60
+ constructor(
61
+ private readonly engine: IcuTranslationProviderMulti<Schema, Params, RequestLocales, Fallback>
62
+ ) {}
63
+
64
+ t = <
65
+ const NS extends keyof Schema & keyof Params & string,
66
+ const K extends keyof Schema[NS] & keyof Params[NS] & string,
67
+ >(
68
+ namespace: NS,
69
+ key: K,
70
+ locale: RequestLocales,
71
+ ...args: ParamArgs<Params[NS][K]>
72
+ ): string => {
73
+ const params = args[0] as Record<string, unknown> | undefined;
74
+ return this.engine.getWithLocale(String(namespace), String(key), locale, params);
75
+ };
76
+
77
+ forLocale = <Locale extends RequestLocales>(
78
+ locale: Locale
79
+ ): I18nScopeMultiForLocaleImpl<Schema, Params, RequestLocales, Locale, Fallback> => {
80
+ return new I18nScopeMultiForLocaleImpl(this.engine, locale);
81
+ };
82
+ }
83
+
84
+ export class I18nScopeMultiForLocaleImpl<
85
+ Schema extends MultiDictionary,
86
+ Params extends MultiParams<Schema>,
87
+ RequestLocales extends string,
88
+ Locale extends RequestLocales,
89
+ Fallback extends LocaleFallbackMap | undefined,
90
+ > implements I18nScopeMultiForLocale<Schema, Params, RequestLocales, Locale> {
91
+ constructor(
92
+ private readonly engine: IcuTranslationProviderMulti<Schema, Params, RequestLocales, Fallback>,
93
+ readonly locale: Locale
94
+ ) {}
95
+
96
+ t = <
97
+ const NS extends keyof Schema & keyof Params & string,
98
+ const K extends keyof Schema[NS] & keyof Params[NS] & string,
99
+ >(
100
+ namespace: NS,
101
+ key: K,
102
+ ...args: ParamArgs<Params[NS][K]>
103
+ ): string => {
104
+ const params = args[0] as Record<string, unknown> | undefined;
105
+ return this.engine.getWithLocale(String(namespace), String(key), this.locale, params);
106
+ };
107
+
108
+ set = <
109
+ const NS extends keyof Schema & keyof Params & string,
110
+ const K extends keyof Schema[NS] & keyof Params[NS] & string,
111
+ >(
112
+ namespace: NS,
113
+ key: K,
114
+ template: string
115
+ ): void => {
116
+ this.engine.patchKeyMulti(String(namespace), String(key), this.locale, template);
117
+ };
118
+
119
+ forLocale = <Next extends RequestLocales>(
120
+ locale: Next
121
+ ): I18nScopeMultiForLocaleImpl<Schema, Params, RequestLocales, Next, Fallback> => {
122
+ return new I18nScopeMultiForLocaleImpl(this.engine, locale);
123
+ };
124
+ }