@vielzeug/i18nit 1.1.2 → 1.1.4
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 -163
- package/dist/i18nit.cjs +1 -1
- package/dist/i18nit.cjs.map +1 -1
- package/dist/i18nit.js +135 -174
- package/dist/i18nit.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +21 -30
- package/dist/index.js +2 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Type-safe, lightweight internationalization (i18n) library for TypeScript applic
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- ✅ **Type-Safe** - Full TypeScript support with generic types
|
|
8
|
-
- ✅ **Lightweight** -
|
|
8
|
+
- ✅ **Lightweight** - 1.6 KB gzipped with zero dependencies
|
|
9
9
|
- ✅ **Universal Pluralization** - 100+ languages via Intl.PluralRules API
|
|
10
10
|
- ✅ **Smart Array Handling** - Auto-join with separators, length access, and safe indexing
|
|
11
11
|
- ✅ **Path Interpolation** - Support for nested objects and array indices
|
|
@@ -14,7 +14,6 @@ Type-safe, lightweight internationalization (i18n) library for TypeScript applic
|
|
|
14
14
|
- ✅ **Fallback Chain** - Multiple fallback locales with automatic language variants
|
|
15
15
|
- ✅ **HTML Escaping** - Built-in XSS protection
|
|
16
16
|
- ✅ **Number & Date Formatting** - Locale-aware formatting with Intl API
|
|
17
|
-
- ✅ **Structured Errors** - Detailed error information with MissingVariableError
|
|
18
17
|
- ✅ **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
|
|
19
18
|
|
|
20
19
|
## Installation
|
|
@@ -160,6 +159,7 @@ i18n.t('count', { items: ['A', 'B', 'C'] });
|
|
|
160
159
|
```
|
|
161
160
|
|
|
162
161
|
**Array Features:**
|
|
162
|
+
|
|
163
163
|
- `{items}` - Join with comma (`, `)
|
|
164
164
|
- `{items|and}` - Natural "and" list with locale-aware conjunction (uses Intl.ListFormat - supports 100+ languages)
|
|
165
165
|
- `{items|or}` - Natural "or" list with locale-aware conjunction (uses Intl.ListFormat - supports 100+ languages)
|
|
@@ -169,6 +169,7 @@ i18n.t('count', { items: ['A', 'B', 'C'] });
|
|
|
169
169
|
|
|
170
170
|
**Locale-Aware List Formatting:**
|
|
171
171
|
The `and` and `or` separators use the built-in **Intl.ListFormat API** which automatically handles:
|
|
172
|
+
|
|
172
173
|
- **100+ languages** - Supports all languages available in the browser/runtime
|
|
173
174
|
- **Proper grammar** - Oxford comma, locale-specific punctuation
|
|
174
175
|
- **Right-to-left languages** - Arabic, Hebrew, etc.
|
|
@@ -176,6 +177,7 @@ The `and` and `or` separators use the built-in **Intl.ListFormat API** which aut
|
|
|
176
177
|
- **No manual configuration** - Zero maintenance required
|
|
177
178
|
|
|
178
179
|
Examples across languages:
|
|
180
|
+
|
|
179
181
|
- **English**: "A, B, and C" (with Oxford comma)
|
|
180
182
|
- **Spanish**: "A, B y C" (uses "y")
|
|
181
183
|
- **French**: "A, B et C" (uses "et")
|
|
@@ -195,46 +197,22 @@ Examples across languages:
|
|
|
195
197
|
- `{data.items[0].value}` - Mixed notation
|
|
196
198
|
|
|
197
199
|
**Limitations:**
|
|
200
|
+
|
|
198
201
|
- Only numeric bracket notation `[0]`, `[123]`
|
|
199
202
|
- Quoted keys not supported `["key"]`
|
|
200
203
|
- Non-numeric brackets not supported `[key]`
|
|
201
204
|
|
|
202
205
|
### Missing Variable Handling
|
|
203
206
|
|
|
204
|
-
|
|
207
|
+
Missing variables are automatically replaced with empty strings:
|
|
205
208
|
|
|
206
209
|
```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({
|
|
210
|
+
const i18n = createI18n({
|
|
225
211
|
messages: { en: { msg: 'Hello, {name}!' } },
|
|
226
|
-
missingVar: 'error',
|
|
227
212
|
});
|
|
228
213
|
|
|
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
|
-
}
|
|
214
|
+
i18n.t('msg'); // "Hello, !"
|
|
215
|
+
i18n.t('msg', { name: 'Alice' }); // "Hello, Alice!"
|
|
238
216
|
```
|
|
239
217
|
|
|
240
218
|
### Pluralization
|
|
@@ -274,39 +252,6 @@ i18nit uses the browser's built-in `Intl.PluralRules` API to automatically suppo
|
|
|
274
252
|
- **Japanese (ja)**: other
|
|
275
253
|
- And 90+ more languages...
|
|
276
254
|
|
|
277
|
-
### Message Functions
|
|
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
255
|
## Advanced Features
|
|
311
256
|
|
|
312
257
|
### Fallback Locales
|
|
@@ -330,6 +275,7 @@ i18n.t('welcome'); // "Welcome!" (en fallback)
|
|
|
330
275
|
```
|
|
331
276
|
|
|
332
277
|
**Fallback Chain:**
|
|
278
|
+
|
|
333
279
|
1. Primary locale (e.g., `de-CH`)
|
|
334
280
|
2. Base language (e.g., `de` from `de-CH`)
|
|
335
281
|
3. First fallback locale
|
|
@@ -352,25 +298,31 @@ const i18n = createI18n({
|
|
|
352
298
|
},
|
|
353
299
|
});
|
|
354
300
|
|
|
355
|
-
//
|
|
356
|
-
|
|
301
|
+
// Preload at app startup
|
|
302
|
+
await i18n.loadAll(['en', 'fr', 'de']);
|
|
357
303
|
|
|
358
304
|
// Or load explicitly
|
|
359
|
-
await i18n.load('
|
|
360
|
-
i18n.
|
|
305
|
+
await i18n.load('fr');
|
|
306
|
+
i18n.setLocale('fr');
|
|
307
|
+
i18n.t('greeting'); // Now uses French
|
|
361
308
|
|
|
362
309
|
// Register loader dynamically
|
|
363
310
|
i18n.register('es', async () => {
|
|
364
311
|
const module = await import('./locales/es.json');
|
|
365
312
|
return module.default;
|
|
366
313
|
});
|
|
314
|
+
|
|
315
|
+
// Load and use
|
|
316
|
+
await i18n.load('es');
|
|
317
|
+
i18n.t('greeting', undefined, { locale: 'es' });
|
|
367
318
|
```
|
|
368
319
|
|
|
369
320
|
**Features:**
|
|
321
|
+
|
|
370
322
|
- Concurrent requests are deduplicated
|
|
371
|
-
- Failed loads can be
|
|
372
|
-
- Errors are logged but don't break fallback
|
|
323
|
+
- Failed loads throw errors (can be caught)
|
|
373
324
|
- Locale is cached after loading
|
|
325
|
+
- Use `loadAll()` to preload multiple locales at once
|
|
374
326
|
|
|
375
327
|
### Namespaces
|
|
376
328
|
|
|
@@ -404,7 +356,7 @@ Protect against XSS attacks with automatic HTML escaping:
|
|
|
404
356
|
```typescript
|
|
405
357
|
const i18n = createI18n({
|
|
406
358
|
messages: {
|
|
407
|
-
en: {
|
|
359
|
+
en: {
|
|
408
360
|
userContent: 'Comment: {content}',
|
|
409
361
|
},
|
|
410
362
|
},
|
|
@@ -416,14 +368,11 @@ const safeI18n = createI18n({
|
|
|
416
368
|
messages: { en: { html: '<script>alert("xss")</script>' } },
|
|
417
369
|
});
|
|
418
370
|
|
|
419
|
-
safeI18n.t('html');
|
|
371
|
+
safeI18n.t('html');
|
|
420
372
|
// "<script>alert("xss")</script>"
|
|
421
373
|
|
|
422
374
|
// Or per translation
|
|
423
|
-
i18n.t('userContent',
|
|
424
|
-
{ content: '<b>Bold</b>' },
|
|
425
|
-
{ escape: true }
|
|
426
|
-
);
|
|
375
|
+
i18n.t('userContent', { content: '<b>Bold</b>' }, { escape: true });
|
|
427
376
|
// "Comment: <b>Bold</b>"
|
|
428
377
|
```
|
|
429
378
|
|
|
@@ -473,27 +422,13 @@ unsubscribe();
|
|
|
473
422
|
```
|
|
474
423
|
|
|
475
424
|
**Use Cases:**
|
|
425
|
+
|
|
476
426
|
- Update UI when locale changes
|
|
477
427
|
- Reload locale-specific data
|
|
478
428
|
- Analytics/tracking
|
|
479
429
|
- State management integration
|
|
480
430
|
|
|
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
|
|
431
|
+
### Subscriptions
|
|
497
432
|
|
|
498
433
|
### createI18n(config?)
|
|
499
434
|
|
|
@@ -501,13 +436,11 @@ Creates a new i18n instance.
|
|
|
501
436
|
|
|
502
437
|
```typescript
|
|
503
438
|
type I18nConfig = {
|
|
504
|
-
locale?: string;
|
|
505
|
-
fallback?: string | string[];
|
|
439
|
+
locale?: string; // Default: 'en'
|
|
440
|
+
fallback?: string | string[]; // Fallback locale(s)
|
|
506
441
|
messages?: Record<string, Messages>; // Initial translations
|
|
507
442
|
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
|
|
443
|
+
escape?: boolean; // Global HTML escaping (default: false)
|
|
511
444
|
};
|
|
512
445
|
```
|
|
513
446
|
|
|
@@ -524,26 +457,18 @@ i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With opt
|
|
|
524
457
|
```
|
|
525
458
|
|
|
526
459
|
**Options:**
|
|
460
|
+
|
|
527
461
|
- `locale?: string` - Override locale for this translation
|
|
528
|
-
- `fallback?: string` - Custom fallback text
|
|
529
462
|
- `escape?: boolean` - Override HTML escaping
|
|
530
463
|
|
|
531
|
-
#### `tl(key, vars?, options?)`
|
|
532
|
-
|
|
533
|
-
Translate a key asynchronously (loads locale if needed).
|
|
534
|
-
|
|
535
|
-
```typescript
|
|
536
|
-
await i18n.tl('greeting', { name: 'Alice' }, { locale: 'fr' });
|
|
537
|
-
```
|
|
538
|
-
|
|
539
464
|
### Locale Management
|
|
540
465
|
|
|
541
466
|
```typescript
|
|
542
|
-
i18n.setLocale('fr');
|
|
543
|
-
i18n.getLocale();
|
|
544
|
-
i18n.hasLocale('es');
|
|
545
|
-
i18n.has('key');
|
|
546
|
-
i18n.has('key', 'fr');
|
|
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
|
|
547
472
|
await i18n.hasAsync('key', 'es'); // Check with async loading
|
|
548
473
|
```
|
|
549
474
|
|
|
@@ -582,7 +507,6 @@ i18n.date(value, options?, locale?);
|
|
|
582
507
|
```typescript
|
|
583
508
|
const ns = i18n.namespace('auth');
|
|
584
509
|
ns.t('login.title');
|
|
585
|
-
await ns.tl('register.title', undefined, { locale: 'fr' });
|
|
586
510
|
```
|
|
587
511
|
|
|
588
512
|
### Subscriptions
|
|
@@ -611,11 +535,7 @@ export function I18nProvider({ children, config }) {
|
|
|
611
535
|
return i18n.subscribe(setLocale);
|
|
612
536
|
}, [i18n]);
|
|
613
537
|
|
|
614
|
-
return
|
|
615
|
-
<I18nContext.Provider value={{ i18n, locale }}>
|
|
616
|
-
{children}
|
|
617
|
-
</I18nContext.Provider>
|
|
618
|
-
);
|
|
538
|
+
return <I18nContext.Provider value={{ i18n, locale }}>{children}</I18nContext.Provider>;
|
|
619
539
|
}
|
|
620
540
|
|
|
621
541
|
export function useI18n() {
|
|
@@ -627,10 +547,9 @@ export function useI18n() {
|
|
|
627
547
|
export function useTranslation(namespace?: string) {
|
|
628
548
|
const { i18n } = useI18n();
|
|
629
549
|
const ns = namespace ? i18n.namespace(namespace) : i18n;
|
|
630
|
-
|
|
550
|
+
|
|
631
551
|
return {
|
|
632
552
|
t: ns.t.bind(ns),
|
|
633
|
-
tl: ns.tl.bind(ns),
|
|
634
553
|
locale: i18n.getLocale(),
|
|
635
554
|
setLocale: i18n.setLocale.bind(i18n),
|
|
636
555
|
};
|
|
@@ -639,7 +558,7 @@ export function useTranslation(namespace?: string) {
|
|
|
639
558
|
// Usage
|
|
640
559
|
function MyComponent() {
|
|
641
560
|
const { t, locale, setLocale } = useTranslation('dashboard');
|
|
642
|
-
|
|
561
|
+
|
|
643
562
|
return (
|
|
644
563
|
<div>
|
|
645
564
|
<h1>{t('welcome')}</h1>
|
|
@@ -666,7 +585,7 @@ export const i18nPlugin: Plugin = {
|
|
|
666
585
|
install(app) {
|
|
667
586
|
app.config.globalProperties.$t = i18n.t.bind(i18n);
|
|
668
587
|
app.config.globalProperties.$i18n = i18n;
|
|
669
|
-
|
|
588
|
+
|
|
670
589
|
app.provide('i18n', i18n);
|
|
671
590
|
app.provide('locale', locale);
|
|
672
591
|
},
|
|
@@ -676,7 +595,6 @@ export const i18nPlugin: Plugin = {
|
|
|
676
595
|
export function useI18n() {
|
|
677
596
|
return {
|
|
678
597
|
t: i18n.t.bind(i18n),
|
|
679
|
-
tl: i18n.tl.bind(i18n),
|
|
680
598
|
locale,
|
|
681
599
|
setLocale: (newLocale: string) => i18n.setLocale(newLocale),
|
|
682
600
|
};
|
|
@@ -758,40 +676,13 @@ const i18n = createI18n({
|
|
|
758
676
|
### 4. Type-Safe Translation Keys
|
|
759
677
|
|
|
760
678
|
```typescript
|
|
761
|
-
type TranslationKeys =
|
|
762
|
-
| 'auth.login.title'
|
|
763
|
-
| 'auth.register.title'
|
|
764
|
-
| 'dashboard.welcome';
|
|
679
|
+
type TranslationKeys = 'auth.login.title' | 'auth.register.title' | 'dashboard.welcome';
|
|
765
680
|
|
|
766
681
|
function t(key: TranslationKeys, vars?: Record<string, unknown>) {
|
|
767
682
|
return i18n.t(key, vars);
|
|
768
683
|
}
|
|
769
684
|
```
|
|
770
685
|
|
|
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
686
|
## TypeScript Support
|
|
796
687
|
|
|
797
688
|
Full TypeScript support with type inference:
|
|
@@ -824,18 +715,16 @@ const i18n = createI18n(config);
|
|
|
824
715
|
|
|
825
716
|
## Comparison
|
|
826
717
|
|
|
827
|
-
| Feature
|
|
828
|
-
|
|
829
|
-
| Bundle Size
|
|
830
|
-
| Dependencies
|
|
831
|
-
| TypeScript
|
|
832
|
-
| Pluralization
|
|
833
|
-
| Async Loading
|
|
834
|
-
| Path Interpolation | ✅ `{user.name}` | ❌
|
|
835
|
-
|
|
|
836
|
-
|
|
|
837
|
-
| Structured Errors | ✅ MissingVariableError | ❌ | ❌ |
|
|
838
|
-
| Framework Agnostic | ✅ | ✅ | ❌ React only |
|
|
718
|
+
| Feature | i18nit | i18next | react-intl |
|
|
719
|
+
| ------------------ | ---------------- | ----------- | ------------- |
|
|
720
|
+
| Bundle Size | **~1.6 KB** | ~12KB | ~15KB |
|
|
721
|
+
| Dependencies | **0** | 2+ | 10+ |
|
|
722
|
+
| TypeScript | ✅ First-class | ✅ Good | ✅ Good |
|
|
723
|
+
| Pluralization | ✅ Built-in | ✅ Plugin | ✅ Built-in |
|
|
724
|
+
| Async Loading | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
|
|
725
|
+
| Path Interpolation | ✅ `{user.name}` | ❌ | ❌ |
|
|
726
|
+
| HTML Escaping | ✅ Built-in | ⚠️ Manual | ✅ Built-in |
|
|
727
|
+
| Framework Agnostic | ✅ | ✅ | ❌ React only |
|
|
839
728
|
|
|
840
729
|
## License
|
|
841
730
|
|
|
@@ -851,4 +740,3 @@ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
|
|
|
851
740
|
---
|
|
852
741
|
|
|
853
742
|
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem - A collection of type-safe utilities for modern web development.
|
|
854
|
-
|
package/dist/i18nit.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function f(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function h(a,t){if(t in a)return a[t];const r=t.match(/[^.[\]]+/g)||[];let e=a;for(const s of r){if(e==null||typeof e!="object")return;if(Array.isArray(e)){const n=Number(s);if(Number.isNaN(n)||n<0||n>=e.length)return;e=e[n]}else e=e[s]}return e}function l(a,t,r){if(a.length===0)return"";const e=a.map(String);try{return new Intl.ListFormat(t,{style:"long",type:r}).format(e)}catch{if(e.length===1)return e[0];if(e.length===2){const o=r==="conjunction"?"and":"or";return`${e[0]} ${o} ${e[1]}`}const s=r==="conjunction"?"and":"or",n=e.pop();return`${e.join(", ")} ${s} ${n}`}}function u(a,t,r){return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g,(e,s,n)=>{const o=s.endsWith(".length"),c=o?s.slice(0,-7):s,i=h(t,c);if(i==null)return"";if(Array.isArray(i))return o?String(i.length):n!==void 0?n==="and"?l(i,r,"conjunction"):n==="or"?l(i,r,"disjunction"):i.map(String).join(n):i.map(String).join(", ");if(typeof i=="number")try{return new Intl.NumberFormat(r).format(i)}catch{return String(i)}return String(i)})}function g(a,t){const r=Math.abs(Math.floor(t));try{return new Intl.PluralRules(a).select(r)}catch{return r===1?"one":"other"}}class d{locale;fallbacks;escape;catalogs=new Map;loaders=new Map;loading=new Map;subscribers=new Set;constructor(t={}){if(this.locale=t.locale??"en",this.fallbacks=Array.isArray(t.fallback)?t.fallback:t.fallback?[t.fallback]:[],this.escape=t.escape??!1,t.messages)for(const[r,e]of Object.entries(t.messages))this.catalogs.set(r,e);if(t.loaders)for(const[r,e]of Object.entries(t.loaders))this.loaders.set(r,e)}setLocale(t){this.locale!==t&&(this.locale=t,this.notifySubscribers())}getLocale(){return this.locale}add(t,r){const e=this.catalogs.get(t)??{};this.catalogs.set(t,{...e,...r}),this.notifySubscribers()}set(t,r){this.catalogs.set(t,r),this.notifySubscribers()}getMessages(t){return this.catalogs.get(t)}hasLocale(t){return this.catalogs.has(t)}has(t,r){return this.findMessage(t,r??this.locale)!==void 0}async load(t){if(this.loading.has(t))return this.loading.get(t);if(this.catalogs.has(t))return;const r=this.loaders.get(t);if(!r)return;const e=(async()=>{try{const s=await r();this.add(t,s)}catch(s){throw console.warn(`[I18n] Failed to load locale '${t}':`,s),s}finally{this.loading.delete(t)}})();return this.loading.set(t,e),e}register(t,r){this.loaders.set(t,r)}async hasAsync(t,r){const e=r??this.locale;return!this.catalogs.has(e)&&this.loaders.has(e)&&await this.load(e),this.has(t,e)}async loadAll(t){await Promise.all(t.map(r=>this.load(r)))}t(t,r,e){const s=e?.locale??this.locale,n=e?.escape??this.escape,o=this.findMessage(t,s);if(o===void 0)return t;const c=this.formatMessage(o,r??{},s);return n?f(c):c}number(t,r,e){try{return new Intl.NumberFormat(e??this.locale,r).format(t)}catch{return String(t)}}date(t,r,e){const s=typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(e??this.locale,r).format(s)}catch{return s.toString()}}namespace(t){return{t:(r,e,s)=>this.t(`${t}.${r}`,e,s)}}subscribe(t){this.subscribers.add(t);try{t(this.locale)}catch{}return()=>this.subscribers.delete(t)}notifySubscribers(){for(const t of this.subscribers)try{t(this.locale)}catch{}}findMessage(t,r){const e=this.getLocaleChain(r);for(const s of e){const n=this.catalogs.get(s);if(!n)continue;const o=h(n,t);if(o!==void 0)return o}}getLocaleChain(t){const r=[t],e=t.split("-")[0];e!==t&&r.push(e);for(const s of this.fallbacks){r.push(s);const n=s.split("-")[0];n!==s&&r.push(n)}return r}formatMessage(t,r,e){if(typeof t=="object"&&"other"in t){const s=Number(r.count??0),n=t;let o;s===0&&n.zero!==void 0?o="zero":o=g(e,s);const c=n[o]??n.other;return u(c,r,e)}return typeof t=="string"?u(t,r,e):""}}function b(a){return new d(a)}exports.createI18n=b;
|
|
2
2
|
//# sourceMappingURL=i18nit.cjs.map
|
package/dist/i18nit.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18nit.cjs","sources":["../src/i18nit.ts"],"sourcesContent":["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 * Join array elements with natural language formatting using Intl.ListFormat.\n * Automatically supports 100+ languages with proper grammar and conjunctions.\n *\n * Uses the browser/Node.js built-in Intl.ListFormat API which handles:\n * - Locale-specific conjunctions (and/or/etc)\n * - Proper grammar for each language\n * - Oxford comma rules\n * - Right-to-left languages\n *\n * @param items - Array items to join\n * @param locale - Target locale\n * @param type - List type ('conjunction' for \"and\", 'disjunction' for \"or\")\n * @returns Formatted list string\n */\nconst formatList = (items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string => {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n // Use Intl.ListFormat for automatic locale-aware formatting\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat support (very rare)\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const conjunction = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${conjunction} ${stringItems[1]}`;\n }\n const conjunction = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems[stringItems.length - 1];\n const rest = stringItems.slice(0, -1);\n return `${rest.join(', ')} ${conjunction} ${last}`;\n }\n};\n\n/**\n * Resolve nested properties using dot notation and numeric bracket notation.\n * Safely handles array access - returns undefined for out-of-bounds indices.\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\n // Safe array access - check bounds\n if (Array.isArray(value)) {\n const index = Number(part);\n if (!Number.isNaN(index) && index >= 0 && index < value.length) {\n value = value[index];\n } else {\n return undefined;\n }\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n};\n\n/**\n * Interpolate variables into a template string.\n *\n * Template format:\n * - {variableName} - Simple variable\n * - {nested.path} - Nested object access\n * - {array[0]} - Array index (safe - returns empty if out of bounds)\n * - {array} - Array join with default separator (', ')\n * - {array|and} - Array join with locale-aware 'and' (automatically supports 100+ languages via Intl.ListFormat)\n * - {array|or} - Array join with locale-aware 'or' (automatically supports 100+ languages via Intl.ListFormat)\n * - {array| - } - Array join with custom separator\n * - {array.length} - Array length\n *\n * Uses Intl.ListFormat for locale-aware list formatting, which automatically handles:\n * - All languages supported by the browser/runtime (100+ languages)\n * - Proper conjunctions for each language\n * - Oxford comma rules\n * - Right-to-left languages\n * - No manual language configuration needed\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, separator) => {\n // Handle array.length special case\n let isLengthAccess = false;\n let actualKey = key;\n if (key.endsWith('.length')) {\n isLengthAccess = true;\n actualKey = key.slice(0, -7); // Remove '.length'\n }\n\n const value = resolvePath(vars, actualKey);\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 // Handle arrays\n if (Array.isArray(value)) {\n // Array length\n if (isLengthAccess) {\n return String(value.length);\n }\n\n // Array joining with separator\n if (separator !== undefined) {\n // Locale-aware special separators using Intl.ListFormat\n if (separator === 'and') {\n const locale = options.locale || 'en';\n return formatList(value, locale, 'conjunction');\n }\n if (separator === 'or') {\n const locale = options.locale || 'en';\n return formatList(value, locale, 'disjunction');\n }\n // Custom separator\n return value.map(String).join(separator);\n }\n\n // Default array join with comma and space\n return value.map(String).join(', ');\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","formatList","items","type","stringItems","conjunction","last","resolvePath","obj","path","parts","value","part","index","interpolate","template","vars","options","missingVar","match","separator","isLengthAccess","actualKey","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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"}
|
|
1
|
+
{"version":3,"file":"i18nit.cjs","sources":["../src/i18nit.ts"],"sourcesContent":["/* ============================================\n i18nit - Lightweight, type-safe i18n library\n ============================================ */\n\n/* -------------------- Core Types -------------------- */\n\nexport type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageValue = string | PluralMessages;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateOptions = {\n locale?: Locale;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, () => Promise<Messages>>;\n escape?: boolean;\n};\n\n/* -------------------- Path Resolution -------------------- */\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Resolves nested properties using dot notation and bracket notation.\n * Supports: 'user.name', 'items[0]', 'user.items[0].name'\n */\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n // Try direct access first (handles keys with dots)\n if (path in obj) return obj[path];\n\n // Parse path segments\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (Array.isArray(value)) {\n const index = Number(part);\n if (Number.isNaN(index) || index < 0 || index >= value.length) {\n return undefined;\n }\n value = value[index];\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n}\n\n/* -------------------- List Formatting -------------------- */\n\n/**\n * Formats an array as a natural language list using Intl.ListFormat.\n * Automatically handles locale-specific conjunctions and grammar.\n */\nfunction formatList(items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const separator = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${separator} ${stringItems[1]}`;\n }\n const separator = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems.pop()!;\n return `${stringItems.join(', ')} ${separator} ${last}`;\n }\n}\n\n/* -------------------- Variable Interpolation -------------------- */\n\n/**\n * Interpolates variables into a template string.\n *\n * Supported formats:\n * - {name} - Simple variable\n * - {user.name} - Nested property\n * - {items[0]} - Array index\n * - {items} - Array (comma-separated)\n * - {items|and} - Array with locale-aware \"and\"\n * - {items|or} - Array with locale-aware \"or\"\n * - {items| - } - Array with custom separator\n * - {items.length} - Array length\n */\nfunction interpolate(template: string, vars: Record<string, unknown>, locale: string): string {\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (_match, key: string, separator?: string) => {\n // Handle .length property\n const isLength = key.endsWith('.length');\n const actualKey = isLength ? key.slice(0, -7) : key;\n\n const value = resolvePath(vars, actualKey);\n\n // Missing variables are replaced with empty string\n if (value == null) return '';\n\n // Handle arrays\n if (Array.isArray(value)) {\n if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n if (separator === 'and') return formatList(value, locale, 'conjunction');\n if (separator === 'or') return formatList(value, locale, 'disjunction');\n return value.map(String).join(separator);\n }\n\n return value.map(String).join(', ');\n }\n\n // Format numbers with locale\n if (typeof value === 'number') {\n try {\n return new Intl.NumberFormat(locale).format(value);\n } catch {\n return String(value);\n }\n }\n\n return String(value);\n });\n}\n\n/* -------------------- Pluralization -------------------- */\n\n/**\n * Gets the plural form for a number using Intl.PluralRules.\n * Automatically handles all locale-specific plural rules.\n */\nfunction getPluralForm(locale: Locale, count: number): PluralForm {\n const n = Math.abs(Math.floor(count));\n\n try {\n const rules = new Intl.PluralRules(locale);\n return rules.select(n) as PluralForm;\n } catch {\n // Fallback to English-like behavior\n return n === 1 ? 'one' : 'other';\n }\n}\n\n/* -------------------- I18n Class -------------------- */\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private escape: boolean;\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, () => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<(locale: Locale) => void>();\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n this.escape = config.escape ?? false;\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n /* -------------------- Locale Management -------------------- */\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n /* -------------------- Message Management -------------------- */\n\n /**\n * Adds messages to a locale (merges with existing).\n */\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n /**\n * Sets messages for a locale (replaces existing).\n */\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale ?? this.locale) !== undefined;\n }\n\n /* -------------------- Async Loaders -------------------- */\n\n async load(locale: Locale): Promise<void> {\n // Return existing loading promise\n if (this.loading.has(locale)) return this.loading.get(locale);\n\n // Already loaded\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader();\n this.add(locale, messages);\n } catch (error) {\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: () => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n\n return this.has(key, targetLocale);\n }\n\n /**\n * Load multiple locales in parallel.\n * Useful for preloading all needed locales at app startup.\n */\n async loadAll(locales: Locale[]): Promise<void> {\n await Promise.all(locales.map((locale) => this.load(locale)));\n }\n\n /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables and options.\n * Synchronous - locale must be loaded first via load() or provided in config.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string {\n const targetLocale = options?.locale ?? this.locale;\n const shouldEscape = options?.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) return key;\n\n const result = this.formatMessage(message, vars ?? {}, targetLocale);\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n /* -------------------- Formatting Helpers -------------------- */\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n /* -------------------- Namespaced Translator -------------------- */\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: (locale: Locale) => void): () => void {\n this.subscribers.add(handler);\n\n // Call handler immediately with the current locale\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n /* -------------------- Internal Helpers -------------------- */\n\n private findMessage(key: string, locale: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n if (value !== undefined) return value as MessageValue;\n }\n\n return undefined;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n private formatMessage(message: MessageValue, vars: Record<string, unknown>, locale: Locale): string {\n // Plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n return interpolate(template, vars, locale);\n }\n\n // String messages\n if (typeof message === 'string') {\n return interpolate(message, vars, locale);\n }\n\n return '';\n }\n}\n\n/* -------------------- Factory Function -------------------- */\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["escapeHtml","str","resolvePath","obj","path","parts","value","part","index","formatList","items","locale","type","stringItems","separator","last","interpolate","template","vars","_match","key","isLength","actualKey","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","locales","options","shouldEscape","message","result","date","ns","handler","loc","chain","lang","fallback","fallbackLang","pluralMsg","form","createI18n"],"mappings":"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"}
|
package/dist/i18nit.js
CHANGED
|
@@ -1,122 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
function f(a) {
|
|
2
|
+
return a.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3
|
+
}
|
|
4
|
+
function h(a, t) {
|
|
5
|
+
if (t in a) return a[t];
|
|
6
|
+
const r = t.match(/[^.[\]]+/g) || [];
|
|
7
|
+
let e = a;
|
|
8
|
+
for (const s of r) {
|
|
9
|
+
if (e == null || typeof e != "object") return;
|
|
10
|
+
if (Array.isArray(e)) {
|
|
11
|
+
const n = Number(s);
|
|
12
|
+
if (Number.isNaN(n) || n < 0 || n >= e.length)
|
|
13
|
+
return;
|
|
14
|
+
e = e[n];
|
|
15
|
+
} else
|
|
16
|
+
e = e[s];
|
|
7
17
|
}
|
|
18
|
+
return e;
|
|
8
19
|
}
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"&": "&",
|
|
13
|
-
"<": "<",
|
|
14
|
-
">": ">"
|
|
15
|
-
}, u = (i) => i.replace(/[&<>"']/g, (t) => b[t]), f = (i, t, e) => {
|
|
16
|
-
if (i.length === 0) return "";
|
|
17
|
-
const s = i.map(String);
|
|
20
|
+
function l(a, t, r) {
|
|
21
|
+
if (a.length === 0) return "";
|
|
22
|
+
const e = a.map(String);
|
|
18
23
|
try {
|
|
19
|
-
return new Intl.ListFormat(t, { style: "long", type:
|
|
24
|
+
return new Intl.ListFormat(t, { style: "long", type: r }).format(e);
|
|
20
25
|
} catch {
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
23
|
-
const o =
|
|
24
|
-
return `${
|
|
26
|
+
if (e.length === 1) return e[0];
|
|
27
|
+
if (e.length === 2) {
|
|
28
|
+
const o = r === "conjunction" ? "and" : "or";
|
|
29
|
+
return `${e[0]} ${o} ${e[1]}`;
|
|
25
30
|
}
|
|
26
|
-
const
|
|
27
|
-
return `${
|
|
28
|
-
}
|
|
29
|
-
}, m = (i, t) => {
|
|
30
|
-
if (t in i) return i[t];
|
|
31
|
-
const e = t.match(/[^.[\]]+/g) || [];
|
|
32
|
-
let s = i;
|
|
33
|
-
for (const r of e) {
|
|
34
|
-
if (s == null || typeof s != "object") return;
|
|
35
|
-
if (Array.isArray(s)) {
|
|
36
|
-
const n = Number(r);
|
|
37
|
-
if (!Number.isNaN(n) && n >= 0 && n < s.length)
|
|
38
|
-
s = s[n];
|
|
39
|
-
else
|
|
40
|
-
return;
|
|
41
|
-
} else
|
|
42
|
-
s = s[r];
|
|
31
|
+
const s = r === "conjunction" ? "and" : "or", n = e.pop();
|
|
32
|
+
return `${e.join(", ")} ${s} ${n}`;
|
|
43
33
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
if (s === "preserve") return r;
|
|
53
|
-
if (s === "error")
|
|
54
|
-
throw new d(e.key ?? "unknown", n, e.locale ?? "unknown");
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
if (Array.isArray(c)) {
|
|
58
|
-
if (o)
|
|
59
|
-
return String(c.length);
|
|
60
|
-
if (a !== void 0) {
|
|
61
|
-
if (a === "and") {
|
|
62
|
-
const h = e.locale || "en";
|
|
63
|
-
return f(c, h, "conjunction");
|
|
64
|
-
}
|
|
65
|
-
if (a === "or") {
|
|
66
|
-
const h = e.locale || "en";
|
|
67
|
-
return f(c, h, "disjunction");
|
|
68
|
-
}
|
|
69
|
-
return c.map(String).join(a);
|
|
70
|
-
}
|
|
71
|
-
return c.map(String).join(", ");
|
|
72
|
-
}
|
|
73
|
-
if (typeof c == "number" && e.locale)
|
|
34
|
+
}
|
|
35
|
+
function u(a, t, r) {
|
|
36
|
+
return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g, (e, s, n) => {
|
|
37
|
+
const o = s.endsWith(".length"), c = o ? s.slice(0, -7) : s, i = h(t, c);
|
|
38
|
+
if (i == null) return "";
|
|
39
|
+
if (Array.isArray(i))
|
|
40
|
+
return o ? String(i.length) : n !== void 0 ? n === "and" ? l(i, r, "conjunction") : n === "or" ? l(i, r, "disjunction") : i.map(String).join(n) : i.map(String).join(", ");
|
|
41
|
+
if (typeof i == "number")
|
|
74
42
|
try {
|
|
75
|
-
return new Intl.NumberFormat(
|
|
43
|
+
return new Intl.NumberFormat(r).format(i);
|
|
76
44
|
} catch {
|
|
45
|
+
return String(i);
|
|
77
46
|
}
|
|
78
|
-
return String(
|
|
47
|
+
return String(i);
|
|
79
48
|
});
|
|
80
|
-
}
|
|
81
|
-
|
|
49
|
+
}
|
|
50
|
+
function g(a, t) {
|
|
51
|
+
const r = Math.abs(Math.floor(t));
|
|
82
52
|
try {
|
|
83
|
-
return new Intl.PluralRules(
|
|
53
|
+
return new Intl.PluralRules(a).select(r);
|
|
84
54
|
} catch {
|
|
85
|
-
return
|
|
55
|
+
return r === 1 ? "one" : "other";
|
|
86
56
|
}
|
|
87
|
-
}
|
|
88
|
-
class
|
|
57
|
+
}
|
|
58
|
+
class d {
|
|
89
59
|
locale;
|
|
90
60
|
fallbacks;
|
|
61
|
+
escape;
|
|
91
62
|
catalogs = /* @__PURE__ */ new Map();
|
|
92
63
|
loaders = /* @__PURE__ */ new Map();
|
|
93
64
|
loading = /* @__PURE__ */ new Map();
|
|
94
65
|
subscribers = /* @__PURE__ */ new Set();
|
|
95
|
-
escape;
|
|
96
|
-
missingKey;
|
|
97
|
-
missingVar;
|
|
98
66
|
constructor(t = {}) {
|
|
99
|
-
if (this.locale = t.locale ?? "en", this.fallbacks = Array.isArray(t.fallback) ? t.fallback : t.fallback ? [t.fallback] : [], this.escape = t.escape ?? !1,
|
|
100
|
-
for (const [
|
|
101
|
-
this.catalogs.set(
|
|
67
|
+
if (this.locale = t.locale ?? "en", this.fallbacks = Array.isArray(t.fallback) ? t.fallback : t.fallback ? [t.fallback] : [], this.escape = t.escape ?? !1, t.messages)
|
|
68
|
+
for (const [r, e] of Object.entries(t.messages))
|
|
69
|
+
this.catalogs.set(r, e);
|
|
102
70
|
if (t.loaders)
|
|
103
|
-
for (const [
|
|
104
|
-
this.loaders.set(
|
|
71
|
+
for (const [r, e] of Object.entries(t.loaders))
|
|
72
|
+
this.loaders.set(r, e);
|
|
105
73
|
}
|
|
106
|
-
|
|
74
|
+
/* -------------------- Locale Management -------------------- */
|
|
107
75
|
setLocale(t) {
|
|
108
76
|
this.locale !== t && (this.locale = t, this.notifySubscribers());
|
|
109
77
|
}
|
|
110
78
|
getLocale() {
|
|
111
79
|
return this.locale;
|
|
112
80
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
81
|
+
/* -------------------- Message Management -------------------- */
|
|
82
|
+
/**
|
|
83
|
+
* Adds messages to a locale (merges with existing).
|
|
84
|
+
*/
|
|
85
|
+
add(t, r) {
|
|
86
|
+
const e = this.catalogs.get(t) ?? {};
|
|
87
|
+
this.catalogs.set(t, { ...e, ...r }), this.notifySubscribers();
|
|
117
88
|
}
|
|
118
|
-
|
|
119
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Sets messages for a locale (replaces existing).
|
|
91
|
+
*/
|
|
92
|
+
set(t, r) {
|
|
93
|
+
this.catalogs.set(t, r), this.notifySubscribers();
|
|
120
94
|
}
|
|
121
95
|
getMessages(t) {
|
|
122
96
|
return this.catalogs.get(t);
|
|
@@ -124,72 +98,75 @@ class p {
|
|
|
124
98
|
hasLocale(t) {
|
|
125
99
|
return this.catalogs.has(t);
|
|
126
100
|
}
|
|
127
|
-
has(t,
|
|
128
|
-
return this.findMessage(t,
|
|
101
|
+
has(t, r) {
|
|
102
|
+
return this.findMessage(t, r ?? this.locale) !== void 0;
|
|
129
103
|
}
|
|
130
|
-
|
|
104
|
+
/* -------------------- Async Loaders -------------------- */
|
|
131
105
|
async load(t) {
|
|
132
106
|
if (this.loading.has(t)) return this.loading.get(t);
|
|
133
107
|
if (this.catalogs.has(t)) return;
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
136
|
-
const
|
|
108
|
+
const r = this.loaders.get(t);
|
|
109
|
+
if (!r) return;
|
|
110
|
+
const e = (async () => {
|
|
137
111
|
try {
|
|
138
|
-
const
|
|
139
|
-
this.add(t,
|
|
140
|
-
} catch (
|
|
141
|
-
throw console.warn(`[I18n] Failed to load locale '${t}':`,
|
|
112
|
+
const s = await r();
|
|
113
|
+
this.add(t, s);
|
|
114
|
+
} catch (s) {
|
|
115
|
+
throw console.warn(`[I18n] Failed to load locale '${t}':`, s), s;
|
|
142
116
|
} finally {
|
|
143
117
|
this.loading.delete(t);
|
|
144
118
|
}
|
|
145
119
|
})();
|
|
146
|
-
return this.loading.set(t,
|
|
147
|
-
}
|
|
148
|
-
register(t,
|
|
149
|
-
this.loaders.set(t,
|
|
150
|
-
}
|
|
151
|
-
async hasAsync(t,
|
|
152
|
-
const
|
|
153
|
-
return !this.catalogs.has(
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
120
|
+
return this.loading.set(t, e), e;
|
|
121
|
+
}
|
|
122
|
+
register(t, r) {
|
|
123
|
+
this.loaders.set(t, r);
|
|
124
|
+
}
|
|
125
|
+
async hasAsync(t, r) {
|
|
126
|
+
const e = r ?? this.locale;
|
|
127
|
+
return !this.catalogs.has(e) && this.loaders.has(e) && await this.load(e), this.has(t, e);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Load multiple locales in parallel.
|
|
131
|
+
* Useful for preloading all needed locales at app startup.
|
|
132
|
+
*/
|
|
133
|
+
async loadAll(t) {
|
|
134
|
+
await Promise.all(t.map((r) => this.load(r)));
|
|
135
|
+
}
|
|
136
|
+
/* -------------------- Translation -------------------- */
|
|
137
|
+
/**
|
|
138
|
+
* Translates a key with optional variables and options.
|
|
139
|
+
* Synchronous - locale must be loaded first via load() or provided in config.
|
|
140
|
+
*/
|
|
141
|
+
t(t, r, e) {
|
|
142
|
+
const s = e?.locale ?? this.locale, n = e?.escape ?? this.escape, o = this.findMessage(t, s);
|
|
143
|
+
if (o === void 0) return t;
|
|
144
|
+
const c = this.formatMessage(o, r ?? {}, s);
|
|
145
|
+
return n ? f(c) : c;
|
|
146
|
+
}
|
|
147
|
+
/* -------------------- Formatting Helpers -------------------- */
|
|
148
|
+
number(t, r, e) {
|
|
171
149
|
try {
|
|
172
|
-
return new Intl.NumberFormat(
|
|
150
|
+
return new Intl.NumberFormat(e ?? this.locale, r).format(t);
|
|
173
151
|
} catch {
|
|
174
152
|
return String(t);
|
|
175
153
|
}
|
|
176
154
|
}
|
|
177
|
-
date(t,
|
|
178
|
-
const
|
|
155
|
+
date(t, r, e) {
|
|
156
|
+
const s = typeof t == "number" ? new Date(t) : t;
|
|
179
157
|
try {
|
|
180
|
-
return new Intl.DateTimeFormat(
|
|
158
|
+
return new Intl.DateTimeFormat(e ?? this.locale, r).format(s);
|
|
181
159
|
} catch {
|
|
182
|
-
return
|
|
160
|
+
return s.toString();
|
|
183
161
|
}
|
|
184
162
|
}
|
|
185
|
-
|
|
163
|
+
/* -------------------- Namespaced Translator -------------------- */
|
|
186
164
|
namespace(t) {
|
|
187
165
|
return {
|
|
188
|
-
t: (e, s
|
|
189
|
-
tl: (e, s, r) => this.tl(`${t}.${e}`, s, r)
|
|
166
|
+
t: (r, e, s) => this.t(`${t}.${r}`, e, s)
|
|
190
167
|
};
|
|
191
168
|
}
|
|
192
|
-
|
|
169
|
+
/* -------------------- Subscriptions -------------------- */
|
|
193
170
|
subscribe(t) {
|
|
194
171
|
this.subscribers.add(t);
|
|
195
172
|
try {
|
|
@@ -205,57 +182,41 @@ class p {
|
|
|
205
182
|
} catch {
|
|
206
183
|
}
|
|
207
184
|
}
|
|
208
|
-
|
|
209
|
-
findMessage(t,
|
|
210
|
-
const
|
|
211
|
-
for (const
|
|
212
|
-
const n = this.catalogs.get(
|
|
185
|
+
/* -------------------- Internal Helpers -------------------- */
|
|
186
|
+
findMessage(t, r) {
|
|
187
|
+
const e = this.getLocaleChain(r);
|
|
188
|
+
for (const s of e) {
|
|
189
|
+
const n = this.catalogs.get(s);
|
|
213
190
|
if (!n) continue;
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
191
|
+
const o = h(n, t);
|
|
192
|
+
if (o !== void 0) return o;
|
|
216
193
|
}
|
|
217
194
|
}
|
|
218
195
|
getLocaleChain(t) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
for (const
|
|
222
|
-
|
|
223
|
-
const n =
|
|
224
|
-
n !==
|
|
196
|
+
const r = [t], e = t.split("-")[0];
|
|
197
|
+
e !== t && r.push(e);
|
|
198
|
+
for (const s of this.fallbacks) {
|
|
199
|
+
r.push(s);
|
|
200
|
+
const n = s.split("-")[0];
|
|
201
|
+
n !== s && r.push(n);
|
|
225
202
|
}
|
|
226
|
-
return
|
|
203
|
+
return r;
|
|
227
204
|
}
|
|
228
|
-
|
|
229
|
-
formatMessage(t, e, s, r, n) {
|
|
230
|
-
if (typeof t == "function")
|
|
231
|
-
try {
|
|
232
|
-
const a = t(e, {
|
|
233
|
-
date: (o, l) => this.date(o, l, s),
|
|
234
|
-
number: (o, l) => this.number(o, l, s)
|
|
235
|
-
});
|
|
236
|
-
return r ? u(a) : a;
|
|
237
|
-
} catch {
|
|
238
|
-
return "";
|
|
239
|
-
}
|
|
205
|
+
formatMessage(t, r, e) {
|
|
240
206
|
if (typeof t == "object" && "other" in t) {
|
|
241
|
-
const
|
|
242
|
-
let
|
|
243
|
-
|
|
244
|
-
const c = o
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
|
-
if (typeof t == "string") {
|
|
248
|
-
const a = g(t, e, { key: n, locale: s, missingVar: this.missingVar });
|
|
249
|
-
return r ? u(a) : a;
|
|
207
|
+
const s = Number(r.count ?? 0), n = t;
|
|
208
|
+
let o;
|
|
209
|
+
s === 0 && n.zero !== void 0 ? o = "zero" : o = g(e, s);
|
|
210
|
+
const c = n[o] ?? n.other;
|
|
211
|
+
return u(c, r, e);
|
|
250
212
|
}
|
|
251
|
-
return "";
|
|
213
|
+
return typeof t == "string" ? u(t, r, e) : "";
|
|
252
214
|
}
|
|
253
215
|
}
|
|
254
|
-
function
|
|
255
|
-
return new
|
|
216
|
+
function b(a) {
|
|
217
|
+
return new d(a);
|
|
256
218
|
}
|
|
257
219
|
export {
|
|
258
|
-
|
|
259
|
-
w as createI18n
|
|
220
|
+
b as createI18n
|
|
260
221
|
};
|
|
261
222
|
//# sourceMappingURL=i18nit.js.map
|
package/dist/i18nit.js.map
CHANGED
|
@@ -1 +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 * Join array elements with natural language formatting using Intl.ListFormat.\n * Automatically supports 100+ languages with proper grammar and conjunctions.\n *\n * Uses the browser/Node.js built-in Intl.ListFormat API which handles:\n * - Locale-specific conjunctions (and/or/etc)\n * - Proper grammar for each language\n * - Oxford comma rules\n * - Right-to-left languages\n *\n * @param items - Array items to join\n * @param locale - Target locale\n * @param type - List type ('conjunction' for \"and\", 'disjunction' for \"or\")\n * @returns Formatted list string\n */\nconst formatList = (items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string => {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n // Use Intl.ListFormat for automatic locale-aware formatting\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat support (very rare)\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const conjunction = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${conjunction} ${stringItems[1]}`;\n }\n const conjunction = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems[stringItems.length - 1];\n const rest = stringItems.slice(0, -1);\n return `${rest.join(', ')} ${conjunction} ${last}`;\n }\n};\n\n/**\n * Resolve nested properties using dot notation and numeric bracket notation.\n * Safely handles array access - returns undefined for out-of-bounds indices.\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\n // Safe array access - check bounds\n if (Array.isArray(value)) {\n const index = Number(part);\n if (!Number.isNaN(index) && index >= 0 && index < value.length) {\n value = value[index];\n } else {\n return undefined;\n }\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n};\n\n/**\n * Interpolate variables into a template string.\n *\n * Template format:\n * - {variableName} - Simple variable\n * - {nested.path} - Nested object access\n * - {array[0]} - Array index (safe - returns empty if out of bounds)\n * - {array} - Array join with default separator (', ')\n * - {array|and} - Array join with locale-aware 'and' (automatically supports 100+ languages via Intl.ListFormat)\n * - {array|or} - Array join with locale-aware 'or' (automatically supports 100+ languages via Intl.ListFormat)\n * - {array| - } - Array join with custom separator\n * - {array.length} - Array length\n *\n * Uses Intl.ListFormat for locale-aware list formatting, which automatically handles:\n * - All languages supported by the browser/runtime (100+ languages)\n * - Proper conjunctions for each language\n * - Oxford comma rules\n * - Right-to-left languages\n * - No manual language configuration needed\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, separator) => {\n // Handle array.length special case\n let isLengthAccess = false;\n let actualKey = key;\n if (key.endsWith('.length')) {\n isLengthAccess = true;\n actualKey = key.slice(0, -7); // Remove '.length'\n }\n\n const value = resolvePath(vars, actualKey);\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 // Handle arrays\n if (Array.isArray(value)) {\n // Array length\n if (isLengthAccess) {\n return String(value.length);\n }\n\n // Array joining with separator\n if (separator !== undefined) {\n // Locale-aware special separators using Intl.ListFormat\n if (separator === 'and') {\n const locale = options.locale || 'en';\n return formatList(value, locale, 'conjunction');\n }\n if (separator === 'or') {\n const locale = options.locale || 'en';\n return formatList(value, locale, 'disjunction');\n }\n // Custom separator\n return value.map(String).join(separator);\n }\n\n // Default array join with comma and space\n return value.map(String).join(', ');\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","formatList","items","type","stringItems","conjunction","last","resolvePath","obj","path","parts","value","part","index","interpolate","template","vars","options","missingVar","match","separator","isLengthAccess","actualKey","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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"}
|
|
1
|
+
{"version":3,"file":"i18nit.js","sources":["../src/i18nit.ts"],"sourcesContent":["/* ============================================\n i18nit - Lightweight, type-safe i18n library\n ============================================ */\n\n/* -------------------- Core Types -------------------- */\n\nexport type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageValue = string | PluralMessages;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateOptions = {\n locale?: Locale;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, () => Promise<Messages>>;\n escape?: boolean;\n};\n\n/* -------------------- Path Resolution -------------------- */\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Resolves nested properties using dot notation and bracket notation.\n * Supports: 'user.name', 'items[0]', 'user.items[0].name'\n */\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n // Try direct access first (handles keys with dots)\n if (path in obj) return obj[path];\n\n // Parse path segments\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (Array.isArray(value)) {\n const index = Number(part);\n if (Number.isNaN(index) || index < 0 || index >= value.length) {\n return undefined;\n }\n value = value[index];\n } else {\n value = (value as Record<string, unknown>)[part];\n }\n }\n\n return value;\n}\n\n/* -------------------- List Formatting -------------------- */\n\n/**\n * Formats an array as a natural language list using Intl.ListFormat.\n * Automatically handles locale-specific conjunctions and grammar.\n */\nfunction formatList(items: unknown[], locale: string, type: 'conjunction' | 'disjunction'): string {\n if (items.length === 0) return '';\n\n const stringItems = items.map(String);\n\n try {\n const formatter = new Intl.ListFormat(locale, { style: 'long', type });\n return formatter.format(stringItems);\n } catch {\n // Fallback for environments without Intl.ListFormat\n if (stringItems.length === 1) return stringItems[0];\n if (stringItems.length === 2) {\n const separator = type === 'conjunction' ? 'and' : 'or';\n return `${stringItems[0]} ${separator} ${stringItems[1]}`;\n }\n const separator = type === 'conjunction' ? 'and' : 'or';\n const last = stringItems.pop()!;\n return `${stringItems.join(', ')} ${separator} ${last}`;\n }\n}\n\n/* -------------------- Variable Interpolation -------------------- */\n\n/**\n * Interpolates variables into a template string.\n *\n * Supported formats:\n * - {name} - Simple variable\n * - {user.name} - Nested property\n * - {items[0]} - Array index\n * - {items} - Array (comma-separated)\n * - {items|and} - Array with locale-aware \"and\"\n * - {items|or} - Array with locale-aware \"or\"\n * - {items| - } - Array with custom separator\n * - {items.length} - Array length\n */\nfunction interpolate(template: string, vars: Record<string, unknown>, locale: string): string {\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (_match, key: string, separator?: string) => {\n // Handle .length property\n const isLength = key.endsWith('.length');\n const actualKey = isLength ? key.slice(0, -7) : key;\n\n const value = resolvePath(vars, actualKey);\n\n // Missing variables are replaced with empty string\n if (value == null) return '';\n\n // Handle arrays\n if (Array.isArray(value)) {\n if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n if (separator === 'and') return formatList(value, locale, 'conjunction');\n if (separator === 'or') return formatList(value, locale, 'disjunction');\n return value.map(String).join(separator);\n }\n\n return value.map(String).join(', ');\n }\n\n // Format numbers with locale\n if (typeof value === 'number') {\n try {\n return new Intl.NumberFormat(locale).format(value);\n } catch {\n return String(value);\n }\n }\n\n return String(value);\n });\n}\n\n/* -------------------- Pluralization -------------------- */\n\n/**\n * Gets the plural form for a number using Intl.PluralRules.\n * Automatically handles all locale-specific plural rules.\n */\nfunction getPluralForm(locale: Locale, count: number): PluralForm {\n const n = Math.abs(Math.floor(count));\n\n try {\n const rules = new Intl.PluralRules(locale);\n return rules.select(n) as PluralForm;\n } catch {\n // Fallback to English-like behavior\n return n === 1 ? 'one' : 'other';\n }\n}\n\n/* -------------------- I18n Class -------------------- */\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private escape: boolean;\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, () => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<(locale: Locale) => void>();\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n this.escape = config.escape ?? false;\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n /* -------------------- Locale Management -------------------- */\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n /* -------------------- Message Management -------------------- */\n\n /**\n * Adds messages to a locale (merges with existing).\n */\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n /**\n * Sets messages for a locale (replaces existing).\n */\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale ?? this.locale) !== undefined;\n }\n\n /* -------------------- Async Loaders -------------------- */\n\n async load(locale: Locale): Promise<void> {\n // Return existing loading promise\n if (this.loading.has(locale)) return this.loading.get(locale);\n\n // Already loaded\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader();\n this.add(locale, messages);\n } catch (error) {\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: () => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n\n return this.has(key, targetLocale);\n }\n\n /**\n * Load multiple locales in parallel.\n * Useful for preloading all needed locales at app startup.\n */\n async loadAll(locales: Locale[]): Promise<void> {\n await Promise.all(locales.map((locale) => this.load(locale)));\n }\n\n /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables and options.\n * Synchronous - locale must be loaded first via load() or provided in config.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string {\n const targetLocale = options?.locale ?? this.locale;\n const shouldEscape = options?.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) return key;\n\n const result = this.formatMessage(message, vars ?? {}, targetLocale);\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n /* -------------------- Formatting Helpers -------------------- */\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n /* -------------------- Namespaced Translator -------------------- */\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: (locale: Locale) => void): () => void {\n this.subscribers.add(handler);\n\n // Call handler immediately with the current locale\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n /* -------------------- Internal Helpers -------------------- */\n\n private findMessage(key: string, locale: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n if (value !== undefined) return value as MessageValue;\n }\n\n return undefined;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n private formatMessage(message: MessageValue, vars: Record<string, unknown>, locale: Locale): string {\n // Plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n return interpolate(template, vars, locale);\n }\n\n // String messages\n if (typeof message === 'string') {\n return interpolate(message, vars, locale);\n }\n\n return '';\n }\n}\n\n/* -------------------- Factory Function -------------------- */\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["escapeHtml","str","resolvePath","obj","path","parts","value","part","index","formatList","items","locale","type","stringItems","separator","last","interpolate","template","vars","_match","key","isLength","actualKey","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","locales","options","shouldEscape","message","result","date","ns","handler","loc","chain","lang","fallback","fallbackLang","pluralMsg","form","createI18n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./i18nit.cjs");exports.createI18n=e.createI18n;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -3,17 +3,21 @@ export declare function createI18n(config?: I18nConfig): I18n;
|
|
|
3
3
|
declare class I18n {
|
|
4
4
|
private locale;
|
|
5
5
|
private fallbacks;
|
|
6
|
+
private escape;
|
|
6
7
|
private catalogs;
|
|
7
8
|
private loaders;
|
|
8
9
|
private loading;
|
|
9
10
|
private subscribers;
|
|
10
|
-
private escape;
|
|
11
|
-
private missingKey;
|
|
12
|
-
private missingVar;
|
|
13
11
|
constructor(config?: I18nConfig);
|
|
14
12
|
setLocale(locale: Locale): void;
|
|
15
13
|
getLocale(): Locale;
|
|
14
|
+
/**
|
|
15
|
+
* Adds messages to a locale (merges with existing).
|
|
16
|
+
*/
|
|
16
17
|
add(locale: Locale, messages: Messages): void;
|
|
18
|
+
/**
|
|
19
|
+
* Sets messages for a locale (replaces existing).
|
|
20
|
+
*/
|
|
17
21
|
set(locale: Locale, messages: Messages): void;
|
|
18
22
|
getMessages(locale: Locale): Messages | undefined;
|
|
19
23
|
hasLocale(locale: Locale): boolean;
|
|
@@ -21,15 +25,22 @@ declare class I18n {
|
|
|
21
25
|
load(locale: Locale): Promise<void>;
|
|
22
26
|
register(locale: Locale, loader: () => Promise<Messages>): void;
|
|
23
27
|
hasAsync(key: string, locale?: Locale): Promise<boolean>;
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Load multiple locales in parallel.
|
|
30
|
+
* Useful for preloading all needed locales at app startup.
|
|
31
|
+
*/
|
|
32
|
+
loadAll(locales: Locale[]): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Translates a key with optional variables and options.
|
|
35
|
+
* Synchronous - locale must be loaded first via load() or provided in config.
|
|
36
|
+
*/
|
|
37
|
+
t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string;
|
|
26
38
|
number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string;
|
|
27
39
|
date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string;
|
|
28
40
|
namespace(ns: string): {
|
|
29
|
-
t: (key: string, vars?: Record<string, unknown>, options?:
|
|
30
|
-
tl: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) => Promise<string>;
|
|
41
|
+
t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) => string;
|
|
31
42
|
};
|
|
32
|
-
subscribe(handler:
|
|
43
|
+
subscribe(handler: (locale: Locale) => void): () => void;
|
|
33
44
|
private notifySubscribers;
|
|
34
45
|
private findMessage;
|
|
35
46
|
private getLocaleChain;
|
|
@@ -42,32 +53,13 @@ export declare type I18nConfig = {
|
|
|
42
53
|
messages?: Record<Locale, Messages>;
|
|
43
54
|
loaders?: Record<Locale, () => Promise<Messages>>;
|
|
44
55
|
escape?: boolean;
|
|
45
|
-
missingKey?: (key: string, locale: Locale) => string;
|
|
46
|
-
missingVar?: 'preserve' | 'empty' | 'error';
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
export declare type Locale = string;
|
|
50
59
|
|
|
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
60
|
export declare type Messages = Record<string, MessageValue>;
|
|
59
61
|
|
|
60
|
-
export declare type MessageValue = string | PluralMessages
|
|
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
|
-
}
|
|
62
|
+
export declare type MessageValue = string | PluralMessages;
|
|
71
63
|
|
|
72
64
|
export declare type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
73
65
|
|
|
@@ -75,9 +67,8 @@ export declare type PluralMessages = Partial<Record<PluralForm, string>> & {
|
|
|
75
67
|
other: string;
|
|
76
68
|
};
|
|
77
69
|
|
|
78
|
-
export declare type
|
|
70
|
+
export declare type TranslateOptions = {
|
|
79
71
|
locale?: Locale;
|
|
80
|
-
fallback?: string;
|
|
81
72
|
escape?: boolean;
|
|
82
73
|
};
|
|
83
74
|
|
package/dist/index.js
CHANGED