@vielzeug/i18nit 1.1.4 → 2.0.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 +52 -710
- package/dist/core.cjs +2 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.ts +35 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +53 -0
- package/dist/core.js.map +1 -0
- package/dist/helpers.cjs +2 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.ts +20 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +47 -0
- package/dist/helpers.js.map +1 -0
- package/dist/i18n.cjs +2 -0
- package/dist/i18n.cjs.map +1 -0
- package/dist/i18n.d.ts +78 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +218 -0
- package/dist/i18n.js.map +1 -0
- package/dist/i18nit.cjs +2 -2
- package/dist/i18nit.cjs.map +1 -1
- package/dist/i18nit.d.ts +3 -0
- package/dist/i18nit.d.ts.map +1 -0
- package/dist/i18nit.js +2 -222
- package/dist/i18nit.js.map +1 -1
- package/dist/index.cjs +1 -2
- package/dist/index.d.ts +4 -75
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -5
- package/dist/interpolate.cjs +2 -0
- package/dist/interpolate.cjs.map +1 -0
- package/dist/interpolate.d.ts +11 -0
- package/dist/interpolate.d.ts.map +1 -0
- package/dist/interpolate.js +13 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/intl.cjs +2 -0
- package/dist/intl.cjs.map +1 -0
- package/dist/intl.d.ts +16 -0
- package/dist/intl.d.ts.map +1 -0
- package/dist/intl.js +65 -0
- package/dist/intl.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +19 -9
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,742 +1,84 @@
|
|
|
1
1
|
# @vielzeug/i18nit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Lightweight, type-safe i18n with nested keys, lazy loaders, interpolation, pluralization, and reactive subscriptions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@vielzeug/i18nit) [](https://opensource.org/licenses/MIT)
|
|
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
|
+
`@vielzeug/i18nit` is a zero-dependency internationalization library for TypeScript. It combines typed key paths, fallback locale chains, async locale loading, and Intl formatting helpers.
|
|
18
8
|
|
|
19
9
|
## Installation
|
|
20
10
|
|
|
21
|
-
```
|
|
22
|
-
# pnpm
|
|
11
|
+
```sh
|
|
23
12
|
pnpm add @vielzeug/i18nit
|
|
13
|
+
# npm install @vielzeug/i18nit
|
|
14
|
+
# yarn add @vielzeug/i18nit
|
|
15
|
+
```
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
npm install @vielzeug/i18nit
|
|
17
|
+
## Entry Points
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
| Entry | Purpose |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `@vielzeug/i18nit` | Main API (`createI18n`, `I18n`, exported types) |
|
|
22
|
+
| `@vielzeug/i18nit/core` | Core bundle entry |
|
|
31
23
|
|
|
32
24
|
## Quick Start
|
|
33
25
|
|
|
34
|
-
```
|
|
26
|
+
```ts
|
|
35
27
|
import { createI18n } from '@vielzeug/i18nit';
|
|
36
28
|
|
|
37
|
-
// Create instance with messages
|
|
38
29
|
const i18n = createI18n({
|
|
30
|
+
fallback: 'en',
|
|
39
31
|
locale: 'en',
|
|
40
32
|
messages: {
|
|
41
|
-
|
|
42
|
-
greeting: '
|
|
43
|
-
|
|
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
|
-
},
|
|
33
|
+
de: {
|
|
34
|
+
greeting: 'Hallo, {name}!',
|
|
35
|
+
inbox: { one: 'Eine Nachricht', other: '{count} Nachrichten' },
|
|
56
36
|
},
|
|
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
37
|
en: {
|
|
83
|
-
|
|
84
|
-
'
|
|
85
|
-
|
|
38
|
+
greeting: 'Hello, {name}!',
|
|
39
|
+
inbox: { zero: 'No messages', one: 'One message', other: '{count} messages' },
|
|
40
|
+
nav: { home: 'Home' },
|
|
86
41
|
},
|
|
87
42
|
},
|
|
88
43
|
});
|
|
89
44
|
|
|
90
|
-
i18n.t('user.profile.title'); // "Profile"
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Variable Interpolation
|
|
94
|
-
|
|
95
|
-
#### Basic Interpolation
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
45
|
i18n.t('greeting', { name: 'Alice' });
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
#### Array Handling
|
|
120
|
-
|
|
121
|
-
Arrays can be intelligently formatted with various separators:
|
|
46
|
+
i18n.t('inbox', { count: 0 });
|
|
47
|
+
i18n.t('inbox', { count: 3 });
|
|
122
48
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
messages: {
|
|
126
|
-
en: {
|
|
127
|
-
shopping: 'Shopping list: {items}',
|
|
128
|
-
guests: 'Invited: {names|and}',
|
|
129
|
-
options: 'Choose: {choices|or}',
|
|
130
|
-
path: 'Path: {folders| / }',
|
|
131
|
-
count: 'You have {items.length} items',
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Default comma separator
|
|
137
|
-
i18n.t('shopping', { items: ['Apple', 'Banana', 'Orange'] });
|
|
138
|
-
// "Shopping list: Apple, Banana, Orange"
|
|
139
|
-
|
|
140
|
-
// Natural "and" lists (locale-aware via Intl.ListFormat - supports 100+ languages automatically)
|
|
141
|
-
i18n.t('guests', { names: ['Alice'] });
|
|
142
|
-
// "Invited: Alice"
|
|
143
|
-
i18n.t('guests', { names: ['Alice', 'Bob'] });
|
|
144
|
-
// "Invited: Alice and Bob"
|
|
145
|
-
i18n.t('guests', { names: ['Alice', 'Bob', 'Charlie'] });
|
|
146
|
-
// "Invited: Alice, Bob, and Charlie" (Oxford comma in English)
|
|
147
|
-
|
|
148
|
-
// Natural "or" lists (locale-aware via Intl.ListFormat - supports 100+ languages automatically)
|
|
149
|
-
i18n.t('options', { choices: ['Tea', 'Coffee', 'Juice'] });
|
|
150
|
-
// "Choose: Tea, Coffee, or Juice"
|
|
151
|
-
|
|
152
|
-
// Custom separators
|
|
153
|
-
i18n.t('path', { folders: ['home', 'user', 'documents'] });
|
|
154
|
-
// "Path: home / user / documents"
|
|
155
|
-
|
|
156
|
-
// Array length
|
|
157
|
-
i18n.t('count', { items: ['A', 'B', 'C'] });
|
|
158
|
-
// "You have 3 items"
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
**Array Features:**
|
|
162
|
-
|
|
163
|
-
- `{items}` - Join with comma (`, `)
|
|
164
|
-
- `{items|and}` - Natural "and" list with locale-aware conjunction (uses Intl.ListFormat - supports 100+ languages)
|
|
165
|
-
- `{items|or}` - Natural "or" list with locale-aware conjunction (uses Intl.ListFormat - supports 100+ languages)
|
|
166
|
-
- `{items| - }` - Custom separator (e.g., "A - B - C")
|
|
167
|
-
- `{items.length}` - Array length
|
|
168
|
-
- `{items[0]}` - Safe index access (returns empty if out of bounds)
|
|
169
|
-
|
|
170
|
-
**Locale-Aware List Formatting:**
|
|
171
|
-
The `and` and `or` separators use the built-in **Intl.ListFormat API** which automatically handles:
|
|
172
|
-
|
|
173
|
-
- **100+ languages** - Supports all languages available in the browser/runtime
|
|
174
|
-
- **Proper grammar** - Oxford comma, locale-specific punctuation
|
|
175
|
-
- **Right-to-left languages** - Arabic, Hebrew, etc.
|
|
176
|
-
- **Unicode CLDR standards** - International standard for list formatting
|
|
177
|
-
- **No manual configuration** - Zero maintenance required
|
|
178
|
-
|
|
179
|
-
Examples across languages:
|
|
180
|
-
|
|
181
|
-
- **English**: "A, B, and C" (with Oxford comma)
|
|
182
|
-
- **Spanish**: "A, B y C" (uses "y")
|
|
183
|
-
- **French**: "A, B et C" (uses "et")
|
|
184
|
-
- **German**: "A, B und C" (uses "und")
|
|
185
|
-
- **Japanese**: "A、B、C" (uses Japanese comma)
|
|
186
|
-
- **Arabic**: Proper RTL formatting with "و"
|
|
187
|
-
- And 90+ more languages automatically!
|
|
188
|
-
|
|
189
|
-
#### Supported Path Formats
|
|
190
|
-
|
|
191
|
-
- `{name}` - Simple variable
|
|
192
|
-
- `{user.name}` - Nested object property
|
|
193
|
-
- `{items[0]}` - Array index (safe - returns empty if out of bounds)
|
|
194
|
-
- `{items}` - Array join with default separator
|
|
195
|
-
- `{items|and}` - Array join with "and"
|
|
196
|
-
- `{items.length}` - Array length
|
|
197
|
-
- `{data.items[0].value}` - Mixed notation
|
|
198
|
-
|
|
199
|
-
**Limitations:**
|
|
200
|
-
|
|
201
|
-
- Only numeric bracket notation `[0]`, `[123]`
|
|
202
|
-
- Quoted keys not supported `["key"]`
|
|
203
|
-
- Non-numeric brackets not supported `[key]`
|
|
204
|
-
|
|
205
|
-
### Missing Variable Handling
|
|
206
|
-
|
|
207
|
-
Missing variables are automatically replaced with empty strings:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
const i18n = createI18n({
|
|
211
|
-
messages: { en: { msg: 'Hello, {name}!' } },
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
i18n.t('msg'); // "Hello, !"
|
|
215
|
-
i18n.t('msg', { name: 'Alice' }); // "Hello, Alice!"
|
|
49
|
+
i18n.locale = 'de';
|
|
50
|
+
i18n.t('nav.home'); // falls back to en
|
|
216
51
|
```
|
|
217
52
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Support for multiple plural forms based on locale-specific rules:
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
const i18n = createI18n({
|
|
224
|
-
locale: 'en',
|
|
225
|
-
messages: {
|
|
226
|
-
en: {
|
|
227
|
-
notifications: {
|
|
228
|
-
zero: 'No notifications',
|
|
229
|
-
one: 'One notification',
|
|
230
|
-
other: '{count} notifications',
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
i18n.t('notifications', { count: 0 }); // "No notifications"
|
|
237
|
-
i18n.t('notifications', { count: 1 }); // "One notification"
|
|
238
|
-
i18n.t('notifications', { count: 5 }); // "5 notifications"
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
#### Supported Plural Rules
|
|
242
|
-
|
|
243
|
-
i18nit uses the browser's built-in `Intl.PluralRules` API to automatically support pluralization for **100+ languages**, including:
|
|
244
|
-
|
|
245
|
-
- **English (en)**: one, other
|
|
246
|
-
- **French (fr)**: one (0-1), other
|
|
247
|
-
- **Arabic (ar)**: zero, one, two, few, many, other
|
|
248
|
-
- **Polish (pl)**: one, few, many
|
|
249
|
-
- **Russian (ru)**: one, few, many, other
|
|
250
|
-
- **German (de)**: one, other
|
|
251
|
-
- **Chinese (zh)**: other
|
|
252
|
-
- **Japanese (ja)**: other
|
|
253
|
-
- And 90+ more languages...
|
|
254
|
-
|
|
255
|
-
## Advanced Features
|
|
256
|
-
|
|
257
|
-
### Fallback Locales
|
|
258
|
-
|
|
259
|
-
Define fallback locales for missing translations:
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
const i18n = createI18n({
|
|
263
|
-
locale: 'de-CH',
|
|
264
|
-
fallback: ['de', 'en'],
|
|
265
|
-
messages: {
|
|
266
|
-
'de-CH': { greeting: 'Grüezi!' },
|
|
267
|
-
de: { greeting: 'Hallo!', goodbye: 'Auf Wiedersehen!' },
|
|
268
|
-
en: { greeting: 'Hello!', goodbye: 'Goodbye!', welcome: 'Welcome!' },
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
i18n.t('greeting'); // "Grüezi!" (de-CH)
|
|
273
|
-
i18n.t('goodbye'); // "Auf Wiedersehen!" (de fallback)
|
|
274
|
-
i18n.t('welcome'); // "Welcome!" (en fallback)
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
**Fallback Chain:**
|
|
278
|
-
|
|
279
|
-
1. Primary locale (e.g., `de-CH`)
|
|
280
|
-
2. Base language (e.g., `de` from `de-CH`)
|
|
281
|
-
3. First fallback locale
|
|
282
|
-
4. Base of first fallback
|
|
283
|
-
5. Continue through all fallbacks
|
|
284
|
-
|
|
285
|
-
### Async Locale Loading
|
|
286
|
-
|
|
287
|
-
Load translations on-demand for better performance:
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
const i18n = createI18n({
|
|
291
|
-
locale: 'en',
|
|
292
|
-
loaders: {
|
|
293
|
-
fr: async () => {
|
|
294
|
-
const response = await fetch('/locales/fr.json');
|
|
295
|
-
return response.json();
|
|
296
|
-
},
|
|
297
|
-
de: async () => import('./locales/de.json'),
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// Preload at app startup
|
|
302
|
-
await i18n.loadAll(['en', 'fr', 'de']);
|
|
303
|
-
|
|
304
|
-
// Or load explicitly
|
|
305
|
-
await i18n.load('fr');
|
|
306
|
-
i18n.setLocale('fr');
|
|
307
|
-
i18n.t('greeting'); // Now uses French
|
|
308
|
-
|
|
309
|
-
// Register loader dynamically
|
|
310
|
-
i18n.register('es', async () => {
|
|
311
|
-
const module = await import('./locales/es.json');
|
|
312
|
-
return module.default;
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Load and use
|
|
316
|
-
await i18n.load('es');
|
|
317
|
-
i18n.t('greeting', undefined, { locale: 'es' });
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
**Features:**
|
|
321
|
-
|
|
322
|
-
- Concurrent requests are deduplicated
|
|
323
|
-
- Failed loads throw errors (can be caught)
|
|
324
|
-
- Locale is cached after loading
|
|
325
|
-
- Use `loadAll()` to preload multiple locales at once
|
|
326
|
-
|
|
327
|
-
### Namespaces
|
|
328
|
-
|
|
329
|
-
Organize translations by feature or module:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
const i18n = createI18n({
|
|
333
|
-
messages: {
|
|
334
|
-
en: {
|
|
335
|
-
'auth.login.title': 'Login',
|
|
336
|
-
'auth.login.button': 'Sign In',
|
|
337
|
-
'auth.register.title': 'Register',
|
|
338
|
-
'dashboard.welcome': 'Welcome back!',
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// Create namespaced translator
|
|
344
|
-
const auth = i18n.namespace('auth.login');
|
|
345
|
-
auth.t('title'); // "Login"
|
|
346
|
-
auth.t('button'); // "Sign In"
|
|
347
|
-
|
|
348
|
-
const dashboard = i18n.namespace('dashboard');
|
|
349
|
-
dashboard.t('welcome'); // "Welcome back!"
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### HTML Escaping
|
|
353
|
-
|
|
354
|
-
Protect against XSS attacks with automatic HTML escaping:
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
const i18n = createI18n({
|
|
358
|
-
messages: {
|
|
359
|
-
en: {
|
|
360
|
-
userContent: 'Comment: {content}',
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// Enable escaping globally
|
|
366
|
-
const safeI18n = createI18n({
|
|
367
|
-
escape: true,
|
|
368
|
-
messages: { en: { html: '<script>alert("xss")</script>' } },
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
safeI18n.t('html');
|
|
372
|
-
// "<script>alert("xss")</script>"
|
|
373
|
-
|
|
374
|
-
// Or per translation
|
|
375
|
-
i18n.t('userContent', { content: '<b>Bold</b>' }, { escape: true });
|
|
376
|
-
// "Comment: <b>Bold</b>"
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Number & Date Formatting
|
|
380
|
-
|
|
381
|
-
Locale-aware formatting using the Intl API:
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
const i18n = createI18n({ locale: 'en-US' });
|
|
385
|
-
|
|
386
|
-
// Number formatting
|
|
387
|
-
i18n.number(1234.56); // "1,234.56"
|
|
388
|
-
i18n.number(99.99, { style: 'currency', currency: 'USD' }); // "$99.99"
|
|
389
|
-
i18n.number(0.15, { style: 'percent' }); // "15%"
|
|
390
|
-
|
|
391
|
-
// Date formatting
|
|
392
|
-
const date = new Date('2024-01-15');
|
|
393
|
-
i18n.date(date); // "1/15/2024"
|
|
394
|
-
i18n.date(date, { dateStyle: 'long' }); // "January 15, 2024"
|
|
395
|
-
i18n.date(date, { timeStyle: 'short' }); // "12:00 AM"
|
|
396
|
-
|
|
397
|
-
// Timestamps
|
|
398
|
-
i18n.date(Date.now(), { dateStyle: 'medium' }); // "Jan 15, 2024"
|
|
399
|
-
|
|
400
|
-
// Custom locale
|
|
401
|
-
i18n.number(1234.56, undefined, 'de-DE'); // "1.234,56"
|
|
402
|
-
i18n.date(date, { dateStyle: 'short' }, 'fr'); // "15/01/2024"
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
### Subscriptions
|
|
406
|
-
|
|
407
|
-
React to locale changes:
|
|
408
|
-
|
|
409
|
-
```typescript
|
|
410
|
-
const i18n = createI18n({ locale: 'en' });
|
|
411
|
-
|
|
412
|
-
// Subscribe to locale changes
|
|
413
|
-
const unsubscribe = i18n.subscribe((locale) => {
|
|
414
|
-
console.log('Locale changed to:', locale);
|
|
415
|
-
// Update UI, reload data, etc.
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
i18n.setLocale('fr'); // Logs: "Locale changed to: fr"
|
|
419
|
-
|
|
420
|
-
// Unsubscribe when done
|
|
421
|
-
unsubscribe();
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
**Use Cases:**
|
|
425
|
-
|
|
426
|
-
- Update UI when locale changes
|
|
427
|
-
- Reload locale-specific data
|
|
428
|
-
- Analytics/tracking
|
|
429
|
-
- State management integration
|
|
430
|
-
|
|
431
|
-
### Subscriptions
|
|
432
|
-
|
|
433
|
-
### createI18n(config?)
|
|
434
|
-
|
|
435
|
-
Creates a new i18n instance.
|
|
436
|
-
|
|
437
|
-
```typescript
|
|
438
|
-
type I18nConfig = {
|
|
439
|
-
locale?: string; // Default: 'en'
|
|
440
|
-
fallback?: string | string[]; // Fallback locale(s)
|
|
441
|
-
messages?: Record<string, Messages>; // Initial translations
|
|
442
|
-
loaders?: Record<string, () => Promise<Messages>>; // Async loaders
|
|
443
|
-
escape?: boolean; // Global HTML escaping (default: false)
|
|
444
|
-
};
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
### Translation Methods
|
|
448
|
-
|
|
449
|
-
#### `t(key, vars?, options?)`
|
|
450
|
-
|
|
451
|
-
Translate a key synchronously.
|
|
452
|
-
|
|
453
|
-
```typescript
|
|
454
|
-
i18n.t('greeting'); // Simple
|
|
455
|
-
i18n.t('greeting', { name: 'Alice' }); // With variables
|
|
456
|
-
i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With options
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
**Options:**
|
|
460
|
-
|
|
461
|
-
- `locale?: string` - Override locale for this translation
|
|
462
|
-
- `escape?: boolean` - Override HTML escaping
|
|
463
|
-
|
|
464
|
-
### Locale Management
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
i18n.setLocale('fr'); // Change locale
|
|
468
|
-
i18n.getLocale(); // Get current locale
|
|
469
|
-
i18n.hasLocale('es'); // Check if locale exists
|
|
470
|
-
i18n.has('key'); // Check if key exists
|
|
471
|
-
i18n.has('key', 'fr'); // Check if key exists in locale
|
|
472
|
-
await i18n.hasAsync('key', 'es'); // Check with async loading
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
### Message Management
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
// Add messages (merge)
|
|
479
|
-
i18n.add('en', { newKey: 'New value' });
|
|
480
|
-
|
|
481
|
-
// Set messages (replace)
|
|
482
|
-
i18n.set('en', { key: 'Value' });
|
|
483
|
-
|
|
484
|
-
// Get messages for locale
|
|
485
|
-
const messages = i18n.getMessages('en');
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### Async Loading
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
// Register loader
|
|
492
|
-
i18n.register('de', async () => import('./locales/de.json'));
|
|
493
|
-
|
|
494
|
-
// Load locale
|
|
495
|
-
await i18n.load('de');
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Formatting
|
|
499
|
-
|
|
500
|
-
```typescript
|
|
501
|
-
i18n.number(value, options?, locale?);
|
|
502
|
-
i18n.date(value, options?, locale?);
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### Namespace
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
const ns = i18n.namespace('auth');
|
|
509
|
-
ns.t('login.title');
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
### Subscriptions
|
|
513
|
-
|
|
514
|
-
```typescript
|
|
515
|
-
const unsubscribe = i18n.subscribe((locale) => {
|
|
516
|
-
console.log('Locale changed:', locale);
|
|
517
|
-
});
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
## Framework Integration
|
|
521
|
-
|
|
522
|
-
### React
|
|
523
|
-
|
|
524
|
-
```tsx
|
|
525
|
-
import { createI18n } from '@vielzeug/i18nit';
|
|
526
|
-
import { createContext, useContext, useState, useEffect } from 'react';
|
|
527
|
-
|
|
528
|
-
const I18nContext = createContext(null);
|
|
529
|
-
|
|
530
|
-
export function I18nProvider({ children, config }) {
|
|
531
|
-
const [i18n] = useState(() => createI18n(config));
|
|
532
|
-
const [locale, setLocale] = useState(i18n.getLocale());
|
|
533
|
-
|
|
534
|
-
useEffect(() => {
|
|
535
|
-
return i18n.subscribe(setLocale);
|
|
536
|
-
}, [i18n]);
|
|
537
|
-
|
|
538
|
-
return <I18nContext.Provider value={{ i18n, locale }}>{children}</I18nContext.Provider>;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export function useI18n() {
|
|
542
|
-
const context = useContext(I18nContext);
|
|
543
|
-
if (!context) throw new Error('useI18n must be used within I18nProvider');
|
|
544
|
-
return context;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
export function useTranslation(namespace?: string) {
|
|
548
|
-
const { i18n } = useI18n();
|
|
549
|
-
const ns = namespace ? i18n.namespace(namespace) : i18n;
|
|
550
|
-
|
|
551
|
-
return {
|
|
552
|
-
t: ns.t.bind(ns),
|
|
553
|
-
locale: i18n.getLocale(),
|
|
554
|
-
setLocale: i18n.setLocale.bind(i18n),
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Usage
|
|
559
|
-
function MyComponent() {
|
|
560
|
-
const { t, locale, setLocale } = useTranslation('dashboard');
|
|
561
|
-
|
|
562
|
-
return (
|
|
563
|
-
<div>
|
|
564
|
-
<h1>{t('welcome')}</h1>
|
|
565
|
-
<button onClick={() => setLocale('fr')}>Français</button>
|
|
566
|
-
</div>
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
### Vue 3
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
|
-
import { createI18n } from '@vielzeug/i18nit';
|
|
575
|
-
import { ref, onUnmounted, Plugin } from 'vue';
|
|
576
|
-
|
|
577
|
-
const i18n = createI18n({ locale: 'en' });
|
|
578
|
-
const locale = ref(i18n.getLocale());
|
|
579
|
-
|
|
580
|
-
const unsubscribe = i18n.subscribe((newLocale) => {
|
|
581
|
-
locale.value = newLocale;
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
export const i18nPlugin: Plugin = {
|
|
585
|
-
install(app) {
|
|
586
|
-
app.config.globalProperties.$t = i18n.t.bind(i18n);
|
|
587
|
-
app.config.globalProperties.$i18n = i18n;
|
|
588
|
-
|
|
589
|
-
app.provide('i18n', i18n);
|
|
590
|
-
app.provide('locale', locale);
|
|
591
|
-
},
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
// Composable
|
|
595
|
-
export function useI18n() {
|
|
596
|
-
return {
|
|
597
|
-
t: i18n.t.bind(i18n),
|
|
598
|
-
locale,
|
|
599
|
-
setLocale: (newLocale: string) => i18n.setLocale(newLocale),
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Usage in component
|
|
604
|
-
<script setup>
|
|
605
|
-
import { useI18n } from './i18n';
|
|
606
|
-
|
|
607
|
-
const { t, locale, setLocale } = useI18n();
|
|
608
|
-
</script>
|
|
609
|
-
|
|
610
|
-
<template>
|
|
611
|
-
<div>
|
|
612
|
-
<h1>{{ t('welcome') }}</h1>
|
|
613
|
-
<button @click="setLocale('fr')">Français</button>
|
|
614
|
-
</div>
|
|
615
|
-
</template>
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### Svelte
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
import { createI18n } from '@vielzeug/i18nit';
|
|
622
|
-
import { writable } from 'svelte/store';
|
|
623
|
-
|
|
624
|
-
const i18n = createI18n({ locale: 'en' });
|
|
625
|
-
export const locale = writable(i18n.getLocale());
|
|
626
|
-
|
|
627
|
-
i18n.subscribe((newLocale) => {
|
|
628
|
-
locale.set(newLocale);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
export const t = i18n.t.bind(i18n);
|
|
632
|
-
export const setLocale = i18n.setLocale.bind(i18n);
|
|
633
|
-
|
|
634
|
-
// Usage
|
|
635
|
-
<script>
|
|
636
|
-
import { t, setLocale } from './i18n';
|
|
637
|
-
</script>
|
|
638
|
-
|
|
639
|
-
<h1>{$t('welcome')}</h1>
|
|
640
|
-
<button on:click={() => setLocale('fr')}>Français</button>
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
## Best Practices
|
|
644
|
-
|
|
645
|
-
### 1. Organize Translations by Feature
|
|
646
|
-
|
|
647
|
-
```typescript
|
|
648
|
-
const messages = {
|
|
649
|
-
en: {
|
|
650
|
-
'auth.login.title': 'Login',
|
|
651
|
-
'auth.register.title': 'Register',
|
|
652
|
-
'dashboard.stats.users': 'Users',
|
|
653
|
-
'dashboard.stats.revenue': 'Revenue',
|
|
654
|
-
},
|
|
655
|
-
};
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
### 2. Use Namespaces for Large Apps
|
|
659
|
-
|
|
660
|
-
```typescript
|
|
661
|
-
const authTranslations = i18n.namespace('auth');
|
|
662
|
-
const dashboardTranslations = i18n.namespace('dashboard');
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### 3. Lazy Load Translations
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
const i18n = createI18n({
|
|
669
|
-
loaders: {
|
|
670
|
-
'en-US': () => import('./locales/en-US.json'),
|
|
671
|
-
'es-ES': () => import('./locales/es-ES.json'),
|
|
672
|
-
},
|
|
673
|
-
});
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### 4. Type-Safe Translation Keys
|
|
677
|
-
|
|
678
|
-
```typescript
|
|
679
|
-
type TranslationKeys = 'auth.login.title' | 'auth.register.title' | 'dashboard.welcome';
|
|
680
|
-
|
|
681
|
-
function t(key: TranslationKeys, vars?: Record<string, unknown>) {
|
|
682
|
-
return i18n.t(key, vars);
|
|
683
|
-
}
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
## TypeScript Support
|
|
687
|
-
|
|
688
|
-
Full TypeScript support with type inference:
|
|
689
|
-
|
|
690
|
-
```typescript
|
|
691
|
-
import { createI18n, type Messages, type I18nConfig } from '@vielzeug/i18nit';
|
|
692
|
-
|
|
693
|
-
// Define your messages type
|
|
694
|
-
interface MyMessages extends Messages {
|
|
695
|
-
greeting: string;
|
|
696
|
-
items: {
|
|
697
|
-
zero: string;
|
|
698
|
-
one: string;
|
|
699
|
-
other: string;
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const config: I18nConfig = {
|
|
704
|
-
locale: 'en',
|
|
705
|
-
messages: {
|
|
706
|
-
en: {
|
|
707
|
-
greeting: 'Hello!',
|
|
708
|
-
items: { zero: 'No items', one: 'One item', other: '{count} items' },
|
|
709
|
-
} satisfies MyMessages,
|
|
710
|
-
},
|
|
711
|
-
};
|
|
712
|
-
|
|
713
|
-
const i18n = createI18n(config);
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
## Comparison
|
|
53
|
+
## Features
|
|
717
54
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
55
|
+
- Typed translation keys from your message tree
|
|
56
|
+
- Dot-notation nested key lookup
|
|
57
|
+
- ICU-style interpolation with object/array path support
|
|
58
|
+
- Plural messages (`zero/one/two/few/many/other`) via `Intl.PluralRules`
|
|
59
|
+
- Locale chain fallback (`sr-Latn-RS -> sr-Latn -> sr`) + configured fallback locales
|
|
60
|
+
- Async locale loading (`load`, `setLocale`, `registerLoader`, `reload`)
|
|
61
|
+
- Catalog updates (`add` deep-merge, `replace` full replace)
|
|
62
|
+
- Subscription API with batched notifications (`batch`, `subscribe`)
|
|
63
|
+
- Intl format helpers (`number`, `date`, `list`, `relative`, `currency`)
|
|
64
|
+
- Namespace and locale-bound views (`scope`, `withLocale`)
|
|
65
|
+
- Diagnostic hooks (`onDiagnostic`) and missing-key hook (`onMissing`)
|
|
66
|
+
|
|
67
|
+
## API At a Glance
|
|
68
|
+
|
|
69
|
+
- `createI18n<T>(options?) => I18n<T>`
|
|
70
|
+
- `class I18n<T> implements BoundI18n<T>`
|
|
71
|
+
- `type BoundI18n<T>`
|
|
72
|
+
- `type I18nOptions<T>`
|
|
73
|
+
- `type Messages`, `TranslationKey`, `TranslationKeyParam`, `PluralKeys`, `NamespaceKeys`
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
- [Overview](https://vielzeug.dev/i18nit/)
|
|
78
|
+
- [Usage Guide](https://vielzeug.dev/i18nit/usage)
|
|
79
|
+
- [API Reference](https://vielzeug.dev/i18nit/api)
|
|
80
|
+
- [Examples](https://vielzeug.dev/i18nit/examples)
|
|
728
81
|
|
|
729
82
|
## License
|
|
730
83
|
|
|
731
|
-
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
|
|
732
|
-
|
|
733
|
-
## Links
|
|
734
|
-
|
|
735
|
-
- [GitHub Repository](https://github.com/helmuthdu/vielzeug)
|
|
736
|
-
- [Documentation](https://vielzeug.dev)
|
|
737
|
-
- [NPM Package](https://www.npmjs.com/package/@vielzeug/i18nit)
|
|
738
|
-
- [Issue Tracker](https://github.com/helmuthdu/vielzeug/issues)
|
|
739
|
-
|
|
740
|
-
---
|
|
741
|
-
|
|
742
|
-
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem - A collection of type-safe utilities for modern web development.
|
|
84
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
|