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