@vielzeug/i18nit 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -214
- package/dist/i18nit.cjs +1 -1
- package/dist/i18nit.cjs.map +1 -1
- package/dist/i18nit.js +105 -141
- package/dist/i18nit.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +19 -31
- package/dist/index.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,36 +1,86 @@
|
|
|
1
1
|
# @vielzeug/i18nit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What is I18nit?
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**I18nit** is a type-safe, lightweight internationalization library for TypeScript. Build multilingual applications with powerful pluralization, nested translations, and lazy loading—all in just 1.6 KB.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- ✅ **Lightweight** - ~2.3KB 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
|
-
- ✅ **Structured Errors** - Detailed error information with MissingVariableError
|
|
18
|
-
- ✅ **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
|
|
7
|
+
### The Problem
|
|
19
8
|
|
|
20
|
-
|
|
9
|
+
Internationalization libraries are often heavy and complex:
|
|
10
|
+
|
|
11
|
+
- **i18next** is feature-rich but adds 11KB+ to your bundle
|
|
12
|
+
- **react-intl** is React-specific and requires setup
|
|
13
|
+
- **FormatJS** has a steep learning curve
|
|
14
|
+
- Manual translations lead to missing keys and runtime errors
|
|
15
|
+
- Type safety requires extra tooling
|
|
16
|
+
|
|
17
|
+
### The Solution
|
|
18
|
+
|
|
19
|
+
I18nit provides a simple, type-safe API using native browser APIs:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { createI18n } from '@vielzeug/i18nit';
|
|
23
|
+
|
|
24
|
+
const i18n = createI18n({
|
|
25
|
+
locale: 'en',
|
|
26
|
+
messages: {
|
|
27
|
+
en: {
|
|
28
|
+
welcome: 'Welcome, {name}!',
|
|
29
|
+
items: 'You have {count} item | You have {count} items',
|
|
30
|
+
},
|
|
31
|
+
es: {
|
|
32
|
+
welcome: '¡Bienvenido, {name}!',
|
|
33
|
+
items: 'Tienes {count} artículo | Tienes {count} artículos',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Interpolation
|
|
39
|
+
i18n.t('welcome', { name: 'Alice' }); // "Welcome, Alice!"
|
|
40
|
+
|
|
41
|
+
// Automatic pluralization
|
|
42
|
+
i18n.t('items', { count: 1 }); // "You have 1 item"
|
|
43
|
+
i18n.t('items', { count: 5 }); // "You have 5 items"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## ✨ Features
|
|
47
|
+
|
|
48
|
+
- ✅ **Type-Safe** – Full TypeScript support with generic types
|
|
49
|
+
- ✅ **Lightweight** – 1.6 KB gzipped with zero dependencies
|
|
50
|
+
- ✅ **Universal Pluralization** – 100+ languages via Intl.PluralRules API
|
|
51
|
+
- ✅ **Smart Array Handling** – Auto-join with separators, length access, and safe indexing
|
|
52
|
+
- ✅ **Path Interpolation** – Support for nested objects and array indices
|
|
53
|
+
- ✅ **Lazy Loading** – Async locale loading with automatic caching
|
|
54
|
+
- ✅ **Namespaces** – Organize translations by feature or module
|
|
55
|
+
- ✅ **Fallback Chain** – Multiple fallback locales with automatic language variants
|
|
56
|
+
- ✅ **HTML Escaping** – Built-in XSS protection
|
|
57
|
+
- ✅ **Number & Date Formatting** – Locale-aware formatting with Intl API
|
|
58
|
+
- ✅ **Framework Agnostic** – Works with React, Vue, Svelte, or vanilla JS
|
|
59
|
+
|
|
60
|
+
## 🆚 Comparison with Alternatives
|
|
61
|
+
|
|
62
|
+
| Feature | I18nit | i18next | react-intl | FormatJS |
|
|
63
|
+
| ------------------- | -------------- | ------- | ---------- | ------------ |
|
|
64
|
+
| Bundle Size (gzip) | **~1.6 KB** | ~11KB | ~14KB | ~14KB |
|
|
65
|
+
| TypeScript Support | ✅ First-class | ✅ Good | ✅ Good | ✅ Excellent |
|
|
66
|
+
| Pluralization | ✅ Native Intl | ✅ ICU | ✅ ICU | ✅ ICU |
|
|
67
|
+
| Nested Translations | ✅ Built-in | ✅ Yes | ⚠️ Limited | ✅ Yes |
|
|
68
|
+
| Lazy Loading | ✅ Async | ✅ Yes | ⚠️ Manual | ✅ Yes |
|
|
69
|
+
| Framework Agnostic | ✅ Yes | ✅ Yes | ❌ React | ❌ React |
|
|
70
|
+
| Dependencies | 0 | 3 | 5 | 7 |
|
|
71
|
+
|
|
72
|
+
## 📦 Installation
|
|
21
73
|
|
|
22
74
|
```bash
|
|
23
75
|
# pnpm
|
|
24
76
|
pnpm add @vielzeug/i18nit
|
|
25
|
-
|
|
26
77
|
# npm
|
|
27
78
|
npm install @vielzeug/i18nit
|
|
28
|
-
|
|
29
79
|
# yarn
|
|
30
80
|
yarn add @vielzeug/i18nit
|
|
31
81
|
```
|
|
32
82
|
|
|
33
|
-
## Quick Start
|
|
83
|
+
## 🚀 Quick Start
|
|
34
84
|
|
|
35
85
|
```typescript
|
|
36
86
|
import { createI18n } from '@vielzeug/i18nit';
|
|
@@ -71,7 +121,7 @@ i18n.setLocale('es');
|
|
|
71
121
|
i18n.t('greeting', { name: 'Mundo' }); // "¡Hola, Mundo!"
|
|
72
122
|
```
|
|
73
123
|
|
|
74
|
-
## Core Concepts
|
|
124
|
+
## 📚 Core Concepts
|
|
75
125
|
|
|
76
126
|
### Translation Keys
|
|
77
127
|
|
|
@@ -91,6 +141,52 @@ const i18n = createI18n({
|
|
|
91
141
|
i18n.t('user.profile.title'); // "Profile"
|
|
92
142
|
```
|
|
93
143
|
|
|
144
|
+
### Nested Message Objects
|
|
145
|
+
|
|
146
|
+
You can organize messages using nested objects for better structure:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const i18n = createI18n({
|
|
150
|
+
locale: 'en',
|
|
151
|
+
messages: {
|
|
152
|
+
en: {
|
|
153
|
+
// Flat structure
|
|
154
|
+
welcome: 'Welcome!',
|
|
155
|
+
|
|
156
|
+
// Nested structure - access with dot notation
|
|
157
|
+
user: {
|
|
158
|
+
greeting: 'Hello, {name}!',
|
|
159
|
+
profile: {
|
|
160
|
+
title: 'User Profile',
|
|
161
|
+
settings: 'Profile Settings',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// Deep nesting
|
|
166
|
+
app: {
|
|
167
|
+
navigation: {
|
|
168
|
+
menu: {
|
|
169
|
+
home: 'Home',
|
|
170
|
+
about: 'About',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Access nested messages with dot notation
|
|
179
|
+
i18n.t('welcome'); // "Welcome!"
|
|
180
|
+
i18n.t('user.greeting', { name: 'Alice' }); // "Hello, Alice!"
|
|
181
|
+
i18n.t('user.profile.title'); // "User Profile"
|
|
182
|
+
i18n.t('app.navigation.menu.home'); // "Home"
|
|
183
|
+
|
|
184
|
+
// Use with namespaces for cleaner code
|
|
185
|
+
const userNs = i18n.namespace('user');
|
|
186
|
+
userNs.t('greeting', { name: 'Bob' }); // "Hello, Bob!"
|
|
187
|
+
userNs.t('profile.title'); // "User Profile"
|
|
188
|
+
```
|
|
189
|
+
|
|
94
190
|
### Variable Interpolation
|
|
95
191
|
|
|
96
192
|
#### Basic Interpolation
|
|
@@ -138,7 +234,7 @@ const i18n = createI18n({
|
|
|
138
234
|
i18n.t('shopping', { items: ['Apple', 'Banana', 'Orange'] });
|
|
139
235
|
// "Shopping list: Apple, Banana, Orange"
|
|
140
236
|
|
|
141
|
-
// Natural "and" lists (locale-aware via Intl.ListFormat
|
|
237
|
+
// Natural "and" lists (locale-aware via Intl.ListFormat – supports 100+ languages automatically)
|
|
142
238
|
i18n.t('guests', { names: ['Alice'] });
|
|
143
239
|
// "Invited: Alice"
|
|
144
240
|
i18n.t('guests', { names: ['Alice', 'Bob'] });
|
|
@@ -146,7 +242,7 @@ i18n.t('guests', { names: ['Alice', 'Bob'] });
|
|
|
146
242
|
i18n.t('guests', { names: ['Alice', 'Bob', 'Charlie'] });
|
|
147
243
|
// "Invited: Alice, Bob, and Charlie" (Oxford comma in English)
|
|
148
244
|
|
|
149
|
-
// Natural "or" lists (locale-aware via Intl.ListFormat
|
|
245
|
+
// Natural "or" lists (locale-aware via Intl.ListFormat – supports 100+ languages automatically)
|
|
150
246
|
i18n.t('options', { choices: ['Tea', 'Coffee', 'Juice'] });
|
|
151
247
|
// "Choose: Tea, Coffee, or Juice"
|
|
152
248
|
|
|
@@ -160,22 +256,25 @@ i18n.t('count', { items: ['A', 'B', 'C'] });
|
|
|
160
256
|
```
|
|
161
257
|
|
|
162
258
|
**Array Features:**
|
|
163
|
-
|
|
164
|
-
- `{items
|
|
165
|
-
- `{items|
|
|
166
|
-
- `{items|
|
|
167
|
-
- `{items
|
|
168
|
-
- `{items
|
|
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)
|
|
169
266
|
|
|
170
267
|
**Locale-Aware List Formatting:**
|
|
171
268
|
The `and` and `or` separators use the built-in **Intl.ListFormat API** which automatically handles:
|
|
172
|
-
|
|
173
|
-
- **
|
|
174
|
-
- **
|
|
175
|
-
- **
|
|
176
|
-
- **
|
|
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
|
|
177
275
|
|
|
178
276
|
Examples across languages:
|
|
277
|
+
|
|
179
278
|
- **English**: "A, B, and C" (with Oxford comma)
|
|
180
279
|
- **Spanish**: "A, B y C" (uses "y")
|
|
181
280
|
- **French**: "A, B et C" (uses "et")
|
|
@@ -186,55 +285,31 @@ Examples across languages:
|
|
|
186
285
|
|
|
187
286
|
#### Supported Path Formats
|
|
188
287
|
|
|
189
|
-
- `{name}`
|
|
190
|
-
- `{user.name}`
|
|
191
|
-
- `{items[0]}`
|
|
192
|
-
- `{items}`
|
|
193
|
-
- `{items|and}`
|
|
194
|
-
- `{items.length}`
|
|
195
|
-
- `{data.items[0].value}`
|
|
288
|
+
- `{name}` – Simple variable
|
|
289
|
+
- `{user.name}` – Nested object property
|
|
290
|
+
- `{items[0]}` – Array index (safe – returns empty if out of bounds)
|
|
291
|
+
- `{items}` – Array join with default separator
|
|
292
|
+
- `{items|and}` – Array join with "and"
|
|
293
|
+
- `{items.length}` – Array length
|
|
294
|
+
- `{data.items[0].value}` – Mixed notation
|
|
196
295
|
|
|
197
296
|
**Limitations:**
|
|
297
|
+
|
|
198
298
|
- Only numeric bracket notation `[0]`, `[123]`
|
|
199
299
|
- Quoted keys not supported `["key"]`
|
|
200
300
|
- Non-numeric brackets not supported `[key]`
|
|
201
301
|
|
|
202
302
|
### Missing Variable Handling
|
|
203
303
|
|
|
204
|
-
|
|
304
|
+
Missing variables are automatically replaced with empty strings:
|
|
205
305
|
|
|
206
306
|
```typescript
|
|
207
|
-
|
|
208
|
-
const i18n1 = createI18n({
|
|
209
|
-
messages: { en: { msg: 'Hello, {name}!' } },
|
|
210
|
-
missingVar: 'empty',
|
|
211
|
-
});
|
|
212
|
-
i18n1.t('msg'); // "Hello, !"
|
|
213
|
-
|
|
214
|
-
// Preserve placeholder
|
|
215
|
-
const i18n2 = createI18n({
|
|
216
|
-
messages: { en: { msg: 'Hello, {name}!' } },
|
|
217
|
-
missingVar: 'preserve',
|
|
218
|
-
});
|
|
219
|
-
i18n2.t('msg'); // "Hello, {name}!"
|
|
220
|
-
|
|
221
|
-
// Throw error
|
|
222
|
-
import { MissingVariableError } from '@vielzeug/i18nit';
|
|
223
|
-
|
|
224
|
-
const i18n3 = createI18n({
|
|
307
|
+
const i18n = createI18n({
|
|
225
308
|
messages: { en: { msg: 'Hello, {name}!' } },
|
|
226
|
-
missingVar: 'error',
|
|
227
309
|
});
|
|
228
310
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
} catch (error) {
|
|
232
|
-
if (error instanceof MissingVariableError) {
|
|
233
|
-
console.log(error.key); // 'msg'
|
|
234
|
-
console.log(error.variable); // 'name'
|
|
235
|
-
console.log(error.locale); // 'en'
|
|
236
|
-
}
|
|
237
|
-
}
|
|
311
|
+
i18n.t('msg'); // "Hello, !"
|
|
312
|
+
i18n.t('msg', { name: 'Alice' }); // "Hello, Alice!"
|
|
238
313
|
```
|
|
239
314
|
|
|
240
315
|
### Pluralization
|
|
@@ -274,40 +349,7 @@ i18nit uses the browser's built-in `Intl.PluralRules` API to automatically suppo
|
|
|
274
349
|
- **Japanese (ja)**: other
|
|
275
350
|
- And 90+ more languages...
|
|
276
351
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
For complex dynamic content, use function-based messages:
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
const i18n = createI18n({
|
|
283
|
-
locale: 'en',
|
|
284
|
-
messages: {
|
|
285
|
-
en: {
|
|
286
|
-
// Simple function
|
|
287
|
-
dynamic: (vars) => `Hello, ${vars.name}!`,
|
|
288
|
-
|
|
289
|
-
// With number formatting
|
|
290
|
-
price: (vars, helpers) =>
|
|
291
|
-
`Price: ${helpers.number(vars.amount as number, {
|
|
292
|
-
style: 'currency',
|
|
293
|
-
currency: 'USD'
|
|
294
|
-
})}`,
|
|
295
|
-
|
|
296
|
-
// With date formatting
|
|
297
|
-
event: (vars, helpers) =>
|
|
298
|
-
`Event on ${helpers.date(vars.date as Date, {
|
|
299
|
-
dateStyle: 'long'
|
|
300
|
-
})}`,
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
i18n.t('dynamic', { name: 'Eve' }); // "Hello, Eve!"
|
|
306
|
-
i18n.t('price', { amount: 99.99 }); // "Price: $99.99"
|
|
307
|
-
i18n.t('event', { date: new Date('2024-01-15') }); // "Event on January 15, 2024"
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
## Advanced Features
|
|
352
|
+
## 🔥 Advanced Features
|
|
311
353
|
|
|
312
354
|
### Fallback Locales
|
|
313
355
|
|
|
@@ -330,6 +372,7 @@ i18n.t('welcome'); // "Welcome!" (en fallback)
|
|
|
330
372
|
```
|
|
331
373
|
|
|
332
374
|
**Fallback Chain:**
|
|
375
|
+
|
|
333
376
|
1. Primary locale (e.g., `de-CH`)
|
|
334
377
|
2. Base language (e.g., `de` from `de-CH`)
|
|
335
378
|
3. First fallback locale
|
|
@@ -338,39 +381,64 @@ i18n.t('welcome'); // "Welcome!" (en fallback)
|
|
|
338
381
|
|
|
339
382
|
### Async Locale Loading
|
|
340
383
|
|
|
341
|
-
Load translations on-demand for better performance:
|
|
384
|
+
Load translations on-demand for better performance. Loaders receive the locale as a parameter, allowing you to reuse a single function:
|
|
342
385
|
|
|
343
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
|
+
|
|
344
393
|
const i18n = createI18n({
|
|
345
394
|
locale: 'en',
|
|
346
395
|
loaders: {
|
|
347
|
-
fr:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
},
|
|
351
|
-
de: async () => import('./locales/de.json'),
|
|
396
|
+
fr: loadLocale, // Loader receives 'fr' as parameter
|
|
397
|
+
de: loadLocale, // Loader receives 'de' as parameter
|
|
398
|
+
es: loadLocale, // Loader receives 'es' as parameter
|
|
352
399
|
},
|
|
353
400
|
});
|
|
354
401
|
|
|
355
|
-
//
|
|
356
|
-
|
|
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']);
|
|
357
419
|
|
|
358
420
|
// Or load explicitly
|
|
359
|
-
await i18n.load('
|
|
360
|
-
i18n.
|
|
421
|
+
await i18n.load('fr');
|
|
422
|
+
i18n.setLocale('fr');
|
|
423
|
+
i18n.t('greeting'); // Now uses French
|
|
361
424
|
|
|
362
425
|
// Register loader dynamically
|
|
363
426
|
i18n.register('es', async () => {
|
|
364
427
|
const module = await import('./locales/es.json');
|
|
365
428
|
return module.default;
|
|
366
429
|
});
|
|
430
|
+
|
|
431
|
+
// Load and use
|
|
432
|
+
await i18n.load('es');
|
|
433
|
+
i18n.t('greeting', undefined, { locale: 'es' });
|
|
367
434
|
```
|
|
368
435
|
|
|
369
436
|
**Features:**
|
|
437
|
+
|
|
370
438
|
- Concurrent requests are deduplicated
|
|
371
|
-
- Failed loads can be
|
|
372
|
-
- Errors are logged but don't break fallback
|
|
439
|
+
- Failed loads throw errors (can be caught)
|
|
373
440
|
- Locale is cached after loading
|
|
441
|
+
- Use `loadAll()` to preload multiple locales at once
|
|
374
442
|
|
|
375
443
|
### Namespaces
|
|
376
444
|
|
|
@@ -404,7 +472,7 @@ Protect against XSS attacks with automatic HTML escaping:
|
|
|
404
472
|
```typescript
|
|
405
473
|
const i18n = createI18n({
|
|
406
474
|
messages: {
|
|
407
|
-
en: {
|
|
475
|
+
en: {
|
|
408
476
|
userContent: 'Comment: {content}',
|
|
409
477
|
},
|
|
410
478
|
},
|
|
@@ -416,14 +484,11 @@ const safeI18n = createI18n({
|
|
|
416
484
|
messages: { en: { html: '<script>alert("xss")</script>' } },
|
|
417
485
|
});
|
|
418
486
|
|
|
419
|
-
safeI18n.t('html');
|
|
487
|
+
safeI18n.t('html');
|
|
420
488
|
// "<script>alert("xss")</script>"
|
|
421
489
|
|
|
422
490
|
// Or per translation
|
|
423
|
-
i18n.t('userContent',
|
|
424
|
-
{ content: '<b>Bold</b>' },
|
|
425
|
-
{ escape: true }
|
|
426
|
-
);
|
|
491
|
+
i18n.t('userContent', { content: '<b>Bold</b>' }, { escape: true });
|
|
427
492
|
// "Comment: <b>Bold</b>"
|
|
428
493
|
```
|
|
429
494
|
|
|
@@ -473,27 +538,13 @@ unsubscribe();
|
|
|
473
538
|
```
|
|
474
539
|
|
|
475
540
|
**Use Cases:**
|
|
541
|
+
|
|
476
542
|
- Update UI when locale changes
|
|
477
543
|
- Reload locale-specific data
|
|
478
544
|
- Analytics/tracking
|
|
479
545
|
- State management integration
|
|
480
546
|
|
|
481
|
-
###
|
|
482
|
-
|
|
483
|
-
Customize behavior for missing translations:
|
|
484
|
-
|
|
485
|
-
```typescript
|
|
486
|
-
const i18n = createI18n({
|
|
487
|
-
missingKey: (key, locale) => {
|
|
488
|
-
console.warn(`Missing translation: ${key} in ${locale}`);
|
|
489
|
-
return `[${locale}:${key}]`;
|
|
490
|
-
},
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
i18n.t('nonexistent.key'); // "[en:nonexistent.key]"
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
## API Reference
|
|
547
|
+
### Subscriptions
|
|
497
548
|
|
|
498
549
|
### createI18n(config?)
|
|
499
550
|
|
|
@@ -501,13 +552,11 @@ Creates a new i18n instance.
|
|
|
501
552
|
|
|
502
553
|
```typescript
|
|
503
554
|
type I18nConfig = {
|
|
504
|
-
locale?: string;
|
|
505
|
-
fallback?: string | string[];
|
|
555
|
+
locale?: string; // Default: 'en'
|
|
556
|
+
fallback?: string | string[]; // Fallback locale(s)
|
|
506
557
|
messages?: Record<string, Messages>; // Initial translations
|
|
507
558
|
loaders?: Record<string, () => Promise<Messages>>; // Async loaders
|
|
508
|
-
escape?: boolean;
|
|
509
|
-
missingKey?: (key: string, locale: string) => string; // Missing key handler
|
|
510
|
-
missingVar?: 'preserve' | 'empty' | 'error'; // Missing variable strategy
|
|
559
|
+
escape?: boolean; // Global HTML escaping (default: false)
|
|
511
560
|
};
|
|
512
561
|
```
|
|
513
562
|
|
|
@@ -524,26 +573,18 @@ i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With opt
|
|
|
524
573
|
```
|
|
525
574
|
|
|
526
575
|
**Options:**
|
|
527
|
-
- `locale?: string` - Override locale for this translation
|
|
528
|
-
- `fallback?: string` - Custom fallback text
|
|
529
|
-
- `escape?: boolean` - Override HTML escaping
|
|
530
576
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
Translate a key asynchronously (loads locale if needed).
|
|
534
|
-
|
|
535
|
-
```typescript
|
|
536
|
-
await i18n.tl('greeting', { name: 'Alice' }, { locale: 'fr' });
|
|
537
|
-
```
|
|
577
|
+
- `locale?: string` – Override locale for this translation
|
|
578
|
+
- `escape?: boolean` – Override HTML escaping
|
|
538
579
|
|
|
539
580
|
### Locale Management
|
|
540
581
|
|
|
541
582
|
```typescript
|
|
542
|
-
i18n.setLocale('fr');
|
|
543
|
-
i18n.getLocale();
|
|
544
|
-
i18n.hasLocale('es');
|
|
545
|
-
i18n.has('key');
|
|
546
|
-
i18n.has('key', 'fr');
|
|
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
|
|
547
588
|
await i18n.hasAsync('key', 'es'); // Check with async loading
|
|
548
589
|
```
|
|
549
590
|
|
|
@@ -582,7 +623,6 @@ i18n.date(value, options?, locale?);
|
|
|
582
623
|
```typescript
|
|
583
624
|
const ns = i18n.namespace('auth');
|
|
584
625
|
ns.t('login.title');
|
|
585
|
-
await ns.tl('register.title', undefined, { locale: 'fr' });
|
|
586
626
|
```
|
|
587
627
|
|
|
588
628
|
### Subscriptions
|
|
@@ -611,11 +651,7 @@ export function I18nProvider({ children, config }) {
|
|
|
611
651
|
return i18n.subscribe(setLocale);
|
|
612
652
|
}, [i18n]);
|
|
613
653
|
|
|
614
|
-
return
|
|
615
|
-
<I18nContext.Provider value={{ i18n, locale }}>
|
|
616
|
-
{children}
|
|
617
|
-
</I18nContext.Provider>
|
|
618
|
-
);
|
|
654
|
+
return <I18nContext.Provider value={{ i18n, locale }}>{children}</I18nContext.Provider>;
|
|
619
655
|
}
|
|
620
656
|
|
|
621
657
|
export function useI18n() {
|
|
@@ -627,10 +663,9 @@ export function useI18n() {
|
|
|
627
663
|
export function useTranslation(namespace?: string) {
|
|
628
664
|
const { i18n } = useI18n();
|
|
629
665
|
const ns = namespace ? i18n.namespace(namespace) : i18n;
|
|
630
|
-
|
|
666
|
+
|
|
631
667
|
return {
|
|
632
668
|
t: ns.t.bind(ns),
|
|
633
|
-
tl: ns.tl.bind(ns),
|
|
634
669
|
locale: i18n.getLocale(),
|
|
635
670
|
setLocale: i18n.setLocale.bind(i18n),
|
|
636
671
|
};
|
|
@@ -639,7 +674,7 @@ export function useTranslation(namespace?: string) {
|
|
|
639
674
|
// Usage
|
|
640
675
|
function MyComponent() {
|
|
641
676
|
const { t, locale, setLocale } = useTranslation('dashboard');
|
|
642
|
-
|
|
677
|
+
|
|
643
678
|
return (
|
|
644
679
|
<div>
|
|
645
680
|
<h1>{t('welcome')}</h1>
|
|
@@ -666,7 +701,7 @@ export const i18nPlugin: Plugin = {
|
|
|
666
701
|
install(app) {
|
|
667
702
|
app.config.globalProperties.$t = i18n.t.bind(i18n);
|
|
668
703
|
app.config.globalProperties.$i18n = i18n;
|
|
669
|
-
|
|
704
|
+
|
|
670
705
|
app.provide('i18n', i18n);
|
|
671
706
|
app.provide('locale', locale);
|
|
672
707
|
},
|
|
@@ -676,7 +711,6 @@ export const i18nPlugin: Plugin = {
|
|
|
676
711
|
export function useI18n() {
|
|
677
712
|
return {
|
|
678
713
|
t: i18n.t.bind(i18n),
|
|
679
|
-
tl: i18n.tl.bind(i18n),
|
|
680
714
|
locale,
|
|
681
715
|
setLocale: (newLocale: string) => i18n.setLocale(newLocale),
|
|
682
716
|
};
|
|
@@ -758,40 +792,13 @@ const i18n = createI18n({
|
|
|
758
792
|
### 4. Type-Safe Translation Keys
|
|
759
793
|
|
|
760
794
|
```typescript
|
|
761
|
-
type TranslationKeys =
|
|
762
|
-
| 'auth.login.title'
|
|
763
|
-
| 'auth.register.title'
|
|
764
|
-
| 'dashboard.welcome';
|
|
795
|
+
type TranslationKeys = 'auth.login.title' | 'auth.register.title' | 'dashboard.welcome';
|
|
765
796
|
|
|
766
797
|
function t(key: TranslationKeys, vars?: Record<string, unknown>) {
|
|
767
798
|
return i18n.t(key, vars);
|
|
768
799
|
}
|
|
769
800
|
```
|
|
770
801
|
|
|
771
|
-
### 5. Use Structured Error Handling
|
|
772
|
-
|
|
773
|
-
```typescript
|
|
774
|
-
import { MissingVariableError } from '@vielzeug/i18nit';
|
|
775
|
-
|
|
776
|
-
const i18n = createI18n({
|
|
777
|
-
missingVar: 'error',
|
|
778
|
-
messages: { en: { greeting: 'Hello, {name}!' } },
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
i18n.t('greeting');
|
|
783
|
-
} catch (error) {
|
|
784
|
-
if (error instanceof MissingVariableError) {
|
|
785
|
-
// Log to error tracking service
|
|
786
|
-
console.error('Missing variable:', {
|
|
787
|
-
key: error.key,
|
|
788
|
-
variable: error.variable,
|
|
789
|
-
locale: error.locale,
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
```
|
|
794
|
-
|
|
795
802
|
## TypeScript Support
|
|
796
803
|
|
|
797
804
|
Full TypeScript support with type inference:
|
|
@@ -822,33 +829,28 @@ const config: I18nConfig = {
|
|
|
822
829
|
const i18n = createI18n(config);
|
|
823
830
|
```
|
|
824
831
|
|
|
825
|
-
##
|
|
832
|
+
## 📖 Documentation
|
|
826
833
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
| TypeScript | ✅ First-class | ✅ Good | ✅ Good |
|
|
832
|
-
| Pluralization | ✅ Built-in | ✅ Plugin | ✅ Built-in |
|
|
833
|
-
| Async Loading | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
|
|
834
|
-
| Path Interpolation | ✅ `{user.name}` | ❌ | ❌ |
|
|
835
|
-
| Message Functions | ✅ Built-in | ⚠️ Limited | ✅ Components |
|
|
836
|
-
| HTML Escaping | ✅ Built-in | ⚠️ Manual | ✅ Built-in |
|
|
837
|
-
| Structured Errors | ✅ MissingVariableError | ❌ | ❌ |
|
|
838
|
-
| Framework Agnostic | ✅ | ✅ | ❌ React only |
|
|
834
|
+
- [**Full Documentation**](https://helmuthdu.github.io/vielzeug/i18nit)
|
|
835
|
+
- [**Usage Guide**](https://helmuthdu.github.io/vielzeug/i18nit/usage)
|
|
836
|
+
- [**API Reference**](https://helmuthdu.github.io/vielzeug/i18nit/api)
|
|
837
|
+
- [**Examples**](https://helmuthdu.github.io/vielzeug/i18nit/examples)
|
|
839
838
|
|
|
840
|
-
## License
|
|
839
|
+
## 📄 License
|
|
841
840
|
|
|
842
841
|
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
|
|
843
842
|
|
|
844
|
-
##
|
|
843
|
+
## 🤝 Contributing
|
|
844
|
+
|
|
845
|
+
Contributions are welcome! Check our [GitHub repository](https://github.com/helmuthdu/vielzeug).
|
|
846
|
+
|
|
847
|
+
## 🔗 Links
|
|
845
848
|
|
|
846
849
|
- [GitHub Repository](https://github.com/helmuthdu/vielzeug)
|
|
847
|
-
- [Documentation](https://vielzeug
|
|
848
|
-
- [NPM Package](https://www.npmjs.com/package/@vielzeug/
|
|
850
|
+
- [Documentation](https://helmuthdu.github.io/vielzeug/deposit)
|
|
851
|
+
- [NPM Package](https://www.npmjs.com/package/@vielzeug/deposit)
|
|
849
852
|
- [Issue Tracker](https://github.com/helmuthdu/vielzeug/issues)
|
|
850
853
|
|
|
851
854
|
---
|
|
852
855
|
|
|
853
|
-
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem
|
|
854
|
-
|
|
856
|
+
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem – A collection of type-safe utilities for modern web development.
|