@vielzeug/i18nit 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,783 @@
1
+ # @vielzeug/i18nit
2
+
3
+ Type-safe, lightweight internationalization (i18n) library for TypeScript applications. Simple, powerful translation management with zero dependencies.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Type-Safe** - Full TypeScript support with generic types
8
+ - ✅ **Lightweight** - ~3KB gzipped with zero dependencies
9
+ - ✅ **Universal Pluralization** - 100+ languages via Intl.PluralRules API
10
+ - ✅ **Path Interpolation** - Support for nested objects and array indices
11
+ - ✅ **Lazy Loading** - Async locale loading with automatic caching
12
+ - ✅ **Namespaces** - Organize translations by feature or module
13
+ - ✅ **Fallback Chain** - Multiple fallback locales with automatic language variants
14
+ - ✅ **HTML Escaping** - Built-in XSS protection
15
+ - ✅ **Number & Date Formatting** - Locale-aware formatting with Intl API
16
+ - ✅ **Structured Errors** - Detailed error information with MissingVariableError
17
+ - ✅ **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ # pnpm
23
+ pnpm add @vielzeug/i18nit
24
+
25
+ # npm
26
+ npm install @vielzeug/i18nit
27
+
28
+ # yarn
29
+ yarn add @vielzeug/i18nit
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```typescript
35
+ import { createI18n } from '@vielzeug/i18nit';
36
+
37
+ // Create instance with messages
38
+ const i18n = createI18n({
39
+ locale: 'en',
40
+ messages: {
41
+ en: {
42
+ greeting: 'Hello, {name}!',
43
+ items: {
44
+ zero: 'No items',
45
+ one: 'One item',
46
+ other: '{count} items',
47
+ },
48
+ },
49
+ es: {
50
+ greeting: '¡Hola, {name}!',
51
+ items: {
52
+ zero: 'Sin artículos',
53
+ one: 'Un artículo',
54
+ other: '{count} artículos',
55
+ },
56
+ },
57
+ },
58
+ });
59
+
60
+ // Simple translation
61
+ i18n.t('greeting', { name: 'World' }); // "Hello, World!"
62
+
63
+ // Pluralization
64
+ i18n.t('items', { count: 0 }); // "No items"
65
+ i18n.t('items', { count: 1 }); // "One item"
66
+ i18n.t('items', { count: 5 }); // "5 items"
67
+
68
+ // Change locale
69
+ i18n.setLocale('es');
70
+ i18n.t('greeting', { name: 'Mundo' }); // "¡Hola, Mundo!"
71
+ ```
72
+
73
+ ## Core Concepts
74
+
75
+ ### Translation Keys
76
+
77
+ Translation keys support dot notation for nested organization:
78
+
79
+ ```typescript
80
+ const i18n = createI18n({
81
+ messages: {
82
+ en: {
83
+ 'user.profile.title': 'Profile',
84
+ 'user.settings.title': 'Settings',
85
+ 'admin.dashboard': 'Dashboard',
86
+ },
87
+ },
88
+ });
89
+
90
+ i18n.t('user.profile.title'); // "Profile"
91
+ ```
92
+
93
+ ### Variable Interpolation
94
+
95
+ #### Basic Interpolation
96
+
97
+ ```typescript
98
+ i18n.t('greeting', { name: 'Alice' });
99
+ // Template: "Hello, {name}!"
100
+ // Result: "Hello, Alice!"
101
+ ```
102
+
103
+ #### Nested Object Access
104
+
105
+ ```typescript
106
+ i18n.t('message', { user: { name: 'Bob', role: 'Admin' } });
107
+ // Template: "User {user.name} is {user.role}"
108
+ // Result: "User Bob is Admin"
109
+ ```
110
+
111
+ #### Array Index Access
112
+
113
+ ```typescript
114
+ i18n.t('friends', { friends: [{ name: 'Charlie' }, { name: 'Dave' }] });
115
+ // Template: "First friend: {friends[0].name}"
116
+ // Result: "First friend: Charlie"
117
+ ```
118
+
119
+ #### Supported Path Formats
120
+
121
+ - `{name}` - Simple variable
122
+ - `{user.name}` - Nested object property
123
+ - `{items[0]}` - Array index
124
+ - `{data.items[0].value}` - Mixed notation
125
+
126
+ **Limitations:**
127
+ - Only numeric bracket notation `[0]`, `[123]`
128
+ - Quoted keys not supported `["key"]`
129
+ - Non-numeric brackets not supported `[key]`
130
+
131
+ ### Missing Variable Handling
132
+
133
+ Control how missing variables are handled:
134
+
135
+ ```typescript
136
+ // Empty string (default)
137
+ const i18n1 = createI18n({
138
+ messages: { en: { msg: 'Hello, {name}!' } },
139
+ missingVar: 'empty',
140
+ });
141
+ i18n1.t('msg'); // "Hello, !"
142
+
143
+ // Preserve placeholder
144
+ const i18n2 = createI18n({
145
+ messages: { en: { msg: 'Hello, {name}!' } },
146
+ missingVar: 'preserve',
147
+ });
148
+ i18n2.t('msg'); // "Hello, {name}!"
149
+
150
+ // Throw error
151
+ import { MissingVariableError } from '@vielzeug/i18nit';
152
+
153
+ const i18n3 = createI18n({
154
+ messages: { en: { msg: 'Hello, {name}!' } },
155
+ missingVar: 'error',
156
+ });
157
+
158
+ try {
159
+ i18n3.t('msg');
160
+ } catch (error) {
161
+ if (error instanceof MissingVariableError) {
162
+ console.log(error.key); // 'msg'
163
+ console.log(error.variable); // 'name'
164
+ console.log(error.locale); // 'en'
165
+ }
166
+ }
167
+ ```
168
+
169
+ ### Pluralization
170
+
171
+ Support for multiple plural forms based on locale-specific rules:
172
+
173
+ ```typescript
174
+ const i18n = createI18n({
175
+ locale: 'en',
176
+ messages: {
177
+ en: {
178
+ notifications: {
179
+ zero: 'No notifications',
180
+ one: 'One notification',
181
+ other: '{count} notifications',
182
+ },
183
+ },
184
+ },
185
+ });
186
+
187
+ i18n.t('notifications', { count: 0 }); // "No notifications"
188
+ i18n.t('notifications', { count: 1 }); // "One notification"
189
+ i18n.t('notifications', { count: 5 }); // "5 notifications"
190
+ ```
191
+
192
+ #### Supported Plural Rules
193
+
194
+ i18nit uses the browser's built-in `Intl.PluralRules` API to automatically support pluralization for **100+ languages**, including:
195
+
196
+ - **English (en)**: one, other
197
+ - **French (fr)**: one (0-1), other
198
+ - **Arabic (ar)**: zero, one, two, few, many, other
199
+ - **Polish (pl)**: one, few, many
200
+ - **Russian (ru)**: one, few, many, other
201
+ - **German (de)**: one, other
202
+ - **Chinese (zh)**: other
203
+ - **Japanese (ja)**: other
204
+ - And 90+ more languages...
205
+
206
+ ### Message Functions
207
+
208
+ For complex dynamic content, use function-based messages:
209
+
210
+ ```typescript
211
+ const i18n = createI18n({
212
+ locale: 'en',
213
+ messages: {
214
+ en: {
215
+ // Simple function
216
+ dynamic: (vars) => `Hello, ${vars.name}!`,
217
+
218
+ // With number formatting
219
+ price: (vars, helpers) =>
220
+ `Price: ${helpers.number(vars.amount as number, {
221
+ style: 'currency',
222
+ currency: 'USD'
223
+ })}`,
224
+
225
+ // With date formatting
226
+ event: (vars, helpers) =>
227
+ `Event on ${helpers.date(vars.date as Date, {
228
+ dateStyle: 'long'
229
+ })}`,
230
+ },
231
+ },
232
+ });
233
+
234
+ i18n.t('dynamic', { name: 'Eve' }); // "Hello, Eve!"
235
+ i18n.t('price', { amount: 99.99 }); // "Price: $99.99"
236
+ i18n.t('event', { date: new Date('2024-01-15') }); // "Event on January 15, 2024"
237
+ ```
238
+
239
+ ## Advanced Features
240
+
241
+ ### Fallback Locales
242
+
243
+ Define fallback locales for missing translations:
244
+
245
+ ```typescript
246
+ const i18n = createI18n({
247
+ locale: 'de-CH',
248
+ fallback: ['de', 'en'],
249
+ messages: {
250
+ 'de-CH': { greeting: 'Grüezi!' },
251
+ de: { greeting: 'Hallo!', goodbye: 'Auf Wiedersehen!' },
252
+ en: { greeting: 'Hello!', goodbye: 'Goodbye!', welcome: 'Welcome!' },
253
+ },
254
+ });
255
+
256
+ i18n.t('greeting'); // "Grüezi!" (de-CH)
257
+ i18n.t('goodbye'); // "Auf Wiedersehen!" (de fallback)
258
+ i18n.t('welcome'); // "Welcome!" (en fallback)
259
+ ```
260
+
261
+ **Fallback Chain:**
262
+ 1. Primary locale (e.g., `de-CH`)
263
+ 2. Base language (e.g., `de` from `de-CH`)
264
+ 3. First fallback locale
265
+ 4. Base of first fallback
266
+ 5. Continue through all fallbacks
267
+
268
+ ### Async Locale Loading
269
+
270
+ Load translations on-demand for better performance:
271
+
272
+ ```typescript
273
+ const i18n = createI18n({
274
+ locale: 'en',
275
+ loaders: {
276
+ fr: async () => {
277
+ const response = await fetch('/locales/fr.json');
278
+ return response.json();
279
+ },
280
+ de: async () => import('./locales/de.json'),
281
+ },
282
+ });
283
+
284
+ // Lazy translation - loads locale first
285
+ const text = await i18n.tl('greeting', { name: 'World' }, { locale: 'fr' });
286
+
287
+ // Or load explicitly
288
+ await i18n.load('de');
289
+ i18n.t('greeting', undefined, { locale: 'de' });
290
+
291
+ // Register loader dynamically
292
+ i18n.register('es', async () => {
293
+ const module = await import('./locales/es.json');
294
+ return module.default;
295
+ });
296
+ ```
297
+
298
+ **Features:**
299
+ - Concurrent requests are deduplicated
300
+ - Failed loads can be retried
301
+ - Errors are logged but don't break fallback
302
+ - Locale is cached after loading
303
+
304
+ ### Namespaces
305
+
306
+ Organize translations by feature or module:
307
+
308
+ ```typescript
309
+ const i18n = createI18n({
310
+ messages: {
311
+ en: {
312
+ 'auth.login.title': 'Login',
313
+ 'auth.login.button': 'Sign In',
314
+ 'auth.register.title': 'Register',
315
+ 'dashboard.welcome': 'Welcome back!',
316
+ },
317
+ },
318
+ });
319
+
320
+ // Create namespaced translator
321
+ const auth = i18n.namespace('auth.login');
322
+ auth.t('title'); // "Login"
323
+ auth.t('button'); // "Sign In"
324
+
325
+ const dashboard = i18n.namespace('dashboard');
326
+ dashboard.t('welcome'); // "Welcome back!"
327
+ ```
328
+
329
+ ### HTML Escaping
330
+
331
+ Protect against XSS attacks with automatic HTML escaping:
332
+
333
+ ```typescript
334
+ const i18n = createI18n({
335
+ messages: {
336
+ en: {
337
+ userContent: 'Comment: {content}',
338
+ },
339
+ },
340
+ });
341
+
342
+ // Enable escaping globally
343
+ const safeI18n = createI18n({
344
+ escape: true,
345
+ messages: { en: { html: '<script>alert("xss")</script>' } },
346
+ });
347
+
348
+ safeI18n.t('html');
349
+ // "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
350
+
351
+ // Or per translation
352
+ i18n.t('userContent',
353
+ { content: '<b>Bold</b>' },
354
+ { escape: true }
355
+ );
356
+ // "Comment: &lt;b&gt;Bold&lt;/b&gt;"
357
+ ```
358
+
359
+ ### Number & Date Formatting
360
+
361
+ Locale-aware formatting using the Intl API:
362
+
363
+ ```typescript
364
+ const i18n = createI18n({ locale: 'en-US' });
365
+
366
+ // Number formatting
367
+ i18n.number(1234.56); // "1,234.56"
368
+ i18n.number(99.99, { style: 'currency', currency: 'USD' }); // "$99.99"
369
+ i18n.number(0.15, { style: 'percent' }); // "15%"
370
+
371
+ // Date formatting
372
+ const date = new Date('2024-01-15');
373
+ i18n.date(date); // "1/15/2024"
374
+ i18n.date(date, { dateStyle: 'long' }); // "January 15, 2024"
375
+ i18n.date(date, { timeStyle: 'short' }); // "12:00 AM"
376
+
377
+ // Timestamps
378
+ i18n.date(Date.now(), { dateStyle: 'medium' }); // "Jan 15, 2024"
379
+
380
+ // Custom locale
381
+ i18n.number(1234.56, undefined, 'de-DE'); // "1.234,56"
382
+ i18n.date(date, { dateStyle: 'short' }, 'fr'); // "15/01/2024"
383
+ ```
384
+
385
+ ### Subscriptions
386
+
387
+ React to locale changes:
388
+
389
+ ```typescript
390
+ const i18n = createI18n({ locale: 'en' });
391
+
392
+ // Subscribe to locale changes
393
+ const unsubscribe = i18n.subscribe((locale) => {
394
+ console.log('Locale changed to:', locale);
395
+ // Update UI, reload data, etc.
396
+ });
397
+
398
+ i18n.setLocale('fr'); // Logs: "Locale changed to: fr"
399
+
400
+ // Unsubscribe when done
401
+ unsubscribe();
402
+ ```
403
+
404
+ **Use Cases:**
405
+ - Update UI when locale changes
406
+ - Reload locale-specific data
407
+ - Analytics/tracking
408
+ - State management integration
409
+
410
+ ### Custom Missing Key Handler
411
+
412
+ Customize behavior for missing translations:
413
+
414
+ ```typescript
415
+ const i18n = createI18n({
416
+ missingKey: (key, locale) => {
417
+ console.warn(`Missing translation: ${key} in ${locale}`);
418
+ return `[${locale}:${key}]`;
419
+ },
420
+ });
421
+
422
+ i18n.t('nonexistent.key'); // "[en:nonexistent.key]"
423
+ ```
424
+
425
+ ## API Reference
426
+
427
+ ### createI18n(config?)
428
+
429
+ Creates a new i18n instance.
430
+
431
+ ```typescript
432
+ type I18nConfig = {
433
+ locale?: string; // Default: 'en'
434
+ fallback?: string | string[]; // Fallback locale(s)
435
+ messages?: Record<string, Messages>; // Initial translations
436
+ loaders?: Record<string, () => Promise<Messages>>; // Async loaders
437
+ escape?: boolean; // Global HTML escaping (default: false)
438
+ missingKey?: (key: string, locale: string) => string; // Missing key handler
439
+ missingVar?: 'preserve' | 'empty' | 'error'; // Missing variable strategy
440
+ };
441
+ ```
442
+
443
+ ### Translation Methods
444
+
445
+ #### `t(key, vars?, options?)`
446
+
447
+ Translate a key synchronously.
448
+
449
+ ```typescript
450
+ i18n.t('greeting'); // Simple
451
+ i18n.t('greeting', { name: 'Alice' }); // With variables
452
+ i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With options
453
+ ```
454
+
455
+ **Options:**
456
+ - `locale?: string` - Override locale for this translation
457
+ - `fallback?: string` - Custom fallback text
458
+ - `escape?: boolean` - Override HTML escaping
459
+
460
+ #### `tl(key, vars?, options?)`
461
+
462
+ Translate a key asynchronously (loads locale if needed).
463
+
464
+ ```typescript
465
+ await i18n.tl('greeting', { name: 'Alice' }, { locale: 'fr' });
466
+ ```
467
+
468
+ ### Locale Management
469
+
470
+ ```typescript
471
+ i18n.setLocale('fr'); // Change locale
472
+ i18n.getLocale(); // Get current locale
473
+ i18n.hasLocale('es'); // Check if locale exists
474
+ i18n.has('key'); // Check if key exists
475
+ i18n.has('key', 'fr'); // Check if key exists in locale
476
+ await i18n.hasAsync('key', 'es'); // Check with async loading
477
+ ```
478
+
479
+ ### Message Management
480
+
481
+ ```typescript
482
+ // Add messages (merge)
483
+ i18n.add('en', { newKey: 'New value' });
484
+
485
+ // Set messages (replace)
486
+ i18n.set('en', { key: 'Value' });
487
+
488
+ // Get messages for locale
489
+ const messages = i18n.getMessages('en');
490
+ ```
491
+
492
+ ### Async Loading
493
+
494
+ ```typescript
495
+ // Register loader
496
+ i18n.register('de', async () => import('./locales/de.json'));
497
+
498
+ // Load locale
499
+ await i18n.load('de');
500
+ ```
501
+
502
+ ### Formatting
503
+
504
+ ```typescript
505
+ i18n.number(value, options?, locale?);
506
+ i18n.date(value, options?, locale?);
507
+ ```
508
+
509
+ ### Namespace
510
+
511
+ ```typescript
512
+ const ns = i18n.namespace('auth');
513
+ ns.t('login.title');
514
+ await ns.tl('register.title', undefined, { locale: 'fr' });
515
+ ```
516
+
517
+ ### Subscriptions
518
+
519
+ ```typescript
520
+ const unsubscribe = i18n.subscribe((locale) => {
521
+ console.log('Locale changed:', locale);
522
+ });
523
+ ```
524
+
525
+ ## Framework Integration
526
+
527
+ ### React
528
+
529
+ ```tsx
530
+ import { createI18n } from '@vielzeug/i18nit';
531
+ import { createContext, useContext, useState, useEffect } from 'react';
532
+
533
+ const I18nContext = createContext(null);
534
+
535
+ export function I18nProvider({ children, config }) {
536
+ const [i18n] = useState(() => createI18n(config));
537
+ const [locale, setLocale] = useState(i18n.getLocale());
538
+
539
+ useEffect(() => {
540
+ return i18n.subscribe(setLocale);
541
+ }, [i18n]);
542
+
543
+ return (
544
+ <I18nContext.Provider value={{ i18n, locale }}>
545
+ {children}
546
+ </I18nContext.Provider>
547
+ );
548
+ }
549
+
550
+ export function useI18n() {
551
+ const context = useContext(I18nContext);
552
+ if (!context) throw new Error('useI18n must be used within I18nProvider');
553
+ return context;
554
+ }
555
+
556
+ export function useTranslation(namespace?: string) {
557
+ const { i18n } = useI18n();
558
+ const ns = namespace ? i18n.namespace(namespace) : i18n;
559
+
560
+ return {
561
+ t: ns.t.bind(ns),
562
+ tl: ns.tl.bind(ns),
563
+ locale: i18n.getLocale(),
564
+ setLocale: i18n.setLocale.bind(i18n),
565
+ };
566
+ }
567
+
568
+ // Usage
569
+ function MyComponent() {
570
+ const { t, locale, setLocale } = useTranslation('dashboard');
571
+
572
+ return (
573
+ <div>
574
+ <h1>{t('welcome')}</h1>
575
+ <button onClick={() => setLocale('fr')}>Français</button>
576
+ </div>
577
+ );
578
+ }
579
+ ```
580
+
581
+ ### Vue 3
582
+
583
+ ```typescript
584
+ import { createI18n } from '@vielzeug/i18nit';
585
+ import { ref, onUnmounted, Plugin } from 'vue';
586
+
587
+ const i18n = createI18n({ locale: 'en' });
588
+ const locale = ref(i18n.getLocale());
589
+
590
+ const unsubscribe = i18n.subscribe((newLocale) => {
591
+ locale.value = newLocale;
592
+ });
593
+
594
+ export const i18nPlugin: Plugin = {
595
+ install(app) {
596
+ app.config.globalProperties.$t = i18n.t.bind(i18n);
597
+ app.config.globalProperties.$i18n = i18n;
598
+
599
+ app.provide('i18n', i18n);
600
+ app.provide('locale', locale);
601
+ },
602
+ };
603
+
604
+ // Composable
605
+ export function useI18n() {
606
+ return {
607
+ t: i18n.t.bind(i18n),
608
+ tl: i18n.tl.bind(i18n),
609
+ locale,
610
+ setLocale: (newLocale: string) => i18n.setLocale(newLocale),
611
+ };
612
+ }
613
+
614
+ // Usage in component
615
+ <script setup>
616
+ import { useI18n } from './i18n';
617
+
618
+ const { t, locale, setLocale } = useI18n();
619
+ </script>
620
+
621
+ <template>
622
+ <div>
623
+ <h1>{{ t('welcome') }}</h1>
624
+ <button @click="setLocale('fr')">Français</button>
625
+ </div>
626
+ </template>
627
+ ```
628
+
629
+ ### Svelte
630
+
631
+ ```typescript
632
+ import { createI18n } from '@vielzeug/i18nit';
633
+ import { writable } from 'svelte/store';
634
+
635
+ const i18n = createI18n({ locale: 'en' });
636
+ export const locale = writable(i18n.getLocale());
637
+
638
+ i18n.subscribe((newLocale) => {
639
+ locale.set(newLocale);
640
+ });
641
+
642
+ export const t = i18n.t.bind(i18n);
643
+ export const setLocale = i18n.setLocale.bind(i18n);
644
+
645
+ // Usage
646
+ <script>
647
+ import { t, setLocale } from './i18n';
648
+ </script>
649
+
650
+ <h1>{$t('welcome')}</h1>
651
+ <button on:click={() => setLocale('fr')}>Français</button>
652
+ ```
653
+
654
+ ## Best Practices
655
+
656
+ ### 1. Organize Translations by Feature
657
+
658
+ ```typescript
659
+ const messages = {
660
+ en: {
661
+ 'auth.login.title': 'Login',
662
+ 'auth.register.title': 'Register',
663
+ 'dashboard.stats.users': 'Users',
664
+ 'dashboard.stats.revenue': 'Revenue',
665
+ },
666
+ };
667
+ ```
668
+
669
+ ### 2. Use Namespaces for Large Apps
670
+
671
+ ```typescript
672
+ const authTranslations = i18n.namespace('auth');
673
+ const dashboardTranslations = i18n.namespace('dashboard');
674
+ ```
675
+
676
+ ### 3. Lazy Load Translations
677
+
678
+ ```typescript
679
+ const i18n = createI18n({
680
+ loaders: {
681
+ 'en-US': () => import('./locales/en-US.json'),
682
+ 'es-ES': () => import('./locales/es-ES.json'),
683
+ },
684
+ });
685
+ ```
686
+
687
+ ### 4. Type-Safe Translation Keys
688
+
689
+ ```typescript
690
+ type TranslationKeys =
691
+ | 'auth.login.title'
692
+ | 'auth.register.title'
693
+ | 'dashboard.welcome';
694
+
695
+ function t(key: TranslationKeys, vars?: Record<string, unknown>) {
696
+ return i18n.t(key, vars);
697
+ }
698
+ ```
699
+
700
+ ### 5. Use Structured Error Handling
701
+
702
+ ```typescript
703
+ import { MissingVariableError } from '@vielzeug/i18nit';
704
+
705
+ const i18n = createI18n({
706
+ missingVar: 'error',
707
+ messages: { en: { greeting: 'Hello, {name}!' } },
708
+ });
709
+
710
+ try {
711
+ i18n.t('greeting');
712
+ } catch (error) {
713
+ if (error instanceof MissingVariableError) {
714
+ // Log to error tracking service
715
+ console.error('Missing variable:', {
716
+ key: error.key,
717
+ variable: error.variable,
718
+ locale: error.locale,
719
+ });
720
+ }
721
+ }
722
+ ```
723
+
724
+ ## TypeScript Support
725
+
726
+ Full TypeScript support with type inference:
727
+
728
+ ```typescript
729
+ import { createI18n, type Messages, type I18nConfig } from '@vielzeug/i18nit';
730
+
731
+ // Define your messages type
732
+ interface MyMessages extends Messages {
733
+ greeting: string;
734
+ items: {
735
+ zero: string;
736
+ one: string;
737
+ other: string;
738
+ };
739
+ }
740
+
741
+ const config: I18nConfig = {
742
+ locale: 'en',
743
+ messages: {
744
+ en: {
745
+ greeting: 'Hello!',
746
+ items: { zero: 'No items', one: 'One item', other: '{count} items' },
747
+ } satisfies MyMessages,
748
+ },
749
+ };
750
+
751
+ const i18n = createI18n(config);
752
+ ```
753
+
754
+ ## Comparison
755
+
756
+ | Feature | i18nit | i18next | react-intl |
757
+ |---------|--------|---------|------------|
758
+ | Bundle Size | **~3KB** | ~12KB | ~15KB |
759
+ | Dependencies | **0** | 2+ | 10+ |
760
+ | TypeScript | ✅ First-class | ✅ Good | ✅ Good |
761
+ | Pluralization | ✅ Built-in | ✅ Plugin | ✅ Built-in |
762
+ | Async Loading | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
763
+ | Path Interpolation | ✅ `{user.name}` | ❌ | ❌ |
764
+ | Message Functions | ✅ Built-in | ⚠️ Limited | ✅ Components |
765
+ | HTML Escaping | ✅ Built-in | ⚠️ Manual | ✅ Built-in |
766
+ | Structured Errors | ✅ MissingVariableError | ❌ | ❌ |
767
+ | Framework Agnostic | ✅ | ✅ | ❌ React only |
768
+
769
+ ## License
770
+
771
+ MIT © [Helmuth Duarte](https://github.com/helmuthdu)
772
+
773
+ ## Links
774
+
775
+ - [GitHub Repository](https://github.com/helmuthdu/vielzeug)
776
+ - [Documentation](https://vielzeug.dev)
777
+ - [NPM Package](https://www.npmjs.com/package/@vielzeug/i18nit)
778
+ - [Issue Tracker](https://github.com/helmuthdu/vielzeug/issues)
779
+
780
+ ---
781
+
782
+ Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem - A collection of type-safe utilities for modern web development.
783
+
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class f extends Error{key;variable;locale;constructor(t,s,e){super(`Missing variable '${s}' for key '${t}' in locale '${e}'`),this.name="MissingVariableError",this.key=t,this.variable=s,this.locale=e}}const d={"'":"&#39;",'"':"&quot;","&":"&amp;","<":"&lt;",">":"&gt;"},c=n=>n.replace(/[&<>"']/g,t=>d[t]),g=(n,t)=>{if(t in n)return n[t];const s=t.match(/[^.[\]]+/g)||[];let e=n;for(const r of s){if(e==null||typeof e!="object")return;e=e[r]}return e},u=(n,t,s={})=>{const e=s.missingVar??"empty";return n.replace(/\{([\w.[\]]+)}/g,(r,i)=>{const a=g(t,i);if(a==null){if(e==="preserve")return r;if(e==="error")throw new f(s.key??"unknown",i,s.locale??"unknown");return""}if(typeof a=="number"&&s.locale)try{return new Intl.NumberFormat(s.locale).format(a)}catch{}return String(a)})},m=(n,t)=>{const s=Math.abs(Math.floor(t));try{return new Intl.PluralRules(n).select(s)}catch{return s===1?"one":"other"}};class y{locale;fallbacks;catalogs=new Map;loaders=new Map;loading=new Map;subscribers=new Set;escape;missingKey;missingVar;constructor(t={}){if(this.locale=t.locale??"en",this.fallbacks=Array.isArray(t.fallback)?t.fallback:t.fallback?[t.fallback]:[],this.escape=t.escape??!1,this.missingKey=t.missingKey??(s=>s),this.missingVar=t.missingVar??"empty",t.messages)for(const[s,e]of Object.entries(t.messages))this.catalogs.set(s,e);if(t.loaders)for(const[s,e]of Object.entries(t.loaders))this.loaders.set(s,e)}setLocale(t){this.locale!==t&&(this.locale=t,this.notifySubscribers())}getLocale(){return this.locale}add(t,s){const e=this.catalogs.get(t)??{};this.catalogs.set(t,{...e,...s}),this.notifySubscribers()}set(t,s){this.catalogs.set(t,s),this.notifySubscribers()}getMessages(t){return this.catalogs.get(t)}hasLocale(t){return this.catalogs.has(t)}has(t,s){return this.findMessage(t,s)!==void 0}async load(t){if(this.loading.has(t))return this.loading.get(t);if(this.catalogs.has(t))return;const s=this.loaders.get(t);if(!s)return;const e=(async()=>{try{const r=await s();this.add(t,r)}catch(r){throw console.warn(`[I18n] Failed to load locale '${t}':`,r),r}finally{this.loading.delete(t)}})();return this.loading.set(t,e),e}register(t,s){this.loaders.set(t,s)}async hasAsync(t,s){const e=s??this.locale;return!this.catalogs.has(e)&&this.loaders.has(e)&&await this.load(e),this.has(t,e)}t(t,s,e){const r=e??{},i=r.locale??this.locale,a=r.escape??this.escape,o=this.findMessage(t,i);return o===void 0?r.fallback??this.missingKey(t,i):this.formatMessage(o,s??{},i,a,t)}async tl(t,s,e){const r=e?.locale??this.locale;if(!this.catalogs.has(r)&&this.loaders.has(r))try{await this.load(r)}catch{}return this.t(t,s,e)}number(t,s,e){try{return new Intl.NumberFormat(e??this.locale,s).format(t)}catch{return String(t)}}date(t,s,e){const r=typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(e??this.locale,s).format(r)}catch{return r.toString()}}namespace(t){return{t:(s,e,r)=>this.t(`${t}.${s}`,e,r),tl:(s,e,r)=>this.tl(`${t}.${s}`,e,r)}}subscribe(t){this.subscribers.add(t);try{t(this.locale)}catch{}return()=>this.subscribers.delete(t)}notifySubscribers(){for(const t of this.subscribers)try{t(this.locale)}catch{}}findMessage(t,s){const e=this.getLocaleChain(s??this.locale);for(const r of e){const i=this.catalogs.get(r);if(!i)continue;const a=g(i,t);if(a!==void 0)return a}}getLocaleChain(t){const s=[t],e=t.split("-")[0];e!==t&&s.push(e);for(const r of this.fallbacks){s.push(r);const i=r.split("-")[0];i!==r&&s.push(i)}return s}formatMessage(t,s,e,r,i){if(typeof t=="function")try{const a=t(s,{date:(o,l)=>this.date(o,l,e),number:(o,l)=>this.number(o,l,e)});return r?c(a):a}catch{return""}if(typeof t=="object"&&"other"in t){const a=Number(s.count??0),o=t;let l;a===0&&o.zero!==void 0?l="zero":l=m(e,a);const b=o[l]??o.other,h=u(b,s,{key:i,locale:e,missingVar:this.missingVar});return r?c(h):h}if(typeof t=="string"){const a=u(t,s,{key:i,locale:e,missingVar:this.missingVar});return r?c(a):a}return""}}function p(n){return new y(n)}exports.MissingVariableError=f;exports.createI18n=p;
2
+ //# sourceMappingURL=i18nit.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18nit.cjs","sources":["../src/i18nit.ts"],"sourcesContent":["export type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageFunction = (\n vars: Record<string, unknown>,\n helpers: {\n number: (value: number, options?: Intl.NumberFormatOptions) => string;\n date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;\n },\n) => string;\n\nexport type MessageValue = string | PluralMessages | MessageFunction;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateParams = {\n locale?: Locale;\n fallback?: string;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, () => Promise<Messages>>;\n escape?: boolean;\n missingKey?: (key: string, locale: Locale) => string;\n missingVar?: 'preserve' | 'empty' | 'error';\n};\n\n/**\n * Error thrown when a required variable is missing during interpolation.\n */\nexport class MissingVariableError extends Error {\n readonly key: string;\n readonly variable: string;\n readonly locale: Locale;\n\n constructor(key: string, variable: string, locale: Locale) {\n super(`Missing variable '${variable}' for key '${key}' in locale '${locale}'`);\n this.name = 'MissingVariableError';\n this.key = key;\n this.variable = variable;\n this.locale = locale;\n }\n}\n\n/* Helpers */\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"'\": '&#39;',\n '\"': '&quot;',\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n};\n\nconst escapeHtml = (str: string): string => str.replace(/[&<>\"']/g, (char) => HTML_ENTITIES[char]);\n\n/**\n * Resolve nested properties using dot notation and numeric bracket notation.\n *\n * @param obj - Object to traverse\n * @param path - Path string to resolve\n * @returns Value at a path or undefined if not found\n */\nconst resolvePath = (obj: Record<string, unknown>, path: string): unknown => {\n // Try direct access first (supports literal keys with dots)\n if (path in obj) return obj[path];\n\n // Parse and traverse path - matches: word characters, numbers\n // Regex: /[^.[\\]]+/g matches segments between dots and brackets\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n value = (value as Record<string, unknown>)[part];\n }\n\n return value;\n};\n\n/**\n * Interpolate variables into a template string.\n *\n * Template format: {variableName} or {nested.path} or {array[0]}\n *\n * @param template - Template string with {variable} placeholders\n * @param vars - Variables object\n * @param options - Interpolation options\n * @returns Interpolated string\n * @throws {MissingVariableError} When missingVar is 'error' and a variable is not found\n */\nconst interpolate = (\n template: string,\n vars: Record<string, unknown>,\n options: {\n locale?: Locale;\n missingVar?: 'preserve' | 'empty' | 'error';\n key?: string; // For better error messages\n } = {},\n): string => {\n const missingVar = options.missingVar ?? 'empty';\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variable interpolation requires conditional logic\n return template.replace(/\\{([\\w.[\\]]+)}/g, (match, key) => {\n const value = resolvePath(vars, key);\n\n if (value == null) {\n if (missingVar === 'preserve') return match;\n if (missingVar === 'error') {\n throw new MissingVariableError(options.key ?? 'unknown', key, options.locale ?? 'unknown');\n }\n return '';\n }\n\n // Format numbers with locale\n if (typeof value === 'number' && options.locale) {\n try {\n return new Intl.NumberFormat(options.locale).format(value);\n } catch {\n // Fall through to string conversion\n }\n }\n\n return String(value);\n });\n};\n\n/* Pluralization */\n\ntype PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\n/**\n * Get the plural form for a number in a given locale using Intl.PluralRules API.\n *\n * Automatically handles all locale-specific plural rules including:\n * - English: one/other\n * - Arabic: zero/one/two/few/many/other\n * - Russian/Polish: one/few/many/other\n * - And 100+ other languages\n *\n * @param locale - Locale string (e.g., 'en-US', 'fr')\n * @param count - Number to pluralize\n * @returns Plural category\n */\nconst getPluralForm = (locale: Locale, count: number): PluralCategory => {\n const n = Math.abs(Math.floor(count));\n\n try {\n const pluralRules = new Intl.PluralRules(locale);\n return pluralRules.select(n) as PluralCategory;\n } catch {\n // Fallback to English-like behavior if locale is invalid\n return n === 1 ? 'one' : 'other';\n }\n};\n\ntype LocaleChangeHandler = (locale: Locale) => void;\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, () => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<LocaleChangeHandler>();\n\n private escape: boolean;\n private missingKey: (key: string, locale: Locale) => string;\n private missingVar: 'preserve' | 'empty' | 'error';\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n\n this.escape = config.escape ?? false;\n this.missingKey = config.missingKey ?? ((key) => key);\n this.missingVar = config.missingVar ?? 'empty';\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n // Locale Management\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n // Message Management\n\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale) !== undefined;\n }\n\n // Async Loaders\n\n async load(locale: Locale): Promise<void> {\n if (this.loading.has(locale)) return this.loading.get(locale);\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader();\n this.add(locale, messages);\n } catch (error) {\n // Log loader failures for visibility\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n // Re-throw so callers can handle errors\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: () => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n return this.has(key, targetLocale);\n }\n\n // Translation\n\n t(key: string, vars?: Record<string, unknown>, options?: TranslateParams): string {\n const opts = options ?? {};\n const targetLocale = opts.locale ?? this.locale;\n const shouldEscape = opts.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) {\n return opts.fallback ?? this.missingKey(key, targetLocale);\n }\n\n return this.formatMessage(message, vars ?? {}, targetLocale, shouldEscape, key);\n }\n\n async tl(key: string, vars?: Record<string, unknown>, options?: TranslateParams): Promise<string> {\n const targetLocale = options?.locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n try {\n await this.load(targetLocale);\n } catch {\n // Loader errors are already logged in load(), continue with fallback\n // This catch prevents the error from propagating to the caller\n }\n }\n\n return this.t(key, vars, options);\n }\n\n // Formatting Helpers\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n // Namespaced Translator\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) =>\n this.t(`${ns}.${key}`, vars, options),\n tl: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) =>\n this.tl(`${ns}.${key}`, vars, options),\n };\n }\n\n // Subscriptions\n\n subscribe(handler: LocaleChangeHandler): () => void {\n this.subscribers.add(handler);\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n // Internal Helpers\n\n private findMessage(key: string, locale?: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale ?? this.locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n if (value !== undefined) return value as MessageValue;\n }\n\n return undefined;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\n private formatMessage(\n message: MessageValue,\n vars: Record<string, unknown>,\n locale: Locale,\n shouldEscape: boolean,\n key?: string,\n ): string {\n // Handle function messages\n if (typeof message === 'function') {\n try {\n const result = message(vars, {\n date: (d, opts) => this.date(d, opts, locale),\n number: (v, opts) => this.number(v, opts, locale),\n });\n return shouldEscape ? escapeHtml(result) : result;\n } catch {\n return '';\n }\n }\n\n // Handle plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form when the count is 0\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n const result = interpolate(template, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n // Handle string messages\n if (typeof message === 'string') {\n const result = interpolate(message, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n return '';\n }\n}\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["MissingVariableError","key","variable","locale","HTML_ENTITIES","escapeHtml","str","char","resolvePath","obj","path","parts","value","part","interpolate","template","vars","options","missingVar","match","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","opts","shouldEscape","message","date","ns","handler","locales","loc","chain","lang","fallback","fallbackLang","result","d","v","pluralMsg","form","createI18n"],"mappings":"gFAqCO,MAAMA,UAA6B,KAAM,CACrC,IACA,SACA,OAET,YAAYC,EAAaC,EAAkBC,EAAgB,CACzD,MAAM,qBAAqBD,CAAQ,cAAcD,CAAG,gBAAgBE,CAAM,GAAG,EAC7E,KAAK,KAAO,uBACZ,KAAK,IAAMF,EACX,KAAK,SAAWC,EAChB,KAAK,OAASC,CAChB,CACF,CAIA,MAAMC,EAAwC,CAC5C,IAAK,QACL,IAAK,SACL,IAAK,QACL,IAAK,OACL,IAAK,MACP,EAEMC,EAAcC,GAAwBA,EAAI,QAAQ,WAAaC,GAASH,EAAcG,CAAI,CAAC,EAS3FC,EAAc,CAACC,EAA8BC,IAA0B,CAE3E,GAAIA,KAAQD,EAAK,OAAOA,EAAIC,CAAI,EAIhC,MAAMC,EAAQD,EAAK,MAAM,WAAW,GAAK,CAAA,EACzC,IAAIE,EAAiBH,EAErB,UAAWI,KAAQF,EAAO,CACxB,GAAIC,GAAS,MAAQ,OAAOA,GAAU,SAAU,OAChDA,EAASA,EAAkCC,CAAI,CACjD,CAEA,OAAOD,CACT,EAaME,EAAc,CAClBC,EACAC,EACAC,EAII,CAAA,IACO,CACX,MAAMC,EAAaD,EAAQ,YAAc,QAGzC,OAAOF,EAAS,QAAQ,kBAAmB,CAACI,EAAOlB,IAAQ,CACzD,MAAMW,EAAQJ,EAAYQ,EAAMf,CAAG,EAEnC,GAAIW,GAAS,KAAM,CACjB,GAAIM,IAAe,WAAY,OAAOC,EACtC,GAAID,IAAe,QACjB,MAAM,IAAIlB,EAAqBiB,EAAQ,KAAO,UAAWhB,EAAKgB,EAAQ,QAAU,SAAS,EAE3F,MAAO,EACT,CAGA,GAAI,OAAOL,GAAU,UAAYK,EAAQ,OACvC,GAAI,CACF,OAAO,IAAI,KAAK,aAAaA,EAAQ,MAAM,EAAE,OAAOL,CAAK,CAC3D,MAAQ,CAER,CAGF,OAAO,OAAOA,CAAK,CACrB,CAAC,CACH,EAmBMQ,EAAgB,CAACjB,EAAgBkB,IAAkC,CACvE,MAAMC,EAAI,KAAK,IAAI,KAAK,MAAMD,CAAK,CAAC,EAEpC,GAAI,CAEF,OADoB,IAAI,KAAK,YAAYlB,CAAM,EAC5B,OAAOmB,CAAC,CAC7B,MAAQ,CAEN,OAAOA,IAAM,EAAI,MAAQ,OAC3B,CACF,EAIA,MAAMC,CAAK,CACD,OACA,UACA,aAAe,IACf,YAAc,IACd,YAAc,IACd,gBAAkB,IAElB,OACA,WACA,WAER,YAAYC,EAAqB,GAAI,CAQnC,GAPA,KAAK,OAASA,EAAO,QAAU,KAC/B,KAAK,UAAY,MAAM,QAAQA,EAAO,QAAQ,EAAIA,EAAO,SAAWA,EAAO,SAAW,CAACA,EAAO,QAAQ,EAAI,CAAA,EAE1G,KAAK,OAASA,EAAO,QAAU,GAC/B,KAAK,WAAaA,EAAO,aAAgBvB,GAAQA,GACjD,KAAK,WAAauB,EAAO,YAAc,QAEnCA,EAAO,SACT,SAAW,CAACrB,EAAQsB,CAAQ,IAAK,OAAO,QAAQD,EAAO,QAAQ,EAC7D,KAAK,SAAS,IAAIrB,EAAQsB,CAAQ,EAItC,GAAID,EAAO,QACT,SAAW,CAACrB,EAAQuB,CAAM,IAAK,OAAO,QAAQF,EAAO,OAAO,EAC1D,KAAK,QAAQ,IAAIrB,EAAQuB,CAAM,CAGrC,CAIA,UAAUvB,EAAsB,CAC1B,KAAK,SAAWA,IACpB,KAAK,OAASA,EACd,KAAK,kBAAA,EACP,CAEA,WAAoB,CAClB,OAAO,KAAK,MACd,CAIA,IAAIA,EAAgBsB,EAA0B,CAC5C,MAAME,EAAW,KAAK,SAAS,IAAIxB,CAAM,GAAK,CAAA,EAC9C,KAAK,SAAS,IAAIA,EAAQ,CAAE,GAAGwB,EAAU,GAAGF,EAAU,EACtD,KAAK,kBAAA,CACP,CAEA,IAAItB,EAAgBsB,EAA0B,CAC5C,KAAK,SAAS,IAAItB,EAAQsB,CAAQ,EAClC,KAAK,kBAAA,CACP,CAEA,YAAYtB,EAAsC,CAChD,OAAO,KAAK,SAAS,IAAIA,CAAM,CACjC,CAEA,UAAUA,EAAyB,CACjC,OAAO,KAAK,SAAS,IAAIA,CAAM,CACjC,CAEA,IAAIF,EAAaE,EAA0B,CACzC,OAAO,KAAK,YAAYF,EAAKE,CAAM,IAAM,MAC3C,CAIA,MAAM,KAAKA,EAA+B,CACxC,GAAI,KAAK,QAAQ,IAAIA,CAAM,EAAG,OAAO,KAAK,QAAQ,IAAIA,CAAM,EAC5D,GAAI,KAAK,SAAS,IAAIA,CAAM,EAAG,OAE/B,MAAMuB,EAAS,KAAK,QAAQ,IAAIvB,CAAM,EACtC,GAAI,CAACuB,EAAQ,OAEb,MAAME,GAAW,SAAY,CAC3B,GAAI,CACF,MAAMH,EAAW,MAAMC,EAAA,EACvB,KAAK,IAAIvB,EAAQsB,CAAQ,CAC3B,OAASI,EAAO,CAEd,cAAQ,KAAK,iCAAiC1B,CAAM,KAAM0B,CAAK,EAEzDA,CACR,QAAA,CACE,KAAK,QAAQ,OAAO1B,CAAM,CAC5B,CACF,GAAA,EAEA,YAAK,QAAQ,IAAIA,EAAQyB,CAAO,EACzBA,CACT,CAEA,SAASzB,EAAgBuB,EAAuC,CAC9D,KAAK,QAAQ,IAAIvB,EAAQuB,CAAM,CACjC,CAEA,MAAM,SAASzB,EAAaE,EAAmC,CAC7D,MAAM2B,EAAe3B,GAAU,KAAK,OACpC,MAAI,CAAC,KAAK,SAAS,IAAI2B,CAAY,GAAK,KAAK,QAAQ,IAAIA,CAAY,GACnE,MAAM,KAAK,KAAKA,CAAY,EAEvB,KAAK,IAAI7B,EAAK6B,CAAY,CACnC,CAIA,EAAE7B,EAAae,EAAgCC,EAAmC,CAChF,MAAMc,EAAOd,GAAW,CAAA,EAClBa,EAAeC,EAAK,QAAU,KAAK,OACnCC,EAAeD,EAAK,QAAU,KAAK,OAEnCE,EAAU,KAAK,YAAYhC,EAAK6B,CAAY,EAClD,OAAIG,IAAY,OACPF,EAAK,UAAY,KAAK,WAAW9B,EAAK6B,CAAY,EAGpD,KAAK,cAAcG,EAASjB,GAAQ,CAAA,EAAIc,EAAcE,EAAc/B,CAAG,CAChF,CAEA,MAAM,GAAGA,EAAae,EAAgCC,EAA4C,CAChG,MAAMa,EAAeb,GAAS,QAAU,KAAK,OAE7C,GAAI,CAAC,KAAK,SAAS,IAAIa,CAAY,GAAK,KAAK,QAAQ,IAAIA,CAAY,EACnE,GAAI,CACF,MAAM,KAAK,KAAKA,CAAY,CAC9B,MAAQ,CAGR,CAGF,OAAO,KAAK,EAAE7B,EAAKe,EAAMC,CAAO,CAClC,CAIA,OAAOL,EAAeK,EAAoCd,EAAyB,CACjF,GAAI,CACF,OAAO,IAAI,KAAK,aAAaA,GAAU,KAAK,OAAQc,CAAO,EAAE,OAAOL,CAAK,CAC3E,MAAQ,CACN,OAAO,OAAOA,CAAK,CACrB,CACF,CAEA,KAAKA,EAAsBK,EAAsCd,EAAyB,CACxF,MAAM+B,EAAO,OAAOtB,GAAU,SAAW,IAAI,KAAKA,CAAK,EAAIA,EAC3D,GAAI,CACF,OAAO,IAAI,KAAK,eAAeT,GAAU,KAAK,OAAQc,CAAO,EAAE,OAAOiB,CAAI,CAC5E,MAAQ,CACN,OAAOA,EAAK,SAAA,CACd,CACF,CAIA,UAAUC,EAAY,CACpB,MAAO,CACL,EAAG,CAAClC,EAAae,EAAgCC,IAC/C,KAAK,EAAE,GAAGkB,CAAE,IAAIlC,CAAG,GAAIe,EAAMC,CAAO,EACtC,GAAI,CAAChB,EAAae,EAAgCC,IAChD,KAAK,GAAG,GAAGkB,CAAE,IAAIlC,CAAG,GAAIe,EAAMC,CAAO,CAAA,CAE3C,CAIA,UAAUmB,EAA0C,CAClD,KAAK,YAAY,IAAIA,CAAO,EAC5B,GAAI,CACFA,EAAQ,KAAK,MAAM,CACrB,MAAQ,CAER,CACA,MAAO,IAAM,KAAK,YAAY,OAAOA,CAAO,CAC9C,CAEQ,mBAA0B,CAChC,UAAWA,KAAW,KAAK,YACzB,GAAI,CACFA,EAAQ,KAAK,MAAM,CACrB,MAAQ,CAER,CAEJ,CAIQ,YAAYnC,EAAaE,EAA2C,CAC1E,MAAMkC,EAAU,KAAK,eAAelC,GAAU,KAAK,MAAM,EAEzD,UAAWmC,KAAOD,EAAS,CACzB,MAAMZ,EAAW,KAAK,SAAS,IAAIa,CAAG,EACtC,GAAI,CAACb,EAAU,SAEf,MAAMb,EAAQJ,EAAYiB,EAAUxB,CAAG,EACvC,GAAIW,IAAU,OAAW,OAAOA,CAClC,CAGF,CAEQ,eAAeT,EAA0B,CAC/C,MAAMoC,EAAkB,CAACpC,CAAM,EAGzBqC,EAAOrC,EAAO,MAAM,GAAG,EAAE,CAAC,EAC5BqC,IAASrC,GAAQoC,EAAM,KAAKC,CAAI,EAGpC,UAAWC,KAAY,KAAK,UAAW,CACrCF,EAAM,KAAKE,CAAQ,EACnB,MAAMC,EAAeD,EAAS,MAAM,GAAG,EAAE,CAAC,EACtCC,IAAiBD,GAAUF,EAAM,KAAKG,CAAY,CACxD,CAEA,OAAOH,CACT,CAGQ,cACNN,EACAjB,EACAb,EACA6B,EACA/B,EACQ,CAER,GAAI,OAAOgC,GAAY,WACrB,GAAI,CACF,MAAMU,EAASV,EAAQjB,EAAM,CAC3B,KAAM,CAAC4B,EAAGb,IAAS,KAAK,KAAKa,EAAGb,EAAM5B,CAAM,EAC5C,OAAQ,CAAC0C,EAAGd,IAAS,KAAK,OAAOc,EAAGd,EAAM5B,CAAM,CAAA,CACjD,EACD,OAAO6B,EAAe3B,EAAWsC,CAAM,EAAIA,CAC7C,MAAQ,CACN,MAAO,EACT,CAIF,GAAI,OAAOV,GAAY,UAAY,UAAWA,EAAS,CACrD,MAAMZ,EAAQ,OAAOL,EAAK,OAAS,CAAC,EAC9B8B,EAAYb,EAGlB,IAAIc,EACA1B,IAAU,GAAKyB,EAAU,OAAS,OACpCC,EAAO,OAEPA,EAAO3B,EAAcjB,EAAQkB,CAAK,EAGpC,MAAMN,EAAW+B,EAAUC,CAAI,GAAKD,EAAU,MACxCH,EAAS7B,EAAYC,EAAUC,EAAM,CAAE,IAAAf,EAAK,OAAAE,EAAQ,WAAY,KAAK,WAAY,EACvF,OAAO6B,EAAe3B,EAAWsC,CAAM,EAAIA,CAC7C,CAGA,GAAI,OAAOV,GAAY,SAAU,CAC/B,MAAMU,EAAS7B,EAAYmB,EAASjB,EAAM,CAAE,IAAAf,EAAK,OAAAE,EAAQ,WAAY,KAAK,WAAY,EACtF,OAAO6B,EAAe3B,EAAWsC,CAAM,EAAIA,CAC7C,CAEA,MAAO,EACT,CACF,CAEO,SAASK,EAAWxB,EAA2B,CACpD,OAAO,IAAID,EAAKC,CAAM,CACxB"}
package/dist/i18nit.js ADDED
@@ -0,0 +1,222 @@
1
+ class b extends Error {
2
+ key;
3
+ variable;
4
+ locale;
5
+ constructor(t, s, e) {
6
+ super(`Missing variable '${s}' for key '${t}' in locale '${e}'`), this.name = "MissingVariableError", this.key = t, this.variable = s, this.locale = e;
7
+ }
8
+ }
9
+ const m = {
10
+ "'": "&#39;",
11
+ '"': "&quot;",
12
+ "&": "&amp;",
13
+ "<": "&lt;",
14
+ ">": "&gt;"
15
+ }, c = (n) => n.replace(/[&<>"']/g, (t) => m[t]), f = (n, t) => {
16
+ if (t in n) return n[t];
17
+ const s = t.match(/[^.[\]]+/g) || [];
18
+ let e = n;
19
+ for (const r of s) {
20
+ if (e == null || typeof e != "object") return;
21
+ e = e[r];
22
+ }
23
+ return e;
24
+ }, u = (n, t, s = {}) => {
25
+ const e = s.missingVar ?? "empty";
26
+ return n.replace(/\{([\w.[\]]+)}/g, (r, i) => {
27
+ const a = f(t, i);
28
+ if (a == null) {
29
+ if (e === "preserve") return r;
30
+ if (e === "error")
31
+ throw new b(s.key ?? "unknown", i, s.locale ?? "unknown");
32
+ return "";
33
+ }
34
+ if (typeof a == "number" && s.locale)
35
+ try {
36
+ return new Intl.NumberFormat(s.locale).format(a);
37
+ } catch {
38
+ }
39
+ return String(a);
40
+ });
41
+ }, d = (n, t) => {
42
+ const s = Math.abs(Math.floor(t));
43
+ try {
44
+ return new Intl.PluralRules(n).select(s);
45
+ } catch {
46
+ return s === 1 ? "one" : "other";
47
+ }
48
+ };
49
+ class y {
50
+ locale;
51
+ fallbacks;
52
+ catalogs = /* @__PURE__ */ new Map();
53
+ loaders = /* @__PURE__ */ new Map();
54
+ loading = /* @__PURE__ */ new Map();
55
+ subscribers = /* @__PURE__ */ new Set();
56
+ escape;
57
+ missingKey;
58
+ missingVar;
59
+ constructor(t = {}) {
60
+ if (this.locale = t.locale ?? "en", this.fallbacks = Array.isArray(t.fallback) ? t.fallback : t.fallback ? [t.fallback] : [], this.escape = t.escape ?? !1, this.missingKey = t.missingKey ?? ((s) => s), this.missingVar = t.missingVar ?? "empty", t.messages)
61
+ for (const [s, e] of Object.entries(t.messages))
62
+ this.catalogs.set(s, e);
63
+ if (t.loaders)
64
+ for (const [s, e] of Object.entries(t.loaders))
65
+ this.loaders.set(s, e);
66
+ }
67
+ // Locale Management
68
+ setLocale(t) {
69
+ this.locale !== t && (this.locale = t, this.notifySubscribers());
70
+ }
71
+ getLocale() {
72
+ return this.locale;
73
+ }
74
+ // Message Management
75
+ add(t, s) {
76
+ const e = this.catalogs.get(t) ?? {};
77
+ this.catalogs.set(t, { ...e, ...s }), this.notifySubscribers();
78
+ }
79
+ set(t, s) {
80
+ this.catalogs.set(t, s), this.notifySubscribers();
81
+ }
82
+ getMessages(t) {
83
+ return this.catalogs.get(t);
84
+ }
85
+ hasLocale(t) {
86
+ return this.catalogs.has(t);
87
+ }
88
+ has(t, s) {
89
+ return this.findMessage(t, s) !== void 0;
90
+ }
91
+ // Async Loaders
92
+ async load(t) {
93
+ if (this.loading.has(t)) return this.loading.get(t);
94
+ if (this.catalogs.has(t)) return;
95
+ const s = this.loaders.get(t);
96
+ if (!s) return;
97
+ const e = (async () => {
98
+ try {
99
+ const r = await s();
100
+ this.add(t, r);
101
+ } catch (r) {
102
+ throw console.warn(`[I18n] Failed to load locale '${t}':`, r), r;
103
+ } finally {
104
+ this.loading.delete(t);
105
+ }
106
+ })();
107
+ return this.loading.set(t, e), e;
108
+ }
109
+ register(t, s) {
110
+ this.loaders.set(t, s);
111
+ }
112
+ async hasAsync(t, s) {
113
+ const e = s ?? this.locale;
114
+ return !this.catalogs.has(e) && this.loaders.has(e) && await this.load(e), this.has(t, e);
115
+ }
116
+ // Translation
117
+ t(t, s, e) {
118
+ const r = e ?? {}, i = r.locale ?? this.locale, a = r.escape ?? this.escape, o = this.findMessage(t, i);
119
+ return o === void 0 ? r.fallback ?? this.missingKey(t, i) : this.formatMessage(o, s ?? {}, i, a, t);
120
+ }
121
+ async tl(t, s, e) {
122
+ const r = e?.locale ?? this.locale;
123
+ if (!this.catalogs.has(r) && this.loaders.has(r))
124
+ try {
125
+ await this.load(r);
126
+ } catch {
127
+ }
128
+ return this.t(t, s, e);
129
+ }
130
+ // Formatting Helpers
131
+ number(t, s, e) {
132
+ try {
133
+ return new Intl.NumberFormat(e ?? this.locale, s).format(t);
134
+ } catch {
135
+ return String(t);
136
+ }
137
+ }
138
+ date(t, s, e) {
139
+ const r = typeof t == "number" ? new Date(t) : t;
140
+ try {
141
+ return new Intl.DateTimeFormat(e ?? this.locale, s).format(r);
142
+ } catch {
143
+ return r.toString();
144
+ }
145
+ }
146
+ // Namespaced Translator
147
+ namespace(t) {
148
+ return {
149
+ t: (s, e, r) => this.t(`${t}.${s}`, e, r),
150
+ tl: (s, e, r) => this.tl(`${t}.${s}`, e, r)
151
+ };
152
+ }
153
+ // Subscriptions
154
+ subscribe(t) {
155
+ this.subscribers.add(t);
156
+ try {
157
+ t(this.locale);
158
+ } catch {
159
+ }
160
+ return () => this.subscribers.delete(t);
161
+ }
162
+ notifySubscribers() {
163
+ for (const t of this.subscribers)
164
+ try {
165
+ t(this.locale);
166
+ } catch {
167
+ }
168
+ }
169
+ // Internal Helpers
170
+ findMessage(t, s) {
171
+ const e = this.getLocaleChain(s ?? this.locale);
172
+ for (const r of e) {
173
+ const i = this.catalogs.get(r);
174
+ if (!i) continue;
175
+ const a = f(i, t);
176
+ if (a !== void 0) return a;
177
+ }
178
+ }
179
+ getLocaleChain(t) {
180
+ const s = [t], e = t.split("-")[0];
181
+ e !== t && s.push(e);
182
+ for (const r of this.fallbacks) {
183
+ s.push(r);
184
+ const i = r.split("-")[0];
185
+ i !== r && s.push(i);
186
+ }
187
+ return s;
188
+ }
189
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -
190
+ formatMessage(t, s, e, r, i) {
191
+ if (typeof t == "function")
192
+ try {
193
+ const a = t(s, {
194
+ date: (o, l) => this.date(o, l, e),
195
+ number: (o, l) => this.number(o, l, e)
196
+ });
197
+ return r ? c(a) : a;
198
+ } catch {
199
+ return "";
200
+ }
201
+ if (typeof t == "object" && "other" in t) {
202
+ const a = Number(s.count ?? 0), o = t;
203
+ let l;
204
+ a === 0 && o.zero !== void 0 ? l = "zero" : l = d(e, a);
205
+ const g = o[l] ?? o.other, h = u(g, s, { key: i, locale: e, missingVar: this.missingVar });
206
+ return r ? c(h) : h;
207
+ }
208
+ if (typeof t == "string") {
209
+ const a = u(t, s, { key: i, locale: e, missingVar: this.missingVar });
210
+ return r ? c(a) : a;
211
+ }
212
+ return "";
213
+ }
214
+ }
215
+ function p(n) {
216
+ return new y(n);
217
+ }
218
+ export {
219
+ b as MissingVariableError,
220
+ p as createI18n
221
+ };
222
+ //# sourceMappingURL=i18nit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18nit.js","sources":["../src/i18nit.ts"],"sourcesContent":["export type Locale = string;\n\nexport type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\nexport type PluralMessages = Partial<Record<PluralForm, string>> & { other: string };\n\nexport type MessageFunction = (\n vars: Record<string, unknown>,\n helpers: {\n number: (value: number, options?: Intl.NumberFormatOptions) => string;\n date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;\n },\n) => string;\n\nexport type MessageValue = string | PluralMessages | MessageFunction;\n\nexport type Messages = Record<string, MessageValue>;\n\nexport type TranslateParams = {\n locale?: Locale;\n fallback?: string;\n escape?: boolean;\n};\n\nexport type I18nConfig = {\n locale?: Locale;\n fallback?: Locale | Locale[];\n messages?: Record<Locale, Messages>;\n loaders?: Record<Locale, () => Promise<Messages>>;\n escape?: boolean;\n missingKey?: (key: string, locale: Locale) => string;\n missingVar?: 'preserve' | 'empty' | 'error';\n};\n\n/**\n * Error thrown when a required variable is missing during interpolation.\n */\nexport class MissingVariableError extends Error {\n readonly key: string;\n readonly variable: string;\n readonly locale: Locale;\n\n constructor(key: string, variable: string, locale: Locale) {\n super(`Missing variable '${variable}' for key '${key}' in locale '${locale}'`);\n this.name = 'MissingVariableError';\n this.key = key;\n this.variable = variable;\n this.locale = locale;\n }\n}\n\n/* Helpers */\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"'\": '&#39;',\n '\"': '&quot;',\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n};\n\nconst escapeHtml = (str: string): string => str.replace(/[&<>\"']/g, (char) => HTML_ENTITIES[char]);\n\n/**\n * Resolve nested properties using dot notation and numeric bracket notation.\n *\n * @param obj - Object to traverse\n * @param path - Path string to resolve\n * @returns Value at a path or undefined if not found\n */\nconst resolvePath = (obj: Record<string, unknown>, path: string): unknown => {\n // Try direct access first (supports literal keys with dots)\n if (path in obj) return obj[path];\n\n // Parse and traverse path - matches: word characters, numbers\n // Regex: /[^.[\\]]+/g matches segments between dots and brackets\n const parts = path.match(/[^.[\\]]+/g) || [];\n let value: unknown = obj;\n\n for (const part of parts) {\n if (value == null || typeof value !== 'object') return undefined;\n value = (value as Record<string, unknown>)[part];\n }\n\n return value;\n};\n\n/**\n * Interpolate variables into a template string.\n *\n * Template format: {variableName} or {nested.path} or {array[0]}\n *\n * @param template - Template string with {variable} placeholders\n * @param vars - Variables object\n * @param options - Interpolation options\n * @returns Interpolated string\n * @throws {MissingVariableError} When missingVar is 'error' and a variable is not found\n */\nconst interpolate = (\n template: string,\n vars: Record<string, unknown>,\n options: {\n locale?: Locale;\n missingVar?: 'preserve' | 'empty' | 'error';\n key?: string; // For better error messages\n } = {},\n): string => {\n const missingVar = options.missingVar ?? 'empty';\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variable interpolation requires conditional logic\n return template.replace(/\\{([\\w.[\\]]+)}/g, (match, key) => {\n const value = resolvePath(vars, key);\n\n if (value == null) {\n if (missingVar === 'preserve') return match;\n if (missingVar === 'error') {\n throw new MissingVariableError(options.key ?? 'unknown', key, options.locale ?? 'unknown');\n }\n return '';\n }\n\n // Format numbers with locale\n if (typeof value === 'number' && options.locale) {\n try {\n return new Intl.NumberFormat(options.locale).format(value);\n } catch {\n // Fall through to string conversion\n }\n }\n\n return String(value);\n });\n};\n\n/* Pluralization */\n\ntype PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\n/**\n * Get the plural form for a number in a given locale using Intl.PluralRules API.\n *\n * Automatically handles all locale-specific plural rules including:\n * - English: one/other\n * - Arabic: zero/one/two/few/many/other\n * - Russian/Polish: one/few/many/other\n * - And 100+ other languages\n *\n * @param locale - Locale string (e.g., 'en-US', 'fr')\n * @param count - Number to pluralize\n * @returns Plural category\n */\nconst getPluralForm = (locale: Locale, count: number): PluralCategory => {\n const n = Math.abs(Math.floor(count));\n\n try {\n const pluralRules = new Intl.PluralRules(locale);\n return pluralRules.select(n) as PluralCategory;\n } catch {\n // Fallback to English-like behavior if locale is invalid\n return n === 1 ? 'one' : 'other';\n }\n};\n\ntype LocaleChangeHandler = (locale: Locale) => void;\n\nclass I18n {\n private locale: Locale;\n private fallbacks: Locale[];\n private catalogs = new Map<Locale, Messages>();\n private loaders = new Map<Locale, () => Promise<Messages>>();\n private loading = new Map<Locale, Promise<void>>();\n private subscribers = new Set<LocaleChangeHandler>();\n\n private escape: boolean;\n private missingKey: (key: string, locale: Locale) => string;\n private missingVar: 'preserve' | 'empty' | 'error';\n\n constructor(config: I18nConfig = {}) {\n this.locale = config.locale ?? 'en';\n this.fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n\n this.escape = config.escape ?? false;\n this.missingKey = config.missingKey ?? ((key) => key);\n this.missingVar = config.missingVar ?? 'empty';\n\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n if (config.loaders) {\n for (const [locale, loader] of Object.entries(config.loaders)) {\n this.loaders.set(locale, loader);\n }\n }\n }\n\n // Locale Management\n\n setLocale(locale: Locale): void {\n if (this.locale === locale) return;\n this.locale = locale;\n this.notifySubscribers();\n }\n\n getLocale(): Locale {\n return this.locale;\n }\n\n // Message Management\n\n add(locale: Locale, messages: Messages): void {\n const existing = this.catalogs.get(locale) ?? {};\n this.catalogs.set(locale, { ...existing, ...messages });\n this.notifySubscribers();\n }\n\n set(locale: Locale, messages: Messages): void {\n this.catalogs.set(locale, messages);\n this.notifySubscribers();\n }\n\n getMessages(locale: Locale): Messages | undefined {\n return this.catalogs.get(locale);\n }\n\n hasLocale(locale: Locale): boolean {\n return this.catalogs.has(locale);\n }\n\n has(key: string, locale?: Locale): boolean {\n return this.findMessage(key, locale) !== undefined;\n }\n\n // Async Loaders\n\n async load(locale: Locale): Promise<void> {\n if (this.loading.has(locale)) return this.loading.get(locale);\n if (this.catalogs.has(locale)) return;\n\n const loader = this.loaders.get(locale);\n if (!loader) return;\n\n const promise = (async () => {\n try {\n const messages = await loader();\n this.add(locale, messages);\n } catch (error) {\n // Log loader failures for visibility\n console.warn(`[I18n] Failed to load locale '${locale}':`, error);\n // Re-throw so callers can handle errors\n throw error;\n } finally {\n this.loading.delete(locale);\n }\n })();\n\n this.loading.set(locale, promise);\n return promise;\n }\n\n register(locale: Locale, loader: () => Promise<Messages>): void {\n this.loaders.set(locale, loader);\n }\n\n async hasAsync(key: string, locale?: Locale): Promise<boolean> {\n const targetLocale = locale ?? this.locale;\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n await this.load(targetLocale);\n }\n return this.has(key, targetLocale);\n }\n\n // Translation\n\n t(key: string, vars?: Record<string, unknown>, options?: TranslateParams): string {\n const opts = options ?? {};\n const targetLocale = opts.locale ?? this.locale;\n const shouldEscape = opts.escape ?? this.escape;\n\n const message = this.findMessage(key, targetLocale);\n if (message === undefined) {\n return opts.fallback ?? this.missingKey(key, targetLocale);\n }\n\n return this.formatMessage(message, vars ?? {}, targetLocale, shouldEscape, key);\n }\n\n async tl(key: string, vars?: Record<string, unknown>, options?: TranslateParams): Promise<string> {\n const targetLocale = options?.locale ?? this.locale;\n\n if (!this.catalogs.has(targetLocale) && this.loaders.has(targetLocale)) {\n try {\n await this.load(targetLocale);\n } catch {\n // Loader errors are already logged in load(), continue with fallback\n // This catch prevents the error from propagating to the caller\n }\n }\n\n return this.t(key, vars, options);\n }\n\n // Formatting Helpers\n\n number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string {\n try {\n return new Intl.NumberFormat(locale ?? this.locale, options).format(value);\n } catch {\n return String(value);\n }\n }\n\n date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string {\n const date = typeof value === 'number' ? new Date(value) : value;\n try {\n return new Intl.DateTimeFormat(locale ?? this.locale, options).format(date);\n } catch {\n return date.toString();\n }\n }\n\n // Namespaced Translator\n\n namespace(ns: string) {\n return {\n t: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) =>\n this.t(`${ns}.${key}`, vars, options),\n tl: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) =>\n this.tl(`${ns}.${key}`, vars, options),\n };\n }\n\n // Subscriptions\n\n subscribe(handler: LocaleChangeHandler): () => void {\n this.subscribers.add(handler);\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n return () => this.subscribers.delete(handler);\n }\n\n private notifySubscribers(): void {\n for (const handler of this.subscribers) {\n try {\n handler(this.locale);\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n // Internal Helpers\n\n private findMessage(key: string, locale?: Locale): MessageValue | undefined {\n const locales = this.getLocaleChain(locale ?? this.locale);\n\n for (const loc of locales) {\n const messages = this.catalogs.get(loc);\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n if (value !== undefined) return value as MessageValue;\n }\n\n return undefined;\n }\n\n private getLocaleChain(locale: Locale): Locale[] {\n const chain: Locale[] = [locale];\n\n // Add base language (e.g., 'en' from 'en-US')\n const lang = locale.split('-')[0];\n if (lang !== locale) chain.push(lang);\n\n // Add fallback locales\n for (const fallback of this.fallbacks) {\n chain.push(fallback);\n const fallbackLang = fallback.split('-')[0];\n if (fallbackLang !== fallback) chain.push(fallbackLang);\n }\n\n return chain;\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\n private formatMessage(\n message: MessageValue,\n vars: Record<string, unknown>,\n locale: Locale,\n shouldEscape: boolean,\n key?: string,\n ): string {\n // Handle function messages\n if (typeof message === 'function') {\n try {\n const result = message(vars, {\n date: (d, opts) => this.date(d, opts, locale),\n number: (v, opts) => this.number(v, opts, locale),\n });\n return shouldEscape ? escapeHtml(result) : result;\n } catch {\n return '';\n }\n }\n\n // Handle plural messages\n if (typeof message === 'object' && 'other' in message) {\n const count = Number(vars.count ?? 0);\n const pluralMsg = message as PluralMessages;\n\n // Prefer an explicit 'zero' form when the count is 0\n let form: PluralForm;\n if (count === 0 && pluralMsg.zero !== undefined) {\n form = 'zero';\n } else {\n form = getPluralForm(locale, count);\n }\n\n const template = pluralMsg[form] ?? pluralMsg.other;\n const result = interpolate(template, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n // Handle string messages\n if (typeof message === 'string') {\n const result = interpolate(message, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n return '';\n }\n}\n\nexport function createI18n(config?: I18nConfig): I18n {\n return new I18n(config);\n}\n"],"names":["MissingVariableError","key","variable","locale","HTML_ENTITIES","escapeHtml","str","char","resolvePath","obj","path","parts","value","part","interpolate","template","vars","options","missingVar","match","getPluralForm","count","n","I18n","config","messages","loader","existing","promise","error","targetLocale","opts","shouldEscape","message","date","ns","handler","locales","loc","chain","lang","fallback","fallbackLang","result","d","v","pluralMsg","form","createI18n"],"mappings":"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"}
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("./i18nit.cjs");exports.MissingVariableError=r.MissingVariableError;exports.createI18n=r.createI18n;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,84 @@
1
+ export declare function createI18n(config?: I18nConfig): I18n;
2
+
3
+ declare class I18n {
4
+ private locale;
5
+ private fallbacks;
6
+ private catalogs;
7
+ private loaders;
8
+ private loading;
9
+ private subscribers;
10
+ private escape;
11
+ private missingKey;
12
+ private missingVar;
13
+ constructor(config?: I18nConfig);
14
+ setLocale(locale: Locale): void;
15
+ getLocale(): Locale;
16
+ add(locale: Locale, messages: Messages): void;
17
+ set(locale: Locale, messages: Messages): void;
18
+ getMessages(locale: Locale): Messages | undefined;
19
+ hasLocale(locale: Locale): boolean;
20
+ has(key: string, locale?: Locale): boolean;
21
+ load(locale: Locale): Promise<void>;
22
+ register(locale: Locale, loader: () => Promise<Messages>): void;
23
+ hasAsync(key: string, locale?: Locale): Promise<boolean>;
24
+ t(key: string, vars?: Record<string, unknown>, options?: TranslateParams): string;
25
+ tl(key: string, vars?: Record<string, unknown>, options?: TranslateParams): Promise<string>;
26
+ number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string;
27
+ date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string;
28
+ namespace(ns: string): {
29
+ t: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) => string;
30
+ tl: (key: string, vars?: Record<string, unknown>, options?: TranslateParams) => Promise<string>;
31
+ };
32
+ subscribe(handler: LocaleChangeHandler): () => void;
33
+ private notifySubscribers;
34
+ private findMessage;
35
+ private getLocaleChain;
36
+ private formatMessage;
37
+ }
38
+
39
+ export declare type I18nConfig = {
40
+ locale?: Locale;
41
+ fallback?: Locale | Locale[];
42
+ messages?: Record<Locale, Messages>;
43
+ loaders?: Record<Locale, () => Promise<Messages>>;
44
+ escape?: boolean;
45
+ missingKey?: (key: string, locale: Locale) => string;
46
+ missingVar?: 'preserve' | 'empty' | 'error';
47
+ };
48
+
49
+ export declare type Locale = string;
50
+
51
+ declare type LocaleChangeHandler = (locale: Locale) => void;
52
+
53
+ export declare type MessageFunction = (vars: Record<string, unknown>, helpers: {
54
+ number: (value: number, options?: Intl.NumberFormatOptions) => string;
55
+ date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
56
+ }) => string;
57
+
58
+ export declare type Messages = Record<string, MessageValue>;
59
+
60
+ export declare type MessageValue = string | PluralMessages | MessageFunction;
61
+
62
+ /**
63
+ * Error thrown when a required variable is missing during interpolation.
64
+ */
65
+ export declare class MissingVariableError extends Error {
66
+ readonly key: string;
67
+ readonly variable: string;
68
+ readonly locale: Locale;
69
+ constructor(key: string, variable: string, locale: Locale);
70
+ }
71
+
72
+ export declare type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
73
+
74
+ export declare type PluralMessages = Partial<Record<PluralForm, string>> & {
75
+ other: string;
76
+ };
77
+
78
+ export declare type TranslateParams = {
79
+ locale?: Locale;
80
+ fallback?: string;
81
+ escape?: boolean;
82
+ };
83
+
84
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { MissingVariableError as a, createI18n as i } from "./i18nit.js";
2
+ export {
3
+ a as MissingVariableError,
4
+ i as createI18n
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@vielzeug/i18nit",
3
+ "version": "1.1.1",
4
+ "type": "module",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "build": "tsc && vite build",
19
+ "fix": "biome check --write --unsafe src",
20
+ "lint": "biome check src",
21
+ "prepublishOnly": "npm run build",
22
+ "preview": "vite preview",
23
+ "test": "vitest"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "~5.9.3",
31
+ "vite": "^7.3.1",
32
+ "vite-plugin-dts": "^4.5.4",
33
+ "vitest": "^4.0.18"
34
+ }
35
+ }