@vielzeug/i18nit 1.1.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 +783 -0
- package/dist/i18nit.cjs +2 -0
- package/dist/i18nit.cjs.map +1 -0
- package/dist/i18nit.js +222 -0
- package/dist/i18nit.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
# @vielzeug/i18nit
|
|
2
|
+
|
|
3
|
+
Type-safe, lightweight internationalization (i18n) library for TypeScript applications. Simple, powerful translation management with zero dependencies.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Type-Safe** - Full TypeScript support with generic types
|
|
8
|
+
- ✅ **Lightweight** - ~3KB gzipped with zero dependencies
|
|
9
|
+
- ✅ **Universal Pluralization** - 100+ languages via Intl.PluralRules API
|
|
10
|
+
- ✅ **Path Interpolation** - Support for nested objects and array indices
|
|
11
|
+
- ✅ **Lazy Loading** - Async locale loading with automatic caching
|
|
12
|
+
- ✅ **Namespaces** - Organize translations by feature or module
|
|
13
|
+
- ✅ **Fallback Chain** - Multiple fallback locales with automatic language variants
|
|
14
|
+
- ✅ **HTML Escaping** - Built-in XSS protection
|
|
15
|
+
- ✅ **Number & Date Formatting** - Locale-aware formatting with Intl API
|
|
16
|
+
- ✅ **Structured Errors** - Detailed error information with MissingVariableError
|
|
17
|
+
- ✅ **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# pnpm
|
|
23
|
+
pnpm add @vielzeug/i18nit
|
|
24
|
+
|
|
25
|
+
# npm
|
|
26
|
+
npm install @vielzeug/i18nit
|
|
27
|
+
|
|
28
|
+
# yarn
|
|
29
|
+
yarn add @vielzeug/i18nit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { createI18n } from '@vielzeug/i18nit';
|
|
36
|
+
|
|
37
|
+
// Create instance with messages
|
|
38
|
+
const i18n = createI18n({
|
|
39
|
+
locale: 'en',
|
|
40
|
+
messages: {
|
|
41
|
+
en: {
|
|
42
|
+
greeting: 'Hello, {name}!',
|
|
43
|
+
items: {
|
|
44
|
+
zero: 'No items',
|
|
45
|
+
one: 'One item',
|
|
46
|
+
other: '{count} items',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
es: {
|
|
50
|
+
greeting: '¡Hola, {name}!',
|
|
51
|
+
items: {
|
|
52
|
+
zero: 'Sin artículos',
|
|
53
|
+
one: 'Un artículo',
|
|
54
|
+
other: '{count} artículos',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Simple translation
|
|
61
|
+
i18n.t('greeting', { name: 'World' }); // "Hello, World!"
|
|
62
|
+
|
|
63
|
+
// Pluralization
|
|
64
|
+
i18n.t('items', { count: 0 }); // "No items"
|
|
65
|
+
i18n.t('items', { count: 1 }); // "One item"
|
|
66
|
+
i18n.t('items', { count: 5 }); // "5 items"
|
|
67
|
+
|
|
68
|
+
// Change locale
|
|
69
|
+
i18n.setLocale('es');
|
|
70
|
+
i18n.t('greeting', { name: 'Mundo' }); // "¡Hola, Mundo!"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Core Concepts
|
|
74
|
+
|
|
75
|
+
### Translation Keys
|
|
76
|
+
|
|
77
|
+
Translation keys support dot notation for nested organization:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const i18n = createI18n({
|
|
81
|
+
messages: {
|
|
82
|
+
en: {
|
|
83
|
+
'user.profile.title': 'Profile',
|
|
84
|
+
'user.settings.title': 'Settings',
|
|
85
|
+
'admin.dashboard': 'Dashboard',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
i18n.t('user.profile.title'); // "Profile"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Variable Interpolation
|
|
94
|
+
|
|
95
|
+
#### Basic Interpolation
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
i18n.t('greeting', { name: 'Alice' });
|
|
99
|
+
// Template: "Hello, {name}!"
|
|
100
|
+
// Result: "Hello, Alice!"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Nested Object Access
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
i18n.t('message', { user: { name: 'Bob', role: 'Admin' } });
|
|
107
|
+
// Template: "User {user.name} is {user.role}"
|
|
108
|
+
// Result: "User Bob is Admin"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Array Index Access
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
i18n.t('friends', { friends: [{ name: 'Charlie' }, { name: 'Dave' }] });
|
|
115
|
+
// Template: "First friend: {friends[0].name}"
|
|
116
|
+
// Result: "First friend: Charlie"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### Supported Path Formats
|
|
120
|
+
|
|
121
|
+
- `{name}` - Simple variable
|
|
122
|
+
- `{user.name}` - Nested object property
|
|
123
|
+
- `{items[0]}` - Array index
|
|
124
|
+
- `{data.items[0].value}` - Mixed notation
|
|
125
|
+
|
|
126
|
+
**Limitations:**
|
|
127
|
+
- Only numeric bracket notation `[0]`, `[123]`
|
|
128
|
+
- Quoted keys not supported `["key"]`
|
|
129
|
+
- Non-numeric brackets not supported `[key]`
|
|
130
|
+
|
|
131
|
+
### Missing Variable Handling
|
|
132
|
+
|
|
133
|
+
Control how missing variables are handled:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Empty string (default)
|
|
137
|
+
const i18n1 = createI18n({
|
|
138
|
+
messages: { en: { msg: 'Hello, {name}!' } },
|
|
139
|
+
missingVar: 'empty',
|
|
140
|
+
});
|
|
141
|
+
i18n1.t('msg'); // "Hello, !"
|
|
142
|
+
|
|
143
|
+
// Preserve placeholder
|
|
144
|
+
const i18n2 = createI18n({
|
|
145
|
+
messages: { en: { msg: 'Hello, {name}!' } },
|
|
146
|
+
missingVar: 'preserve',
|
|
147
|
+
});
|
|
148
|
+
i18n2.t('msg'); // "Hello, {name}!"
|
|
149
|
+
|
|
150
|
+
// Throw error
|
|
151
|
+
import { MissingVariableError } from '@vielzeug/i18nit';
|
|
152
|
+
|
|
153
|
+
const i18n3 = createI18n({
|
|
154
|
+
messages: { en: { msg: 'Hello, {name}!' } },
|
|
155
|
+
missingVar: 'error',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
i18n3.t('msg');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof MissingVariableError) {
|
|
162
|
+
console.log(error.key); // 'msg'
|
|
163
|
+
console.log(error.variable); // 'name'
|
|
164
|
+
console.log(error.locale); // 'en'
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Pluralization
|
|
170
|
+
|
|
171
|
+
Support for multiple plural forms based on locale-specific rules:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const i18n = createI18n({
|
|
175
|
+
locale: 'en',
|
|
176
|
+
messages: {
|
|
177
|
+
en: {
|
|
178
|
+
notifications: {
|
|
179
|
+
zero: 'No notifications',
|
|
180
|
+
one: 'One notification',
|
|
181
|
+
other: '{count} notifications',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
i18n.t('notifications', { count: 0 }); // "No notifications"
|
|
188
|
+
i18n.t('notifications', { count: 1 }); // "One notification"
|
|
189
|
+
i18n.t('notifications', { count: 5 }); // "5 notifications"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### Supported Plural Rules
|
|
193
|
+
|
|
194
|
+
i18nit uses the browser's built-in `Intl.PluralRules` API to automatically support pluralization for **100+ languages**, including:
|
|
195
|
+
|
|
196
|
+
- **English (en)**: one, other
|
|
197
|
+
- **French (fr)**: one (0-1), other
|
|
198
|
+
- **Arabic (ar)**: zero, one, two, few, many, other
|
|
199
|
+
- **Polish (pl)**: one, few, many
|
|
200
|
+
- **Russian (ru)**: one, few, many, other
|
|
201
|
+
- **German (de)**: one, other
|
|
202
|
+
- **Chinese (zh)**: other
|
|
203
|
+
- **Japanese (ja)**: other
|
|
204
|
+
- And 90+ more languages...
|
|
205
|
+
|
|
206
|
+
### Message Functions
|
|
207
|
+
|
|
208
|
+
For complex dynamic content, use function-based messages:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const i18n = createI18n({
|
|
212
|
+
locale: 'en',
|
|
213
|
+
messages: {
|
|
214
|
+
en: {
|
|
215
|
+
// Simple function
|
|
216
|
+
dynamic: (vars) => `Hello, ${vars.name}!`,
|
|
217
|
+
|
|
218
|
+
// With number formatting
|
|
219
|
+
price: (vars, helpers) =>
|
|
220
|
+
`Price: ${helpers.number(vars.amount as number, {
|
|
221
|
+
style: 'currency',
|
|
222
|
+
currency: 'USD'
|
|
223
|
+
})}`,
|
|
224
|
+
|
|
225
|
+
// With date formatting
|
|
226
|
+
event: (vars, helpers) =>
|
|
227
|
+
`Event on ${helpers.date(vars.date as Date, {
|
|
228
|
+
dateStyle: 'long'
|
|
229
|
+
})}`,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
i18n.t('dynamic', { name: 'Eve' }); // "Hello, Eve!"
|
|
235
|
+
i18n.t('price', { amount: 99.99 }); // "Price: $99.99"
|
|
236
|
+
i18n.t('event', { date: new Date('2024-01-15') }); // "Event on January 15, 2024"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Advanced Features
|
|
240
|
+
|
|
241
|
+
### Fallback Locales
|
|
242
|
+
|
|
243
|
+
Define fallback locales for missing translations:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
const i18n = createI18n({
|
|
247
|
+
locale: 'de-CH',
|
|
248
|
+
fallback: ['de', 'en'],
|
|
249
|
+
messages: {
|
|
250
|
+
'de-CH': { greeting: 'Grüezi!' },
|
|
251
|
+
de: { greeting: 'Hallo!', goodbye: 'Auf Wiedersehen!' },
|
|
252
|
+
en: { greeting: 'Hello!', goodbye: 'Goodbye!', welcome: 'Welcome!' },
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
i18n.t('greeting'); // "Grüezi!" (de-CH)
|
|
257
|
+
i18n.t('goodbye'); // "Auf Wiedersehen!" (de fallback)
|
|
258
|
+
i18n.t('welcome'); // "Welcome!" (en fallback)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Fallback Chain:**
|
|
262
|
+
1. Primary locale (e.g., `de-CH`)
|
|
263
|
+
2. Base language (e.g., `de` from `de-CH`)
|
|
264
|
+
3. First fallback locale
|
|
265
|
+
4. Base of first fallback
|
|
266
|
+
5. Continue through all fallbacks
|
|
267
|
+
|
|
268
|
+
### Async Locale Loading
|
|
269
|
+
|
|
270
|
+
Load translations on-demand for better performance:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
const i18n = createI18n({
|
|
274
|
+
locale: 'en',
|
|
275
|
+
loaders: {
|
|
276
|
+
fr: async () => {
|
|
277
|
+
const response = await fetch('/locales/fr.json');
|
|
278
|
+
return response.json();
|
|
279
|
+
},
|
|
280
|
+
de: async () => import('./locales/de.json'),
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Lazy translation - loads locale first
|
|
285
|
+
const text = await i18n.tl('greeting', { name: 'World' }, { locale: 'fr' });
|
|
286
|
+
|
|
287
|
+
// Or load explicitly
|
|
288
|
+
await i18n.load('de');
|
|
289
|
+
i18n.t('greeting', undefined, { locale: 'de' });
|
|
290
|
+
|
|
291
|
+
// Register loader dynamically
|
|
292
|
+
i18n.register('es', async () => {
|
|
293
|
+
const module = await import('./locales/es.json');
|
|
294
|
+
return module.default;
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Features:**
|
|
299
|
+
- Concurrent requests are deduplicated
|
|
300
|
+
- Failed loads can be retried
|
|
301
|
+
- Errors are logged but don't break fallback
|
|
302
|
+
- Locale is cached after loading
|
|
303
|
+
|
|
304
|
+
### Namespaces
|
|
305
|
+
|
|
306
|
+
Organize translations by feature or module:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const i18n = createI18n({
|
|
310
|
+
messages: {
|
|
311
|
+
en: {
|
|
312
|
+
'auth.login.title': 'Login',
|
|
313
|
+
'auth.login.button': 'Sign In',
|
|
314
|
+
'auth.register.title': 'Register',
|
|
315
|
+
'dashboard.welcome': 'Welcome back!',
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Create namespaced translator
|
|
321
|
+
const auth = i18n.namespace('auth.login');
|
|
322
|
+
auth.t('title'); // "Login"
|
|
323
|
+
auth.t('button'); // "Sign In"
|
|
324
|
+
|
|
325
|
+
const dashboard = i18n.namespace('dashboard');
|
|
326
|
+
dashboard.t('welcome'); // "Welcome back!"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### HTML Escaping
|
|
330
|
+
|
|
331
|
+
Protect against XSS attacks with automatic HTML escaping:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const i18n = createI18n({
|
|
335
|
+
messages: {
|
|
336
|
+
en: {
|
|
337
|
+
userContent: 'Comment: {content}',
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Enable escaping globally
|
|
343
|
+
const safeI18n = createI18n({
|
|
344
|
+
escape: true,
|
|
345
|
+
messages: { en: { html: '<script>alert("xss")</script>' } },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
safeI18n.t('html');
|
|
349
|
+
// "<script>alert("xss")</script>"
|
|
350
|
+
|
|
351
|
+
// Or per translation
|
|
352
|
+
i18n.t('userContent',
|
|
353
|
+
{ content: '<b>Bold</b>' },
|
|
354
|
+
{ escape: true }
|
|
355
|
+
);
|
|
356
|
+
// "Comment: <b>Bold</b>"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Number & Date Formatting
|
|
360
|
+
|
|
361
|
+
Locale-aware formatting using the Intl API:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
const i18n = createI18n({ locale: 'en-US' });
|
|
365
|
+
|
|
366
|
+
// Number formatting
|
|
367
|
+
i18n.number(1234.56); // "1,234.56"
|
|
368
|
+
i18n.number(99.99, { style: 'currency', currency: 'USD' }); // "$99.99"
|
|
369
|
+
i18n.number(0.15, { style: 'percent' }); // "15%"
|
|
370
|
+
|
|
371
|
+
// Date formatting
|
|
372
|
+
const date = new Date('2024-01-15');
|
|
373
|
+
i18n.date(date); // "1/15/2024"
|
|
374
|
+
i18n.date(date, { dateStyle: 'long' }); // "January 15, 2024"
|
|
375
|
+
i18n.date(date, { timeStyle: 'short' }); // "12:00 AM"
|
|
376
|
+
|
|
377
|
+
// Timestamps
|
|
378
|
+
i18n.date(Date.now(), { dateStyle: 'medium' }); // "Jan 15, 2024"
|
|
379
|
+
|
|
380
|
+
// Custom locale
|
|
381
|
+
i18n.number(1234.56, undefined, 'de-DE'); // "1.234,56"
|
|
382
|
+
i18n.date(date, { dateStyle: 'short' }, 'fr'); // "15/01/2024"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Subscriptions
|
|
386
|
+
|
|
387
|
+
React to locale changes:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
const i18n = createI18n({ locale: 'en' });
|
|
391
|
+
|
|
392
|
+
// Subscribe to locale changes
|
|
393
|
+
const unsubscribe = i18n.subscribe((locale) => {
|
|
394
|
+
console.log('Locale changed to:', locale);
|
|
395
|
+
// Update UI, reload data, etc.
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
i18n.setLocale('fr'); // Logs: "Locale changed to: fr"
|
|
399
|
+
|
|
400
|
+
// Unsubscribe when done
|
|
401
|
+
unsubscribe();
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Use Cases:**
|
|
405
|
+
- Update UI when locale changes
|
|
406
|
+
- Reload locale-specific data
|
|
407
|
+
- Analytics/tracking
|
|
408
|
+
- State management integration
|
|
409
|
+
|
|
410
|
+
### Custom Missing Key Handler
|
|
411
|
+
|
|
412
|
+
Customize behavior for missing translations:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
const i18n = createI18n({
|
|
416
|
+
missingKey: (key, locale) => {
|
|
417
|
+
console.warn(`Missing translation: ${key} in ${locale}`);
|
|
418
|
+
return `[${locale}:${key}]`;
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
i18n.t('nonexistent.key'); // "[en:nonexistent.key]"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## API Reference
|
|
426
|
+
|
|
427
|
+
### createI18n(config?)
|
|
428
|
+
|
|
429
|
+
Creates a new i18n instance.
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
type I18nConfig = {
|
|
433
|
+
locale?: string; // Default: 'en'
|
|
434
|
+
fallback?: string | string[]; // Fallback locale(s)
|
|
435
|
+
messages?: Record<string, Messages>; // Initial translations
|
|
436
|
+
loaders?: Record<string, () => Promise<Messages>>; // Async loaders
|
|
437
|
+
escape?: boolean; // Global HTML escaping (default: false)
|
|
438
|
+
missingKey?: (key: string, locale: string) => string; // Missing key handler
|
|
439
|
+
missingVar?: 'preserve' | 'empty' | 'error'; // Missing variable strategy
|
|
440
|
+
};
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Translation Methods
|
|
444
|
+
|
|
445
|
+
#### `t(key, vars?, options?)`
|
|
446
|
+
|
|
447
|
+
Translate a key synchronously.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
i18n.t('greeting'); // Simple
|
|
451
|
+
i18n.t('greeting', { name: 'Alice' }); // With variables
|
|
452
|
+
i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With options
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Options:**
|
|
456
|
+
- `locale?: string` - Override locale for this translation
|
|
457
|
+
- `fallback?: string` - Custom fallback text
|
|
458
|
+
- `escape?: boolean` - Override HTML escaping
|
|
459
|
+
|
|
460
|
+
#### `tl(key, vars?, options?)`
|
|
461
|
+
|
|
462
|
+
Translate a key asynchronously (loads locale if needed).
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
await i18n.tl('greeting', { name: 'Alice' }, { locale: 'fr' });
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Locale Management
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
i18n.setLocale('fr'); // Change locale
|
|
472
|
+
i18n.getLocale(); // Get current locale
|
|
473
|
+
i18n.hasLocale('es'); // Check if locale exists
|
|
474
|
+
i18n.has('key'); // Check if key exists
|
|
475
|
+
i18n.has('key', 'fr'); // Check if key exists in locale
|
|
476
|
+
await i18n.hasAsync('key', 'es'); // Check with async loading
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Message Management
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// Add messages (merge)
|
|
483
|
+
i18n.add('en', { newKey: 'New value' });
|
|
484
|
+
|
|
485
|
+
// Set messages (replace)
|
|
486
|
+
i18n.set('en', { key: 'Value' });
|
|
487
|
+
|
|
488
|
+
// Get messages for locale
|
|
489
|
+
const messages = i18n.getMessages('en');
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Async Loading
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// Register loader
|
|
496
|
+
i18n.register('de', async () => import('./locales/de.json'));
|
|
497
|
+
|
|
498
|
+
// Load locale
|
|
499
|
+
await i18n.load('de');
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Formatting
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
i18n.number(value, options?, locale?);
|
|
506
|
+
i18n.date(value, options?, locale?);
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Namespace
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
const ns = i18n.namespace('auth');
|
|
513
|
+
ns.t('login.title');
|
|
514
|
+
await ns.tl('register.title', undefined, { locale: 'fr' });
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Subscriptions
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const unsubscribe = i18n.subscribe((locale) => {
|
|
521
|
+
console.log('Locale changed:', locale);
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Framework Integration
|
|
526
|
+
|
|
527
|
+
### React
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
import { createI18n } from '@vielzeug/i18nit';
|
|
531
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
532
|
+
|
|
533
|
+
const I18nContext = createContext(null);
|
|
534
|
+
|
|
535
|
+
export function I18nProvider({ children, config }) {
|
|
536
|
+
const [i18n] = useState(() => createI18n(config));
|
|
537
|
+
const [locale, setLocale] = useState(i18n.getLocale());
|
|
538
|
+
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
return i18n.subscribe(setLocale);
|
|
541
|
+
}, [i18n]);
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<I18nContext.Provider value={{ i18n, locale }}>
|
|
545
|
+
{children}
|
|
546
|
+
</I18nContext.Provider>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function useI18n() {
|
|
551
|
+
const context = useContext(I18nContext);
|
|
552
|
+
if (!context) throw new Error('useI18n must be used within I18nProvider');
|
|
553
|
+
return context;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function useTranslation(namespace?: string) {
|
|
557
|
+
const { i18n } = useI18n();
|
|
558
|
+
const ns = namespace ? i18n.namespace(namespace) : i18n;
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
t: ns.t.bind(ns),
|
|
562
|
+
tl: ns.tl.bind(ns),
|
|
563
|
+
locale: i18n.getLocale(),
|
|
564
|
+
setLocale: i18n.setLocale.bind(i18n),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Usage
|
|
569
|
+
function MyComponent() {
|
|
570
|
+
const { t, locale, setLocale } = useTranslation('dashboard');
|
|
571
|
+
|
|
572
|
+
return (
|
|
573
|
+
<div>
|
|
574
|
+
<h1>{t('welcome')}</h1>
|
|
575
|
+
<button onClick={() => setLocale('fr')}>Français</button>
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Vue 3
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
import { createI18n } from '@vielzeug/i18nit';
|
|
585
|
+
import { ref, onUnmounted, Plugin } from 'vue';
|
|
586
|
+
|
|
587
|
+
const i18n = createI18n({ locale: 'en' });
|
|
588
|
+
const locale = ref(i18n.getLocale());
|
|
589
|
+
|
|
590
|
+
const unsubscribe = i18n.subscribe((newLocale) => {
|
|
591
|
+
locale.value = newLocale;
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
export const i18nPlugin: Plugin = {
|
|
595
|
+
install(app) {
|
|
596
|
+
app.config.globalProperties.$t = i18n.t.bind(i18n);
|
|
597
|
+
app.config.globalProperties.$i18n = i18n;
|
|
598
|
+
|
|
599
|
+
app.provide('i18n', i18n);
|
|
600
|
+
app.provide('locale', locale);
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// Composable
|
|
605
|
+
export function useI18n() {
|
|
606
|
+
return {
|
|
607
|
+
t: i18n.t.bind(i18n),
|
|
608
|
+
tl: i18n.tl.bind(i18n),
|
|
609
|
+
locale,
|
|
610
|
+
setLocale: (newLocale: string) => i18n.setLocale(newLocale),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Usage in component
|
|
615
|
+
<script setup>
|
|
616
|
+
import { useI18n } from './i18n';
|
|
617
|
+
|
|
618
|
+
const { t, locale, setLocale } = useI18n();
|
|
619
|
+
</script>
|
|
620
|
+
|
|
621
|
+
<template>
|
|
622
|
+
<div>
|
|
623
|
+
<h1>{{ t('welcome') }}</h1>
|
|
624
|
+
<button @click="setLocale('fr')">Français</button>
|
|
625
|
+
</div>
|
|
626
|
+
</template>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Svelte
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import { createI18n } from '@vielzeug/i18nit';
|
|
633
|
+
import { writable } from 'svelte/store';
|
|
634
|
+
|
|
635
|
+
const i18n = createI18n({ locale: 'en' });
|
|
636
|
+
export const locale = writable(i18n.getLocale());
|
|
637
|
+
|
|
638
|
+
i18n.subscribe((newLocale) => {
|
|
639
|
+
locale.set(newLocale);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
export const t = i18n.t.bind(i18n);
|
|
643
|
+
export const setLocale = i18n.setLocale.bind(i18n);
|
|
644
|
+
|
|
645
|
+
// Usage
|
|
646
|
+
<script>
|
|
647
|
+
import { t, setLocale } from './i18n';
|
|
648
|
+
</script>
|
|
649
|
+
|
|
650
|
+
<h1>{$t('welcome')}</h1>
|
|
651
|
+
<button on:click={() => setLocale('fr')}>Français</button>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Best Practices
|
|
655
|
+
|
|
656
|
+
### 1. Organize Translations by Feature
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
const messages = {
|
|
660
|
+
en: {
|
|
661
|
+
'auth.login.title': 'Login',
|
|
662
|
+
'auth.register.title': 'Register',
|
|
663
|
+
'dashboard.stats.users': 'Users',
|
|
664
|
+
'dashboard.stats.revenue': 'Revenue',
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### 2. Use Namespaces for Large Apps
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
const authTranslations = i18n.namespace('auth');
|
|
673
|
+
const dashboardTranslations = i18n.namespace('dashboard');
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### 3. Lazy Load Translations
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
const i18n = createI18n({
|
|
680
|
+
loaders: {
|
|
681
|
+
'en-US': () => import('./locales/en-US.json'),
|
|
682
|
+
'es-ES': () => import('./locales/es-ES.json'),
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### 4. Type-Safe Translation Keys
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
type TranslationKeys =
|
|
691
|
+
| 'auth.login.title'
|
|
692
|
+
| 'auth.register.title'
|
|
693
|
+
| 'dashboard.welcome';
|
|
694
|
+
|
|
695
|
+
function t(key: TranslationKeys, vars?: Record<string, unknown>) {
|
|
696
|
+
return i18n.t(key, vars);
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### 5. Use Structured Error Handling
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
import { MissingVariableError } from '@vielzeug/i18nit';
|
|
704
|
+
|
|
705
|
+
const i18n = createI18n({
|
|
706
|
+
missingVar: 'error',
|
|
707
|
+
messages: { en: { greeting: 'Hello, {name}!' } },
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
i18n.t('greeting');
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (error instanceof MissingVariableError) {
|
|
714
|
+
// Log to error tracking service
|
|
715
|
+
console.error('Missing variable:', {
|
|
716
|
+
key: error.key,
|
|
717
|
+
variable: error.variable,
|
|
718
|
+
locale: error.locale,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## TypeScript Support
|
|
725
|
+
|
|
726
|
+
Full TypeScript support with type inference:
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
import { createI18n, type Messages, type I18nConfig } from '@vielzeug/i18nit';
|
|
730
|
+
|
|
731
|
+
// Define your messages type
|
|
732
|
+
interface MyMessages extends Messages {
|
|
733
|
+
greeting: string;
|
|
734
|
+
items: {
|
|
735
|
+
zero: string;
|
|
736
|
+
one: string;
|
|
737
|
+
other: string;
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const config: I18nConfig = {
|
|
742
|
+
locale: 'en',
|
|
743
|
+
messages: {
|
|
744
|
+
en: {
|
|
745
|
+
greeting: 'Hello!',
|
|
746
|
+
items: { zero: 'No items', one: 'One item', other: '{count} items' },
|
|
747
|
+
} satisfies MyMessages,
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const i18n = createI18n(config);
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
## Comparison
|
|
755
|
+
|
|
756
|
+
| Feature | i18nit | i18next | react-intl |
|
|
757
|
+
|---------|--------|---------|------------|
|
|
758
|
+
| Bundle Size | **~3KB** | ~12KB | ~15KB |
|
|
759
|
+
| Dependencies | **0** | 2+ | 10+ |
|
|
760
|
+
| TypeScript | ✅ First-class | ✅ Good | ✅ Good |
|
|
761
|
+
| Pluralization | ✅ Built-in | ✅ Plugin | ✅ Built-in |
|
|
762
|
+
| Async Loading | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
|
|
763
|
+
| Path Interpolation | ✅ `{user.name}` | ❌ | ❌ |
|
|
764
|
+
| Message Functions | ✅ Built-in | ⚠️ Limited | ✅ Components |
|
|
765
|
+
| HTML Escaping | ✅ Built-in | ⚠️ Manual | ✅ Built-in |
|
|
766
|
+
| Structured Errors | ✅ MissingVariableError | ❌ | ❌ |
|
|
767
|
+
| Framework Agnostic | ✅ | ✅ | ❌ React only |
|
|
768
|
+
|
|
769
|
+
## License
|
|
770
|
+
|
|
771
|
+
MIT © [Helmuth Duarte](https://github.com/helmuthdu)
|
|
772
|
+
|
|
773
|
+
## Links
|
|
774
|
+
|
|
775
|
+
- [GitHub Repository](https://github.com/helmuthdu/vielzeug)
|
|
776
|
+
- [Documentation](https://vielzeug.dev)
|
|
777
|
+
- [NPM Package](https://www.npmjs.com/package/@vielzeug/i18nit)
|
|
778
|
+
- [Issue Tracker](https://github.com/helmuthdu/vielzeug/issues)
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem - A collection of type-safe utilities for modern web development.
|
|
783
|
+
|
package/dist/i18nit.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class f extends Error{key;variable;locale;constructor(t,s,e){super(`Missing variable '${s}' for key '${t}' in locale '${e}'`),this.name="MissingVariableError",this.key=t,this.variable=s,this.locale=e}}const d={"'":"'",'"':""","&":"&","<":"<",">":">"},c=n=>n.replace(/[&<>"']/g,t=>d[t]),g=(n,t)=>{if(t in n)return n[t];const s=t.match(/[^.[\]]+/g)||[];let e=n;for(const r of s){if(e==null||typeof e!="object")return;e=e[r]}return e},u=(n,t,s={})=>{const e=s.missingVar??"empty";return n.replace(/\{([\w.[\]]+)}/g,(r,i)=>{const a=g(t,i);if(a==null){if(e==="preserve")return r;if(e==="error")throw new f(s.key??"unknown",i,s.locale??"unknown");return""}if(typeof a=="number"&&s.locale)try{return new Intl.NumberFormat(s.locale).format(a)}catch{}return String(a)})},m=(n,t)=>{const s=Math.abs(Math.floor(t));try{return new Intl.PluralRules(n).select(s)}catch{return s===1?"one":"other"}};class y{locale;fallbacks;catalogs=new Map;loaders=new Map;loading=new Map;subscribers=new Set;escape;missingKey;missingVar;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,this.missingKey=t.missingKey??(s=>s),this.missingVar=t.missingVar??"empty",t.messages)for(const[s,e]of Object.entries(t.messages))this.catalogs.set(s,e);if(t.loaders)for(const[s,e]of Object.entries(t.loaders))this.loaders.set(s,e)}setLocale(t){this.locale!==t&&(this.locale=t,this.notifySubscribers())}getLocale(){return this.locale}add(t,s){const e=this.catalogs.get(t)??{};this.catalogs.set(t,{...e,...s}),this.notifySubscribers()}set(t,s){this.catalogs.set(t,s),this.notifySubscribers()}getMessages(t){return this.catalogs.get(t)}hasLocale(t){return this.catalogs.has(t)}has(t,s){return this.findMessage(t,s)!==void 0}async load(t){if(this.loading.has(t))return this.loading.get(t);if(this.catalogs.has(t))return;const s=this.loaders.get(t);if(!s)return;const e=(async()=>{try{const r=await s();this.add(t,r)}catch(r){throw console.warn(`[I18n] Failed to load locale '${t}':`,r),r}finally{this.loading.delete(t)}})();return this.loading.set(t,e),e}register(t,s){this.loaders.set(t,s)}async hasAsync(t,s){const e=s??this.locale;return!this.catalogs.has(e)&&this.loaders.has(e)&&await this.load(e),this.has(t,e)}t(t,s,e){const r=e??{},i=r.locale??this.locale,a=r.escape??this.escape,o=this.findMessage(t,i);return o===void 0?r.fallback??this.missingKey(t,i):this.formatMessage(o,s??{},i,a,t)}async tl(t,s,e){const r=e?.locale??this.locale;if(!this.catalogs.has(r)&&this.loaders.has(r))try{await this.load(r)}catch{}return this.t(t,s,e)}number(t,s,e){try{return new Intl.NumberFormat(e??this.locale,s).format(t)}catch{return String(t)}}date(t,s,e){const r=typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(e??this.locale,s).format(r)}catch{return r.toString()}}namespace(t){return{t:(s,e,r)=>this.t(`${t}.${s}`,e,r),tl:(s,e,r)=>this.tl(`${t}.${s}`,e,r)}}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,s){const e=this.getLocaleChain(s??this.locale);for(const r of e){const i=this.catalogs.get(r);if(!i)continue;const a=g(i,t);if(a!==void 0)return a}}getLocaleChain(t){const s=[t],e=t.split("-")[0];e!==t&&s.push(e);for(const r of this.fallbacks){s.push(r);const i=r.split("-")[0];i!==r&&s.push(i)}return s}formatMessage(t,s,e,r,i){if(typeof t=="function")try{const a=t(s,{date:(o,l)=>this.date(o,l,e),number:(o,l)=>this.number(o,l,e)});return r?c(a):a}catch{return""}if(typeof t=="object"&&"other"in t){const a=Number(s.count??0),o=t;let l;a===0&&o.zero!==void 0?l="zero":l=m(e,a);const b=o[l]??o.other,h=u(b,s,{key:i,locale:e,missingVar:this.missingVar});return r?c(h):h}if(typeof t=="string"){const a=u(t,s,{key:i,locale:e,missingVar:this.missingVar});return r?c(a):a}return""}}function p(n){return new y(n)}exports.MissingVariableError=f;exports.createI18n=p;
|
|
2
|
+
//# sourceMappingURL=i18nit.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18nit.cjs","sources":["../src/i18nit.ts"],"sourcesContent":["export 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 MessageFunction = (\n vars: Record<string, unknown>,\n helpers: {\n number: (value: number, options?: Intl.NumberFormatOptions) => string;\n date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;\n },\n) => string;\n\nexport type MessageValue = string | PluralMessages | MessageFunction;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateParams = {\n locale?: Locale;\n fallback?: string;\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 missingKey?: (key: string, locale: Locale) => string;\n missingVar?: 'preserve' | 'empty' | 'error';\n};\n\n/**\n * Error thrown when a required variable is missing during interpolation.\n */\nexport class MissingVariableError extends Error {\n readonly key: string;\n readonly variable: string;\n readonly locale: Locale;\n\n constructor(key: string, variable: string, locale: Locale) {\n super(`Missing variable '${variable}' for key '${key}' in locale '${locale}'`);\n this.name = 'MissingVariableError';\n this.key = key;\n this.variable = variable;\n this.locale = locale;\n }\n}\n\n/* Helpers */\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"'\": ''',\n '\"': '"',\n '&': '&',\n '<': '<',\n '>': '>',\n};\n\nconst escapeHtml = (str: string): string => str.replace(/[&<>\"']/g, (char) => HTML_ENTITIES[char]);\n\n/**\n * Resolve nested properties using dot notation and numeric bracket notation.\n *\n * @param obj - Object to traverse\n * @param path - Path string to resolve\n * @returns Value at a path or undefined if not found\n */\nconst resolvePath = (obj: Record<string, unknown>, path: string): unknown => {\n // Try direct access first (supports literal keys with dots)\n if (path in obj) return obj[path];\n\n // Parse and traverse path - matches: word characters, numbers\n // Regex: /[^.[\\]]+/g matches segments between dots and brackets\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 value = (value as Record<string, unknown>)[part];\n }\n\n return value;\n};\n\n/**\n * Interpolate variables into a template string.\n *\n * Template format: {variableName} or {nested.path} or {array[0]}\n *\n * @param template - Template string with {variable} placeholders\n * @param vars - Variables object\n * @param options - Interpolation options\n * @returns Interpolated string\n * @throws {MissingVariableError} When missingVar is 'error' and a variable is not found\n */\nconst interpolate = (\n template: string,\n vars: Record<string, unknown>,\n options: {\n locale?: Locale;\n missingVar?: 'preserve' | 'empty' | 'error';\n key?: string; // For better error messages\n } = {},\n): string => {\n const missingVar = options.missingVar ?? 'empty';\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variable interpolation requires conditional logic\n return template.replace(/\\{([\\w.[\\]]+)}/g, (match, key) => {\n const value = resolvePath(vars, key);\n\n if (value == null) {\n if (missingVar === 'preserve') return match;\n if (missingVar === 'error') {\n throw new MissingVariableError(options.key ?? 'unknown', key, options.locale ?? 'unknown');\n }\n return '';\n }\n\n // Format numbers with locale\n if (typeof value === 'number' && options.locale) {\n try {\n return new Intl.NumberFormat(options.locale).format(value);\n } catch {\n // Fall through to string conversion\n }\n }\n\n return String(value);\n });\n};\n\n/* Pluralization */\n\ntype PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\n/**\n * Get the plural form for a number in a given locale using Intl.PluralRules API.\n *\n * Automatically handles all locale-specific plural rules including:\n * - English: one/other\n * - Arabic: zero/one/two/few/many/other\n * - Russian/Polish: one/few/many/other\n * - And 100+ other languages\n *\n * @param locale - Locale string (e.g., 'en-US', 'fr')\n * @param count - Number to pluralize\n * @returns Plural category\n */\nconst getPluralForm = (locale: Locale, count: number): PluralCategory => {\n const n = Math.abs(Math.floor(count));\n\n try {\n const pluralRules = new Intl.PluralRules(locale);\n return pluralRules.select(n) as PluralCategory;\n } catch {\n // Fallback to English-like behavior if locale is invalid\n return n === 1 ? 'one' : 'other';\n }\n};\n\ntype LocaleChangeHandler = (locale: Locale) => void;\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\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<LocaleChangeHandler>();\n\n private escape: boolean;\n private missingKey: (key: string, locale: Locale) => string;\n private missingVar: 'preserve' | 'empty' | 'error';\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\n this.escape = config.escape ?? false;\n this.missingKey = config.missingKey ?? ((key) => key);\n this.missingVar = config.missingVar ?? 'empty';\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 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 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) !== undefined;\n }\n\n // Async Loaders\n\n async load(locale: Locale): Promise<void> {\n if (this.loading.has(locale)) return this.loading.get(locale);\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 // Log loader failures for visibility\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n // Re-throw so callers can handle errors\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 if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n return this.has(key, targetLocale);\n }\n\n // Translation\n\n t(key: string, vars?: Record<string, unknown>, options?: TranslateParams): string {\n const opts = options ?? {};\n const targetLocale = opts.locale ?? this.locale;\n const shouldEscape = opts.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) {\n return opts.fallback ?? this.missingKey(key, targetLocale);\n }\n\n return this.formatMessage(message, vars ?? {}, targetLocale, shouldEscape, key);\n }\n\n async tl(key: string, vars?: Record<string, unknown>, options?: TranslateParams): Promise<string> {\n const targetLocale = options?.locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n try {\n await this.load(targetLocale);\n } catch {\n // Loader errors are already logged in load(), continue with fallback\n // This catch prevents the error from propagating to the caller\n }\n }\n\n return this.t(key, vars, options);\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?: TranslateParams) =>\n this.t(`${ns}.${key}`, vars, options),\n tl: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) =>\n this.tl(`${ns}.${key}`, vars, options),\n };\n }\n\n // Subscriptions\n\n subscribe(handler: LocaleChangeHandler): () => void {\n this.subscribers.add(handler);\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\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 ?? this.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 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\n private formatMessage(\n message: MessageValue,\n vars: Record<string, unknown>,\n locale: Locale,\n shouldEscape: boolean,\n key?: string,\n ): string {\n // Handle function messages\n if (typeof message === 'function') {\n try {\n const result = message(vars, {\n date: (d, opts) => this.date(d, opts, locale),\n number: (v, opts) => this.number(v, opts, locale),\n });\n return shouldEscape ? escapeHtml(result) : result;\n } catch {\n return '';\n }\n }\n\n // Handle 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 when the count is 0\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 const result = interpolate(template, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n // Handle string messages\n if (typeof message === 'string') {\n const result = interpolate(message, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n return '';\n }\n}\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["MissingVariableError","key","variable","locale","HTML_ENTITIES","escapeHtml","str","char","resolvePath","obj","path","parts","value","part","interpolate","template","vars","options","missingVar","match","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","opts","shouldEscape","message","date","ns","handler","locales","loc","chain","lang","fallback","fallbackLang","result","d","v","pluralMsg","form","createI18n"],"mappings":"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"}
|
package/dist/i18nit.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
class b extends Error {
|
|
2
|
+
key;
|
|
3
|
+
variable;
|
|
4
|
+
locale;
|
|
5
|
+
constructor(t, s, e) {
|
|
6
|
+
super(`Missing variable '${s}' for key '${t}' in locale '${e}'`), this.name = "MissingVariableError", this.key = t, this.variable = s, this.locale = e;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const m = {
|
|
10
|
+
"'": "'",
|
|
11
|
+
'"': """,
|
|
12
|
+
"&": "&",
|
|
13
|
+
"<": "<",
|
|
14
|
+
">": ">"
|
|
15
|
+
}, c = (n) => n.replace(/[&<>"']/g, (t) => m[t]), f = (n, t) => {
|
|
16
|
+
if (t in n) return n[t];
|
|
17
|
+
const s = t.match(/[^.[\]]+/g) || [];
|
|
18
|
+
let e = n;
|
|
19
|
+
for (const r of s) {
|
|
20
|
+
if (e == null || typeof e != "object") return;
|
|
21
|
+
e = e[r];
|
|
22
|
+
}
|
|
23
|
+
return e;
|
|
24
|
+
}, u = (n, t, s = {}) => {
|
|
25
|
+
const e = s.missingVar ?? "empty";
|
|
26
|
+
return n.replace(/\{([\w.[\]]+)}/g, (r, i) => {
|
|
27
|
+
const a = f(t, i);
|
|
28
|
+
if (a == null) {
|
|
29
|
+
if (e === "preserve") return r;
|
|
30
|
+
if (e === "error")
|
|
31
|
+
throw new b(s.key ?? "unknown", i, s.locale ?? "unknown");
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
if (typeof a == "number" && s.locale)
|
|
35
|
+
try {
|
|
36
|
+
return new Intl.NumberFormat(s.locale).format(a);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return String(a);
|
|
40
|
+
});
|
|
41
|
+
}, d = (n, t) => {
|
|
42
|
+
const s = Math.abs(Math.floor(t));
|
|
43
|
+
try {
|
|
44
|
+
return new Intl.PluralRules(n).select(s);
|
|
45
|
+
} catch {
|
|
46
|
+
return s === 1 ? "one" : "other";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
class y {
|
|
50
|
+
locale;
|
|
51
|
+
fallbacks;
|
|
52
|
+
catalogs = /* @__PURE__ */ new Map();
|
|
53
|
+
loaders = /* @__PURE__ */ new Map();
|
|
54
|
+
loading = /* @__PURE__ */ new Map();
|
|
55
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
56
|
+
escape;
|
|
57
|
+
missingKey;
|
|
58
|
+
missingVar;
|
|
59
|
+
constructor(t = {}) {
|
|
60
|
+
if (this.locale = t.locale ?? "en", this.fallbacks = Array.isArray(t.fallback) ? t.fallback : t.fallback ? [t.fallback] : [], this.escape = t.escape ?? !1, this.missingKey = t.missingKey ?? ((s) => s), this.missingVar = t.missingVar ?? "empty", t.messages)
|
|
61
|
+
for (const [s, e] of Object.entries(t.messages))
|
|
62
|
+
this.catalogs.set(s, e);
|
|
63
|
+
if (t.loaders)
|
|
64
|
+
for (const [s, e] of Object.entries(t.loaders))
|
|
65
|
+
this.loaders.set(s, e);
|
|
66
|
+
}
|
|
67
|
+
// Locale Management
|
|
68
|
+
setLocale(t) {
|
|
69
|
+
this.locale !== t && (this.locale = t, this.notifySubscribers());
|
|
70
|
+
}
|
|
71
|
+
getLocale() {
|
|
72
|
+
return this.locale;
|
|
73
|
+
}
|
|
74
|
+
// Message Management
|
|
75
|
+
add(t, s) {
|
|
76
|
+
const e = this.catalogs.get(t) ?? {};
|
|
77
|
+
this.catalogs.set(t, { ...e, ...s }), this.notifySubscribers();
|
|
78
|
+
}
|
|
79
|
+
set(t, s) {
|
|
80
|
+
this.catalogs.set(t, s), this.notifySubscribers();
|
|
81
|
+
}
|
|
82
|
+
getMessages(t) {
|
|
83
|
+
return this.catalogs.get(t);
|
|
84
|
+
}
|
|
85
|
+
hasLocale(t) {
|
|
86
|
+
return this.catalogs.has(t);
|
|
87
|
+
}
|
|
88
|
+
has(t, s) {
|
|
89
|
+
return this.findMessage(t, s) !== void 0;
|
|
90
|
+
}
|
|
91
|
+
// Async Loaders
|
|
92
|
+
async load(t) {
|
|
93
|
+
if (this.loading.has(t)) return this.loading.get(t);
|
|
94
|
+
if (this.catalogs.has(t)) return;
|
|
95
|
+
const s = this.loaders.get(t);
|
|
96
|
+
if (!s) return;
|
|
97
|
+
const e = (async () => {
|
|
98
|
+
try {
|
|
99
|
+
const r = await s();
|
|
100
|
+
this.add(t, r);
|
|
101
|
+
} catch (r) {
|
|
102
|
+
throw console.warn(`[I18n] Failed to load locale '${t}':`, r), r;
|
|
103
|
+
} finally {
|
|
104
|
+
this.loading.delete(t);
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
return this.loading.set(t, e), e;
|
|
108
|
+
}
|
|
109
|
+
register(t, s) {
|
|
110
|
+
this.loaders.set(t, s);
|
|
111
|
+
}
|
|
112
|
+
async hasAsync(t, s) {
|
|
113
|
+
const e = s ?? this.locale;
|
|
114
|
+
return !this.catalogs.has(e) && this.loaders.has(e) && await this.load(e), this.has(t, e);
|
|
115
|
+
}
|
|
116
|
+
// Translation
|
|
117
|
+
t(t, s, e) {
|
|
118
|
+
const r = e ?? {}, i = r.locale ?? this.locale, a = r.escape ?? this.escape, o = this.findMessage(t, i);
|
|
119
|
+
return o === void 0 ? r.fallback ?? this.missingKey(t, i) : this.formatMessage(o, s ?? {}, i, a, t);
|
|
120
|
+
}
|
|
121
|
+
async tl(t, s, e) {
|
|
122
|
+
const r = e?.locale ?? this.locale;
|
|
123
|
+
if (!this.catalogs.has(r) && this.loaders.has(r))
|
|
124
|
+
try {
|
|
125
|
+
await this.load(r);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
return this.t(t, s, e);
|
|
129
|
+
}
|
|
130
|
+
// Formatting Helpers
|
|
131
|
+
number(t, s, e) {
|
|
132
|
+
try {
|
|
133
|
+
return new Intl.NumberFormat(e ?? this.locale, s).format(t);
|
|
134
|
+
} catch {
|
|
135
|
+
return String(t);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
date(t, s, e) {
|
|
139
|
+
const r = typeof t == "number" ? new Date(t) : t;
|
|
140
|
+
try {
|
|
141
|
+
return new Intl.DateTimeFormat(e ?? this.locale, s).format(r);
|
|
142
|
+
} catch {
|
|
143
|
+
return r.toString();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Namespaced Translator
|
|
147
|
+
namespace(t) {
|
|
148
|
+
return {
|
|
149
|
+
t: (s, e, r) => this.t(`${t}.${s}`, e, r),
|
|
150
|
+
tl: (s, e, r) => this.tl(`${t}.${s}`, e, r)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Subscriptions
|
|
154
|
+
subscribe(t) {
|
|
155
|
+
this.subscribers.add(t);
|
|
156
|
+
try {
|
|
157
|
+
t(this.locale);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
return () => this.subscribers.delete(t);
|
|
161
|
+
}
|
|
162
|
+
notifySubscribers() {
|
|
163
|
+
for (const t of this.subscribers)
|
|
164
|
+
try {
|
|
165
|
+
t(this.locale);
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Internal Helpers
|
|
170
|
+
findMessage(t, s) {
|
|
171
|
+
const e = this.getLocaleChain(s ?? this.locale);
|
|
172
|
+
for (const r of e) {
|
|
173
|
+
const i = this.catalogs.get(r);
|
|
174
|
+
if (!i) continue;
|
|
175
|
+
const a = f(i, t);
|
|
176
|
+
if (a !== void 0) return a;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
getLocaleChain(t) {
|
|
180
|
+
const s = [t], e = t.split("-")[0];
|
|
181
|
+
e !== t && s.push(e);
|
|
182
|
+
for (const r of this.fallbacks) {
|
|
183
|
+
s.push(r);
|
|
184
|
+
const i = r.split("-")[0];
|
|
185
|
+
i !== r && s.push(i);
|
|
186
|
+
}
|
|
187
|
+
return s;
|
|
188
|
+
}
|
|
189
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -
|
|
190
|
+
formatMessage(t, s, e, r, i) {
|
|
191
|
+
if (typeof t == "function")
|
|
192
|
+
try {
|
|
193
|
+
const a = t(s, {
|
|
194
|
+
date: (o, l) => this.date(o, l, e),
|
|
195
|
+
number: (o, l) => this.number(o, l, e)
|
|
196
|
+
});
|
|
197
|
+
return r ? c(a) : a;
|
|
198
|
+
} catch {
|
|
199
|
+
return "";
|
|
200
|
+
}
|
|
201
|
+
if (typeof t == "object" && "other" in t) {
|
|
202
|
+
const a = Number(s.count ?? 0), o = t;
|
|
203
|
+
let l;
|
|
204
|
+
a === 0 && o.zero !== void 0 ? l = "zero" : l = d(e, a);
|
|
205
|
+
const g = o[l] ?? o.other, h = u(g, s, { key: i, locale: e, missingVar: this.missingVar });
|
|
206
|
+
return r ? c(h) : h;
|
|
207
|
+
}
|
|
208
|
+
if (typeof t == "string") {
|
|
209
|
+
const a = u(t, s, { key: i, locale: e, missingVar: this.missingVar });
|
|
210
|
+
return r ? c(a) : a;
|
|
211
|
+
}
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function p(n) {
|
|
216
|
+
return new y(n);
|
|
217
|
+
}
|
|
218
|
+
export {
|
|
219
|
+
b as MissingVariableError,
|
|
220
|
+
p as createI18n
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=i18nit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18nit.js","sources":["../src/i18nit.ts"],"sourcesContent":["export 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 MessageFunction = (\n vars: Record<string, unknown>,\n helpers: {\n number: (value: number, options?: Intl.NumberFormatOptions) => string;\n date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;\n },\n) => string;\n\nexport type MessageValue = string | PluralMessages | MessageFunction;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateParams = {\n locale?: Locale;\n fallback?: string;\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 missingKey?: (key: string, locale: Locale) => string;\n missingVar?: 'preserve' | 'empty' | 'error';\n};\n\n/**\n * Error thrown when a required variable is missing during interpolation.\n */\nexport class MissingVariableError extends Error {\n readonly key: string;\n readonly variable: string;\n readonly locale: Locale;\n\n constructor(key: string, variable: string, locale: Locale) {\n super(`Missing variable '${variable}' for key '${key}' in locale '${locale}'`);\n this.name = 'MissingVariableError';\n this.key = key;\n this.variable = variable;\n this.locale = locale;\n }\n}\n\n/* Helpers */\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"'\": ''',\n '\"': '"',\n '&': '&',\n '<': '<',\n '>': '>',\n};\n\nconst escapeHtml = (str: string): string => str.replace(/[&<>\"']/g, (char) => HTML_ENTITIES[char]);\n\n/**\n * Resolve nested properties using dot notation and numeric bracket notation.\n *\n * @param obj - Object to traverse\n * @param path - Path string to resolve\n * @returns Value at a path or undefined if not found\n */\nconst resolvePath = (obj: Record<string, unknown>, path: string): unknown => {\n // Try direct access first (supports literal keys with dots)\n if (path in obj) return obj[path];\n\n // Parse and traverse path - matches: word characters, numbers\n // Regex: /[^.[\\]]+/g matches segments between dots and brackets\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 value = (value as Record<string, unknown>)[part];\n }\n\n return value;\n};\n\n/**\n * Interpolate variables into a template string.\n *\n * Template format: {variableName} or {nested.path} or {array[0]}\n *\n * @param template - Template string with {variable} placeholders\n * @param vars - Variables object\n * @param options - Interpolation options\n * @returns Interpolated string\n * @throws {MissingVariableError} When missingVar is 'error' and a variable is not found\n */\nconst interpolate = (\n template: string,\n vars: Record<string, unknown>,\n options: {\n locale?: Locale;\n missingVar?: 'preserve' | 'empty' | 'error';\n key?: string; // For better error messages\n } = {},\n): string => {\n const missingVar = options.missingVar ?? 'empty';\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variable interpolation requires conditional logic\n return template.replace(/\\{([\\w.[\\]]+)}/g, (match, key) => {\n const value = resolvePath(vars, key);\n\n if (value == null) {\n if (missingVar === 'preserve') return match;\n if (missingVar === 'error') {\n throw new MissingVariableError(options.key ?? 'unknown', key, options.locale ?? 'unknown');\n }\n return '';\n }\n\n // Format numbers with locale\n if (typeof value === 'number' && options.locale) {\n try {\n return new Intl.NumberFormat(options.locale).format(value);\n } catch {\n // Fall through to string conversion\n }\n }\n\n return String(value);\n });\n};\n\n/* Pluralization */\n\ntype PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\n/**\n * Get the plural form for a number in a given locale using Intl.PluralRules API.\n *\n * Automatically handles all locale-specific plural rules including:\n * - English: one/other\n * - Arabic: zero/one/two/few/many/other\n * - Russian/Polish: one/few/many/other\n * - And 100+ other languages\n *\n * @param locale - Locale string (e.g., 'en-US', 'fr')\n * @param count - Number to pluralize\n * @returns Plural category\n */\nconst getPluralForm = (locale: Locale, count: number): PluralCategory => {\n const n = Math.abs(Math.floor(count));\n\n try {\n const pluralRules = new Intl.PluralRules(locale);\n return pluralRules.select(n) as PluralCategory;\n } catch {\n // Fallback to English-like behavior if locale is invalid\n return n === 1 ? 'one' : 'other';\n }\n};\n\ntype LocaleChangeHandler = (locale: Locale) => void;\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\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<LocaleChangeHandler>();\n\n private escape: boolean;\n private missingKey: (key: string, locale: Locale) => string;\n private missingVar: 'preserve' | 'empty' | 'error';\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\n this.escape = config.escape ?? false;\n this.missingKey = config.missingKey ?? ((key) => key);\n this.missingVar = config.missingVar ?? 'empty';\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 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 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) !== undefined;\n }\n\n // Async Loaders\n\n async load(locale: Locale): Promise<void> {\n if (this.loading.has(locale)) return this.loading.get(locale);\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 // Log loader failures for visibility\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n // Re-throw so callers can handle errors\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 if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n return this.has(key, targetLocale);\n }\n\n // Translation\n\n t(key: string, vars?: Record<string, unknown>, options?: TranslateParams): string {\n const opts = options ?? {};\n const targetLocale = opts.locale ?? this.locale;\n const shouldEscape = opts.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) {\n return opts.fallback ?? this.missingKey(key, targetLocale);\n }\n\n return this.formatMessage(message, vars ?? {}, targetLocale, shouldEscape, key);\n }\n\n async tl(key: string, vars?: Record<string, unknown>, options?: TranslateParams): Promise<string> {\n const targetLocale = options?.locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n try {\n await this.load(targetLocale);\n } catch {\n // Loader errors are already logged in load(), continue with fallback\n // This catch prevents the error from propagating to the caller\n }\n }\n\n return this.t(key, vars, options);\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?: TranslateParams) =>\n this.t(`${ns}.${key}`, vars, options),\n tl: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) =>\n this.tl(`${ns}.${key}`, vars, options),\n };\n }\n\n // Subscriptions\n\n subscribe(handler: LocaleChangeHandler): () => void {\n this.subscribers.add(handler);\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\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 ?? this.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 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\n private formatMessage(\n message: MessageValue,\n vars: Record<string, unknown>,\n locale: Locale,\n shouldEscape: boolean,\n key?: string,\n ): string {\n // Handle function messages\n if (typeof message === 'function') {\n try {\n const result = message(vars, {\n date: (d, opts) => this.date(d, opts, locale),\n number: (v, opts) => this.number(v, opts, locale),\n });\n return shouldEscape ? escapeHtml(result) : result;\n } catch {\n return '';\n }\n }\n\n // Handle 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 when the count is 0\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 const result = interpolate(template, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n // Handle string messages\n if (typeof message === 'string') {\n const result = interpolate(message, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n return '';\n }\n}\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["MissingVariableError","key","variable","locale","HTML_ENTITIES","escapeHtml","str","char","resolvePath","obj","path","parts","value","part","interpolate","template","vars","options","missingVar","match","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","opts","shouldEscape","message","date","ns","handler","locales","loc","chain","lang","fallback","fallbackLang","result","d","v","pluralMsg","form","createI18n"],"mappings":"AAqCO,MAAMA,UAA6B,MAAM;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAYC,GAAaC,GAAkBC,GAAgB;AACzD,UAAM,qBAAqBD,CAAQ,cAAcD,CAAG,gBAAgBE,CAAM,GAAG,GAC7E,KAAK,OAAO,wBACZ,KAAK,MAAMF,GACX,KAAK,WAAWC,GAChB,KAAK,SAASC;AAAA,EAChB;AACF;AAIA,MAAMC,IAAwC;AAAA,EAC5C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP,GAEMC,IAAa,CAACC,MAAwBA,EAAI,QAAQ,YAAY,CAACC,MAASH,EAAcG,CAAI,CAAC,GAS3FC,IAAc,CAACC,GAA8BC,MAA0B;AAE3E,MAAIA,KAAQD,EAAK,QAAOA,EAAIC,CAAI;AAIhC,QAAMC,IAAQD,EAAK,MAAM,WAAW,KAAK,CAAA;AACzC,MAAIE,IAAiBH;AAErB,aAAWI,KAAQF,GAAO;AACxB,QAAIC,KAAS,QAAQ,OAAOA,KAAU,SAAU;AAChD,IAAAA,IAASA,EAAkCC,CAAI;AAAA,EACjD;AAEA,SAAOD;AACT,GAaME,IAAc,CAClBC,GACAC,GACAC,IAII,CAAA,MACO;AACX,QAAMC,IAAaD,EAAQ,cAAc;AAGzC,SAAOF,EAAS,QAAQ,mBAAmB,CAACI,GAAOlB,MAAQ;AACzD,UAAMW,IAAQJ,EAAYQ,GAAMf,CAAG;AAEnC,QAAIW,KAAS,MAAM;AACjB,UAAIM,MAAe,WAAY,QAAOC;AACtC,UAAID,MAAe;AACjB,cAAM,IAAIlB,EAAqBiB,EAAQ,OAAO,WAAWhB,GAAKgB,EAAQ,UAAU,SAAS;AAE3F,aAAO;AAAA,IACT;AAGA,QAAI,OAAOL,KAAU,YAAYK,EAAQ;AACvC,UAAI;AACF,eAAO,IAAI,KAAK,aAAaA,EAAQ,MAAM,EAAE,OAAOL,CAAK;AAAA,MAC3D,QAAQ;AAAA,MAER;AAGF,WAAO,OAAOA,CAAK;AAAA,EACrB,CAAC;AACH,GAmBMQ,IAAgB,CAACjB,GAAgBkB,MAAkC;AACvE,QAAMC,IAAI,KAAK,IAAI,KAAK,MAAMD,CAAK,CAAC;AAEpC,MAAI;AAEF,WADoB,IAAI,KAAK,YAAYlB,CAAM,EAC5B,OAAOmB,CAAC;AAAA,EAC7B,QAAQ;AAEN,WAAOA,MAAM,IAAI,QAAQ;AAAA,EAC3B;AACF;AAIA,MAAMC,EAAK;AAAA,EACD;AAAA,EACA;AAAA,EACA,+BAAe,IAAA;AAAA,EACf,8BAAc,IAAA;AAAA,EACd,8BAAc,IAAA;AAAA,EACd,kCAAkB,IAAA;AAAA,EAElB;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAYC,IAAqB,IAAI;AAQnC,QAPA,KAAK,SAASA,EAAO,UAAU,MAC/B,KAAK,YAAY,MAAM,QAAQA,EAAO,QAAQ,IAAIA,EAAO,WAAWA,EAAO,WAAW,CAACA,EAAO,QAAQ,IAAI,CAAA,GAE1G,KAAK,SAASA,EAAO,UAAU,IAC/B,KAAK,aAAaA,EAAO,eAAe,CAACvB,MAAQA,IACjD,KAAK,aAAauB,EAAO,cAAc,SAEnCA,EAAO;AACT,iBAAW,CAACrB,GAAQsB,CAAQ,KAAK,OAAO,QAAQD,EAAO,QAAQ;AAC7D,aAAK,SAAS,IAAIrB,GAAQsB,CAAQ;AAItC,QAAID,EAAO;AACT,iBAAW,CAACrB,GAAQuB,CAAM,KAAK,OAAO,QAAQF,EAAO,OAAO;AAC1D,aAAK,QAAQ,IAAIrB,GAAQuB,CAAM;AAAA,EAGrC;AAAA;AAAA,EAIA,UAAUvB,GAAsB;AAC9B,IAAI,KAAK,WAAWA,MACpB,KAAK,SAASA,GACd,KAAK,kBAAA;AAAA,EACP;AAAA,EAEA,YAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIA,IAAIA,GAAgBsB,GAA0B;AAC5C,UAAME,IAAW,KAAK,SAAS,IAAIxB,CAAM,KAAK,CAAA;AAC9C,SAAK,SAAS,IAAIA,GAAQ,EAAE,GAAGwB,GAAU,GAAGF,GAAU,GACtD,KAAK,kBAAA;AAAA,EACP;AAAA,EAEA,IAAItB,GAAgBsB,GAA0B;AAC5C,SAAK,SAAS,IAAItB,GAAQsB,CAAQ,GAClC,KAAK,kBAAA;AAAA,EACP;AAAA,EAEA,YAAYtB,GAAsC;AAChD,WAAO,KAAK,SAAS,IAAIA,CAAM;AAAA,EACjC;AAAA,EAEA,UAAUA,GAAyB;AACjC,WAAO,KAAK,SAAS,IAAIA,CAAM;AAAA,EACjC;AAAA,EAEA,IAAIF,GAAaE,GAA0B;AACzC,WAAO,KAAK,YAAYF,GAAKE,CAAM,MAAM;AAAA,EAC3C;AAAA;AAAA,EAIA,MAAM,KAAKA,GAA+B;AACxC,QAAI,KAAK,QAAQ,IAAIA,CAAM,EAAG,QAAO,KAAK,QAAQ,IAAIA,CAAM;AAC5D,QAAI,KAAK,SAAS,IAAIA,CAAM,EAAG;AAE/B,UAAMuB,IAAS,KAAK,QAAQ,IAAIvB,CAAM;AACtC,QAAI,CAACuB,EAAQ;AAEb,UAAME,KAAW,YAAY;AAC3B,UAAI;AACF,cAAMH,IAAW,MAAMC,EAAA;AACvB,aAAK,IAAIvB,GAAQsB,CAAQ;AAAA,MAC3B,SAASI,GAAO;AAEd,sBAAQ,KAAK,iCAAiC1B,CAAM,MAAM0B,CAAK,GAEzDA;AAAA,MACR,UAAA;AACE,aAAK,QAAQ,OAAO1B,CAAM;AAAA,MAC5B;AAAA,IACF,GAAA;AAEA,gBAAK,QAAQ,IAAIA,GAAQyB,CAAO,GACzBA;AAAA,EACT;AAAA,EAEA,SAASzB,GAAgBuB,GAAuC;AAC9D,SAAK,QAAQ,IAAIvB,GAAQuB,CAAM;AAAA,EACjC;AAAA,EAEA,MAAM,SAASzB,GAAaE,GAAmC;AAC7D,UAAM2B,IAAe3B,KAAU,KAAK;AACpC,WAAI,CAAC,KAAK,SAAS,IAAI2B,CAAY,KAAK,KAAK,QAAQ,IAAIA,CAAY,KACnE,MAAM,KAAK,KAAKA,CAAY,GAEvB,KAAK,IAAI7B,GAAK6B,CAAY;AAAA,EACnC;AAAA;AAAA,EAIA,EAAE7B,GAAae,GAAgCC,GAAmC;AAChF,UAAMc,IAAOd,KAAW,CAAA,GAClBa,IAAeC,EAAK,UAAU,KAAK,QACnCC,IAAeD,EAAK,UAAU,KAAK,QAEnCE,IAAU,KAAK,YAAYhC,GAAK6B,CAAY;AAClD,WAAIG,MAAY,SACPF,EAAK,YAAY,KAAK,WAAW9B,GAAK6B,CAAY,IAGpD,KAAK,cAAcG,GAASjB,KAAQ,CAAA,GAAIc,GAAcE,GAAc/B,CAAG;AAAA,EAChF;AAAA,EAEA,MAAM,GAAGA,GAAae,GAAgCC,GAA4C;AAChG,UAAMa,IAAeb,GAAS,UAAU,KAAK;AAE7C,QAAI,CAAC,KAAK,SAAS,IAAIa,CAAY,KAAK,KAAK,QAAQ,IAAIA,CAAY;AACnE,UAAI;AACF,cAAM,KAAK,KAAKA,CAAY;AAAA,MAC9B,QAAQ;AAAA,MAGR;AAGF,WAAO,KAAK,EAAE7B,GAAKe,GAAMC,CAAO;AAAA,EAClC;AAAA;AAAA,EAIA,OAAOL,GAAeK,GAAoCd,GAAyB;AACjF,QAAI;AACF,aAAO,IAAI,KAAK,aAAaA,KAAU,KAAK,QAAQc,CAAO,EAAE,OAAOL,CAAK;AAAA,IAC3E,QAAQ;AACN,aAAO,OAAOA,CAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,KAAKA,GAAsBK,GAAsCd,GAAyB;AACxF,UAAM+B,IAAO,OAAOtB,KAAU,WAAW,IAAI,KAAKA,CAAK,IAAIA;AAC3D,QAAI;AACF,aAAO,IAAI,KAAK,eAAeT,KAAU,KAAK,QAAQc,CAAO,EAAE,OAAOiB,CAAI;AAAA,IAC5E,QAAQ;AACN,aAAOA,EAAK,SAAA;AAAA,IACd;AAAA,EACF;AAAA;AAAA,EAIA,UAAUC,GAAY;AACpB,WAAO;AAAA,MACL,GAAG,CAAClC,GAAae,GAAgCC,MAC/C,KAAK,EAAE,GAAGkB,CAAE,IAAIlC,CAAG,IAAIe,GAAMC,CAAO;AAAA,MACtC,IAAI,CAAChB,GAAae,GAAgCC,MAChD,KAAK,GAAG,GAAGkB,CAAE,IAAIlC,CAAG,IAAIe,GAAMC,CAAO;AAAA,IAAA;AAAA,EAE3C;AAAA;AAAA,EAIA,UAAUmB,GAA0C;AAClD,SAAK,YAAY,IAAIA,CAAO;AAC5B,QAAI;AACF,MAAAA,EAAQ,KAAK,MAAM;AAAA,IACrB,QAAQ;AAAA,IAER;AACA,WAAO,MAAM,KAAK,YAAY,OAAOA,CAAO;AAAA,EAC9C;AAAA,EAEQ,oBAA0B;AAChC,eAAWA,KAAW,KAAK;AACzB,UAAI;AACF,QAAAA,EAAQ,KAAK,MAAM;AAAA,MACrB,QAAQ;AAAA,MAER;AAAA,EAEJ;AAAA;AAAA,EAIQ,YAAYnC,GAAaE,GAA2C;AAC1E,UAAMkC,IAAU,KAAK,eAAelC,KAAU,KAAK,MAAM;AAEzD,eAAWmC,KAAOD,GAAS;AACzB,YAAMZ,IAAW,KAAK,SAAS,IAAIa,CAAG;AACtC,UAAI,CAACb,EAAU;AAEf,YAAMb,IAAQJ,EAAYiB,GAAUxB,CAAG;AACvC,UAAIW,MAAU,OAAW,QAAOA;AAAA,IAClC;AAAA,EAGF;AAAA,EAEQ,eAAeT,GAA0B;AAC/C,UAAMoC,IAAkB,CAACpC,CAAM,GAGzBqC,IAAOrC,EAAO,MAAM,GAAG,EAAE,CAAC;AAChC,IAAIqC,MAASrC,KAAQoC,EAAM,KAAKC,CAAI;AAGpC,eAAWC,KAAY,KAAK,WAAW;AACrC,MAAAF,EAAM,KAAKE,CAAQ;AACnB,YAAMC,IAAeD,EAAS,MAAM,GAAG,EAAE,CAAC;AAC1C,MAAIC,MAAiBD,KAAUF,EAAM,KAAKG,CAAY;AAAA,IACxD;AAEA,WAAOH;AAAA,EACT;AAAA;AAAA,EAGQ,cACNN,GACAjB,GACAb,GACA6B,GACA/B,GACQ;AAER,QAAI,OAAOgC,KAAY;AACrB,UAAI;AACF,cAAMU,IAASV,EAAQjB,GAAM;AAAA,UAC3B,MAAM,CAAC4B,GAAGb,MAAS,KAAK,KAAKa,GAAGb,GAAM5B,CAAM;AAAA,UAC5C,QAAQ,CAAC0C,GAAGd,MAAS,KAAK,OAAOc,GAAGd,GAAM5B,CAAM;AAAA,QAAA,CACjD;AACD,eAAO6B,IAAe3B,EAAWsC,CAAM,IAAIA;AAAA,MAC7C,QAAQ;AACN,eAAO;AAAA,MACT;AAIF,QAAI,OAAOV,KAAY,YAAY,WAAWA,GAAS;AACrD,YAAMZ,IAAQ,OAAOL,EAAK,SAAS,CAAC,GAC9B8B,IAAYb;AAGlB,UAAIc;AACJ,MAAI1B,MAAU,KAAKyB,EAAU,SAAS,SACpCC,IAAO,SAEPA,IAAO3B,EAAcjB,GAAQkB,CAAK;AAGpC,YAAMN,IAAW+B,EAAUC,CAAI,KAAKD,EAAU,OACxCH,IAAS7B,EAAYC,GAAUC,GAAM,EAAE,KAAAf,GAAK,QAAAE,GAAQ,YAAY,KAAK,YAAY;AACvF,aAAO6B,IAAe3B,EAAWsC,CAAM,IAAIA;AAAA,IAC7C;AAGA,QAAI,OAAOV,KAAY,UAAU;AAC/B,YAAMU,IAAS7B,EAAYmB,GAASjB,GAAM,EAAE,KAAAf,GAAK,QAAAE,GAAQ,YAAY,KAAK,YAAY;AACtF,aAAO6B,IAAe3B,EAAWsC,CAAM,IAAIA;AAAA,IAC7C;AAEA,WAAO;AAAA,EACT;AACF;AAEO,SAASK,EAAWxB,GAA2B;AACpD,SAAO,IAAID,EAAKC,CAAM;AACxB;"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export declare function createI18n(config?: I18nConfig): I18n;
|
|
2
|
+
|
|
3
|
+
declare class I18n {
|
|
4
|
+
private locale;
|
|
5
|
+
private fallbacks;
|
|
6
|
+
private catalogs;
|
|
7
|
+
private loaders;
|
|
8
|
+
private loading;
|
|
9
|
+
private subscribers;
|
|
10
|
+
private escape;
|
|
11
|
+
private missingKey;
|
|
12
|
+
private missingVar;
|
|
13
|
+
constructor(config?: I18nConfig);
|
|
14
|
+
setLocale(locale: Locale): void;
|
|
15
|
+
getLocale(): Locale;
|
|
16
|
+
add(locale: Locale, messages: Messages): void;
|
|
17
|
+
set(locale: Locale, messages: Messages): void;
|
|
18
|
+
getMessages(locale: Locale): Messages | undefined;
|
|
19
|
+
hasLocale(locale: Locale): boolean;
|
|
20
|
+
has(key: string, locale?: Locale): boolean;
|
|
21
|
+
load(locale: Locale): Promise<void>;
|
|
22
|
+
register(locale: Locale, loader: () => Promise<Messages>): void;
|
|
23
|
+
hasAsync(key: string, locale?: Locale): Promise<boolean>;
|
|
24
|
+
t(key: string, vars?: Record<string, unknown>, options?: TranslateParams): string;
|
|
25
|
+
tl(key: string, vars?: Record<string, unknown>, options?: TranslateParams): Promise<string>;
|
|
26
|
+
number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string;
|
|
27
|
+
date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string;
|
|
28
|
+
namespace(ns: string): {
|
|
29
|
+
t: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) => string;
|
|
30
|
+
tl: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) => Promise<string>;
|
|
31
|
+
};
|
|
32
|
+
subscribe(handler: LocaleChangeHandler): () => void;
|
|
33
|
+
private notifySubscribers;
|
|
34
|
+
private findMessage;
|
|
35
|
+
private getLocaleChain;
|
|
36
|
+
private formatMessage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare type I18nConfig = {
|
|
40
|
+
locale?: Locale;
|
|
41
|
+
fallback?: Locale | Locale[];
|
|
42
|
+
messages?: Record<Locale, Messages>;
|
|
43
|
+
loaders?: Record<Locale, () => Promise<Messages>>;
|
|
44
|
+
escape?: boolean;
|
|
45
|
+
missingKey?: (key: string, locale: Locale) => string;
|
|
46
|
+
missingVar?: 'preserve' | 'empty' | 'error';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export declare type Locale = string;
|
|
50
|
+
|
|
51
|
+
declare type LocaleChangeHandler = (locale: Locale) => void;
|
|
52
|
+
|
|
53
|
+
export declare type MessageFunction = (vars: Record<string, unknown>, helpers: {
|
|
54
|
+
number: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
55
|
+
date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
|
|
56
|
+
}) => string;
|
|
57
|
+
|
|
58
|
+
export declare type Messages = Record<string, MessageValue>;
|
|
59
|
+
|
|
60
|
+
export declare type MessageValue = string | PluralMessages | MessageFunction;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Error thrown when a required variable is missing during interpolation.
|
|
64
|
+
*/
|
|
65
|
+
export declare class MissingVariableError extends Error {
|
|
66
|
+
readonly key: string;
|
|
67
|
+
readonly variable: string;
|
|
68
|
+
readonly locale: Locale;
|
|
69
|
+
constructor(key: string, variable: string, locale: Locale);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export declare type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
73
|
+
|
|
74
|
+
export declare type PluralMessages = Partial<Record<PluralForm, string>> & {
|
|
75
|
+
other: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export declare type TranslateParams = {
|
|
79
|
+
locale?: Locale;
|
|
80
|
+
fallback?: string;
|
|
81
|
+
escape?: boolean;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vielzeug/i18nit",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc && vite build",
|
|
19
|
+
"fix": "biome check --write --unsafe src",
|
|
20
|
+
"lint": "biome check src",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"test": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "~5.9.3",
|
|
31
|
+
"vite": "^7.3.1",
|
|
32
|
+
"vite-plugin-dts": "^4.5.4",
|
|
33
|
+
"vitest": "^4.0.18"
|
|
34
|
+
}
|
|
35
|
+
}
|