@vielzeug/i18nit 2.1.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.
- package/README.md +145 -51
- package/dist/format.d.ts +54 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/i18n.cjs +1 -1
- package/dist/i18n.cjs.map +1 -1
- package/dist/i18n.d.ts +4 -2
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +156 -165
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +62 -103
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -12
- package/dist/helpers.cjs +0 -2
- package/dist/helpers.cjs.map +0 -1
- package/dist/helpers.d.ts +0 -20
- package/dist/helpers.d.ts.map +0 -1
- package/dist/helpers.js +0 -47
- package/dist/helpers.js.map +0 -1
- package/dist/i18nit.cjs +0 -2
- package/dist/i18nit.cjs.map +0 -1
- package/dist/i18nit.js +0 -2
- package/dist/i18nit.js.map +0 -1
- package/dist/interpolate.cjs +0 -2
- package/dist/interpolate.cjs.map +0 -1
- package/dist/interpolate.d.ts +0 -11
- package/dist/interpolate.d.ts.map +0 -1
- package/dist/interpolate.js +0 -13
- package/dist/interpolate.js.map +0 -1
- package/dist/intl.cjs +0 -2
- package/dist/intl.cjs.map +0 -1
- package/dist/intl.d.ts +0 -16
- package/dist/intl.d.ts.map +0 -1
- package/dist/intl.js +0 -65
- package/dist/intl.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,84 +1,178 @@
|
|
|
1
1
|
# @vielzeug/i18nit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@vielzeug/i18nit) [](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`, 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
|
-
|
|
33
|
-
|
|
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: {
|
|
40
|
-
|
|
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.
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
166
|
+
:::
|
|
68
167
|
|
|
69
|
-
|
|
70
|
-
- `type BoundI18n<T>`
|
|
71
|
-
- `type I18n<T>`
|
|
72
|
-
- `type I18nOptions<T>`
|
|
73
|
-
- `type Messages`, `TranslationKey`, `TranslationKeyParam`, `PluralKeys`, `NamespaceKeys`
|
|
168
|
+
For more complete framework samples, see:
|
|
74
169
|
|
|
75
|
-
|
|
170
|
+
- `docs/i18nit/examples/framework-integration.md`
|
|
76
171
|
|
|
77
|
-
|
|
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
|
-
|
|
174
|
+
Formatting lives in `@vielzeug/i18nit/format` and can bind to:
|
|
83
175
|
|
|
84
|
-
|
|
176
|
+
- a static locale string
|
|
177
|
+
- an i18n-like source with `locale`
|
|
178
|
+
- a getter function that returns a locale
|
package/dist/format.d.ts
ADDED
|
@@ -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
|
-
|
|
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":[],"sources":["../src/i18n.ts"],"sourcesContent":["import type {\n BoundI18n,\n I18n,\n I18nOptions,\n Loader,\n Locale,\n LocaleChangeEvent,\n LocaleChangeReason,\n Messages,\n MessageValue,\n NamespaceKeys,\n SwitchMode,\n Unsubscribe,\n Vars,\n} from './types';\n\nimport { BoundedMap, deepMerge, isMessageValue, resolvePath } from './helpers';\nimport { interpolate } from './interpolate';\nimport {\n formatDate,\n formatList,\n formatNumber,\n formatRelative,\n getPluralForm,\n type IntlCaches,\n makeIntlCaches,\n} from './intl';\n\nexport function createI18n<T extends Messages = Messages>(config: I18nOptions<T> = {}): I18n<T> {\n let locale = config.locale ?? 'en';\n const defaultSwitchMode: SwitchMode = config.switchMode ?? 'strict';\n const fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n const catalogs = new Map<Locale, Messages>();\n const loaders = new Map<Locale, Loader>();\n const loading = new Map<Locale, Promise<void>>();\n const subscribers = new Set<(event: LocaleChangeEvent) => void>();\n const chainCache = new BoundedMap<Locale, Locale[]>(128);\n const caches: IntlCaches = makeIntlCaches();\n\n let localesCache: Locale[] | null = null;\n let loadersCache: Locale[] | null = null;\n let disposed = false;\n let batchDepth = 0;\n let pendingNotify: LocaleChangeReason | null = null;\n\n const onMissing = config.onMissing;\n const onDiagnostic = config.onDiagnostic;\n\n if (config.messages) {\n for (const [loc, messages] of Object.entries(config.messages)) {\n catalogs.set(loc, structuredClone(messages) as Messages);\n }\n }\n\n if (config.loaders) {\n for (const [loc, loader] of Object.entries(config.loaders)) {\n loaders.set(loc, loader);\n }\n }\n\n function diagnoseSubscriber(error: unknown): void {\n if (onDiagnostic) {\n onDiagnostic({ error, kind: 'subscriber-error' });\n } else {\n console.error('[i18nit] Subscriber threw:', error);\n }\n }\n\n function diagnoseLoader(error: unknown, loc: Locale): void {\n if (onDiagnostic) {\n onDiagnostic({ error, kind: 'loader-error', locale: loc });\n } else {\n console.warn('[i18nit] Loader error:', error);\n }\n }\n\n function notify(reason: LocaleChangeReason): void {\n if (batchDepth > 0) {\n if (pendingNotify !== 'locale-change') pendingNotify = reason;\n\n return;\n }\n\n const event: LocaleChangeEvent = { locale, reason };\n\n for (const listener of subscribers) {\n try {\n listener(event);\n } catch (error) {\n diagnoseSubscriber(error);\n }\n }\n }\n\n function getLocaleChain(loc: Locale): Locale[] {\n const cached = chainCache.get(loc);\n\n if (cached) return cached;\n\n const seen = new Set<Locale>();\n\n const push = (value: Locale) => {\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 push(loc);\n for (const fallback of fallbacks) push(fallback);\n\n const chain = [...seen];\n\n chainCache.set(loc, chain);\n\n return chain;\n }\n\n function checkOwn(key: string, loc: Locale): boolean {\n const catalog = catalogs.get(loc);\n\n if (!catalog) return false;\n\n const value = resolvePath(catalog, key);\n\n return value !== undefined && isMessageValue(value);\n }\n\n function findMessage(key: string, loc: Locale): MessageValue | undefined {\n for (const localeInChain of getLocaleChain(loc)) {\n const messages = catalogs.get(localeInChain);\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 function translate(key: string, vars: Vars | undefined, loc: Locale): string {\n const message = findMessage(key, loc);\n\n if (message === undefined) return onMissing?.(key, loc) ?? key;\n\n if (typeof message === 'string') {\n return interpolate(message, vars ?? {}, loc, caches);\n }\n\n const context = vars ?? {};\n\n if (import.meta.env?.DEV && context.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(context.count ?? 0);\n const form = count === 0 && message.zero !== undefined ? 'zero' : getPluralForm(caches, loc, count);\n\n return interpolate(message[form] ?? message.other, context, loc, caches);\n }\n\n function loadOne(loc: Locale, mode: SwitchMode): Promise<void> {\n if (loading.has(loc)) return loading.get(loc)!;\n\n if (catalogs.has(loc)) return Promise.resolve();\n\n const loader = loaders.get(loc);\n\n if (!loader) {\n if (mode === 'strict') {\n return Promise.reject(new Error(`[i18nit] Missing loader for locale \"${loc}\".`));\n }\n\n return Promise.resolve();\n }\n\n const promise = (async () => {\n try {\n const messages = await loader(loc);\n\n if (!disposed) api.replace(loc, messages);\n } catch (error) {\n diagnoseLoader(error, loc);\n throw error;\n } finally {\n loading.delete(loc);\n }\n })();\n\n loading.set(loc, promise);\n\n return promise;\n }\n\n function createView<U extends Messages = Messages>(fixedLocale: Locale | null, prefix?: string): BoundI18n<U> {\n const activeLocale = (): Locale => fixedLocale ?? locale;\n const keyWithPrefix = (key: string): string => (prefix ? `${prefix}.${key}` : key);\n const t: BoundI18n<U>['t'] = (key: NamespaceKeys<U>, vars?: Record<string, unknown>) =>\n translate(keyWithPrefix(key as string), vars, activeLocale());\n\n const view = {\n currency(\n value: number,\n currency: string,\n options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>,\n ): string {\n return formatNumber(caches, value, { ...options, currency, style: 'currency' }, activeLocale());\n },\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string {\n return formatDate(caches, value, options, activeLocale());\n },\n has(key: string): boolean {\n return findMessage(keyWithPrefix(key), activeLocale()) !== undefined;\n },\n hasOwn(key: string): boolean {\n return checkOwn(keyWithPrefix(key), activeLocale());\n },\n list(items: unknown[], type: 'and' | 'or' = 'and'): string {\n return formatList(caches, items, activeLocale(), type);\n },\n get locale(): Locale {\n return activeLocale();\n },\n number(value: number, options?: Intl.NumberFormatOptions): string {\n return formatNumber(caches, value, options, activeLocale());\n },\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string {\n return formatRelative(caches, value, unit, options, activeLocale());\n },\n scope<K extends NamespaceKeys<U>>(ns: K): BoundI18n<U[K] & Messages> {\n const nextPrefix = prefix ? `${prefix}.${String(ns)}` : String(ns);\n\n return createView<U[K] & Messages>(fixedLocale, nextPrefix);\n },\n t,\n withLocale(nextLocale: Locale): BoundI18n<U> {\n return createView<U>(nextLocale, prefix);\n },\n } satisfies BoundI18n<U>;\n\n return view;\n }\n\n const rootView = createView<T>(null);\n\n const api = Object.create(rootView) as I18n<T>;\n\n api.add = (loc: Locale, messages: Messages): void => {\n const existing = catalogs.get(loc) ?? {};\n\n catalogs.set(loc, deepMerge(existing, messages));\n localesCache = null;\n\n if (getLocaleChain(locale).includes(loc)) notify('catalog-update');\n };\n\n api.batch = (fn: () => void): void => {\n batchDepth++;\n\n try {\n fn();\n } finally {\n batchDepth--;\n\n if (batchDepth === 0 && pendingNotify !== null) {\n const reason = pendingNotify;\n\n pendingNotify = null;\n notify(reason);\n }\n }\n };\n\n api.dispose = (): void => {\n disposed = true;\n subscribers.clear();\n catalogs.clear();\n loaders.clear();\n loading.clear();\n chainCache.clear();\n localesCache = null;\n loadersCache = null;\n };\n\n api.ensureLocale = async (loc: Locale, mode: SwitchMode = defaultSwitchMode): Promise<void> => {\n await loadOne(loc, mode);\n };\n\n api.hasLocale = (loc: Locale): boolean => catalogs.has(loc);\n api.isReady = (loc: Locale): boolean => catalogs.has(loc);\n\n Object.defineProperties(api, {\n loadableLocales: {\n get(): Locale[] {\n loadersCache ??= [...loaders.keys()];\n\n return loadersCache;\n },\n },\n locales: {\n get(): Locale[] {\n localesCache ??= [...catalogs.keys()];\n\n return localesCache;\n },\n },\n });\n\n api.registerLoader = (loc: Locale, loader: Loader): void => {\n loaders.set(loc, loader);\n loadersCache = null;\n };\n\n api.reload = async (loc: Locale): Promise<void> => {\n if (!loaders.has(loc)) {\n throw new Error(`[i18nit] Cannot reload locale \"${loc}\" without a registered loader.`);\n }\n\n catalogs.delete(loc);\n localesCache = null;\n await loadOne(loc, 'strict');\n };\n\n api.replace = (loc: Locale, messages: Messages): void => {\n catalogs.set(loc, structuredClone(messages));\n localesCache = null;\n\n if (getLocaleChain(locale).includes(loc)) notify('catalog-update');\n };\n\n api.subscribe = (listener: (event: LocaleChangeEvent) => void, immediate?: boolean): Unsubscribe => {\n subscribers.add(listener);\n\n if (immediate) {\n try {\n listener({ locale, reason: 'locale-change' });\n } catch (error) {\n diagnoseSubscriber(error);\n }\n }\n\n return () => subscribers.delete(listener);\n };\n\n api.switchLocale = async (nextLocale: Locale, mode: SwitchMode = defaultSwitchMode): Promise<void> => {\n if (nextLocale === locale) return;\n\n await loadOne(nextLocale, mode);\n locale = nextLocale;\n notify('locale-change');\n };\n\n api[Symbol.asyncDispose] = async (): Promise<void> => {\n await Promise.allSettled([...loading.values()]);\n api.dispose();\n };\n\n api[Symbol.dispose] = (): void => {\n api.dispose();\n };\n\n return api;\n}\n\nexport type { I18n };\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,4 +1,6 @@
|
|
|
1
1
|
import type { I18n, I18nOptions, Messages } from './types';
|
|
2
|
-
|
|
3
|
-
export
|
|
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>;
|
|
4
6
|
//# sourceMappingURL=i18n.d.ts.map
|
package/dist/i18n.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,IAAI,EACJ,WAAW,
|
|
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"}
|