@vielzeug/i18nit 2.0.0 → 3.0.1

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 (46) hide show
  1. package/README.md +145 -51
  2. package/dist/format.d.ts +54 -0
  3. package/dist/format.d.ts.map +1 -0
  4. package/dist/i18n.cjs +1 -1
  5. package/dist/i18n.cjs.map +1 -1
  6. package/dist/i18n.d.ts +5 -77
  7. package/dist/i18n.d.ts.map +1 -1
  8. package/dist/i18n.js +156 -202
  9. package/dist/i18n.js.map +1 -1
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/types.d.ts +62 -83
  15. package/dist/types.d.ts.map +1 -1
  16. package/package.json +11 -12
  17. package/dist/core.cjs +0 -2
  18. package/dist/core.cjs.map +0 -1
  19. package/dist/core.d.ts +0 -35
  20. package/dist/core.d.ts.map +0 -1
  21. package/dist/core.js +0 -53
  22. package/dist/core.js.map +0 -1
  23. package/dist/helpers.cjs +0 -2
  24. package/dist/helpers.cjs.map +0 -1
  25. package/dist/helpers.d.ts +0 -20
  26. package/dist/helpers.d.ts.map +0 -1
  27. package/dist/helpers.js +0 -47
  28. package/dist/helpers.js.map +0 -1
  29. package/dist/i18nit.cjs +0 -2
  30. package/dist/i18nit.cjs.map +0 -1
  31. package/dist/i18nit.d.ts +0 -3
  32. package/dist/i18nit.d.ts.map +0 -1
  33. package/dist/i18nit.js +0 -2
  34. package/dist/i18nit.js.map +0 -1
  35. package/dist/interpolate.cjs +0 -2
  36. package/dist/interpolate.cjs.map +0 -1
  37. package/dist/interpolate.d.ts +0 -11
  38. package/dist/interpolate.d.ts.map +0 -1
  39. package/dist/interpolate.js +0 -13
  40. package/dist/interpolate.js.map +0 -1
  41. package/dist/intl.cjs +0 -2
  42. package/dist/intl.cjs.map +0 -1
  43. package/dist/intl.d.ts +0 -16
  44. package/dist/intl.d.ts.map +0 -1
  45. package/dist/intl.js +0 -65
  46. package/dist/intl.js.map +0 -1
package/README.md CHANGED
@@ -1,84 +1,178 @@
1
1
  # @vielzeug/i18nit
2
2
 
3
- > Lightweight, type-safe i18n with nested keys, lazy loaders, interpolation, pluralization, and reactive subscriptions.
4
-
5
- [![npm version](https://img.shields.io/npm/v/@vielzeug/i18nit)](https://www.npmjs.com/package/@vielzeug/i18nit) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
-
7
- `@vielzeug/i18nit` is a zero-dependency internationalization library for TypeScript. It combines typed key paths, fallback locale chains, async locale loading, and Intl formatting helpers.
3
+ Minimal i18n runtime with typed keys, explicit locale sources, and framework-friendly subscriptions.
8
4
 
9
5
  ## Installation
10
6
 
11
7
  ```sh
12
8
  pnpm add @vielzeug/i18nit
13
- # npm install @vielzeug/i18nit
14
- # yarn add @vielzeug/i18nit
15
9
  ```
16
10
 
17
- ## Entry Points
18
-
19
- | Entry | Purpose |
20
- | --- | --- |
21
- | `@vielzeug/i18nit` | Main API (`createI18n`, `I18n`, exported types) |
22
- | `@vielzeug/i18nit/core` | Core bundle entry |
23
-
24
11
  ## Quick Start
25
12
 
26
13
  ```ts
27
14
  import { createI18n } from '@vielzeug/i18nit';
15
+ import { createFormatter } from '@vielzeug/i18nit/format';
28
16
 
29
17
  const i18n = createI18n({
30
- fallback: 'en',
31
18
  locale: 'en',
32
- messages: {
33
- de: {
34
- greeting: 'Hallo, {name}!',
35
- inbox: { one: 'Eine Nachricht', other: '{count} Nachrichten' },
36
- },
19
+ fallback: 'en',
20
+ catalogs: {
37
21
  en: {
38
22
  greeting: 'Hello, {name}!',
39
- inbox: { zero: 'No messages', one: 'One message', other: '{count} messages' },
40
- nav: { home: 'Home' },
23
+ inbox: {
24
+ zero: 'No messages',
25
+ one: 'One message',
26
+ other: '{count} messages',
27
+ },
41
28
  },
29
+ de: () => import('./locales/de.json').then((m) => m.default),
42
30
  },
43
31
  });
44
32
 
33
+ await i18n.preload('de');
34
+ await i18n.setLocale('de');
35
+
45
36
  i18n.t('greeting', { name: 'Alice' });
46
- i18n.t('inbox', { count: 0 });
47
- i18n.t('inbox', { count: 3 });
37
+ i18n.tp('inbox', 3);
38
+
39
+ const fmt = createFormatter(i18n);
40
+ fmt.currency(19.99, 'EUR');
41
+ ```
42
+
43
+ ## Core API
44
+
45
+ - `createI18n(options?)`
46
+ - `i18n.t(key, vars?)`
47
+ - `i18n.tp(key, count, options?)`
48
+ - `i18n.preload(locale)`
49
+ - `i18n.setLocale(locale)`
50
+ - `i18n.register(locale, source)`
51
+ - `i18n.getSnapshot()`
52
+ - `i18n.subscribe(callback, options?)`
53
+ - `i18n.getSupportedLocales(options?)`
54
+ - `i18n.has(leafKey)`
55
+
56
+ ## Translation options
57
+
58
+ ```ts
59
+ type PluralTranslateOptions = {
60
+ ordinal?: boolean;
61
+ vars?: Record<string, unknown>;
62
+ };
63
+ ```
64
+
65
+ - Leaf keys use `t('greeting', vars)`.
66
+ - Branch keys use `tp('inbox', 3, options?)`.
67
+
68
+ ## Missing handling
69
+
70
+ A single callback handles missing keys and missing interpolation variables.
48
71
 
49
- i18n.locale = 'de';
50
- i18n.t('nav.home'); // falls back to en
72
+ Default behavior:
73
+
74
+ - missing keys return the key string
75
+ - missing interpolation vars keep the original placeholder (for example `{name}`)
76
+
77
+ ```ts
78
+ const i18n = createI18n({
79
+ onMissing(info) {
80
+ if (info.type === 'var') return `<${info.varName}>`;
81
+
82
+ return `[missing:${info.key}]`;
83
+ },
84
+ });
51
85
  ```
52
86
 
53
- ## Features
87
+ ## Subscriber error handling
88
+
89
+ By default, exceptions thrown inside `subscribe` callbacks are swallowed so the store
90
+ stays stable. Provide `onSubscriberError` to observe or log those failures:
54
91
 
55
- - Typed translation keys from your message tree
56
- - Dot-notation nested key lookup
57
- - ICU-style interpolation with object/array path support
58
- - Plural messages (`zero/one/two/few/many/other`) via `Intl.PluralRules`
59
- - Locale chain fallback (`sr-Latn-RS -> sr-Latn -> sr`) + configured fallback locales
60
- - Async locale loading (`load`, `setLocale`, `registerLoader`, `reload`)
61
- - Catalog updates (`add` deep-merge, `replace` full replace)
62
- - Subscription API with batched notifications (`batch`, `subscribe`)
63
- - Intl format helpers (`number`, `date`, `list`, `relative`, `currency`)
64
- - Namespace and locale-bound views (`scope`, `withLocale`)
65
- - Diagnostic hooks (`onDiagnostic`) and missing-key hook (`onMissing`)
92
+ ```ts
93
+ const i18n = createI18n({
94
+ onSubscriberError(error) {
95
+ console.error('[i18n] subscriber threw:', error);
96
+ },
97
+ });
98
+ ```
99
+
100
+ ## Listing supported locales
101
+
102
+ `getSupportedLocales()` returns locales in registration order.
103
+ Pass `{ sorted: true }` for a deterministic code-point-sorted list:
104
+
105
+ ```ts
106
+ i18n.getSupportedLocales(); // ['en', 'fr', 'de'] — insertion order
107
+ i18n.getSupportedLocales({ sorted: true }); // ['de', 'en', 'fr'] — code-point order
108
+ ```
109
+
110
+ ## Framework integration
111
+
112
+ `i18nit` is framework-agnostic and exposes a single subscription primitive:
113
+
114
+ - `subscribe(callback, options?)`
115
+ - default: change-only notifications (React external store style)
116
+ - `{ immediate: true }`: immediate callback + change notifications (Svelte/Vue/Solid friendly)
117
+
118
+ Use these directly rather than package-level framework adapters.
119
+
120
+ ```ts
121
+ const unsubscribe = i18n.subscribe((snapshot) => {
122
+ const { locale, version } = snapshot;
123
+ console.log(locale, version);
124
+ });
125
+
126
+ unsubscribe();
127
+ ```
128
+
129
+ ::: code-group
130
+
131
+ ```tsx [React]
132
+ import { useSyncExternalStore } from 'react';
133
+
134
+ export function useI18n() {
135
+ const snapshot = useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);
136
+
137
+ return {
138
+ locale: snapshot.locale,
139
+ t: i18n.t,
140
+ setLocale: i18n.setLocale,
141
+ };
142
+ }
143
+ ```
144
+
145
+ ```ts [Vue]
146
+ import { onUnmounted, shallowRef } from 'vue';
147
+
148
+ const snapshot = shallowRef(i18n.getSnapshot());
149
+
150
+ const stop = i18n.subscribe((next) => {
151
+ snapshot.value = next;
152
+ }, { immediate: true });
153
+
154
+ onUnmounted(stop);
155
+ ```
156
+
157
+ ```ts [Svelte]
158
+ import type { Readable } from 'svelte/store';
159
+ import type { I18nSnapshot } from '@vielzeug/i18nit';
160
+
161
+ export const i18nStore: Readable<I18nSnapshot> = {
162
+ subscribe: (run) => i18n.subscribe(run, { immediate: true }),
163
+ };
164
+ ```
66
165
 
67
- ## API At a Glance
166
+ :::
68
167
 
69
- - `createI18n<T>(options?) => I18n<T>`
70
- - `class I18n<T> implements BoundI18n<T>`
71
- - `type BoundI18n<T>`
72
- - `type I18nOptions<T>`
73
- - `type Messages`, `TranslationKey`, `TranslationKeyParam`, `PluralKeys`, `NamespaceKeys`
168
+ For more complete framework samples, see:
74
169
 
75
- ## Documentation
170
+ - `docs/i18nit/examples/framework-integration.md`
76
171
 
77
- - [Overview](https://vielzeug.dev/i18nit/)
78
- - [Usage Guide](https://vielzeug.dev/i18nit/usage)
79
- - [API Reference](https://vielzeug.dev/i18nit/api)
80
- - [Examples](https://vielzeug.dev/i18nit/examples)
172
+ ## Formatting
81
173
 
82
- ## License
174
+ Formatting lives in `@vielzeug/i18nit/format` and can bind to:
83
175
 
84
- MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
176
+ - a static locale string
177
+ - an i18n-like source with `locale`
178
+ - a getter function that returns a locale
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @vielzeug/i18nit/format
3
+ *
4
+ * Standalone Intl formatter factory. Import from `@vielzeug/i18nit/format`.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { createFormatter } from '@vielzeug/i18nit/format';
9
+ *
10
+ * // Static locale
11
+ * const fmt = createFormatter('en-US');
12
+ *
13
+ * // Reactive — always reads the current locale from the i18n instance
14
+ * const fmt = createFormatter(i18n);
15
+ *
16
+ * fmt.number(1_234.56);
17
+ * fmt.currency(9.99, 'USD');
18
+ * fmt.date(new Date());
19
+ * fmt.relative(-1, 'day');
20
+ * fmt.list(['A', 'B', 'C']);
21
+ * fmt.duration({ hours: 1, minutes: 30 });
22
+ * ```
23
+ */
24
+ export type DurationValue = Partial<Record<'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'months' | 'nanoseconds' | 'seconds' | 'weeks' | 'years', number>>;
25
+ export type DurationFormatOptions = {
26
+ hours?: '2-digit' | 'numeric';
27
+ microseconds?: 'numeric';
28
+ milliseconds?: 'numeric';
29
+ minutes?: '2-digit' | 'numeric';
30
+ nanoseconds?: 'numeric';
31
+ seconds?: '2-digit' | 'numeric';
32
+ style?: 'digital' | 'long' | 'narrow' | 'short';
33
+ };
34
+ export type ListFormatOptions = {
35
+ style?: 'long' | 'narrow' | 'short';
36
+ type?: 'and' | 'or';
37
+ };
38
+ export type Formatter = {
39
+ currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'currency' | 'style'>): string;
40
+ date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
41
+ duration(value: DurationValue, options?: DurationFormatOptions): string;
42
+ list(value: unknown[], options?: ListFormatOptions): string;
43
+ number(value: number, options?: Intl.NumberFormatOptions): string;
44
+ relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
45
+ };
46
+ /**
47
+ * Creates a formatter bound to a locale. Pass a string for a static locale,
48
+ * a getter function (() => string), or an object with a `locale` property
49
+ * (e.g. an `I18n` instance) for reactive binding.
50
+ */
51
+ export declare function createFormatter(source: string | (() => string) | {
52
+ readonly locale: string;
53
+ }): Formatter;
54
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,CACjC,MAAM,CACF,MAAM,GACN,OAAO,GACP,cAAc,GACd,cAAc,GACd,SAAS,GACT,QAAQ,GACR,aAAa,GACb,SAAS,GACT,OAAO,GACP,OAAO,EACT,MAAM,CACP,CACF,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,MAAM,CAAC;IAClH,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAAC;IACzE,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IACxE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAC5D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,yBAAyB,GAAG,MAAM,CAAC;CAC9G,CAAC;AAyGF;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CA0ExG"}
package/dist/i18n.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require(`./core.cjs`),t=require(`./helpers.cjs`),n=require(`./intl.cjs`),r=require(`./interpolate.cjs`);var i=class{#e;#t;#n=new Map;#r=new Map;#i=new Map;#a=new Set;#o=new t.BoundedMap(128);#s;#c;#l=null;#u=null;#d=!1;#f=0;#p=null;#m;#h=n.makeIntlCaches();#g;constructor({fallback:r,loaders:i,locale:a=`en`,messages:o,onDiagnostic:s,onMissing:c}={}){if(this.#e=a,this.#t=Array.isArray(r)?r:r?[r]:[],this.#s=c,this.#c=s,o)for(let[e,t]of Object.entries(o))this.#n.set(e,structuredClone(t));if(i)for(let[e,t]of Object.entries(i))this.#r.set(e,t);this.#m={checkOwn:(e,n)=>{let r=this.#n.get(n);if(!r)return!1;let i=t.resolvePath(r,e);return i!==void 0&&t.isMessageValue(i)},findMessage:(e,t)=>this.#b(e,t),formatDate:(e,t,r)=>n.formatDate(this.#h,e,t,r),formatList:(e,t,r)=>n.formatList(this.#h,e,t,r),formatNumber:(e,t,r)=>n.formatNumber(this.#h,e,t,r),formatRelative:(e,t,r,i)=>n.formatRelative(this.#h,e,t,r,i),getLocale:()=>this.#e,translate:(e,t,n)=>this.#S(e,t,n)},this.#g=new e.BoundView(this.#m,null)}get locale(){return this.#e}get locales(){return this.#l??=[...this.#n.keys()],this.#l}set locale(e){this.#e!==e&&(this.#e=e,this.#y(`locale-change`))}async setLocale(e){e!==this.#e&&(await this.load(e),this.locale=e)}add(e,n){let r=this.#n.get(e)??{};this.#n.set(e,t.deepMerge(r,n)),this.#l=null,this.#x(this.#e).includes(e)&&this.#y(`catalog-update`)}replace(e,t){this.#n.set(e,structuredClone(t)),this.#l=null,this.#x(this.#e).includes(e)&&this.#y(`catalog-update`)}has(e){return this.#g.has(e)}hasOwn(e){return this.#g.hasOwn(e)}hasLocale(e){return this.#n.has(e)}async load(...e){await Promise.all(e.map(e=>this.#C(e)))}async reload(e){this.#r.has(e)&&(this.#n.delete(e),this.#l=null,await this.#C(e))}registerLoader(e,t){this.#r.set(e,t),this.#u=null}get loadableLocales(){return this.#u??=[...this.#r.keys()],this.#u}t(e,t){return this.#g.t(e,t)}number(e,t){return this.#g.number(e,t)}date(e,t){return this.#g.date(e,t)}list(e,t=`and`){return this.#g.list(e,t)}relative(e,t,n){return this.#g.relative(e,t,n)}currency(e,t,n){return this.#g.currency(e,t,n)}withLocale(e){return this.#g.withLocale(e)}scope(e){return this.#g.scope(e)}batch(e){this.#f++;try{e()}finally{if(this.#f--,this.#f===0&&this.#p!==null){let e=this.#p;this.#p=null,this.#y(e)}}}subscribe(e,t){if(this.#a.add(e),t)try{e({locale:this.#e,reason:`locale-change`})}catch(e){this.#_(e)}return()=>this.#a.delete(e)}dispose(){this.#d=!0,this.#a.clear(),this.#n.clear(),this.#r.clear(),this.#i.clear(),this.#o.clear(),this.#l=null,this.#u=null}[Symbol.dispose](){this.dispose()}async[Symbol.asyncDispose](){await Promise.allSettled([...this.#i.values()]),this.dispose()}#_(e){this.#c?this.#c({error:e,kind:`subscriber-error`}):console.error(`[i18nit] Subscriber threw:`,e)}#v(e,t){this.#c?this.#c({error:e,kind:`loader-error`,locale:t}):console.warn(`[i18nit] Loader error:`,e)}#y(e){if(this.#f>0){this.#p!==`locale-change`&&(this.#p=e);return}let t={locale:this.#e,reason:e};for(let e of this.#a)try{e(t)}catch(e){this.#_(e)}}#b(e,n){for(let r of this.#x(n)){let n=this.#n.get(r);if(!n)continue;let i=t.resolvePath(n,e);if(i!==void 0&&t.isMessageValue(i))return i}}#x(e){let t=this.#o.get(e);if(t)return t;let n=new Set,r=e=>{n.add(e);let t=e.split(`-`);for(let e=t.length-1;e>0;e--)n.add(t.slice(0,e).join(`-`))};r(e);for(let e of this.#t)r(e);let i=[...n];return this.#o.set(e,i),i}#S(e,t,i){let a=this.#b(e,i);if(a===void 0)return this.#s?.(e,i)??e;if(typeof a==`string`)return r.interpolate(a,t??{},i,this.#h);let o=t??{},s=Number(o.count??0);return r.interpolate(a[s===0&&a.zero!==void 0?`zero`:n.getPluralForm(this.#h,i,s)]??a.other,o,i,this.#h)}#C(e){if(this.#i.has(e))return this.#i.get(e);if(this.#n.has(e))return Promise.resolve();let t=this.#r.get(e);if(!t)return Promise.resolve();let n=(async()=>{try{let n=await t(e);this.#d||this.replace(e,n)}catch(t){throw this.#v(t,e),t}finally{this.#i.delete(e)}})();return this.#i.set(e,n),n}};function a(e){return new i(e)}exports.I18n=i,exports.createI18n=a;
1
+ var e=/\{([\p{ID_Continue}\-.]+)\}/gu;function t(e){try{return Intl.getCanonicalLocales(e)[0]??e}catch{return e}}function n(e,t){let n=e;for(let e of t.split(`.`)){if(typeof n!=`object`||!n||!Object.hasOwn(n,e))return;n=n[e]}return n}function r(e,t){let n=new Set;for(let r of[e,...t]){n.add(r);let e=r.split(`-`);for(let t=e.length-1;t>0;t--)n.add(e.slice(0,t).join(`-`))}return[...n]}function i(e,t,n,r){let i=`${t}:${r?`ordinal`:`cardinal`}`,a=e.get(i);if(!a){try{a=new Intl.PluralRules(t,{type:r?`ordinal`:`cardinal`})}catch{a={select:e=>e===1?`one`:`other`}}e.set(i,a)}return a.select(n)}function a(t,r,i,a,o){return t.includes(`{`)?t.replace(e,(e,t)=>{let s=r==null?void 0:n(r,t);return s==null?o({key:i,locale:a,type:`var`,varName:t}):String(s)}):t}function o(e){let o=e??{},s=t(o.locale??`en`),c=Array.isArray(o.fallback)?o.fallback.map(t):o.fallback?[t(o.fallback)]:[],l=new Map,u=new Map,d=new Set,f=new Map,p=o.onMissing??(e=>e.type===`key`?e.key:`{${e.varName}}`),m=o.onSubscriberError??(()=>{}),h=0,g={locale:s,version:h},_=r(s,c),v=0,y=()=>{h++,g={locale:s,version:h};let e=[...d];for(let t of e)try{t(g)}catch(e){m(e)}},b=(e,t=!1)=>{if(d.add(e),t)try{e(g)}catch(e){m(e)}return()=>d.delete(e)},x=e=>{for(let t of _){let r=l.get(t)?.messages;if(!r)continue;let i=n(r,e);if(typeof i==`string`)return i}},S=(e,t)=>{if(typeof t==`function`){l.set(e,{kind:`dynamic`,loader:t});return}l.set(e,{kind:`static`,messages:t})},C=(e,n)=>{let r=t(e);S(r,n),_.includes(r)&&y()};if(o.catalogs)for(let[e,n]of Object.entries(o.catalogs))S(t(e),n);let w=async e=>{let n=t(e),r=l.get(n);if(!r)throw Error(`Missing locale source for "${n}".`);if(r.kind===`static`||r.messages)return;let i=u.get(n);if(i?.entry===r){await i.task;return}let a=(async()=>{let e=await r.loader(n);l.get(n)===r&&(r.messages=e,_.includes(n)&&y())})();u.set(n,{entry:r,task:a});try{await a}finally{u.get(n)?.task===a&&u.delete(n)}};function T(e,t){let n=String(e),r=x(n);return r===void 0?p({key:n,locale:s,type:`key`}):a(r,t,n,s,p)}function E(e,t,n){if(!Number.isFinite(t))throw TypeError("`count` must be a finite number.");if(n?.vars&&Object.hasOwn(n.vars,`count`))throw Error("`tp` does not allow `vars.count`; `count` is injected automatically.");let r=String(e),o=n?.ordinal===!0,c=i(f,s,t,o),l=x(!o&&t===0?`${r}.zero`:`${r}.${c}`)??x(`${r}.other`);return l===void 0?p({key:r,locale:s,type:`key`}):a(l,{...n?.vars??{},count:t},r,s,p)}return{getSnapshot(){return g},getSupportedLocales(e){let t=[...l.keys()];return e?.sorted===!0?t.sort():t},has(e){return x(String(e))!==void 0},get locale(){return s},preload:w,register:C,async setLocale(e){let n=t(e);if(s===n)return;let i=++v;await w(n),i===v&&(s=n,_=r(s,c),y())},subscribe(e,t){return b(e,t?.immediate===!0)},t:T,tp:E}}exports.createI18n=o;
2
2
  //# sourceMappingURL=i18n.cjs.map
package/dist/i18n.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.cjs","names":["#caches","#view","#locale","#fallbacks","#onMissing","#onDiagnostic","#catalogs","#loaders","#core","#findMessage","#translate","#localesCache","#notify","#getLocaleChain","#loadOne","#loadersCache","#batchDepth","#pendingNotify","#subscribers","#diagnoseSubscriber","#disposed","#loading","#chainCache","#diagnoseLoader"],"sources":["../src/i18n.ts"],"sourcesContent":["import type {\n BoundI18n,\n DiagnosticEvent,\n I18nOptions,\n Loader,\n Locale,\n LocaleChangeEvent,\n LocaleChangeReason,\n MessageValue,\n Messages,\n NamespaceKeys,\n TranslationKeyParam,\n Unsubscribe,\n Vars,\n} from './types';\n\nimport { BoundView, type I18nCore } from './core';\nimport { BoundedMap, deepMerge, isMessageValue, resolvePath } from './helpers';\nimport { interpolate } from './interpolate';\nimport {\n type IntlCaches,\n formatDate,\n formatList,\n formatNumber,\n formatRelative,\n getPluralForm,\n makeIntlCaches,\n} from './intl';\n\nexport class I18n<T extends Messages = Messages> implements BoundI18n<T> {\n #locale: Locale;\n #fallbacks: Locale[];\n #catalogs = new Map<Locale, Messages>();\n #loaders = new Map<Locale, Loader>();\n #loading = new Map<Locale, Promise<void>>();\n #subscribers = new Set<(event: LocaleChangeEvent) => void>();\n /** Bounded at 128 entries — prevents unbounded growth when locale tags come from user input. */\n #chainCache = new BoundedMap<Locale, Locale[]>(128);\n #onMissing?: (key: string, locale: Locale) => string | undefined;\n #onDiagnostic?: (event: DiagnosticEvent) => void;\n #localesCache: Locale[] | null = null;\n #loadersCache: Locale[] | null = null;\n #disposed = false;\n #batchDepth = 0;\n #pendingNotify: LocaleChangeReason | null = null;\n #core: I18nCore;\n\n // Instance-scoped Intl caches — GC'd with the instance (important for SSR with many locales).\n readonly #caches: IntlCaches = makeIntlCaches();\n\n /** Internal BoundView — I18n delegates its BoundI18n surface here to avoid duplicating every method. */\n readonly #view: BoundView<T>;\n\n constructor({ fallback, loaders, locale = 'en', messages, onDiagnostic, onMissing }: I18nOptions<T> = {}) {\n this.#locale = locale;\n this.#fallbacks = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];\n this.#onMissing = onMissing;\n this.#onDiagnostic = onDiagnostic;\n\n if (messages) {\n for (const [l, m] of Object.entries(messages)) {\n // Deep-clone at init so external mutations to the source object can't corrupt the catalog.\n this.#catalogs.set(l, structuredClone(m) as Messages);\n }\n }\n\n if (loaders) for (const [l, fn] of Object.entries(loaders)) this.#loaders.set(l, fn);\n\n this.#core = {\n checkOwn: (key: string, locale: Locale) => {\n const catalog = this.#catalogs.get(locale);\n\n if (!catalog) return false;\n\n const value = resolvePath(catalog, key);\n\n return value !== undefined && isMessageValue(value);\n },\n findMessage: (key: string, locale: Locale) => this.#findMessage(key, locale),\n formatDate: (value, options, locale) => formatDate(this.#caches, value, options, locale),\n formatList: (items, locale, type) => formatList(this.#caches, items, locale, type),\n formatNumber: (value, options, locale) => formatNumber(this.#caches, value, options, locale),\n formatRelative: (value, unit, options, locale) => formatRelative(this.#caches, value, unit, options, locale),\n getLocale: () => this.#locale,\n translate: (key, vars, locale) => this.#translate(key, vars, locale),\n };\n\n this.#view = new BoundView<T>(this.#core, null);\n }\n\n /* -------------------- Locale -------------------- */\n\n get locale(): Locale {\n return this.#locale;\n }\n\n get locales(): Locale[] {\n this.#localesCache ??= [...this.#catalogs.keys()];\n\n return this.#localesCache;\n }\n\n set locale(value: Locale) {\n if (this.#locale === value) return;\n\n if (import.meta.env?.DEV && !this.#catalogs.has(value) && this.#loaders.has(value)) {\n console.warn(\n `[i18nit] locale \"${value}\" has a registered loader but is not loaded. ` +\n 'Use setLocale() to load and switch atomically.',\n );\n }\n\n this.#locale = value;\n this.#notify('locale-change');\n }\n\n async setLocale(locale: Locale): Promise<void> {\n if (locale === this.#locale) return;\n\n await this.load(locale);\n this.locale = locale;\n }\n\n /* -------------------- Message Management -------------------- */\n\n /** Deep-merges messages into an existing locale catalog. */\n add(locale: Locale, messages: Messages): void {\n const existing = this.#catalogs.get(locale) ?? {};\n\n this.#catalogs.set(locale, deepMerge(existing, messages));\n this.#localesCache = null;\n\n if (this.#getLocaleChain(this.#locale).includes(locale)) this.#notify('catalog-update');\n }\n\n /** Replaces the entire locale catalog for `locale` with a deep clone of `messages`. */\n replace(locale: Locale, messages: Messages): void {\n this.#catalogs.set(locale, structuredClone(messages));\n this.#localesCache = null;\n\n if (this.#getLocaleChain(this.#locale).includes(locale)) this.#notify('catalog-update');\n }\n\n has(key: string): boolean {\n return this.#view.has(key);\n }\n\n /** Like `has()`, but only checks the exact locale without walking the fallback chain. */\n hasOwn(key: string): boolean {\n return this.#view.hasOwn(key);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.#catalogs.has(locale);\n }\n\n /* -------------------- Async Loaders -------------------- */\n\n async load(...locales: Locale[]): Promise<void> {\n await Promise.all(locales.map((locale) => this.#loadOne(locale)));\n }\n\n /**\n * Force-reloads a locale catalog even if already populated. Useful for hot-reload and forced bundle refresh.\n * No-op (with a dev warning) when no loader is registered for the locale, to prevent silently clearing the catalog.\n */\n async reload(locale: Locale): Promise<void> {\n if (!this.#loaders.has(locale)) {\n if (import.meta.env?.DEV) {\n console.warn(`[i18nit] reload(\"${locale}\") skipped — no loader registered for this locale.`);\n }\n\n return;\n }\n\n this.#catalogs.delete(locale);\n this.#localesCache = null;\n await this.#loadOne(locale);\n }\n\n registerLoader(locale: Locale, loader: Loader): void {\n this.#loaders.set(locale, loader);\n this.#loadersCache = null;\n }\n\n /** Returns the locale keys for which a loader has been registered. */\n get loadableLocales(): Locale[] {\n this.#loadersCache ??= [...this.#loaders.keys()];\n\n return this.#loadersCache;\n }\n\n /* -------------------- BoundI18n surface (delegated to #view) -------------------- */\n\n /**\n * Translates a key with optional interpolation variables.\n * Locale must be loaded first via `load()` or provided via `messages` in config.\n * For a per-call locale override use `withLocale(locale).t(key, vars)`.\n */\n t(key: TranslationKeyParam<T>, vars?: Vars): string {\n return this.#view.t(key, vars);\n }\n\n number(value: number, options?: Intl.NumberFormatOptions): string {\n return this.#view.number(value, options);\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string {\n return this.#view.date(value, options);\n }\n\n list(items: unknown[], type: 'and' | 'or' = 'and'): string {\n return this.#view.list(items, type);\n }\n\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string {\n return this.#view.relative(value, unit, options);\n }\n\n currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>): string {\n return this.#view.currency(value, currency, options);\n }\n\n /**\n * Returns a bound interface that translates in the given locale without\n * changing the active locale on the instance. Useful for SSR and\n * multi-locale rendering in a single pass.\n */\n withLocale(locale: Locale): BoundI18n<T> {\n return this.#view.withLocale(locale);\n }\n\n /**\n * Returns a translator scoped to a key namespace prefix. Reacts to locale changes on the\n * instance. When `T` is a concrete message type, the returned `BoundI18n` is narrowed to\n * the subtree type so `t()` autocomplete works within the scope.\n * Only keys whose values are nested message objects are valid scope targets.\n */\n scope<K extends NamespaceKeys<T>>(ns: K): BoundI18n<T[K] & Messages> {\n return this.#view.scope(ns);\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n /**\n * Executes `fn` while deferring subscriber notifications. A single notification fires\n * after `fn` completes, collapsing any number of `add()` / `replace()` calls made within.\n * Nested `batch()` calls are supported; notification fires when the outermost batch exits.\n * If both a locale change and a catalog update are triggered, `'locale-change'` takes priority.\n *\n * @remarks\n * `batch()` is synchronous. Async operations (e.g. `load()`) started inside `fn` complete\n * after the batch exits and will notify subscribers individually. To batch-load multiple\n * locales and notify once, await `load()` before entering the batch:\n * ```ts\n * await i18n.load('fr', 'de');\n * i18n.batch(() => { i18n.locale = 'fr'; });\n * ```\n */\n batch(fn: () => void): void {\n this.#batchDepth++;\n\n try {\n fn();\n } finally {\n this.#batchDepth--;\n\n if (this.#batchDepth === 0 && this.#pendingNotify !== null) {\n const reason = this.#pendingNotify;\n\n this.#pendingNotify = null;\n this.#notify(reason);\n }\n }\n }\n\n subscribe(listener: (event: LocaleChangeEvent) => void, immediate?: boolean): Unsubscribe {\n this.#subscribers.add(listener);\n\n if (immediate) {\n try {\n listener({ locale: this.#locale, reason: 'locale-change' });\n } catch (err) {\n this.#diagnoseSubscriber(err);\n }\n }\n\n return () => this.#subscribers.delete(listener);\n }\n\n /** Releases all resources held by this instance. */\n dispose(): void {\n this.#disposed = true;\n this.#subscribers.clear();\n this.#catalogs.clear();\n this.#loaders.clear();\n this.#loading.clear();\n this.#chainCache.clear();\n this.#localesCache = null;\n this.#loadersCache = null;\n }\n\n /** Enables `using i18n = createI18n(...)` for deterministic resource release. */\n [Symbol.dispose](): void {\n this.dispose();\n }\n\n /**\n * Awaits any in-flight `load()` calls and then releases all resources.\n * Enables `await using i18n = createI18n(...)` in environments that support `Symbol.asyncDispose`.\n */\n async [Symbol.asyncDispose](): Promise<void> {\n await Promise.allSettled([...this.#loading.values()]);\n this.dispose();\n }\n\n /* -------------------- Private -------------------- */\n\n #diagnoseSubscriber(error: unknown): void {\n if (this.#onDiagnostic) {\n this.#onDiagnostic({ error, kind: 'subscriber-error' });\n } else {\n console.error('[i18nit] Subscriber threw:', error);\n }\n }\n\n #diagnoseLoader(error: unknown, locale: Locale): void {\n if (this.#onDiagnostic) {\n this.#onDiagnostic({ error, kind: 'loader-error', locale });\n } else {\n console.warn('[i18nit] Loader error:', error);\n }\n }\n\n #notify(reason: LocaleChangeReason): void {\n if (this.#batchDepth > 0) {\n // 'locale-change' takes priority over 'catalog-update' if both occur in one batch\n if (this.#pendingNotify !== 'locale-change') this.#pendingNotify = reason;\n\n return;\n }\n\n const event: LocaleChangeEvent = { locale: this.#locale, reason };\n\n for (const listener of this.#subscribers) {\n try {\n listener(event);\n } catch (err) {\n this.#diagnoseSubscriber(err);\n }\n }\n }\n\n #findMessage(key: string, locale: Locale): MessageValue | undefined {\n for (const loc of this.#getLocaleChain(locale)) {\n const messages = this.#catalogs.get(loc);\n\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n\n if (value !== undefined && isMessageValue(value)) return value;\n }\n\n return undefined;\n }\n\n #getLocaleChain(locale: Locale): Locale[] {\n const cached = this.#chainCache.get(locale);\n\n if (cached) return cached;\n\n const seen = new Set<Locale>();\n const push = (l: Locale) => {\n seen.add(l);\n\n const parts = l.split('-');\n\n for (let i = parts.length - 1; i > 0; i--) {\n seen.add(parts.slice(0, i).join('-'));\n }\n };\n\n push(locale);\n for (const fallback of this.#fallbacks) push(fallback);\n\n const chain = [...seen];\n\n this.#chainCache.set(locale, chain);\n\n return chain;\n }\n\n #translate(key: string, vars: Vars | undefined, locale: Locale): string {\n const message = this.#findMessage(key, locale);\n\n if (message === undefined) return this.#onMissing?.(key, locale) ?? key;\n\n if (typeof message === 'string') return interpolate(message, vars ?? {}, locale, this.#caches);\n\n const v = vars ?? {};\n\n if (import.meta.env?.DEV && v.count === undefined) {\n console.warn(`[i18nit] Key \"${key}\" is a plural message but vars.count is missing. Defaulting to 0.`);\n }\n\n const count = Number(v.count ?? 0);\n const form = count === 0 && message.zero !== undefined ? 'zero' : getPluralForm(this.#caches, locale, count);\n\n return interpolate(message[form] ?? message.other, v, locale, this.#caches);\n }\n\n #loadOne(locale: Locale): Promise<void> {\n if (this.#loading.has(locale)) return this.#loading.get(locale)!;\n\n if (this.#catalogs.has(locale)) return Promise.resolve();\n\n const loader = this.#loaders.get(locale);\n\n if (!loader) return Promise.resolve();\n\n const promise = (async () => {\n try {\n const messages = await loader(locale);\n\n // Use replace() so the loader result is the authoritative catalog for this locale,\n // not merged on top of any pre-seeded static messages.\n if (!this.#disposed) this.replace(locale, messages);\n } catch (error) {\n this.#diagnoseLoader(error, locale);\n throw error;\n } finally {\n this.#loading.delete(locale);\n }\n })();\n\n this.#loading.set(locale, promise);\n\n return promise;\n }\n}\n\nexport function createI18n<T extends Messages = Messages>(config?: I18nOptions<T>): I18n<T> {\n return new I18n<T>(config);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"i18n.cjs","names":[],"sources":["../src/i18n.ts"],"sourcesContent":["import type {\n AnyKey,\n I18n,\n I18nOptions,\n I18nSnapshot,\n Loader,\n Locale,\n LocaleSource,\n MessageBranchKeys,\n MessageLeafKeys,\n Messages,\n MissingInfo,\n PluralTranslateOptions,\n SubscribeOptions,\n TranslateVars,\n Unsubscribe,\n} from './types';\n\ntype PluralRuleSelector = { select: (count: number) => string };\ntype PluralCaches = Map<string, PluralRuleSelector>;\ntype LocaleRecord<M extends Messages> =\n | { kind: 'dynamic'; loader: Loader<M>; messages?: M }\n | { kind: 'static'; messages: M };\ntype LoadingRecord<M extends Messages> = {\n entry: Extract<LocaleRecord<M>, { kind: 'dynamic' }>;\n task: Promise<void>;\n};\n\nconst INTERPOLATION_PATTERN = /\\{([\\p{ID_Continue}\\-.]+)\\}/gu;\n\nfunction canon(locale: string): string {\n try {\n return Intl.getCanonicalLocales(locale)[0] ?? locale;\n } catch {\n return locale;\n }\n}\n\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n let value: unknown = obj;\n\n for (const part of path.split('.')) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (!Object.hasOwn(value as object, part)) return undefined;\n\n value = (value as Record<string, unknown>)[part];\n }\n\n return value;\n}\n\nfunction buildLocaleChain(locale: Locale, fallback: Locale[]): Locale[] {\n const seen = new Set<Locale>();\n\n for (const value of [locale, ...fallback]) {\n seen.add(value);\n\n const parts = value.split('-');\n\n for (let i = parts.length - 1; i > 0; i--) {\n seen.add(parts.slice(0, i).join('-'));\n }\n }\n\n return [...seen];\n}\n\nfunction selectPluralForm(cache: PluralCaches, locale: Locale, count: number, ordinal: boolean): string {\n const key = `${locale}:${ordinal ? 'ordinal' : 'cardinal'}`;\n let rules = cache.get(key);\n\n if (!rules) {\n try {\n rules = new Intl.PluralRules(locale, { type: ordinal ? 'ordinal' : 'cardinal' });\n } catch {\n rules = { select: (value: number) => (value === 1 ? 'one' : 'other') };\n }\n\n cache.set(key, rules);\n }\n\n return rules.select(count);\n}\n\nfunction interpolate(\n template: string,\n vars: TranslateVars | undefined,\n key: string,\n locale: Locale,\n onMissing: (info: MissingInfo) => string,\n): string {\n if (!template.includes('{')) return template;\n\n return template.replace(INTERPOLATION_PATTERN, (_match, varName: string) => {\n const value = vars != null ? resolvePath(vars, varName) : undefined;\n\n if (value == null) {\n return onMissing({ key, locale, type: 'var', varName });\n }\n\n return String(value);\n });\n}\n\n/** Overload: explicit type parameter (strict typing) */\nexport function createI18n<M extends Messages>(config: I18nOptions<M>): I18n<M>;\n/** Overload: no type parameter (loose typing, allows heterogeneous catalogs) */\nexport function createI18n(config?: I18nOptions<Messages>): I18n<Messages>;\nexport function createI18n<M extends Messages = Messages>(config?: I18nOptions<M>): I18n<M> {\n const cfg = (config ?? {}) as I18nOptions<M>;\n let locale = canon(cfg.locale ?? 'en');\n const fallback = Array.isArray(cfg.fallback) ? cfg.fallback.map(canon) : cfg.fallback ? [canon(cfg.fallback)] : [];\n\n const registry = new Map<Locale, LocaleRecord<M>>();\n const loading = new Map<Locale, LoadingRecord<M>>();\n const subscribers = new Set<(snapshot: I18nSnapshot) => void>();\n const pluralCache: PluralCaches = new Map();\n const onMissing =\n cfg.onMissing ??\n ((info: MissingInfo) => {\n if (info.type === 'key') return info.key;\n\n return `{${info.varName}}`;\n });\n const onSubscriberError = cfg.onSubscriberError ?? (() => {});\n\n let version = 0;\n let snapshot: I18nSnapshot = { locale, version };\n let activeChain = buildLocaleChain(locale, fallback);\n let switchId = 0;\n\n const bump = (): void => {\n version++;\n snapshot = { locale, version };\n\n const listeners = [...subscribers];\n\n for (const listener of listeners) {\n try {\n listener(snapshot);\n } catch (error) {\n onSubscriberError(error);\n }\n }\n };\n\n const addListener = (callback: (snapshot: I18nSnapshot) => void, immediate = false): Unsubscribe => {\n subscribers.add(callback);\n\n if (immediate) {\n try {\n callback(snapshot);\n } catch (error) {\n onSubscriberError(error);\n }\n }\n\n return () => subscribers.delete(callback);\n };\n\n const findMessage = (key: string): string | undefined => {\n for (const candidate of activeChain) {\n const messages = registry.get(candidate)?.messages;\n\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n\n if (typeof value === 'string') return value;\n }\n\n return undefined;\n };\n\n const setLocaleSource = (normalized: Locale, source: LocaleSource<M>): void => {\n if (typeof source === 'function') {\n registry.set(normalized, { kind: 'dynamic', loader: source as Loader<M> });\n\n return;\n }\n\n registry.set(normalized, { kind: 'static', messages: source as M });\n };\n\n const register = (loc: Locale, source: LocaleSource<M>): void => {\n const normalized = canon(loc);\n\n setLocaleSource(normalized, source);\n\n if (activeChain.includes(normalized)) bump();\n };\n\n if (cfg.catalogs) {\n for (const [loc, source] of Object.entries(cfg.catalogs)) {\n setLocaleSource(canon(loc), source as LocaleSource<M>);\n }\n }\n\n const preload = async (loc: Locale): Promise<void> => {\n const normalized = canon(loc);\n const entry = registry.get(normalized);\n\n if (!entry) {\n throw new Error(`Missing locale source for \"${normalized}\".`);\n }\n\n if (entry.kind === 'static') return;\n\n if (entry.messages) return;\n\n const active = loading.get(normalized);\n\n if (active?.entry === entry) {\n await active.task;\n\n return;\n }\n\n const task = (async () => {\n const messages = await entry.loader(normalized);\n\n // Only apply if the registry entry is still the exact object we started\n // loading from. Any call to register() replaces the entry with a new\n // object, so a stale loader result from a superseded source is discarded.\n if (registry.get(normalized) !== entry) return;\n\n entry.messages = messages;\n\n if (activeChain.includes(normalized)) bump();\n })();\n\n loading.set(normalized, { entry, task });\n\n try {\n await task;\n } finally {\n if (loading.get(normalized)?.task === task) {\n loading.delete(normalized);\n }\n }\n };\n\n function translate(key: MessageLeafKeys<M> | AnyKey, vars?: TranslateVars): string {\n const base = String(key);\n const message = findMessage(base);\n\n return message === undefined\n ? onMissing({ key: base, locale, type: 'key' })\n : interpolate(message, vars, base, locale, onMissing);\n }\n\n function translatePlural(\n key: MessageBranchKeys<M> | AnyKey,\n count: number,\n options?: PluralTranslateOptions,\n ): string {\n if (!Number.isFinite(count)) {\n throw new TypeError('`count` must be a finite number.');\n }\n\n if (options?.vars && Object.hasOwn(options.vars, 'count')) {\n throw new Error('`tp` does not allow `vars.count`; `count` is injected automatically.');\n }\n\n const base = String(key);\n const ordinal = options?.ordinal === true;\n const form = selectPluralForm(pluralCache, locale, count, ordinal);\n const selectedKey = !ordinal && count === 0 ? `${base}.zero` : `${base}.${form}`;\n const message = findMessage(selectedKey) ?? findMessage(`${base}.other`);\n\n if (message === undefined) {\n return onMissing({ key: base, locale, type: 'key' });\n }\n\n return interpolate(message, { ...(options?.vars ?? {}), count }, base, locale, onMissing);\n }\n\n return {\n getSnapshot() {\n return snapshot;\n },\n\n getSupportedLocales(options): Locale[] {\n const locales = [...registry.keys()];\n\n // Code-point sort: deterministic across all environments and locales.\n return options?.sorted === true ? locales.sort() : locales;\n },\n\n has(key: MessageLeafKeys<M> | AnyKey): boolean {\n return findMessage(String(key)) !== undefined;\n },\n\n get locale(): Locale {\n return locale;\n },\n\n preload,\n\n register,\n\n async setLocale(next: Locale): Promise<void> {\n const normalized = canon(next);\n\n if (locale === normalized) return;\n\n const id = ++switchId;\n\n await preload(normalized);\n\n if (id !== switchId) return;\n\n locale = normalized;\n activeChain = buildLocaleChain(locale, fallback);\n bump();\n },\n\n subscribe(callback: (snapshot: I18nSnapshot) => void, options?: SubscribeOptions): Unsubscribe {\n return addListener(callback, options?.immediate === true);\n },\n\n t: translate,\n tp: translatePlural,\n };\n}\n"],"mappings":"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"}
package/dist/i18n.d.ts CHANGED
@@ -1,78 +1,6 @@
1
- import type { BoundI18n, I18nOptions, Loader, Locale, LocaleChangeEvent, Messages, NamespaceKeys, TranslationKeyParam, Unsubscribe, Vars } from './types';
2
- export declare class I18n<T extends Messages = Messages> implements BoundI18n<T> {
3
- #private;
4
- constructor({ fallback, loaders, locale, messages, onDiagnostic, onMissing }?: I18nOptions<T>);
5
- get locale(): Locale;
6
- get locales(): Locale[];
7
- set locale(value: Locale);
8
- setLocale(locale: Locale): Promise<void>;
9
- /** Deep-merges messages into an existing locale catalog. */
10
- add(locale: Locale, messages: Messages): void;
11
- /** Replaces the entire locale catalog for `locale` with a deep clone of `messages`. */
12
- replace(locale: Locale, messages: Messages): void;
13
- has(key: string): boolean;
14
- /** Like `has()`, but only checks the exact locale without walking the fallback chain. */
15
- hasOwn(key: string): boolean;
16
- hasLocale(locale: Locale): boolean;
17
- load(...locales: Locale[]): Promise<void>;
18
- /**
19
- * Force-reloads a locale catalog even if already populated. Useful for hot-reload and forced bundle refresh.
20
- * No-op (with a dev warning) when no loader is registered for the locale, to prevent silently clearing the catalog.
21
- */
22
- reload(locale: Locale): Promise<void>;
23
- registerLoader(locale: Locale, loader: Loader): void;
24
- /** Returns the locale keys for which a loader has been registered. */
25
- get loadableLocales(): Locale[];
26
- /**
27
- * Translates a key with optional interpolation variables.
28
- * Locale must be loaded first via `load()` or provided via `messages` in config.
29
- * For a per-call locale override use `withLocale(locale).t(key, vars)`.
30
- */
31
- t(key: TranslationKeyParam<T>, vars?: Vars): string;
32
- number(value: number, options?: Intl.NumberFormatOptions): string;
33
- date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
34
- list(items: unknown[], type?: 'and' | 'or'): string;
35
- relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
36
- currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>): string;
37
- /**
38
- * Returns a bound interface that translates in the given locale without
39
- * changing the active locale on the instance. Useful for SSR and
40
- * multi-locale rendering in a single pass.
41
- */
42
- withLocale(locale: Locale): BoundI18n<T>;
43
- /**
44
- * Returns a translator scoped to a key namespace prefix. Reacts to locale changes on the
45
- * instance. When `T` is a concrete message type, the returned `BoundI18n` is narrowed to
46
- * the subtree type so `t()` autocomplete works within the scope.
47
- * Only keys whose values are nested message objects are valid scope targets.
48
- */
49
- scope<K extends NamespaceKeys<T>>(ns: K): BoundI18n<T[K] & Messages>;
50
- /**
51
- * Executes `fn` while deferring subscriber notifications. A single notification fires
52
- * after `fn` completes, collapsing any number of `add()` / `replace()` calls made within.
53
- * Nested `batch()` calls are supported; notification fires when the outermost batch exits.
54
- * If both a locale change and a catalog update are triggered, `'locale-change'` takes priority.
55
- *
56
- * @remarks
57
- * `batch()` is synchronous. Async operations (e.g. `load()`) started inside `fn` complete
58
- * after the batch exits and will notify subscribers individually. To batch-load multiple
59
- * locales and notify once, await `load()` before entering the batch:
60
- * ```ts
61
- * await i18n.load('fr', 'de');
62
- * i18n.batch(() => { i18n.locale = 'fr'; });
63
- * ```
64
- */
65
- batch(fn: () => void): void;
66
- subscribe(listener: (event: LocaleChangeEvent) => void, immediate?: boolean): Unsubscribe;
67
- /** Releases all resources held by this instance. */
68
- dispose(): void;
69
- /** Enables `using i18n = createI18n(...)` for deterministic resource release. */
70
- [Symbol.dispose](): void;
71
- /**
72
- * Awaits any in-flight `load()` calls and then releases all resources.
73
- * Enables `await using i18n = createI18n(...)` in environments that support `Symbol.asyncDispose`.
74
- */
75
- [Symbol.asyncDispose](): Promise<void>;
76
- }
77
- export declare function createI18n<T extends Messages = Messages>(config?: I18nOptions<T>): I18n<T>;
1
+ import type { I18n, I18nOptions, Messages } from './types';
2
+ /** Overload: explicit type parameter (strict typing) */
3
+ export declare function createI18n<M extends Messages>(config: I18nOptions<M>): I18n<M>;
4
+ /** Overload: no type parameter (loose typing, allows heterogeneous catalogs) */
5
+ export declare function createI18n(config?: I18nOptions<Messages>): I18n<Messages>;
78
6
  //# sourceMappingURL=i18n.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,IAAI,EACJ,WAAW,EAOX,QAAQ,EAMT,MAAM,SAAS,CAAC;AAyFjB,wDAAwD;AACxD,wBAAgB,UAAU,CAAC,CAAC,SAAS,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAChF,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC"}