@vielzeug/i18nit 1.1.3 → 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 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** - ~2.3KB gzipped with zero dependencies
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
- Control how missing variables are handled:
207
+ Missing variables are automatically replaced with empty strings:
205
208
 
206
209
  ```typescript
207
- // Empty string (default)
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
- try {
230
- i18n3.t('msg');
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
- // Lazy translation - loads locale first
356
- const text = await i18n.tl('greeting', { name: 'World' }, { locale: 'fr' });
301
+ // Preload at app startup
302
+ await i18n.loadAll(['en', 'fr', 'de']);
357
303
 
358
304
  // Or load explicitly
359
- await i18n.load('de');
360
- i18n.t('greeting', undefined, { locale: 'de' });
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 retried
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
  // "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
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: &lt;b&gt;Bold&lt;/b&gt;"
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
- ### Custom Missing Key Handler
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; // Default: 'en'
505
- fallback?: string | string[]; // Fallback locale(s)
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; // Global HTML escaping (default: false)
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'); // Change locale
543
- i18n.getLocale(); // Get current locale
544
- i18n.hasLocale('es'); // Check if locale exists
545
- i18n.has('key'); // Check if key exists
546
- i18n.has('key', 'fr'); // Check if key exists in locale
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 | i18nit | i18next | react-intl |
828
- |---------|--------|---------|------------|
829
- | Bundle Size | **~3KB** | ~12KB | ~15KB |
830
- | Dependencies | **0** | 2+ | 10+ |
831
- | TypeScript | ✅ First-class | ✅ Good | ✅ Good |
832
- | Pluralization | ✅ Built-in | ✅ Plugin | ✅ Built-in |
833
- | Async Loading | ✅ Built-in | ✅ Built-in | ⚠️ Manual |
834
- | Path Interpolation | ✅ `{user.name}` | ❌ | ❌ |
835
- | Message Functions | ✅ Built-in | ⚠️ Limited | ✅ Components |
836
- | HTML Escaping | ✅ Built-in | ⚠️ Manual | ✅ Built-in |
837
- | Structured Errors | ✅ MissingVariableError | ❌ | ❌ |
838
- | Framework Agnostic | ✅ | ✅ | ❌ React only |
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"});class m extends Error{key;variable;locale;constructor(t,e,s){super(`Missing variable '${e}' for key '${t}' in locale '${s}'`),this.name="MissingVariableError",this.key=t,this.variable=e,this.locale=s}}const b={"'":"&#39;",'"':"&quot;","&":"&amp;","<":"&lt;",">":"&gt;"};function h(i){return i.replace(/[&<>"']/g,t=>b[t])}function d(i,t){if(t in i)return i[t];const e=t.match(/[^.[\]]+/g)||[];let s=i;for(const r of e){if(s==null||typeof s!="object")return;if(Array.isArray(s)){const n=Number(r);if(Number.isNaN(n)||n<0||n>=s.length)return;s=s[n]}else s=s[r]}return s}function f(i,t,e){if(i.length===0)return"";const s=i.map(String);try{return new Intl.ListFormat(t,{style:"long",type:e}).format(s)}catch{if(s.length===1)return s[0];if(s.length===2){const a=e==="conjunction"?"and":"or";return`${s[0]} ${a} ${s[1]}`}const r=e==="conjunction"?"and":"or",n=s.pop();return`${s.join(", ")} ${r} ${n}`}}function g(i,t,e={}){const s=e.missingVar??"empty";return i.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g,(r,n,a)=>{let o=n,l=!1;n.endsWith(".length")&&(o=n.slice(0,-7),l=!0);const c=d(t,o);if(c==null){if(s==="preserve")return r;if(s==="error")throw new m(e.key??"unknown",n,e.locale??"unknown");return""}if(Array.isArray(c)){if(l)return String(c.length);if(a!==void 0){const u=e.locale||"en";return a==="and"?f(c,u,"conjunction"):a==="or"?f(c,u,"disjunction"):c.map(String).join(a)}return c.map(String).join(", ")}if(typeof c=="number"&&e.locale)try{return new Intl.NumberFormat(e.locale).format(c)}catch{}return String(c)})}function y(i,t){const e=Math.abs(Math.floor(t));try{return new Intl.PluralRules(i).select(e)}catch{return e===1?"one":"other"}}class p{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??(e=>e),this.missingVar=t.missingVar??"empty",t.messages)for(const[e,s]of Object.entries(t.messages))this.catalogs.set(e,s);if(t.loaders)for(const[e,s]of Object.entries(t.loaders))this.loaders.set(e,s)}setLocale(t){this.locale!==t&&(this.locale=t,this.notifySubscribers())}getLocale(){return this.locale}add(t,e){const s=this.catalogs.get(t)??{};this.catalogs.set(t,{...s,...e}),this.notifySubscribers()}set(t,e){this.catalogs.set(t,e),this.notifySubscribers()}getMessages(t){return this.catalogs.get(t)}hasLocale(t){return this.catalogs.has(t)}has(t,e){return this.findMessage(t,e??this.locale)!==void 0}async load(t){if(this.loading.has(t))return this.loading.get(t);if(this.catalogs.has(t))return;const e=this.loaders.get(t);if(!e)return;const s=(async()=>{try{const r=await e();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,s),s}register(t,e){this.loaders.set(t,e)}async hasAsync(t,e){const s=e??this.locale;return!this.catalogs.has(s)&&this.loaders.has(s)&&await this.load(s),this.has(t,s)}t(t,e,s){const r=s??{},n=r.locale??this.locale,a=r.escape??this.escape,o=this.findMessage(t,n);return o===void 0?r.fallback??this.missingKey(t,n):this.formatMessage(o,e??{},n,a,t)}async tl(t,e,s){const r=s?.locale??this.locale;if(!this.catalogs.has(r)&&this.loaders.has(r))try{await this.load(r)}catch{}return this.t(t,e,s)}number(t,e,s){try{return new Intl.NumberFormat(s??this.locale,e).format(t)}catch{return String(t)}}date(t,e,s){const r=typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(s??this.locale,e).format(r)}catch{return r.toString()}}namespace(t){return{t:(e,s,r)=>this.t(`${t}.${e}`,s,r),tl:(e,s,r)=>this.tl(`${t}.${e}`,s,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,e){const s=this.getLocaleChain(e);for(const r of s){const n=this.catalogs.get(r);if(!n)continue;const a=d(n,t);if(a!==void 0)return a}}getLocaleChain(t){const e=[t],s=t.split("-")[0];s!==t&&e.push(s);for(const r of this.fallbacks){e.push(r);const n=r.split("-")[0];n!==r&&e.push(n)}return e}formatMessage(t,e,s,r,n){if(typeof t=="function")try{const a=t(e,{date:(o,l)=>this.date(o,l,s),number:(o,l)=>this.number(o,l,s)});return r?h(a):a}catch{return""}if(typeof t=="object"&&"other"in t){const a=Number(e.count??0),o=t;let l;a===0&&o.zero!==void 0?l="zero":l=y(s,a);const c=o[l]??o.other,u=g(c,e,{key:n,locale:s,missingVar:this.missingVar});return r?h(u):u}if(typeof t=="string"){const a=g(t,e,{key:n,locale:s,missingVar:this.missingVar});return r?h(a):a}return""}}function w(i){return new p(i)}exports.MissingVariableError=m;exports.createI18n=w;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function f(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}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
@@ -1 +1 @@
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 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 TranslateOptions = {\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\nexport type LocaleChangeHandler = (locale: Locale) => void;\n\n/* -------------------- Errors -------------------- */\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/* -------------------- HTML Escaping -------------------- */\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"'\": '&#39;',\n '\"': '&quot;',\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n};\n\nfunction escapeHtml(str: string): string {\n return str.replace(/[&<>\"']/g, (char) => HTML_ENTITIES[char]);\n}\n\n/* -------------------- Path Resolution -------------------- */\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\ntype InterpolateOptions = {\n locale?: Locale;\n missingVar?: 'preserve' | 'empty' | 'error';\n key?: string;\n};\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>, options: InterpolateOptions = {}): string {\n const missingVar = options.missingVar ?? 'empty';\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Legitimate complexity for variable interpolation\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (match, key: string, separator?: string) => {\n // Handle .length property\n let actualKey = key;\n let isLength = false;\n\n if (key.endsWith('.length')) {\n actualKey = key.slice(0, -7);\n isLength = true;\n }\n\n const value = resolvePath(vars, actualKey);\n\n // Handle missing variables\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 if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n const locale = options.locale || 'en';\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' && options.locale) {\n try {\n return new Intl.NumberFormat(options.locale).format(value);\n } catch {\n // Fall through\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 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 this.escape = config.escape ?? false;\n this.missingKey = config.missingKey ?? ((key) => key);\n this.missingVar = config.missingVar ?? 'empty';\n\n // Load initial messages\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n // Register loaders\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 /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): 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\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 /**\n * Translates a key with automatic async loading.\n */\n async tl(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): 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 // Error already logged in load()\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?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n tl: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.tl(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: LocaleChangeHandler): () => 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 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Legitimate complexity for message formatting\n private formatMessage(\n message: MessageValue,\n vars: Record<string, unknown>,\n locale: Locale,\n shouldEscape: boolean,\n key?: string,\n ): string {\n // 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 // 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 const result = interpolate(template, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n // 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\n/* -------------------- Factory Function -------------------- */\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","index","formatList","items","type","stringItems","separator","last","interpolate","template","vars","options","missingVar","match","actualKey","isLength","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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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,105 +1,75 @@
1
- class d extends Error {
2
- key;
3
- variable;
4
- locale;
5
- constructor(t, e, s) {
6
- super(`Missing variable '${e}' for key '${t}' in locale '${s}'`), this.name = "MissingVariableError", this.key = t, this.variable = e, this.locale = s;
7
- }
1
+ function f(a) {
2
+ return a.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8
3
  }
9
- const b = {
10
- "'": "&#39;",
11
- '"': "&quot;",
12
- "&": "&amp;",
13
- "<": "&lt;",
14
- ">": "&gt;"
15
- };
16
- function h(i) {
17
- return i.replace(/[&<>"']/g, (t) => b[t]);
18
- }
19
- function m(i, t) {
20
- if (t in i) return i[t];
21
- const e = t.match(/[^.[\]]+/g) || [];
22
- let s = i;
23
- for (const r of e) {
24
- if (s == null || typeof s != "object") return;
25
- if (Array.isArray(s)) {
26
- const n = Number(r);
27
- if (Number.isNaN(n) || n < 0 || n >= s.length)
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)
28
13
  return;
29
- s = s[n];
14
+ e = e[n];
30
15
  } else
31
- s = s[r];
16
+ e = e[s];
32
17
  }
33
- return s;
18
+ return e;
34
19
  }
35
- function f(i, t, e) {
36
- if (i.length === 0) return "";
37
- const s = i.map(String);
20
+ function l(a, t, r) {
21
+ if (a.length === 0) return "";
22
+ const e = a.map(String);
38
23
  try {
39
- return new Intl.ListFormat(t, { style: "long", type: e }).format(s);
24
+ return new Intl.ListFormat(t, { style: "long", type: r }).format(e);
40
25
  } catch {
41
- if (s.length === 1) return s[0];
42
- if (s.length === 2) {
43
- const a = e === "conjunction" ? "and" : "or";
44
- return `${s[0]} ${a} ${s[1]}`;
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]}`;
45
30
  }
46
- const r = e === "conjunction" ? "and" : "or", n = s.pop();
47
- return `${s.join(", ")} ${r} ${n}`;
31
+ const s = r === "conjunction" ? "and" : "or", n = e.pop();
32
+ return `${e.join(", ")} ${s} ${n}`;
48
33
  }
49
34
  }
50
- function g(i, t, e = {}) {
51
- const s = e.missingVar ?? "empty";
52
- return i.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g, (r, n, a) => {
53
- let o = n, l = !1;
54
- n.endsWith(".length") && (o = n.slice(0, -7), l = !0);
55
- const c = m(t, o);
56
- if (c == null) {
57
- if (s === "preserve") return r;
58
- if (s === "error")
59
- throw new d(e.key ?? "unknown", n, e.locale ?? "unknown");
60
- return "";
61
- }
62
- if (Array.isArray(c)) {
63
- if (l) return String(c.length);
64
- if (a !== void 0) {
65
- const u = e.locale || "en";
66
- return a === "and" ? f(c, u, "conjunction") : a === "or" ? f(c, u, "disjunction") : c.map(String).join(a);
67
- }
68
- return c.map(String).join(", ");
69
- }
70
- if (typeof c == "number" && e.locale)
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")
71
42
  try {
72
- return new Intl.NumberFormat(e.locale).format(c);
43
+ return new Intl.NumberFormat(r).format(i);
73
44
  } catch {
45
+ return String(i);
74
46
  }
75
- return String(c);
47
+ return String(i);
76
48
  });
77
49
  }
78
- function y(i, t) {
79
- const e = Math.abs(Math.floor(t));
50
+ function g(a, t) {
51
+ const r = Math.abs(Math.floor(t));
80
52
  try {
81
- return new Intl.PluralRules(i).select(e);
53
+ return new Intl.PluralRules(a).select(r);
82
54
  } catch {
83
- return e === 1 ? "one" : "other";
55
+ return r === 1 ? "one" : "other";
84
56
  }
85
57
  }
86
- class p {
58
+ class d {
87
59
  locale;
88
60
  fallbacks;
61
+ escape;
89
62
  catalogs = /* @__PURE__ */ new Map();
90
63
  loaders = /* @__PURE__ */ new Map();
91
64
  loading = /* @__PURE__ */ new Map();
92
65
  subscribers = /* @__PURE__ */ new Set();
93
- escape;
94
- missingKey;
95
- missingVar;
96
66
  constructor(t = {}) {
97
- 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 ?? ((e) => e), this.missingVar = t.missingVar ?? "empty", t.messages)
98
- for (const [e, s] of Object.entries(t.messages))
99
- this.catalogs.set(e, s);
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);
100
70
  if (t.loaders)
101
- for (const [e, s] of Object.entries(t.loaders))
102
- this.loaders.set(e, s);
71
+ for (const [r, e] of Object.entries(t.loaders))
72
+ this.loaders.set(r, e);
103
73
  }
104
74
  /* -------------------- Locale Management -------------------- */
105
75
  setLocale(t) {
@@ -112,15 +82,15 @@ class p {
112
82
  /**
113
83
  * Adds messages to a locale (merges with existing).
114
84
  */
115
- add(t, e) {
116
- const s = this.catalogs.get(t) ?? {};
117
- this.catalogs.set(t, { ...s, ...e }), this.notifySubscribers();
85
+ add(t, r) {
86
+ const e = this.catalogs.get(t) ?? {};
87
+ this.catalogs.set(t, { ...e, ...r }), this.notifySubscribers();
118
88
  }
119
89
  /**
120
90
  * Sets messages for a locale (replaces existing).
121
91
  */
122
- set(t, e) {
123
- this.catalogs.set(t, e), this.notifySubscribers();
92
+ set(t, r) {
93
+ this.catalogs.set(t, r), this.notifySubscribers();
124
94
  }
125
95
  getMessages(t) {
126
96
  return this.catalogs.get(t);
@@ -128,75 +98,72 @@ class p {
128
98
  hasLocale(t) {
129
99
  return this.catalogs.has(t);
130
100
  }
131
- has(t, e) {
132
- return this.findMessage(t, e ?? this.locale) !== void 0;
101
+ has(t, r) {
102
+ return this.findMessage(t, r ?? this.locale) !== void 0;
133
103
  }
134
104
  /* -------------------- Async Loaders -------------------- */
135
105
  async load(t) {
136
106
  if (this.loading.has(t)) return this.loading.get(t);
137
107
  if (this.catalogs.has(t)) return;
138
- const e = this.loaders.get(t);
139
- if (!e) return;
140
- const s = (async () => {
108
+ const r = this.loaders.get(t);
109
+ if (!r) return;
110
+ const e = (async () => {
141
111
  try {
142
- const r = await e();
143
- this.add(t, r);
144
- } catch (r) {
145
- throw console.warn(`[I18n] Failed to load locale '${t}':`, r), r;
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;
146
116
  } finally {
147
117
  this.loading.delete(t);
148
118
  }
149
119
  })();
150
- return this.loading.set(t, s), s;
120
+ return this.loading.set(t, e), e;
151
121
  }
152
- register(t, e) {
153
- this.loaders.set(t, e);
122
+ register(t, r) {
123
+ this.loaders.set(t, r);
154
124
  }
155
- async hasAsync(t, e) {
156
- const s = e ?? this.locale;
157
- return !this.catalogs.has(s) && this.loaders.has(s) && await this.load(s), this.has(t, s);
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);
158
128
  }
159
- /* -------------------- Translation -------------------- */
160
129
  /**
161
- * Translates a key with optional variables.
130
+ * Load multiple locales in parallel.
131
+ * Useful for preloading all needed locales at app startup.
162
132
  */
163
- t(t, e, s) {
164
- const r = s ?? {}, n = r.locale ?? this.locale, a = r.escape ?? this.escape, o = this.findMessage(t, n);
165
- return o === void 0 ? r.fallback ?? this.missingKey(t, n) : this.formatMessage(o, e ?? {}, n, a, t);
133
+ async loadAll(t) {
134
+ await Promise.all(t.map((r) => this.load(r)));
166
135
  }
136
+ /* -------------------- Translation -------------------- */
167
137
  /**
168
- * Translates a key with automatic async loading.
138
+ * Translates a key with optional variables and options.
139
+ * Synchronous - locale must be loaded first via load() or provided in config.
169
140
  */
170
- async tl(t, e, s) {
171
- const r = s?.locale ?? this.locale;
172
- if (!this.catalogs.has(r) && this.loaders.has(r))
173
- try {
174
- await this.load(r);
175
- } catch {
176
- }
177
- return this.t(t, e, s);
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;
178
146
  }
179
147
  /* -------------------- Formatting Helpers -------------------- */
180
- number(t, e, s) {
148
+ number(t, r, e) {
181
149
  try {
182
- return new Intl.NumberFormat(s ?? this.locale, e).format(t);
150
+ return new Intl.NumberFormat(e ?? this.locale, r).format(t);
183
151
  } catch {
184
152
  return String(t);
185
153
  }
186
154
  }
187
- date(t, e, s) {
188
- const r = typeof t == "number" ? new Date(t) : t;
155
+ date(t, r, e) {
156
+ const s = typeof t == "number" ? new Date(t) : t;
189
157
  try {
190
- return new Intl.DateTimeFormat(s ?? this.locale, e).format(r);
158
+ return new Intl.DateTimeFormat(e ?? this.locale, r).format(s);
191
159
  } catch {
192
- return r.toString();
160
+ return s.toString();
193
161
  }
194
162
  }
195
163
  /* -------------------- Namespaced Translator -------------------- */
196
164
  namespace(t) {
197
165
  return {
198
- t: (e, s, r) => this.t(`${t}.${e}`, s, r),
199
- tl: (e, s, r) => this.tl(`${t}.${e}`, s, r)
166
+ t: (r, e, s) => this.t(`${t}.${r}`, e, s)
200
167
  };
201
168
  }
202
169
  /* -------------------- Subscriptions -------------------- */
@@ -216,56 +183,40 @@ class p {
216
183
  }
217
184
  }
218
185
  /* -------------------- Internal Helpers -------------------- */
219
- findMessage(t, e) {
220
- const s = this.getLocaleChain(e);
221
- for (const r of s) {
222
- const n = this.catalogs.get(r);
186
+ findMessage(t, r) {
187
+ const e = this.getLocaleChain(r);
188
+ for (const s of e) {
189
+ const n = this.catalogs.get(s);
223
190
  if (!n) continue;
224
- const a = m(n, t);
225
- if (a !== void 0) return a;
191
+ const o = h(n, t);
192
+ if (o !== void 0) return o;
226
193
  }
227
194
  }
228
195
  getLocaleChain(t) {
229
- const e = [t], s = t.split("-")[0];
230
- s !== t && e.push(s);
231
- for (const r of this.fallbacks) {
232
- e.push(r);
233
- const n = r.split("-")[0];
234
- n !== r && e.push(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);
235
202
  }
236
- return e;
203
+ return r;
237
204
  }
238
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Legitimate complexity for message formatting
239
- formatMessage(t, e, s, r, n) {
240
- if (typeof t == "function")
241
- try {
242
- const a = t(e, {
243
- date: (o, l) => this.date(o, l, s),
244
- number: (o, l) => this.number(o, l, s)
245
- });
246
- return r ? h(a) : a;
247
- } catch {
248
- return "";
249
- }
205
+ formatMessage(t, r, e) {
250
206
  if (typeof t == "object" && "other" in t) {
251
- const a = Number(e.count ?? 0), o = t;
252
- let l;
253
- a === 0 && o.zero !== void 0 ? l = "zero" : l = y(s, a);
254
- const c = o[l] ?? o.other, u = g(c, e, { key: n, locale: s, missingVar: this.missingVar });
255
- return r ? h(u) : u;
256
- }
257
- if (typeof t == "string") {
258
- const a = g(t, e, { key: n, locale: s, missingVar: this.missingVar });
259
- return r ? h(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);
260
212
  }
261
- return "";
213
+ return typeof t == "string" ? u(t, r, e) : "";
262
214
  }
263
215
  }
264
- function w(i) {
265
- return new p(i);
216
+ function b(a) {
217
+ return new d(a);
266
218
  }
267
219
  export {
268
- d as MissingVariableError,
269
- w as createI18n
220
+ b as createI18n
270
221
  };
271
222
  //# sourceMappingURL=i18nit.js.map
@@ -1 +1 @@
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 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 TranslateOptions = {\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\nexport type LocaleChangeHandler = (locale: Locale) => void;\n\n/* -------------------- Errors -------------------- */\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/* -------------------- HTML Escaping -------------------- */\n\nconst HTML_ENTITIES: Record<string, string> = {\n \"'\": '&#39;',\n '\"': '&quot;',\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n};\n\nfunction escapeHtml(str: string): string {\n return str.replace(/[&<>\"']/g, (char) => HTML_ENTITIES[char]);\n}\n\n/* -------------------- Path Resolution -------------------- */\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\ntype InterpolateOptions = {\n locale?: Locale;\n missingVar?: 'preserve' | 'empty' | 'error';\n key?: string;\n};\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>, options: InterpolateOptions = {}): string {\n const missingVar = options.missingVar ?? 'empty';\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Legitimate complexity for variable interpolation\n return template.replace(/\\{([\\w.[\\]]+)(?:\\|([^}]+))?\\}/g, (match, key: string, separator?: string) => {\n // Handle .length property\n let actualKey = key;\n let isLength = false;\n\n if (key.endsWith('.length')) {\n actualKey = key.slice(0, -7);\n isLength = true;\n }\n\n const value = resolvePath(vars, actualKey);\n\n // Handle missing variables\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 if (isLength) return String(value.length);\n\n if (separator !== undefined) {\n const locale = options.locale || 'en';\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' && options.locale) {\n try {\n return new Intl.NumberFormat(options.locale).format(value);\n } catch {\n // Fall through\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 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 this.escape = config.escape ?? false;\n this.missingKey = config.missingKey ?? ((key) => key);\n this.missingVar = config.missingVar ?? 'empty';\n\n // Load initial messages\n if (config.messages) {\n for (const [locale, messages] of Object.entries(config.messages)) {\n this.catalogs.set(locale, messages);\n }\n }\n\n // Register loaders\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 /* -------------------- Translation -------------------- */\n\n /**\n * Translates a key with optional variables.\n */\n t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): 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\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 /**\n * Translates a key with automatic async loading.\n */\n async tl(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): 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 // Error already logged in load()\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?: TranslateOptions) =>\n this.t(`${ns}.${key}`, vars, options),\n tl: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) =>\n this.tl(`${ns}.${key}`, vars, options),\n };\n }\n\n /* -------------------- Subscriptions -------------------- */\n\n subscribe(handler: LocaleChangeHandler): () => 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 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Legitimate complexity for message formatting\n private formatMessage(\n message: MessageValue,\n vars: Record<string, unknown>,\n locale: Locale,\n shouldEscape: boolean,\n key?: string,\n ): string {\n // 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 // 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 const result = interpolate(template, vars, { key, locale, missingVar: this.missingVar });\n return shouldEscape ? escapeHtml(result) : result;\n }\n\n // 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\n/* -------------------- Factory Function -------------------- */\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","index","formatList","items","type","stringItems","separator","last","interpolate","template","vars","options","missingVar","match","actualKey","isLength","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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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 r=require("./i18nit.cjs");exports.MissingVariableError=r.MissingVariableError;exports.createI18n=r.createI18n;
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,13 +3,11 @@ 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;
@@ -28,20 +26,21 @@ declare class I18n {
28
26
  register(locale: Locale, loader: () => Promise<Messages>): void;
29
27
  hasAsync(key: string, locale?: Locale): Promise<boolean>;
30
28
  /**
31
- * Translates a key with optional variables.
29
+ * Load multiple locales in parallel.
30
+ * Useful for preloading all needed locales at app startup.
32
31
  */
33
- t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string;
32
+ loadAll(locales: Locale[]): Promise<void>;
34
33
  /**
35
- * Translates a key with automatic async loading.
34
+ * Translates a key with optional variables and options.
35
+ * Synchronous - locale must be loaded first via load() or provided in config.
36
36
  */
37
- tl(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): Promise<string>;
37
+ t(key: string, vars?: Record<string, unknown>, options?: TranslateOptions): string;
38
38
  number(value: number, options?: Intl.NumberFormatOptions, locale?: Locale): string;
39
39
  date(value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: Locale): string;
40
40
  namespace(ns: string): {
41
41
  t: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) => string;
42
- tl: (key: string, vars?: Record<string, unknown>, options?: TranslateOptions) => Promise<string>;
43
42
  };
44
- subscribe(handler: LocaleChangeHandler): () => void;
43
+ subscribe(handler: (locale: Locale) => void): () => void;
45
44
  private notifySubscribers;
46
45
  private findMessage;
47
46
  private getLocaleChain;
@@ -54,29 +53,13 @@ export declare type I18nConfig = {
54
53
  messages?: Record<Locale, Messages>;
55
54
  loaders?: Record<Locale, () => Promise<Messages>>;
56
55
  escape?: boolean;
57
- missingKey?: (key: string, locale: Locale) => string;
58
- missingVar?: 'preserve' | 'empty' | 'error';
59
56
  };
60
57
 
61
58
  export declare type Locale = string;
62
59
 
63
- export declare type LocaleChangeHandler = (locale: Locale) => void;
64
-
65
- export declare type MessageFunction = (vars: Record<string, unknown>, helpers: {
66
- number: (value: number, options?: Intl.NumberFormatOptions) => string;
67
- date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
68
- }) => string;
69
-
70
60
  export declare type Messages = Record<string, MessageValue>;
71
61
 
72
- export declare type MessageValue = string | PluralMessages | MessageFunction;
73
-
74
- export declare class MissingVariableError extends Error {
75
- readonly key: string;
76
- readonly variable: string;
77
- readonly locale: Locale;
78
- constructor(key: string, variable: string, locale: Locale);
79
- }
62
+ export declare type MessageValue = string | PluralMessages;
80
63
 
81
64
  export declare type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
82
65
 
@@ -86,7 +69,6 @@ export declare type PluralMessages = Partial<Record<PluralForm, string>> & {
86
69
 
87
70
  export declare type TranslateOptions = {
88
71
  locale?: Locale;
89
- fallback?: string;
90
72
  escape?: boolean;
91
73
  };
92
74
 
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
- import { MissingVariableError as a, createI18n as i } from "./i18nit.js";
1
+ import { createI18n as o } from "./i18nit.js";
2
2
  export {
3
- a as MissingVariableError,
4
- i as createI18n
3
+ o as createI18n
5
4
  };
6
5
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/i18nit",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"