@vielzeug/i18nit 1.1.4 → 1.2.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.
- package/README.md +177 -63
- package/dist/i18nit.cjs +1 -1
- package/dist/i18nit.cjs.map +1 -1
- package/dist/i18nit.js +82 -69
- package/dist/i18nit.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,35 +1,86 @@
|
|
|
1
1
|
# @vielzeug/i18nit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What is I18nit?
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**I18nit** is a type-safe, lightweight internationalization library for TypeScript. Build multilingual applications with powerful pluralization, nested translations, and lazy loading—all in just 1.6 KB.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- ✅ **Lightweight** - 1.6 KB gzipped with zero dependencies
|
|
9
|
-
- ✅ **Universal Pluralization** - 100+ languages via Intl.PluralRules API
|
|
10
|
-
- ✅ **Smart Array Handling** - Auto-join with separators, length access, and safe indexing
|
|
11
|
-
- ✅ **Path Interpolation** - Support for nested objects and array indices
|
|
12
|
-
- ✅ **Lazy Loading** - Async locale loading with automatic caching
|
|
13
|
-
- ✅ **Namespaces** - Organize translations by feature or module
|
|
14
|
-
- ✅ **Fallback Chain** - Multiple fallback locales with automatic language variants
|
|
15
|
-
- ✅ **HTML Escaping** - Built-in XSS protection
|
|
16
|
-
- ✅ **Number & Date Formatting** - Locale-aware formatting with Intl API
|
|
17
|
-
- ✅ **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
|
|
7
|
+
### The Problem
|
|
18
8
|
|
|
19
|
-
|
|
9
|
+
Internationalization libraries are often heavy and complex:
|
|
10
|
+
|
|
11
|
+
- **i18next** is feature-rich but adds 11KB+ to your bundle
|
|
12
|
+
- **react-intl** is React-specific and requires setup
|
|
13
|
+
- **FormatJS** has a steep learning curve
|
|
14
|
+
- Manual translations lead to missing keys and runtime errors
|
|
15
|
+
- Type safety requires extra tooling
|
|
16
|
+
|
|
17
|
+
### The Solution
|
|
18
|
+
|
|
19
|
+
I18nit provides a simple, type-safe API using native browser APIs:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { createI18n } from '@vielzeug/i18nit';
|
|
23
|
+
|
|
24
|
+
const i18n = createI18n({
|
|
25
|
+
locale: 'en',
|
|
26
|
+
messages: {
|
|
27
|
+
en: {
|
|
28
|
+
welcome: 'Welcome, {name}!',
|
|
29
|
+
items: 'You have {count} item | You have {count} items',
|
|
30
|
+
},
|
|
31
|
+
es: {
|
|
32
|
+
welcome: '¡Bienvenido, {name}!',
|
|
33
|
+
items: 'Tienes {count} artículo | Tienes {count} artículos',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Interpolation
|
|
39
|
+
i18n.t('welcome', { name: 'Alice' }); // "Welcome, Alice!"
|
|
40
|
+
|
|
41
|
+
// Automatic pluralization
|
|
42
|
+
i18n.t('items', { count: 1 }); // "You have 1 item"
|
|
43
|
+
i18n.t('items', { count: 5 }); // "You have 5 items"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## ✨ Features
|
|
47
|
+
|
|
48
|
+
- ✅ **Type-Safe** – Full TypeScript support with generic types
|
|
49
|
+
- ✅ **Lightweight** – 1.6 KB gzipped with zero dependencies
|
|
50
|
+
- ✅ **Universal Pluralization** – 100+ languages via Intl.PluralRules API
|
|
51
|
+
- ✅ **Smart Array Handling** – Auto-join with separators, length access, and safe indexing
|
|
52
|
+
- ✅ **Path Interpolation** – Support for nested objects and array indices
|
|
53
|
+
- ✅ **Lazy Loading** – Async locale loading with automatic caching
|
|
54
|
+
- ✅ **Namespaces** – Organize translations by feature or module
|
|
55
|
+
- ✅ **Fallback Chain** – Multiple fallback locales with automatic language variants
|
|
56
|
+
- ✅ **HTML Escaping** – Built-in XSS protection
|
|
57
|
+
- ✅ **Number & Date Formatting** – Locale-aware formatting with Intl API
|
|
58
|
+
- ✅ **Framework Agnostic** – Works with React, Vue, Svelte, or vanilla JS
|
|
59
|
+
|
|
60
|
+
## 🆚 Comparison with Alternatives
|
|
61
|
+
|
|
62
|
+
| Feature | I18nit | i18next | react-intl | FormatJS |
|
|
63
|
+
| ------------------- | -------------- | ------- | ---------- | ------------ |
|
|
64
|
+
| Bundle Size (gzip) | **~1.6 KB** | ~11KB | ~14KB | ~14KB |
|
|
65
|
+
| TypeScript Support | ✅ First-class | ✅ Good | ✅ Good | ✅ Excellent |
|
|
66
|
+
| Pluralization | ✅ Native Intl | ✅ ICU | ✅ ICU | ✅ ICU |
|
|
67
|
+
| Nested Translations | ✅ Built-in | ✅ Yes | ⚠️ Limited | ✅ Yes |
|
|
68
|
+
| Lazy Loading | ✅ Async | ✅ Yes | ⚠️ Manual | ✅ Yes |
|
|
69
|
+
| Framework Agnostic | ✅ Yes | ✅ Yes | ❌ React | ❌ React |
|
|
70
|
+
| Dependencies | 0 | 3 | 5 | 7 |
|
|
71
|
+
|
|
72
|
+
## 📦 Installation
|
|
20
73
|
|
|
21
74
|
```bash
|
|
22
75
|
# pnpm
|
|
23
76
|
pnpm add @vielzeug/i18nit
|
|
24
|
-
|
|
25
77
|
# npm
|
|
26
78
|
npm install @vielzeug/i18nit
|
|
27
|
-
|
|
28
79
|
# yarn
|
|
29
80
|
yarn add @vielzeug/i18nit
|
|
30
81
|
```
|
|
31
82
|
|
|
32
|
-
## Quick Start
|
|
83
|
+
## 🚀 Quick Start
|
|
33
84
|
|
|
34
85
|
```typescript
|
|
35
86
|
import { createI18n } from '@vielzeug/i18nit';
|
|
@@ -70,7 +121,7 @@ i18n.setLocale('es');
|
|
|
70
121
|
i18n.t('greeting', { name: 'Mundo' }); // "¡Hola, Mundo!"
|
|
71
122
|
```
|
|
72
123
|
|
|
73
|
-
## Core Concepts
|
|
124
|
+
## 📚 Core Concepts
|
|
74
125
|
|
|
75
126
|
### Translation Keys
|
|
76
127
|
|
|
@@ -90,6 +141,52 @@ const i18n = createI18n({
|
|
|
90
141
|
i18n.t('user.profile.title'); // "Profile"
|
|
91
142
|
```
|
|
92
143
|
|
|
144
|
+
### Nested Message Objects
|
|
145
|
+
|
|
146
|
+
You can organize messages using nested objects for better structure:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const i18n = createI18n({
|
|
150
|
+
locale: 'en',
|
|
151
|
+
messages: {
|
|
152
|
+
en: {
|
|
153
|
+
// Flat structure
|
|
154
|
+
welcome: 'Welcome!',
|
|
155
|
+
|
|
156
|
+
// Nested structure - access with dot notation
|
|
157
|
+
user: {
|
|
158
|
+
greeting: 'Hello, {name}!',
|
|
159
|
+
profile: {
|
|
160
|
+
title: 'User Profile',
|
|
161
|
+
settings: 'Profile Settings',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// Deep nesting
|
|
166
|
+
app: {
|
|
167
|
+
navigation: {
|
|
168
|
+
menu: {
|
|
169
|
+
home: 'Home',
|
|
170
|
+
about: 'About',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Access nested messages with dot notation
|
|
179
|
+
i18n.t('welcome'); // "Welcome!"
|
|
180
|
+
i18n.t('user.greeting', { name: 'Alice' }); // "Hello, Alice!"
|
|
181
|
+
i18n.t('user.profile.title'); // "User Profile"
|
|
182
|
+
i18n.t('app.navigation.menu.home'); // "Home"
|
|
183
|
+
|
|
184
|
+
// Use with namespaces for cleaner code
|
|
185
|
+
const userNs = i18n.namespace('user');
|
|
186
|
+
userNs.t('greeting', { name: 'Bob' }); // "Hello, Bob!"
|
|
187
|
+
userNs.t('profile.title'); // "User Profile"
|
|
188
|
+
```
|
|
189
|
+
|
|
93
190
|
### Variable Interpolation
|
|
94
191
|
|
|
95
192
|
#### Basic Interpolation
|
|
@@ -137,7 +234,7 @@ const i18n = createI18n({
|
|
|
137
234
|
i18n.t('shopping', { items: ['Apple', 'Banana', 'Orange'] });
|
|
138
235
|
// "Shopping list: Apple, Banana, Orange"
|
|
139
236
|
|
|
140
|
-
// Natural "and" lists (locale-aware via Intl.ListFormat
|
|
237
|
+
// Natural "and" lists (locale-aware via Intl.ListFormat – supports 100+ languages automatically)
|
|
141
238
|
i18n.t('guests', { names: ['Alice'] });
|
|
142
239
|
// "Invited: Alice"
|
|
143
240
|
i18n.t('guests', { names: ['Alice', 'Bob'] });
|
|
@@ -145,7 +242,7 @@ i18n.t('guests', { names: ['Alice', 'Bob'] });
|
|
|
145
242
|
i18n.t('guests', { names: ['Alice', 'Bob', 'Charlie'] });
|
|
146
243
|
// "Invited: Alice, Bob, and Charlie" (Oxford comma in English)
|
|
147
244
|
|
|
148
|
-
// Natural "or" lists (locale-aware via Intl.ListFormat
|
|
245
|
+
// Natural "or" lists (locale-aware via Intl.ListFormat – supports 100+ languages automatically)
|
|
149
246
|
i18n.t('options', { choices: ['Tea', 'Coffee', 'Juice'] });
|
|
150
247
|
// "Choose: Tea, Coffee, or Juice"
|
|
151
248
|
|
|
@@ -160,21 +257,21 @@ i18n.t('count', { items: ['A', 'B', 'C'] });
|
|
|
160
257
|
|
|
161
258
|
**Array Features:**
|
|
162
259
|
|
|
163
|
-
- `{items}`
|
|
164
|
-
- `{items|and}`
|
|
165
|
-
- `{items|or}`
|
|
166
|
-
- `{items|
|
|
167
|
-
- `{items.length}`
|
|
168
|
-
- `{items[0]}`
|
|
260
|
+
- `{items}` – Join with comma (`, `)
|
|
261
|
+
- `{items|and}` – Natural "and" list with locale-aware conjunction (uses Intl.ListFormat – supports 100+ languages)
|
|
262
|
+
- `{items|or}` – Natural "or" list with locale-aware conjunction (uses Intl.ListFormat – supports 100+ languages)
|
|
263
|
+
- `{items| – }` – Custom separator (e.g., "A – B – C")
|
|
264
|
+
- `{items.length}` – Array length
|
|
265
|
+
- `{items[0]}` – Safe index access (returns empty if out of bounds)
|
|
169
266
|
|
|
170
267
|
**Locale-Aware List Formatting:**
|
|
171
268
|
The `and` and `or` separators use the built-in **Intl.ListFormat API** which automatically handles:
|
|
172
269
|
|
|
173
|
-
- **100+ languages**
|
|
174
|
-
- **Proper grammar**
|
|
175
|
-
- **Right-to-left languages**
|
|
176
|
-
- **Unicode CLDR standards**
|
|
177
|
-
- **No manual configuration**
|
|
270
|
+
- **100+ languages** – Supports all languages available in the browser/runtime
|
|
271
|
+
- **Proper grammar** – Oxford comma, locale-specific punctuation
|
|
272
|
+
- **Right-to-left languages** – Arabic, Hebrew, etc.
|
|
273
|
+
- **Unicode CLDR standards** – International standard for list formatting
|
|
274
|
+
- **No manual configuration** – Zero maintenance required
|
|
178
275
|
|
|
179
276
|
Examples across languages:
|
|
180
277
|
|
|
@@ -188,13 +285,13 @@ Examples across languages:
|
|
|
188
285
|
|
|
189
286
|
#### Supported Path Formats
|
|
190
287
|
|
|
191
|
-
- `{name}`
|
|
192
|
-
- `{user.name}`
|
|
193
|
-
- `{items[0]}`
|
|
194
|
-
- `{items}`
|
|
195
|
-
- `{items|and}`
|
|
196
|
-
- `{items.length}`
|
|
197
|
-
- `{data.items[0].value}`
|
|
288
|
+
- `{name}` – Simple variable
|
|
289
|
+
- `{user.name}` – Nested object property
|
|
290
|
+
- `{items[0]}` – Array index (safe – returns empty if out of bounds)
|
|
291
|
+
- `{items}` – Array join with default separator
|
|
292
|
+
- `{items|and}` – Array join with "and"
|
|
293
|
+
- `{items.length}` – Array length
|
|
294
|
+
- `{data.items[0].value}` – Mixed notation
|
|
198
295
|
|
|
199
296
|
**Limitations:**
|
|
200
297
|
|
|
@@ -252,7 +349,7 @@ i18nit uses the browser's built-in `Intl.PluralRules` API to automatically suppo
|
|
|
252
349
|
- **Japanese (ja)**: other
|
|
253
350
|
- And 90+ more languages...
|
|
254
351
|
|
|
255
|
-
## Advanced Features
|
|
352
|
+
## 🔥 Advanced Features
|
|
256
353
|
|
|
257
354
|
### Fallback Locales
|
|
258
355
|
|
|
@@ -284,20 +381,39 @@ i18n.t('welcome'); // "Welcome!" (en fallback)
|
|
|
284
381
|
|
|
285
382
|
### Async Locale Loading
|
|
286
383
|
|
|
287
|
-
Load translations on-demand for better performance:
|
|
384
|
+
Load translations on-demand for better performance. Loaders receive the locale as a parameter, allowing you to reuse a single function:
|
|
288
385
|
|
|
289
386
|
```typescript
|
|
387
|
+
// Define a reusable loader function
|
|
388
|
+
const loadLocale = async (locale: string) => {
|
|
389
|
+
const response = await fetch(`/locales/${locale}.json`);
|
|
390
|
+
return response.json();
|
|
391
|
+
};
|
|
392
|
+
|
|
290
393
|
const i18n = createI18n({
|
|
291
394
|
locale: 'en',
|
|
292
395
|
loaders: {
|
|
293
|
-
fr:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
},
|
|
297
|
-
de: async () => import('./locales/de.json'),
|
|
396
|
+
fr: loadLocale, // Loader receives 'fr' as parameter
|
|
397
|
+
de: loadLocale, // Loader receives 'de' as parameter
|
|
398
|
+
es: loadLocale, // Loader receives 'es' as parameter
|
|
298
399
|
},
|
|
299
400
|
});
|
|
300
401
|
|
|
402
|
+
// Load a locale before using it
|
|
403
|
+
await i18n.load('fr');
|
|
404
|
+
i18n.setLocale('fr');
|
|
405
|
+
i18n.t('greeting'); // Uses loaded French messages
|
|
406
|
+
|
|
407
|
+
// Or use dynamic imports
|
|
408
|
+
const importLoader = async (locale: string) => {
|
|
409
|
+
const module = await import(`./locales/${locale}.json`);
|
|
410
|
+
return module.default;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
i18n.register('it', importLoader);
|
|
414
|
+
await i18n.load('it');
|
|
415
|
+
```
|
|
416
|
+
|
|
301
417
|
// Preload at app startup
|
|
302
418
|
await i18n.loadAll(['en', 'fr', 'de']);
|
|
303
419
|
|
|
@@ -458,8 +574,8 @@ i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With opt
|
|
|
458
574
|
|
|
459
575
|
**Options:**
|
|
460
576
|
|
|
461
|
-
- `locale?: string`
|
|
462
|
-
- `escape?: boolean`
|
|
577
|
+
- `locale?: string` – Override locale for this translation
|
|
578
|
+
- `escape?: boolean` – Override HTML escaping
|
|
463
579
|
|
|
464
580
|
### Locale Management
|
|
465
581
|
|
|
@@ -713,30 +829,28 @@ const config: I18nConfig = {
|
|
|
713
829
|
const i18n = createI18n(config);
|
|
714
830
|
```
|
|
715
831
|
|
|
716
|
-
##
|
|
832
|
+
## 📖 Documentation
|
|
717
833
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
| TypeScript | ✅ First-class | ✅ Good | ✅ Good |
|
|
723
|
-
| Pluralization | ✅ Built-in | ✅ Plugin | ✅ Built-in |
|
|
724
|
-
| Async Loading | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
|
|
725
|
-
| Path Interpolation | ✅ `{user.name}` | ❌ | ❌ |
|
|
726
|
-
| HTML Escaping | ✅ Built-in | ⚠️ Manual | ✅ Built-in |
|
|
727
|
-
| Framework Agnostic | ✅ | ✅ | ❌ React only |
|
|
834
|
+
- [**Full Documentation**](https://helmuthdu.github.io/vielzeug/i18nit)
|
|
835
|
+
- [**Usage Guide**](https://helmuthdu.github.io/vielzeug/i18nit/usage)
|
|
836
|
+
- [**API Reference**](https://helmuthdu.github.io/vielzeug/i18nit/api)
|
|
837
|
+
- [**Examples**](https://helmuthdu.github.io/vielzeug/i18nit/examples)
|
|
728
838
|
|
|
729
|
-
## License
|
|
839
|
+
## 📄 License
|
|
730
840
|
|
|
731
841
|
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
|
|
732
842
|
|
|
733
|
-
##
|
|
843
|
+
## 🤝 Contributing
|
|
844
|
+
|
|
845
|
+
Contributions are welcome! Check our [GitHub repository](https://github.com/helmuthdu/vielzeug).
|
|
846
|
+
|
|
847
|
+
## 🔗 Links
|
|
734
848
|
|
|
735
849
|
- [GitHub Repository](https://github.com/helmuthdu/vielzeug)
|
|
736
|
-
- [Documentation](https://vielzeug
|
|
737
|
-
- [NPM Package](https://www.npmjs.com/package/@vielzeug/
|
|
850
|
+
- [Documentation](https://helmuthdu.github.io/vielzeug/deposit)
|
|
851
|
+
- [NPM Package](https://www.npmjs.com/package/@vielzeug/deposit)
|
|
738
852
|
- [Issue Tracker](https://github.com/helmuthdu/vielzeug/issues)
|
|
739
853
|
|
|
740
854
|
---
|
|
741
855
|
|
|
742
|
-
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem
|
|
856
|
+
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem – A collection of type-safe utilities for modern web development.
|
package/dist/i18nit.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function g(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function h(a,t){if(t in a)return a[t];const e=t.match(/[^.[\]]+/g)||[];let r=a;for(const s of e){if(r==null||typeof r!="object")return;if(Array.isArray(r)){const n=Number(s);if(Number.isNaN(n)||n<0||n>=r.length)return;r=r[n]}else r=r[s]}return r}function l(a,t,e){if(a.length===0)return"";const r=a.map(String);try{return new Intl.ListFormat(t,{style:"long",type:e}).format(r)}catch{if(r.length===1)return r[0];if(r.length===2){const o=e==="conjunction"?"and":"or";return`${r[0]} ${o} ${r[1]}`}const s=e==="conjunction"?"and":"or",n=r.pop();return`${r.join(", ")} ${s} ${n}`}}function u(a,t,e){return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g,(r,s,n)=>{const o=s.endsWith(".length"),c=o?s.slice(0,-7):s,i=h(t,c);if(i==null)return"";if(Array.isArray(i))return o?String(i.length):n!==void 0?n==="and"?l(i,e,"conjunction"):n==="or"?l(i,e,"disjunction"):i.map(String).join(n):i.map(String).join(", ");if(typeof i=="number")try{return new Intl.NumberFormat(e).format(i)}catch{return String(i)}return String(i)})}function d(a,t){const e=Math.abs(Math.floor(t));try{return new Intl.PluralRules(a).select(e)}catch{return e===1?"one":"other"}}class f{locale;fallbacks;escape;catalogs=new Map;loaders=new Map;loading=new Map;subscribers=new Set;constructor(t={}){if(this.locale=t.locale??"en",this.fallbacks=Array.isArray(t.fallback)?t.fallback:t.fallback?[t.fallback]:[],this.escape=t.escape??!1,t.messages)for(const[e,r]of Object.entries(t.messages))this.catalogs.set(e,r);if(t.loaders)for(const[e,r]of Object.entries(t.loaders))this.loaders.set(e,r)}setLocale(t){this.locale!==t&&(this.locale=t,this.notifySubscribers())}getLocale(){return this.locale}add(t,e){const r=this.catalogs.get(t)??{};this.catalogs.set(t,{...r,...e}),this.notifySubscribers()}set(t,e){this.catalogs.set(t,e),this.notifySubscribers()}getMessages(t){return this.catalogs.get(t)}hasLocale(t){return this.catalogs.has(t)}has(t,e){return this.findMessage(t,e??this.locale)!==void 0}async load(t){if(this.loading.has(t))return this.loading.get(t);if(this.catalogs.has(t))return;const e=this.loaders.get(t);if(!e)return;const r=(async()=>{try{const s=await e(t);this.add(t,s)}catch(s){throw console.warn(`[I18n] Failed to load locale '${t}':`,s),s}finally{this.loading.delete(t)}})();return this.loading.set(t,r),r}register(t,e){this.loaders.set(t,e)}async hasAsync(t,e){const r=e??this.locale;return!this.catalogs.has(r)&&this.loaders.has(r)&&await this.load(r),this.has(t,r)}async loadAll(t){await Promise.all(t.map(e=>this.load(e)))}t(t,e,r){const s=r?.locale??this.locale,n=r?.escape??this.escape,o=this.findMessage(t,s);if(o===void 0)return t;const c=this.formatMessage(o,e??{},s);return n?g(c):c}number(t,e,r){try{return new Intl.NumberFormat(r??this.locale,e).format(t)}catch{return String(t)}}date(t,e,r){const s=typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(r??this.locale,e).format(s)}catch{return s.toString()}}namespace(t){return{t:(e,r,s)=>this.t(`${t}.${e}`,r,s)}}subscribe(t){this.subscribers.add(t);try{t(this.locale)}catch{}return()=>this.subscribers.delete(t)}notifySubscribers(){for(const t of this.subscribers)try{t(this.locale)}catch{}}findMessage(t,e){const r=this.getLocaleChain(e);for(const s of r){const n=this.catalogs.get(s);if(!n)continue;const o=h(n,t);if(o!==void 0&&this.isMessageValue(o))return o}}isMessageValue(t){if(typeof t=="string")return!0;if(typeof t=="object"&&t!==null&&!Array.isArray(t)){const e=t;return"other"in e&&typeof e.other=="string"?Object.values(e).every(r=>typeof r=="string"):!1}return!1}getLocaleChain(t){const e=[t],r=t.split("-")[0];r!==t&&e.push(r);for(const s of this.fallbacks){e.push(s);const n=s.split("-")[0];n!==s&&e.push(n)}return e}formatMessage(t,e,r){if(typeof t=="object"&&"other"in t){const s=Number(e.count??0),n=t;let o;s===0&&n.zero!==void 0?o="zero":o=d(r,s);const c=n[o]??n.other;return u(c,e,r)}return typeof t=="string"?u(t,e,r):""}}function b(a){return new f(a)}exports.I18n=f;exports.createI18n=b;
|
|
2
2
|
//# sourceMappingURL=i18nit.cjs.map
|
package/dist/i18nit.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18nit.cjs","sources":["../src/i18nit.ts"],"sourcesContent":["/* ============================================\n i18nit - Lightweight, type-safe i18n library\n ============================================ */\n\n/* -------------------- Core Types -------------------- */\n\nexport type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageValue = string | PluralMessages;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateOptions = {\n locale?: Locale;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, () => Promise<Messages>>;\n escape?: boolean;\n};\n\n/* -------------------- Path Resolution -------------------- */\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Resolves nested properties using dot notation and bracket notation.\n * Supports: 'user.name', 'items[0]', 'user.items[0].name'\n */\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n // Try direct access first (handles keys with dots)\n if (path in obj) return obj[path];\n\n // Parse path segments\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (Array.isArray(value)) {\n const index = Number(part);\n if (Number.isNaN(index) || index < 0 || index >= value.length) {\n return undefined;\n }\n value = value[index];\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n}\n\n/* -------------------- List Formatting -------------------- */\n\n/**\n * Formats an array as a natural language list using Intl.ListFormat.\n * Automatically handles locale-specific conjunctions and grammar.\n */\nfunction formatList(items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const separator = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${separator} ${stringItems[1]}`;\n }\n const separator = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems.pop()!;\n return `${stringItems.join(', ')} ${separator} ${last}`;\n }\n}\n\n/* -------------------- Variable Interpolation -------------------- */\n\n/**\n * Interpolates variables into a template string.\n *\n * Supported formats:\n * - {name} - Simple variable\n * - {user.name} - Nested property\n * - {items[0]} - Array index\n * - {items} - Array (comma-separated)\n * - {items|and} - Array with locale-aware \"and\"\n * - {items|or} - Array with locale-aware \"or\"\n * - {items| - } - Array with custom separator\n * - {items.length} - Array length\n */\nfunction interpolate(template: string, vars: Record<string, unknown>, locale: string): string {\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (_match, key: string, separator?: string) => {\n // Handle .length property\n const isLength = key.endsWith('.length');\n const actualKey = isLength ? key.slice(0, -7) : key;\n\n const value = resolvePath(vars, actualKey);\n\n // Missing variables are replaced with empty string\n if (value == null) return '';\n\n // Handle arrays\n if (Array.isArray(value)) {\n if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n if (separator === 'and') return formatList(value, locale, 'conjunction');\n if (separator === 'or') return formatList(value, locale, 'disjunction');\n return value.map(String).join(separator);\n }\n\n return value.map(String).join(', ');\n }\n\n // Format numbers with locale\n if (typeof value === 'number') {\n try {\n return new Intl.NumberFormat(locale).format(value);\n } catch {\n return String(value);\n }\n }\n\n return String(value);\n });\n}\n\n/* -------------------- Pluralization -------------------- */\n\n/**\n * Gets the plural form for a number using Intl.PluralRules.\n * Automatically handles all locale-specific plural rules.\n */\nfunction getPluralForm(locale: Locale, count: number): PluralForm {\n const n = Math.abs(Math.floor(count));\n\n try {\n const rules = new Intl.PluralRules(locale);\n return rules.select(n) as PluralForm;\n } catch {\n // Fallback to English-like behavior\n return n === 1 ? 'one' : 'other';\n }\n}\n\n/* -------------------- I18n Class -------------------- */\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private escape: boolean;\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, () => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<(locale: Locale) => void>();\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n this.escape = config.escape ?? false;\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n /* -------------------- Locale Management -------------------- */\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n /* -------------------- Message Management -------------------- */\n\n /**\n * Adds messages to a locale (merges with existing).\n */\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n /**\n * Sets messages for a locale (replaces existing).\n */\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale ?? this.locale) !== undefined;\n }\n\n /* -------------------- Async Loaders -------------------- */\n\n async load(locale: Locale): Promise<void> {\n // Return existing loading promise\n if (this.loading.has(locale)) return this.loading.get(locale);\n\n // Already loaded\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader();\n this.add(locale, messages);\n } catch (error) {\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: () => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n\n return this.has(key, targetLocale);\n }\n\n /**\n * Load multiple locales in parallel.\n * Useful for preloading all needed locales at app startup.\n */\n async loadAll(locales: Locale[]): Promise<void> {\n await Promise.all(locales.map((locale) => this.load(locale)));\n }\n\n /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables and options.\n * Synchronous - locale must be loaded first via load() or provided in config.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string {\n const targetLocale = options?.locale ?? this.locale;\n const shouldEscape = options?.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) return key;\n\n const result = this.formatMessage(message, vars ?? {}, targetLocale);\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n /* -------------------- Formatting Helpers -------------------- */\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n /* -------------------- Namespaced Translator -------------------- */\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: (locale: Locale) => void): () => void {\n this.subscribers.add(handler);\n\n // Call handler immediately with the current locale\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n /* -------------------- Internal Helpers -------------------- */\n\n private findMessage(key: string, locale: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n if (value !== undefined) return value as MessageValue;\n }\n\n return undefined;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n private formatMessage(message: MessageValue, vars: Record<string, unknown>, locale: Locale): string {\n // Plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n return interpolate(template, vars, locale);\n }\n\n // String messages\n if (typeof message === 'string') {\n return interpolate(message, vars, locale);\n }\n\n return '';\n }\n}\n\n/* -------------------- Factory Function -------------------- */\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["escapeHtml","str","resolvePath","obj","path","parts","value","part","index","formatList","items","locale","type","stringItems","separator","last","interpolate","template","vars","_match","key","isLength","actualKey","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","locales","options","shouldEscape","message","result","date","ns","handler","loc","chain","lang","fallback","fallbackLang","pluralMsg","form","createI18n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"i18nit.cjs","sources":["../src/i18nit.ts"],"sourcesContent":["/* ============================================\n i18nit - Lightweight, type-safe i18n library\n ============================================ */\n\n/* -------------------- Core Types -------------------- */\n\nexport type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageValue = string | PluralMessages;\n\n// Support nested message objects with dot notation access\nexport type Messages = {\n [key: string]: MessageValue | Messages;\n};\n\nexport type TranslateOptions = {\n locale?: Locale;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, (locale: Locale) => Promise<Messages>>;\n escape?: boolean;\n};\n\n/* -------------------- Path Resolution -------------------- */\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Resolves nested properties using dot notation and bracket notation.\n * Supports: 'user.name', 'items[0]', 'user.items[0].name'\n */\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n // Try direct access first (handles keys with dots)\n if (path in obj) return obj[path];\n\n // Parse path segments\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (Array.isArray(value)) {\n const index = Number(part);\n if (Number.isNaN(index) || index < 0 || index >= value.length) {\n return undefined;\n }\n value = value[index];\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n}\n\n/* -------------------- List Formatting -------------------- */\n\n/**\n * Formats an array as a natural language list using Intl.ListFormat.\n * Automatically handles locale-specific conjunctions and grammar.\n */\nfunction formatList(items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const separator = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${separator} ${stringItems[1]}`;\n }\n const separator = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems.pop()!;\n return `${stringItems.join(', ')} ${separator} ${last}`;\n }\n}\n\n/* -------------------- Variable Interpolation -------------------- */\n\n/**\n * Interpolates variables into a template string.\n *\n * Supported formats:\n * - {name} - Simple variable\n * - {user.name} - Nested property\n * - {items[0]} - Array index\n * - {items} - Array (comma-separated)\n * - {items|and} - Array with locale-aware \"and\"\n * - {items|or} - Array with locale-aware \"or\"\n * - {items| - } - Array with custom separator\n * - {items.length} - Array length\n */\nfunction interpolate(template: string, vars: Record<string, unknown>, locale: string): string {\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (_match, key: string, separator?: string) => {\n // Handle .length property\n const isLength = key.endsWith('.length');\n const actualKey = isLength ? key.slice(0, -7) : key;\n\n const value = resolvePath(vars, actualKey);\n\n // Missing variables are replaced with empty string\n if (value == null) return '';\n\n // Handle arrays\n if (Array.isArray(value)) {\n if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n if (separator === 'and') return formatList(value, locale, 'conjunction');\n if (separator === 'or') return formatList(value, locale, 'disjunction');\n return value.map(String).join(separator);\n }\n\n return value.map(String).join(', ');\n }\n\n // Format numbers with locale\n if (typeof value === 'number') {\n try {\n return new Intl.NumberFormat(locale).format(value);\n } catch {\n return String(value);\n }\n }\n\n return String(value);\n });\n}\n\n/* -------------------- Pluralization -------------------- */\n\n/**\n * Gets the plural form for a number using Intl.PluralRules.\n * Automatically handles all locale-specific plural rules.\n */\nfunction getPluralForm(locale: Locale, count: number): PluralForm {\n const n = Math.abs(Math.floor(count));\n\n try {\n const rules = new Intl.PluralRules(locale);\n return rules.select(n) as PluralForm;\n } catch {\n // Fallback to English-like behavior\n return n === 1 ? 'one' : 'other';\n }\n}\n\n/* -------------------- I18n Class -------------------- */\n\nexport class I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private escape: boolean;\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, (locale: Locale) => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<(locale: Locale) => void>();\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n this.escape = config.escape ?? false;\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n /* -------------------- Locale Management -------------------- */\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n /* -------------------- Message Management -------------------- */\n\n /**\n * Adds messages to a locale (merges with existing).\n */\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n /**\n * Sets messages for a locale (replaces existing).\n */\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale ?? this.locale) !== undefined;\n }\n\n /* -------------------- Async Loaders -------------------- */\n\n async load(locale: Locale): Promise<void> {\n // Return existing loading promise\n if (this.loading.has(locale)) return this.loading.get(locale);\n\n // Already loaded\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader(locale);\n this.add(locale, messages);\n } catch (error) {\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: (locale: Locale) => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n\n return this.has(key, targetLocale);\n }\n\n /**\n * Load multiple locales in parallel.\n * Useful for preloading all needed locales at app startup.\n */\n async loadAll(locales: Locale[]): Promise<void> {\n await Promise.all(locales.map((locale) => this.load(locale)));\n }\n\n /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables and options.\n * Synchronous - locale must be loaded first via load() or provided in config.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string {\n const targetLocale = options?.locale ?? this.locale;\n const shouldEscape = options?.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) return key;\n\n const result = this.formatMessage(message, vars ?? {}, targetLocale);\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n /* -------------------- Formatting Helpers -------------------- */\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n /* -------------------- Namespaced Translator -------------------- */\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: (locale: Locale) => void): () => void {\n this.subscribers.add(handler);\n\n // Call handler immediately with the current locale\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n /* -------------------- Internal Helpers -------------------- */\n\n private findMessage(key: string, locale: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n\n // If the value is a nested Messages object (not a MessageValue), return undefined\n // This ensures we only return actual translatable strings, not object containers\n if (value !== undefined && this.isMessageValue(value)) {\n return value as MessageValue;\n }\n }\n\n return undefined;\n }\n\n /**\n * Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object\n */\n private isMessageValue(value: unknown): boolean {\n if (typeof value === 'string') return true;\n\n // Check if it's a PluralMessages object (has 'other' key and all values are strings)\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const obj = value as Record<string, unknown>;\n\n // If it has 'other' key and values are strings, it's a PluralMessages\n if ('other' in obj && typeof obj.other === 'string') {\n return Object.values(obj).every((v) => typeof v === 'string');\n }\n\n // Otherwise it's a nested Messages object\n return false;\n }\n\n return false;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n private formatMessage(message: MessageValue, vars: Record<string, unknown>, locale: Locale): string {\n // Plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n return interpolate(template, vars, locale);\n }\n\n // String messages\n if (typeof message === 'string') {\n return interpolate(message, vars, locale);\n }\n\n return '';\n }\n}\n\n/* -------------------- Factory Function -------------------- */\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["escapeHtml","str","resolvePath","obj","path","parts","value","part","index","formatList","items","locale","type","stringItems","separator","last","interpolate","template","vars","_match","key","isLength","actualKey","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","locales","options","shouldEscape","message","result","date","ns","handler","loc","v","chain","lang","fallback","fallbackLang","pluralMsg","form","createI18n"],"mappings":"gFAkCA,SAASA,EAAWC,EAAqB,CACvC,OAAOA,EACJ,QAAQ,KAAM,OAAO,EACrB,QAAQ,KAAM,MAAM,EACpB,QAAQ,KAAM,MAAM,EACpB,QAAQ,KAAM,QAAQ,EACtB,QAAQ,KAAM,OAAO,CAC1B,CAMA,SAASC,EAAYC,EAA8BC,EAAuB,CAExE,GAAIA,KAAQD,EAAK,OAAOA,EAAIC,CAAI,EAGhC,MAAMC,EAAQD,EAAK,MAAM,WAAW,GAAK,CAAA,EACzC,IAAIE,EAAiBH,EAErB,UAAWI,KAAQF,EAAO,CACxB,GAAIC,GAAS,MAAQ,OAAOA,GAAU,SAAU,OAEhD,GAAI,MAAM,QAAQA,CAAK,EAAG,CACxB,MAAME,EAAQ,OAAOD,CAAI,EACzB,GAAI,OAAO,MAAMC,CAAK,GAAKA,EAAQ,GAAKA,GAASF,EAAM,OACrD,OAEFA,EAAQA,EAAME,CAAK,CACrB,MACEF,EAASA,EAAkCC,CAAI,CAEnD,CAEA,OAAOD,CACT,CAQA,SAASG,EAAWC,EAAkBC,EAAgBC,EAA6C,CACjG,GAAIF,EAAM,SAAW,EAAG,MAAO,GAE/B,MAAMG,EAAcH,EAAM,IAAI,MAAM,EAEpC,GAAI,CAEF,OADkB,IAAI,KAAK,WAAWC,EAAQ,CAAE,MAAO,OAAQ,KAAAC,EAAM,EACpD,OAAOC,CAAW,CACrC,MAAQ,CAEN,GAAIA,EAAY,SAAW,EAAG,OAAOA,EAAY,CAAC,EAClD,GAAIA,EAAY,SAAW,EAAG,CAC5B,MAAMC,EAAYF,IAAS,cAAgB,MAAQ,KACnD,MAAO,GAAGC,EAAY,CAAC,CAAC,IAAIC,CAAS,IAAID,EAAY,CAAC,CAAC,EACzD,CACA,MAAMC,EAAYF,IAAS,cAAgB,MAAQ,KAC7CG,EAAOF,EAAY,IAAA,EACzB,MAAO,GAAGA,EAAY,KAAK,IAAI,CAAC,IAAIC,CAAS,IAAIC,CAAI,EACvD,CACF,CAiBA,SAASC,EAAYC,EAAkBC,EAA+BP,EAAwB,CAE5F,OAAOM,EAAS,QAAQ,iCAAkC,CAACE,EAAQC,EAAaN,IAAuB,CAErG,MAAMO,EAAWD,EAAI,SAAS,SAAS,EACjCE,EAAYD,EAAWD,EAAI,MAAM,EAAG,EAAE,EAAIA,EAE1Cd,EAAQJ,EAAYgB,EAAMI,CAAS,EAGzC,GAAIhB,GAAS,KAAM,MAAO,GAG1B,GAAI,MAAM,QAAQA,CAAK,EACrB,OAAIe,EAAiB,OAAOf,EAAM,MAAM,EAEpCQ,IAAc,OACZA,IAAc,MAAcL,EAAWH,EAAOK,EAAQ,aAAa,EACnEG,IAAc,KAAaL,EAAWH,EAAOK,EAAQ,aAAa,EAC/DL,EAAM,IAAI,MAAM,EAAE,KAAKQ,CAAS,EAGlCR,EAAM,IAAI,MAAM,EAAE,KAAK,IAAI,EAIpC,GAAI,OAAOA,GAAU,SACnB,GAAI,CACF,OAAO,IAAI,KAAK,aAAaK,CAAM,EAAE,OAAOL,CAAK,CACnD,MAAQ,CACN,OAAO,OAAOA,CAAK,CACrB,CAGF,OAAO,OAAOA,CAAK,CACrB,CAAC,CACH,CAQA,SAASiB,EAAcZ,EAAgBa,EAA2B,CAChE,MAAMC,EAAI,KAAK,IAAI,KAAK,MAAMD,CAAK,CAAC,EAEpC,GAAI,CAEF,OADc,IAAI,KAAK,YAAYb,CAAM,EAC5B,OAAOc,CAAC,CACvB,MAAQ,CAEN,OAAOA,IAAM,EAAI,MAAQ,OAC3B,CACF,CAIO,MAAMC,CAAK,CACR,OACA,UACA,OACA,aAAe,IACf,YAAc,IACd,YAAc,IACd,gBAAkB,IAE1B,YAAYC,EAAqB,GAAI,CAKnC,GAJA,KAAK,OAASA,EAAO,QAAU,KAC/B,KAAK,UAAY,MAAM,QAAQA,EAAO,QAAQ,EAAIA,EAAO,SAAWA,EAAO,SAAW,CAACA,EAAO,QAAQ,EAAI,CAAA,EAC1G,KAAK,OAASA,EAAO,QAAU,GAE3BA,EAAO,SACT,SAAW,CAAChB,EAAQiB,CAAQ,IAAK,OAAO,QAAQD,EAAO,QAAQ,EAC7D,KAAK,SAAS,IAAIhB,EAAQiB,CAAQ,EAItC,GAAID,EAAO,QACT,SAAW,CAAChB,EAAQkB,CAAM,IAAK,OAAO,QAAQF,EAAO,OAAO,EAC1D,KAAK,QAAQ,IAAIhB,EAAQkB,CAAM,CAGrC,CAIA,UAAUlB,EAAsB,CAC1B,KAAK,SAAWA,IACpB,KAAK,OAASA,EACd,KAAK,kBAAA,EACP,CAEA,WAAoB,CAClB,OAAO,KAAK,MACd,CAOA,IAAIA,EAAgBiB,EAA0B,CAC5C,MAAME,EAAW,KAAK,SAAS,IAAInB,CAAM,GAAK,CAAA,EAC9C,KAAK,SAAS,IAAIA,EAAQ,CAAE,GAAGmB,EAAU,GAAGF,EAAU,EACtD,KAAK,kBAAA,CACP,CAKA,IAAIjB,EAAgBiB,EAA0B,CAC5C,KAAK,SAAS,IAAIjB,EAAQiB,CAAQ,EAClC,KAAK,kBAAA,CACP,CAEA,YAAYjB,EAAsC,CAChD,OAAO,KAAK,SAAS,IAAIA,CAAM,CACjC,CAEA,UAAUA,EAAyB,CACjC,OAAO,KAAK,SAAS,IAAIA,CAAM,CACjC,CAEA,IAAIS,EAAaT,EAA0B,CACzC,OAAO,KAAK,YAAYS,EAAKT,GAAU,KAAK,MAAM,IAAM,MAC1D,CAIA,MAAM,KAAKA,EAA+B,CAExC,GAAI,KAAK,QAAQ,IAAIA,CAAM,EAAG,OAAO,KAAK,QAAQ,IAAIA,CAAM,EAG5D,GAAI,KAAK,SAAS,IAAIA,CAAM,EAAG,OAE/B,MAAMkB,EAAS,KAAK,QAAQ,IAAIlB,CAAM,EACtC,GAAI,CAACkB,EAAQ,OAEb,MAAME,GAAW,SAAY,CAC3B,GAAI,CACF,MAAMH,EAAW,MAAMC,EAAOlB,CAAM,EACpC,KAAK,IAAIA,EAAQiB,CAAQ,CAC3B,OAASI,EAAO,CACd,cAAQ,KAAK,iCAAiCrB,CAAM,KAAMqB,CAAK,EACzDA,CACR,QAAA,CACE,KAAK,QAAQ,OAAOrB,CAAM,CAC5B,CACF,GAAA,EAEA,YAAK,QAAQ,IAAIA,EAAQoB,CAAO,EACzBA,CACT,CAEA,SAASpB,EAAgBkB,EAAqD,CAC5E,KAAK,QAAQ,IAAIlB,EAAQkB,CAAM,CACjC,CAEA,MAAM,SAAST,EAAaT,EAAmC,CAC7D,MAAMsB,EAAetB,GAAU,KAAK,OAEpC,MAAI,CAAC,KAAK,SAAS,IAAIsB,CAAY,GAAK,KAAK,QAAQ,IAAIA,CAAY,GACnE,MAAM,KAAK,KAAKA,CAAY,EAGvB,KAAK,IAAIb,EAAKa,CAAY,CACnC,CAMA,MAAM,QAAQC,EAAkC,CAC9C,MAAM,QAAQ,IAAIA,EAAQ,IAAKvB,GAAW,KAAK,KAAKA,CAAM,CAAC,CAAC,CAC9D,CAQA,EAAES,EAAaF,EAAgCiB,EAAoC,CACjF,MAAMF,EAAeE,GAAS,QAAU,KAAK,OACvCC,EAAeD,GAAS,QAAU,KAAK,OAEvCE,EAAU,KAAK,YAAYjB,EAAKa,CAAY,EAClD,GAAII,IAAY,OAAW,OAAOjB,EAElC,MAAMkB,EAAS,KAAK,cAAcD,EAASnB,GAAQ,CAAA,EAAIe,CAAY,EACnE,OAAOG,EAAepC,EAAWsC,CAAM,EAAIA,CAC7C,CAIA,OAAOhC,EAAe6B,EAAoCxB,EAAyB,CACjF,GAAI,CACF,OAAO,IAAI,KAAK,aAAaA,GAAU,KAAK,OAAQwB,CAAO,EAAE,OAAO7B,CAAK,CAC3E,MAAQ,CACN,OAAO,OAAOA,CAAK,CACrB,CACF,CAEA,KAAKA,EAAsB6B,EAAsCxB,EAAyB,CACxF,MAAM4B,EAAO,OAAOjC,GAAU,SAAW,IAAI,KAAKA,CAAK,EAAIA,EAC3D,GAAI,CACF,OAAO,IAAI,KAAK,eAAeK,GAAU,KAAK,OAAQwB,CAAO,EAAE,OAAOI,CAAI,CAC5E,MAAQ,CACN,OAAOA,EAAK,SAAA,CACd,CACF,CAIA,UAAUC,EAAY,CACpB,MAAO,CACL,EAAG,CAACpB,EAAaF,EAAgCiB,IAC/C,KAAK,EAAE,GAAGK,CAAE,IAAIpB,CAAG,GAAIF,EAAMiB,CAAO,CAAA,CAE1C,CAIA,UAAUM,EAA+C,CACvD,KAAK,YAAY,IAAIA,CAAO,EAG5B,GAAI,CACFA,EAAQ,KAAK,MAAM,CACrB,MAAQ,CAER,CAEA,MAAO,IAAM,KAAK,YAAY,OAAOA,CAAO,CAC9C,CAEQ,mBAA0B,CAChC,UAAWA,KAAW,KAAK,YACzB,GAAI,CACFA,EAAQ,KAAK,MAAM,CACrB,MAAQ,CAER,CAEJ,CAIQ,YAAYrB,EAAaT,EAA0C,CACzE,MAAMuB,EAAU,KAAK,eAAevB,CAAM,EAE1C,UAAW+B,KAAOR,EAAS,CACzB,MAAMN,EAAW,KAAK,SAAS,IAAIc,CAAG,EACtC,GAAI,CAACd,EAAU,SAEf,MAAMtB,EAAQJ,EAAY0B,EAAUR,CAAG,EAIvC,GAAId,IAAU,QAAa,KAAK,eAAeA,CAAK,EAClD,OAAOA,CAEX,CAGF,CAKQ,eAAeA,EAAyB,CAC9C,GAAI,OAAOA,GAAU,SAAU,MAAO,GAGtC,GAAI,OAAOA,GAAU,UAAYA,IAAU,MAAQ,CAAC,MAAM,QAAQA,CAAK,EAAG,CACxE,MAAMH,EAAMG,EAGZ,MAAI,UAAWH,GAAO,OAAOA,EAAI,OAAU,SAClC,OAAO,OAAOA,CAAG,EAAE,MAAOwC,GAAM,OAAOA,GAAM,QAAQ,EAIvD,EACT,CAEA,MAAO,EACT,CAEQ,eAAehC,EAA0B,CAC/C,MAAMiC,EAAkB,CAACjC,CAAM,EAGzBkC,EAAOlC,EAAO,MAAM,GAAG,EAAE,CAAC,EAC5BkC,IAASlC,GAAQiC,EAAM,KAAKC,CAAI,EAGpC,UAAWC,KAAY,KAAK,UAAW,CACrCF,EAAM,KAAKE,CAAQ,EACnB,MAAMC,EAAeD,EAAS,MAAM,GAAG,EAAE,CAAC,EACtCC,IAAiBD,GAAUF,EAAM,KAAKG,CAAY,CACxD,CAEA,OAAOH,CACT,CAEQ,cAAcP,EAAuBnB,EAA+BP,EAAwB,CAElG,GAAI,OAAO0B,GAAY,UAAY,UAAWA,EAAS,CACrD,MAAMb,EAAQ,OAAON,EAAK,OAAS,CAAC,EAC9B8B,EAAYX,EAGlB,IAAIY,EACAzB,IAAU,GAAKwB,EAAU,OAAS,OACpCC,EAAO,OAEPA,EAAO1B,EAAcZ,EAAQa,CAAK,EAGpC,MAAMP,EAAW+B,EAAUC,CAAI,GAAKD,EAAU,MAC9C,OAAOhC,EAAYC,EAAUC,EAAMP,CAAM,CAC3C,CAGA,OAAI,OAAO0B,GAAY,SACdrB,EAAYqB,EAASnB,EAAMP,CAAM,EAGnC,EACT,CACF,CAIO,SAASuC,EAAWvB,EAA2B,CACpD,OAAO,IAAID,EAAKC,CAAM,CACxB"}
|
package/dist/i18nit.js
CHANGED
|
@@ -3,44 +3,44 @@ function f(a) {
|
|
|
3
3
|
}
|
|
4
4
|
function h(a, t) {
|
|
5
5
|
if (t in a) return a[t];
|
|
6
|
-
const
|
|
7
|
-
let
|
|
8
|
-
for (const s of
|
|
9
|
-
if (
|
|
10
|
-
if (Array.isArray(
|
|
6
|
+
const e = t.match(/[^.[\]]+/g) || [];
|
|
7
|
+
let r = a;
|
|
8
|
+
for (const s of e) {
|
|
9
|
+
if (r == null || typeof r != "object") return;
|
|
10
|
+
if (Array.isArray(r)) {
|
|
11
11
|
const n = Number(s);
|
|
12
|
-
if (Number.isNaN(n) || n < 0 || n >=
|
|
12
|
+
if (Number.isNaN(n) || n < 0 || n >= r.length)
|
|
13
13
|
return;
|
|
14
|
-
|
|
14
|
+
r = r[n];
|
|
15
15
|
} else
|
|
16
|
-
|
|
16
|
+
r = r[s];
|
|
17
17
|
}
|
|
18
|
-
return
|
|
18
|
+
return r;
|
|
19
19
|
}
|
|
20
|
-
function l(a, t,
|
|
20
|
+
function l(a, t, e) {
|
|
21
21
|
if (a.length === 0) return "";
|
|
22
|
-
const
|
|
22
|
+
const r = a.map(String);
|
|
23
23
|
try {
|
|
24
|
-
return new Intl.ListFormat(t, { style: "long", type:
|
|
24
|
+
return new Intl.ListFormat(t, { style: "long", type: e }).format(r);
|
|
25
25
|
} catch {
|
|
26
|
-
if (
|
|
27
|
-
if (
|
|
28
|
-
const o =
|
|
29
|
-
return `${
|
|
26
|
+
if (r.length === 1) return r[0];
|
|
27
|
+
if (r.length === 2) {
|
|
28
|
+
const o = e === "conjunction" ? "and" : "or";
|
|
29
|
+
return `${r[0]} ${o} ${r[1]}`;
|
|
30
30
|
}
|
|
31
|
-
const s =
|
|
32
|
-
return `${
|
|
31
|
+
const s = e === "conjunction" ? "and" : "or", n = r.pop();
|
|
32
|
+
return `${r.join(", ")} ${s} ${n}`;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
function u(a, t,
|
|
36
|
-
return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g, (
|
|
35
|
+
function u(a, t, e) {
|
|
36
|
+
return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g, (r, s, n) => {
|
|
37
37
|
const o = s.endsWith(".length"), c = o ? s.slice(0, -7) : s, i = h(t, c);
|
|
38
38
|
if (i == null) return "";
|
|
39
39
|
if (Array.isArray(i))
|
|
40
|
-
return o ? String(i.length) : n !== void 0 ? n === "and" ? l(i,
|
|
40
|
+
return o ? String(i.length) : n !== void 0 ? n === "and" ? l(i, e, "conjunction") : n === "or" ? l(i, e, "disjunction") : i.map(String).join(n) : i.map(String).join(", ");
|
|
41
41
|
if (typeof i == "number")
|
|
42
42
|
try {
|
|
43
|
-
return new Intl.NumberFormat(
|
|
43
|
+
return new Intl.NumberFormat(e).format(i);
|
|
44
44
|
} catch {
|
|
45
45
|
return String(i);
|
|
46
46
|
}
|
|
@@ -48,11 +48,11 @@ function u(a, t, r) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
function g(a, t) {
|
|
51
|
-
const
|
|
51
|
+
const e = Math.abs(Math.floor(t));
|
|
52
52
|
try {
|
|
53
|
-
return new Intl.PluralRules(a).select(
|
|
53
|
+
return new Intl.PluralRules(a).select(e);
|
|
54
54
|
} catch {
|
|
55
|
-
return
|
|
55
|
+
return e === 1 ? "one" : "other";
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
class d {
|
|
@@ -65,11 +65,11 @@ class d {
|
|
|
65
65
|
subscribers = /* @__PURE__ */ new Set();
|
|
66
66
|
constructor(t = {}) {
|
|
67
67
|
if (this.locale = t.locale ?? "en", this.fallbacks = Array.isArray(t.fallback) ? t.fallback : t.fallback ? [t.fallback] : [], this.escape = t.escape ?? !1, t.messages)
|
|
68
|
-
for (const [
|
|
69
|
-
this.catalogs.set(
|
|
68
|
+
for (const [e, r] of Object.entries(t.messages))
|
|
69
|
+
this.catalogs.set(e, r);
|
|
70
70
|
if (t.loaders)
|
|
71
|
-
for (const [
|
|
72
|
-
this.loaders.set(
|
|
71
|
+
for (const [e, r] of Object.entries(t.loaders))
|
|
72
|
+
this.loaders.set(e, r);
|
|
73
73
|
}
|
|
74
74
|
/* -------------------- Locale Management -------------------- */
|
|
75
75
|
setLocale(t) {
|
|
@@ -82,15 +82,15 @@ class d {
|
|
|
82
82
|
/**
|
|
83
83
|
* Adds messages to a locale (merges with existing).
|
|
84
84
|
*/
|
|
85
|
-
add(t,
|
|
86
|
-
const
|
|
87
|
-
this.catalogs.set(t, { ...
|
|
85
|
+
add(t, e) {
|
|
86
|
+
const r = this.catalogs.get(t) ?? {};
|
|
87
|
+
this.catalogs.set(t, { ...r, ...e }), this.notifySubscribers();
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
90
|
* Sets messages for a locale (replaces existing).
|
|
91
91
|
*/
|
|
92
|
-
set(t,
|
|
93
|
-
this.catalogs.set(t,
|
|
92
|
+
set(t, e) {
|
|
93
|
+
this.catalogs.set(t, e), this.notifySubscribers();
|
|
94
94
|
}
|
|
95
95
|
getMessages(t) {
|
|
96
96
|
return this.catalogs.get(t);
|
|
@@ -98,18 +98,18 @@ class d {
|
|
|
98
98
|
hasLocale(t) {
|
|
99
99
|
return this.catalogs.has(t);
|
|
100
100
|
}
|
|
101
|
-
has(t,
|
|
102
|
-
return this.findMessage(t,
|
|
101
|
+
has(t, e) {
|
|
102
|
+
return this.findMessage(t, e ?? this.locale) !== void 0;
|
|
103
103
|
}
|
|
104
104
|
/* -------------------- Async Loaders -------------------- */
|
|
105
105
|
async load(t) {
|
|
106
106
|
if (this.loading.has(t)) return this.loading.get(t);
|
|
107
107
|
if (this.catalogs.has(t)) return;
|
|
108
|
-
const
|
|
109
|
-
if (!
|
|
110
|
-
const
|
|
108
|
+
const e = this.loaders.get(t);
|
|
109
|
+
if (!e) return;
|
|
110
|
+
const r = (async () => {
|
|
111
111
|
try {
|
|
112
|
-
const s = await
|
|
112
|
+
const s = await e(t);
|
|
113
113
|
this.add(t, s);
|
|
114
114
|
} catch (s) {
|
|
115
115
|
throw console.warn(`[I18n] Failed to load locale '${t}':`, s), s;
|
|
@@ -117,45 +117,45 @@ class d {
|
|
|
117
117
|
this.loading.delete(t);
|
|
118
118
|
}
|
|
119
119
|
})();
|
|
120
|
-
return this.loading.set(t,
|
|
120
|
+
return this.loading.set(t, r), r;
|
|
121
121
|
}
|
|
122
|
-
register(t,
|
|
123
|
-
this.loaders.set(t,
|
|
122
|
+
register(t, e) {
|
|
123
|
+
this.loaders.set(t, e);
|
|
124
124
|
}
|
|
125
|
-
async hasAsync(t,
|
|
126
|
-
const
|
|
127
|
-
return !this.catalogs.has(
|
|
125
|
+
async hasAsync(t, e) {
|
|
126
|
+
const r = e ?? this.locale;
|
|
127
|
+
return !this.catalogs.has(r) && this.loaders.has(r) && await this.load(r), this.has(t, r);
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
130
|
* Load multiple locales in parallel.
|
|
131
131
|
* Useful for preloading all needed locales at app startup.
|
|
132
132
|
*/
|
|
133
133
|
async loadAll(t) {
|
|
134
|
-
await Promise.all(t.map((
|
|
134
|
+
await Promise.all(t.map((e) => this.load(e)));
|
|
135
135
|
}
|
|
136
136
|
/* -------------------- Translation -------------------- */
|
|
137
137
|
/**
|
|
138
138
|
* Translates a key with optional variables and options.
|
|
139
139
|
* Synchronous - locale must be loaded first via load() or provided in config.
|
|
140
140
|
*/
|
|
141
|
-
t(t,
|
|
142
|
-
const s =
|
|
141
|
+
t(t, e, r) {
|
|
142
|
+
const s = r?.locale ?? this.locale, n = r?.escape ?? this.escape, o = this.findMessage(t, s);
|
|
143
143
|
if (o === void 0) return t;
|
|
144
|
-
const c = this.formatMessage(o,
|
|
144
|
+
const c = this.formatMessage(o, e ?? {}, s);
|
|
145
145
|
return n ? f(c) : c;
|
|
146
146
|
}
|
|
147
147
|
/* -------------------- Formatting Helpers -------------------- */
|
|
148
|
-
number(t,
|
|
148
|
+
number(t, e, r) {
|
|
149
149
|
try {
|
|
150
|
-
return new Intl.NumberFormat(
|
|
150
|
+
return new Intl.NumberFormat(r ?? this.locale, e).format(t);
|
|
151
151
|
} catch {
|
|
152
152
|
return String(t);
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
date(t,
|
|
155
|
+
date(t, e, r) {
|
|
156
156
|
const s = typeof t == "number" ? new Date(t) : t;
|
|
157
157
|
try {
|
|
158
|
-
return new Intl.DateTimeFormat(
|
|
158
|
+
return new Intl.DateTimeFormat(r ?? this.locale, e).format(s);
|
|
159
159
|
} catch {
|
|
160
160
|
return s.toString();
|
|
161
161
|
}
|
|
@@ -163,7 +163,7 @@ class d {
|
|
|
163
163
|
/* -------------------- Namespaced Translator -------------------- */
|
|
164
164
|
namespace(t) {
|
|
165
165
|
return {
|
|
166
|
-
t: (
|
|
166
|
+
t: (e, r, s) => this.t(`${t}.${e}`, r, s)
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
/* -------------------- Subscriptions -------------------- */
|
|
@@ -183,40 +183,53 @@ class d {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
/* -------------------- Internal Helpers -------------------- */
|
|
186
|
-
findMessage(t,
|
|
187
|
-
const
|
|
188
|
-
for (const s of
|
|
186
|
+
findMessage(t, e) {
|
|
187
|
+
const r = this.getLocaleChain(e);
|
|
188
|
+
for (const s of r) {
|
|
189
189
|
const n = this.catalogs.get(s);
|
|
190
190
|
if (!n) continue;
|
|
191
191
|
const o = h(n, t);
|
|
192
|
-
if (o !== void 0
|
|
192
|
+
if (o !== void 0 && this.isMessageValue(o))
|
|
193
|
+
return o;
|
|
193
194
|
}
|
|
194
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object
|
|
198
|
+
*/
|
|
199
|
+
isMessageValue(t) {
|
|
200
|
+
if (typeof t == "string") return !0;
|
|
201
|
+
if (typeof t == "object" && t !== null && !Array.isArray(t)) {
|
|
202
|
+
const e = t;
|
|
203
|
+
return "other" in e && typeof e.other == "string" ? Object.values(e).every((r) => typeof r == "string") : !1;
|
|
204
|
+
}
|
|
205
|
+
return !1;
|
|
206
|
+
}
|
|
195
207
|
getLocaleChain(t) {
|
|
196
|
-
const
|
|
197
|
-
|
|
208
|
+
const e = [t], r = t.split("-")[0];
|
|
209
|
+
r !== t && e.push(r);
|
|
198
210
|
for (const s of this.fallbacks) {
|
|
199
|
-
|
|
211
|
+
e.push(s);
|
|
200
212
|
const n = s.split("-")[0];
|
|
201
|
-
n !== s &&
|
|
213
|
+
n !== s && e.push(n);
|
|
202
214
|
}
|
|
203
|
-
return
|
|
215
|
+
return e;
|
|
204
216
|
}
|
|
205
|
-
formatMessage(t,
|
|
217
|
+
formatMessage(t, e, r) {
|
|
206
218
|
if (typeof t == "object" && "other" in t) {
|
|
207
|
-
const s = Number(
|
|
219
|
+
const s = Number(e.count ?? 0), n = t;
|
|
208
220
|
let o;
|
|
209
|
-
s === 0 && n.zero !== void 0 ? o = "zero" : o = g(
|
|
221
|
+
s === 0 && n.zero !== void 0 ? o = "zero" : o = g(r, s);
|
|
210
222
|
const c = n[o] ?? n.other;
|
|
211
|
-
return u(c,
|
|
223
|
+
return u(c, e, r);
|
|
212
224
|
}
|
|
213
|
-
return typeof t == "string" ? u(t,
|
|
225
|
+
return typeof t == "string" ? u(t, e, r) : "";
|
|
214
226
|
}
|
|
215
227
|
}
|
|
216
228
|
function b(a) {
|
|
217
229
|
return new d(a);
|
|
218
230
|
}
|
|
219
231
|
export {
|
|
232
|
+
d as I18n,
|
|
220
233
|
b as createI18n
|
|
221
234
|
};
|
|
222
235
|
//# sourceMappingURL=i18nit.js.map
|
package/dist/i18nit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18nit.js","sources":["../src/i18nit.ts"],"sourcesContent":["/* ============================================\n i18nit - Lightweight, type-safe i18n library\n ============================================ */\n\n/* -------------------- Core Types -------------------- */\n\nexport type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageValue = string | PluralMessages;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateOptions = {\n locale?: Locale;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, () => Promise<Messages>>;\n escape?: boolean;\n};\n\n/* -------------------- Path Resolution -------------------- */\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Resolves nested properties using dot notation and bracket notation.\n * Supports: 'user.name', 'items[0]', 'user.items[0].name'\n */\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n // Try direct access first (handles keys with dots)\n if (path in obj) return obj[path];\n\n // Parse path segments\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (Array.isArray(value)) {\n const index = Number(part);\n if (Number.isNaN(index) || index < 0 || index >= value.length) {\n return undefined;\n }\n value = value[index];\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n}\n\n/* -------------------- List Formatting -------------------- */\n\n/**\n * Formats an array as a natural language list using Intl.ListFormat.\n * Automatically handles locale-specific conjunctions and grammar.\n */\nfunction formatList(items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const separator = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${separator} ${stringItems[1]}`;\n }\n const separator = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems.pop()!;\n return `${stringItems.join(', ')} ${separator} ${last}`;\n }\n}\n\n/* -------------------- Variable Interpolation -------------------- */\n\n/**\n * Interpolates variables into a template string.\n *\n * Supported formats:\n * - {name} - Simple variable\n * - {user.name} - Nested property\n * - {items[0]} - Array index\n * - {items} - Array (comma-separated)\n * - {items|and} - Array with locale-aware \"and\"\n * - {items|or} - Array with locale-aware \"or\"\n * - {items| - } - Array with custom separator\n * - {items.length} - Array length\n */\nfunction interpolate(template: string, vars: Record<string, unknown>, locale: string): string {\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (_match, key: string, separator?: string) => {\n // Handle .length property\n const isLength = key.endsWith('.length');\n const actualKey = isLength ? key.slice(0, -7) : key;\n\n const value = resolvePath(vars, actualKey);\n\n // Missing variables are replaced with empty string\n if (value == null) return '';\n\n // Handle arrays\n if (Array.isArray(value)) {\n if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n if (separator === 'and') return formatList(value, locale, 'conjunction');\n if (separator === 'or') return formatList(value, locale, 'disjunction');\n return value.map(String).join(separator);\n }\n\n return value.map(String).join(', ');\n }\n\n // Format numbers with locale\n if (typeof value === 'number') {\n try {\n return new Intl.NumberFormat(locale).format(value);\n } catch {\n return String(value);\n }\n }\n\n return String(value);\n });\n}\n\n/* -------------------- Pluralization -------------------- */\n\n/**\n * Gets the plural form for a number using Intl.PluralRules.\n * Automatically handles all locale-specific plural rules.\n */\nfunction getPluralForm(locale: Locale, count: number): PluralForm {\n const n = Math.abs(Math.floor(count));\n\n try {\n const rules = new Intl.PluralRules(locale);\n return rules.select(n) as PluralForm;\n } catch {\n // Fallback to English-like behavior\n return n === 1 ? 'one' : 'other';\n }\n}\n\n/* -------------------- I18n Class -------------------- */\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private escape: boolean;\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, () => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<(locale: Locale) => void>();\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n this.escape = config.escape ?? false;\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n /* -------------------- Locale Management -------------------- */\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n /* -------------------- Message Management -------------------- */\n\n /**\n * Adds messages to a locale (merges with existing).\n */\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n /**\n * Sets messages for a locale (replaces existing).\n */\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale ?? this.locale) !== undefined;\n }\n\n /* -------------------- Async Loaders -------------------- */\n\n async load(locale: Locale): Promise<void> {\n // Return existing loading promise\n if (this.loading.has(locale)) return this.loading.get(locale);\n\n // Already loaded\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader();\n this.add(locale, messages);\n } catch (error) {\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: () => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n\n return this.has(key, targetLocale);\n }\n\n /**\n * Load multiple locales in parallel.\n * Useful for preloading all needed locales at app startup.\n */\n async loadAll(locales: Locale[]): Promise<void> {\n await Promise.all(locales.map((locale) => this.load(locale)));\n }\n\n /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables and options.\n * Synchronous - locale must be loaded first via load() or provided in config.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string {\n const targetLocale = options?.locale ?? this.locale;\n const shouldEscape = options?.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) return key;\n\n const result = this.formatMessage(message, vars ?? {}, targetLocale);\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n /* -------------------- Formatting Helpers -------------------- */\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n /* -------------------- Namespaced Translator -------------------- */\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: (locale: Locale) => void): () => void {\n this.subscribers.add(handler);\n\n // Call handler immediately with the current locale\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n /* -------------------- Internal Helpers -------------------- */\n\n private findMessage(key: string, locale: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n if (value !== undefined) return value as MessageValue;\n }\n\n return undefined;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n private formatMessage(message: MessageValue, vars: Record<string, unknown>, locale: Locale): string {\n // Plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n return interpolate(template, vars, locale);\n }\n\n // String messages\n if (typeof message === 'string') {\n return interpolate(message, vars, locale);\n }\n\n return '';\n }\n}\n\n/* -------------------- Factory Function -------------------- */\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["escapeHtml","str","resolvePath","obj","path","parts","value","part","index","formatList","items","locale","type","stringItems","separator","last","interpolate","template","vars","_match","key","isLength","actualKey","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","locales","options","shouldEscape","message","result","date","ns","handler","loc","chain","lang","fallback","fallbackLang","pluralMsg","form","createI18n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"i18nit.js","sources":["../src/i18nit.ts"],"sourcesContent":["/* ============================================\n i18nit - Lightweight, type-safe i18n library\n ============================================ */\n\n/* -------------------- Core Types -------------------- */\n\nexport type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageValue = string | PluralMessages;\n\n// Support nested message objects with dot notation access\nexport type Messages = {\n [key: string]: MessageValue | Messages;\n};\n\nexport type TranslateOptions = {\n locale?: Locale;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, (locale: Locale) => Promise<Messages>>;\n escape?: boolean;\n};\n\n/* -------------------- Path Resolution -------------------- */\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Resolves nested properties using dot notation and bracket notation.\n * Supports: 'user.name', 'items[0]', 'user.items[0].name'\n */\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n // Try direct access first (handles keys with dots)\n if (path in obj) return obj[path];\n\n // Parse path segments\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (Array.isArray(value)) {\n const index = Number(part);\n if (Number.isNaN(index) || index < 0 || index >= value.length) {\n return undefined;\n }\n value = value[index];\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n}\n\n/* -------------------- List Formatting -------------------- */\n\n/**\n * Formats an array as a natural language list using Intl.ListFormat.\n * Automatically handles locale-specific conjunctions and grammar.\n */\nfunction formatList(items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const separator = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${separator} ${stringItems[1]}`;\n }\n const separator = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems.pop()!;\n return `${stringItems.join(', ')} ${separator} ${last}`;\n }\n}\n\n/* -------------------- Variable Interpolation -------------------- */\n\n/**\n * Interpolates variables into a template string.\n *\n * Supported formats:\n * - {name} - Simple variable\n * - {user.name} - Nested property\n * - {items[0]} - Array index\n * - {items} - Array (comma-separated)\n * - {items|and} - Array with locale-aware \"and\"\n * - {items|or} - Array with locale-aware \"or\"\n * - {items| - } - Array with custom separator\n * - {items.length} - Array length\n */\nfunction interpolate(template: string, vars: Record<string, unknown>, locale: string): string {\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (_match, key: string, separator?: string) => {\n // Handle .length property\n const isLength = key.endsWith('.length');\n const actualKey = isLength ? key.slice(0, -7) : key;\n\n const value = resolvePath(vars, actualKey);\n\n // Missing variables are replaced with empty string\n if (value == null) return '';\n\n // Handle arrays\n if (Array.isArray(value)) {\n if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n if (separator === 'and') return formatList(value, locale, 'conjunction');\n if (separator === 'or') return formatList(value, locale, 'disjunction');\n return value.map(String).join(separator);\n }\n\n return value.map(String).join(', ');\n }\n\n // Format numbers with locale\n if (typeof value === 'number') {\n try {\n return new Intl.NumberFormat(locale).format(value);\n } catch {\n return String(value);\n }\n }\n\n return String(value);\n });\n}\n\n/* -------------------- Pluralization -------------------- */\n\n/**\n * Gets the plural form for a number using Intl.PluralRules.\n * Automatically handles all locale-specific plural rules.\n */\nfunction getPluralForm(locale: Locale, count: number): PluralForm {\n const n = Math.abs(Math.floor(count));\n\n try {\n const rules = new Intl.PluralRules(locale);\n return rules.select(n) as PluralForm;\n } catch {\n // Fallback to English-like behavior\n return n === 1 ? 'one' : 'other';\n }\n}\n\n/* -------------------- I18n Class -------------------- */\n\nexport class I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private escape: boolean;\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, (locale: Locale) => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<(locale: Locale) => void>();\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n this.escape = config.escape ?? false;\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n /* -------------------- Locale Management -------------------- */\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n /* -------------------- Message Management -------------------- */\n\n /**\n * Adds messages to a locale (merges with existing).\n */\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n /**\n * Sets messages for a locale (replaces existing).\n */\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale ?? this.locale) !== undefined;\n }\n\n /* -------------------- Async Loaders -------------------- */\n\n async load(locale: Locale): Promise<void> {\n // Return existing loading promise\n if (this.loading.has(locale)) return this.loading.get(locale);\n\n // Already loaded\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader(locale);\n this.add(locale, messages);\n } catch (error) {\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: (locale: Locale) => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n\n return this.has(key, targetLocale);\n }\n\n /**\n * Load multiple locales in parallel.\n * Useful for preloading all needed locales at app startup.\n */\n async loadAll(locales: Locale[]): Promise<void> {\n await Promise.all(locales.map((locale) => this.load(locale)));\n }\n\n /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables and options.\n * Synchronous - locale must be loaded first via load() or provided in config.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string {\n const targetLocale = options?.locale ?? this.locale;\n const shouldEscape = options?.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) return key;\n\n const result = this.formatMessage(message, vars ?? {}, targetLocale);\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n /* -------------------- Formatting Helpers -------------------- */\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n /* -------------------- Namespaced Translator -------------------- */\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: (locale: Locale) => void): () => void {\n this.subscribers.add(handler);\n\n // Call handler immediately with the current locale\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n /* -------------------- Internal Helpers -------------------- */\n\n private findMessage(key: string, locale: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n\n // If the value is a nested Messages object (not a MessageValue), return undefined\n // This ensures we only return actual translatable strings, not object containers\n if (value !== undefined && this.isMessageValue(value)) {\n return value as MessageValue;\n }\n }\n\n return undefined;\n }\n\n /**\n * Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object\n */\n private isMessageValue(value: unknown): boolean {\n if (typeof value === 'string') return true;\n\n // Check if it's a PluralMessages object (has 'other' key and all values are strings)\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const obj = value as Record<string, unknown>;\n\n // If it has 'other' key and values are strings, it's a PluralMessages\n if ('other' in obj && typeof obj.other === 'string') {\n return Object.values(obj).every((v) => typeof v === 'string');\n }\n\n // Otherwise it's a nested Messages object\n return false;\n }\n\n return false;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n private formatMessage(message: MessageValue, vars: Record<string, unknown>, locale: Locale): string {\n // Plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n return interpolate(template, vars, locale);\n }\n\n // String messages\n if (typeof message === 'string') {\n return interpolate(message, vars, locale);\n }\n\n return '';\n }\n}\n\n/* -------------------- Factory Function -------------------- */\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["escapeHtml","str","resolvePath","obj","path","parts","value","part","index","formatList","items","locale","type","stringItems","separator","last","interpolate","template","vars","_match","key","isLength","actualKey","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","locales","options","shouldEscape","message","result","date","ns","handler","loc","v","chain","lang","fallback","fallbackLang","pluralMsg","form","createI18n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./i18nit.cjs");exports.createI18n=e.createI18n;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./i18nit.cjs");exports.I18n=e.I18n;exports.createI18n=e.createI18n;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function createI18n(config?: I18nConfig): I18n;
|
|
2
2
|
|
|
3
|
-
declare class I18n {
|
|
3
|
+
export declare class I18n {
|
|
4
4
|
private locale;
|
|
5
5
|
private fallbacks;
|
|
6
6
|
private escape;
|
|
@@ -23,7 +23,7 @@ declare class I18n {
|
|
|
23
23
|
hasLocale(locale: Locale): boolean;
|
|
24
24
|
has(key: string, locale?: Locale): boolean;
|
|
25
25
|
load(locale: Locale): Promise<void>;
|
|
26
|
-
register(locale: Locale, loader: () => Promise<Messages>): void;
|
|
26
|
+
register(locale: Locale, loader: (locale: Locale) => Promise<Messages>): void;
|
|
27
27
|
hasAsync(key: string, locale?: Locale): Promise<boolean>;
|
|
28
28
|
/**
|
|
29
29
|
* Load multiple locales in parallel.
|
|
@@ -43,6 +43,10 @@ declare class I18n {
|
|
|
43
43
|
subscribe(handler: (locale: Locale) => void): () => void;
|
|
44
44
|
private notifySubscribers;
|
|
45
45
|
private findMessage;
|
|
46
|
+
/**
|
|
47
|
+
* Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object
|
|
48
|
+
*/
|
|
49
|
+
private isMessageValue;
|
|
46
50
|
private getLocaleChain;
|
|
47
51
|
private formatMessage;
|
|
48
52
|
}
|
|
@@ -51,13 +55,15 @@ export declare type I18nConfig = {
|
|
|
51
55
|
locale?: Locale;
|
|
52
56
|
fallback?: Locale | Locale[];
|
|
53
57
|
messages?: Record<Locale, Messages>;
|
|
54
|
-
loaders?: Record<Locale, () => Promise<Messages>>;
|
|
58
|
+
loaders?: Record<Locale, (locale: Locale) => Promise<Messages>>;
|
|
55
59
|
escape?: boolean;
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
export declare type Locale = string;
|
|
59
63
|
|
|
60
|
-
export declare type Messages =
|
|
64
|
+
export declare type Messages = {
|
|
65
|
+
[key: string]: MessageValue | Messages;
|
|
66
|
+
};
|
|
61
67
|
|
|
62
68
|
export declare type MessageValue = string | PluralMessages;
|
|
63
69
|
|
package/dist/index.js
CHANGED