@vielzeug/i18nit 1.1.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,35 +1,86 @@
1
1
  # @vielzeug/i18nit
2
2
 
3
- Type-safe, lightweight internationalization (i18n) library for TypeScript applications. Simple, powerful translation management with zero dependencies.
3
+ ## What is I18nit?
4
4
 
5
- ## Features
5
+ **I18nit** is a type-safe, lightweight internationalization library for TypeScript. Build multilingual applications with powerful pluralization, nested translations, and lazy loading—all in just 1.6 KB.
6
6
 
7
- - **Type-Safe** - Full TypeScript support with generic types
8
- - ✅ **Lightweight** - 1.6 KB gzipped with zero dependencies
9
- - ✅ **Universal Pluralization** - 100+ languages via Intl.PluralRules API
10
- - ✅ **Smart Array Handling** - Auto-join with separators, length access, and safe indexing
11
- - ✅ **Path Interpolation** - Support for nested objects and array indices
12
- - ✅ **Lazy Loading** - Async locale loading with automatic caching
13
- - ✅ **Namespaces** - Organize translations by feature or module
14
- - ✅ **Fallback Chain** - Multiple fallback locales with automatic language variants
15
- - ✅ **HTML Escaping** - Built-in XSS protection
16
- - ✅ **Number & Date Formatting** - Locale-aware formatting with Intl API
17
- - ✅ **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
7
+ ### The Problem
18
8
 
19
- ## Installation
9
+ Internationalization libraries are often heavy and complex:
10
+
11
+ - **i18next** is feature-rich but adds 11KB+ to your bundle
12
+ - **react-intl** is React-specific and requires setup
13
+ - **FormatJS** has a steep learning curve
14
+ - Manual translations lead to missing keys and runtime errors
15
+ - Type safety requires extra tooling
16
+
17
+ ### The Solution
18
+
19
+ I18nit provides a simple, type-safe API using native browser APIs:
20
+
21
+ ```typescript
22
+ import { createI18n } from '@vielzeug/i18nit';
23
+
24
+ const i18n = createI18n({
25
+ locale: 'en',
26
+ messages: {
27
+ en: {
28
+ welcome: 'Welcome, {name}!',
29
+ items: 'You have {count} item | You have {count} items',
30
+ },
31
+ es: {
32
+ welcome: '¡Bienvenido, {name}!',
33
+ items: 'Tienes {count} artículo | Tienes {count} artículos',
34
+ },
35
+ },
36
+ });
37
+
38
+ // Interpolation
39
+ i18n.t('welcome', { name: 'Alice' }); // "Welcome, Alice!"
40
+
41
+ // Automatic pluralization
42
+ i18n.t('items', { count: 1 }); // "You have 1 item"
43
+ i18n.t('items', { count: 5 }); // "You have 5 items"
44
+ ```
45
+
46
+ ## ✨ Features
47
+
48
+ - ✅ **Type-Safe** – Full TypeScript support with generic types
49
+ - ✅ **Lightweight** – 1.6 KB gzipped with zero dependencies
50
+ - ✅ **Universal Pluralization** – 100+ languages via Intl.PluralRules API
51
+ - ✅ **Smart Array Handling** – Auto-join with separators, length access, and safe indexing
52
+ - ✅ **Path Interpolation** – Support for nested objects and array indices
53
+ - ✅ **Lazy Loading** – Async locale loading with automatic caching
54
+ - ✅ **Namespaces** – Organize translations by feature or module
55
+ - ✅ **Fallback Chain** – Multiple fallback locales with automatic language variants
56
+ - ✅ **HTML Escaping** – Built-in XSS protection
57
+ - ✅ **Number & Date Formatting** – Locale-aware formatting with Intl API
58
+ - ✅ **Framework Agnostic** – Works with React, Vue, Svelte, or vanilla JS
59
+
60
+ ## 🆚 Comparison with Alternatives
61
+
62
+ | Feature | I18nit | i18next | react-intl | FormatJS |
63
+ | ------------------- | -------------- | ------- | ---------- | ------------ |
64
+ | Bundle Size (gzip) | **~1.6 KB** | ~11KB | ~14KB | ~14KB |
65
+ | TypeScript Support | ✅ First-class | ✅ Good | ✅ Good | ✅ Excellent |
66
+ | Pluralization | ✅ Native Intl | ✅ ICU | ✅ ICU | ✅ ICU |
67
+ | Nested Translations | ✅ Built-in | ✅ Yes | ⚠️ Limited | ✅ Yes |
68
+ | Lazy Loading | ✅ Async | ✅ Yes | ⚠️ Manual | ✅ Yes |
69
+ | Framework Agnostic | ✅ Yes | ✅ Yes | ❌ React | ❌ React |
70
+ | Dependencies | 0 | 3 | 5 | 7 |
71
+
72
+ ## 📦 Installation
20
73
 
21
74
  ```bash
22
75
  # pnpm
23
76
  pnpm add @vielzeug/i18nit
24
-
25
77
  # npm
26
78
  npm install @vielzeug/i18nit
27
-
28
79
  # yarn
29
80
  yarn add @vielzeug/i18nit
30
81
  ```
31
82
 
32
- ## Quick Start
83
+ ## 🚀 Quick Start
33
84
 
34
85
  ```typescript
35
86
  import { createI18n } from '@vielzeug/i18nit';
@@ -70,7 +121,7 @@ i18n.setLocale('es');
70
121
  i18n.t('greeting', { name: 'Mundo' }); // "¡Hola, Mundo!"
71
122
  ```
72
123
 
73
- ## Core Concepts
124
+ ## 📚 Core Concepts
74
125
 
75
126
  ### Translation Keys
76
127
 
@@ -90,6 +141,52 @@ const i18n = createI18n({
90
141
  i18n.t('user.profile.title'); // "Profile"
91
142
  ```
92
143
 
144
+ ### Nested Message Objects
145
+
146
+ You can organize messages using nested objects for better structure:
147
+
148
+ ```typescript
149
+ const i18n = createI18n({
150
+ locale: 'en',
151
+ messages: {
152
+ en: {
153
+ // Flat structure
154
+ welcome: 'Welcome!',
155
+
156
+ // Nested structure - access with dot notation
157
+ user: {
158
+ greeting: 'Hello, {name}!',
159
+ profile: {
160
+ title: 'User Profile',
161
+ settings: 'Profile Settings',
162
+ },
163
+ },
164
+
165
+ // Deep nesting
166
+ app: {
167
+ navigation: {
168
+ menu: {
169
+ home: 'Home',
170
+ about: 'About',
171
+ },
172
+ },
173
+ },
174
+ },
175
+ },
176
+ });
177
+
178
+ // Access nested messages with dot notation
179
+ i18n.t('welcome'); // "Welcome!"
180
+ i18n.t('user.greeting', { name: 'Alice' }); // "Hello, Alice!"
181
+ i18n.t('user.profile.title'); // "User Profile"
182
+ i18n.t('app.navigation.menu.home'); // "Home"
183
+
184
+ // Use with namespaces for cleaner code
185
+ const userNs = i18n.namespace('user');
186
+ userNs.t('greeting', { name: 'Bob' }); // "Hello, Bob!"
187
+ userNs.t('profile.title'); // "User Profile"
188
+ ```
189
+
93
190
  ### Variable Interpolation
94
191
 
95
192
  #### Basic Interpolation
@@ -137,7 +234,7 @@ const i18n = createI18n({
137
234
  i18n.t('shopping', { items: ['Apple', 'Banana', 'Orange'] });
138
235
  // "Shopping list: Apple, Banana, Orange"
139
236
 
140
- // Natural "and" lists (locale-aware via Intl.ListFormat - supports 100+ languages automatically)
237
+ // Natural "and" lists (locale-aware via Intl.ListFormat supports 100+ languages automatically)
141
238
  i18n.t('guests', { names: ['Alice'] });
142
239
  // "Invited: Alice"
143
240
  i18n.t('guests', { names: ['Alice', 'Bob'] });
@@ -145,7 +242,7 @@ i18n.t('guests', { names: ['Alice', 'Bob'] });
145
242
  i18n.t('guests', { names: ['Alice', 'Bob', 'Charlie'] });
146
243
  // "Invited: Alice, Bob, and Charlie" (Oxford comma in English)
147
244
 
148
- // Natural "or" lists (locale-aware via Intl.ListFormat - supports 100+ languages automatically)
245
+ // Natural "or" lists (locale-aware via Intl.ListFormat supports 100+ languages automatically)
149
246
  i18n.t('options', { choices: ['Tea', 'Coffee', 'Juice'] });
150
247
  // "Choose: Tea, Coffee, or Juice"
151
248
 
@@ -160,21 +257,21 @@ i18n.t('count', { items: ['A', 'B', 'C'] });
160
257
 
161
258
  **Array Features:**
162
259
 
163
- - `{items}` - Join with comma (`, `)
164
- - `{items|and}` - Natural "and" list with locale-aware conjunction (uses Intl.ListFormat - supports 100+ languages)
165
- - `{items|or}` - Natural "or" list with locale-aware conjunction (uses Intl.ListFormat - supports 100+ languages)
166
- - `{items| - }` - Custom separator (e.g., "A - B - C")
167
- - `{items.length}` - Array length
168
- - `{items[0]}` - Safe index access (returns empty if out of bounds)
260
+ - `{items}` Join with comma (`, `)
261
+ - `{items|and}` Natural "and" list with locale-aware conjunction (uses Intl.ListFormat supports 100+ languages)
262
+ - `{items|or}` Natural "or" list with locale-aware conjunction (uses Intl.ListFormat supports 100+ languages)
263
+ - `{items| }` Custom separator (e.g., "A B C")
264
+ - `{items.length}` Array length
265
+ - `{items[0]}` Safe index access (returns empty if out of bounds)
169
266
 
170
267
  **Locale-Aware List Formatting:**
171
268
  The `and` and `or` separators use the built-in **Intl.ListFormat API** which automatically handles:
172
269
 
173
- - **100+ languages** - Supports all languages available in the browser/runtime
174
- - **Proper grammar** - Oxford comma, locale-specific punctuation
175
- - **Right-to-left languages** - Arabic, Hebrew, etc.
176
- - **Unicode CLDR standards** - International standard for list formatting
177
- - **No manual configuration** - Zero maintenance required
270
+ - **100+ languages** Supports all languages available in the browser/runtime
271
+ - **Proper grammar** Oxford comma, locale-specific punctuation
272
+ - **Right-to-left languages** Arabic, Hebrew, etc.
273
+ - **Unicode CLDR standards** International standard for list formatting
274
+ - **No manual configuration** Zero maintenance required
178
275
 
179
276
  Examples across languages:
180
277
 
@@ -188,13 +285,13 @@ Examples across languages:
188
285
 
189
286
  #### Supported Path Formats
190
287
 
191
- - `{name}` - Simple variable
192
- - `{user.name}` - Nested object property
193
- - `{items[0]}` - Array index (safe - returns empty if out of bounds)
194
- - `{items}` - Array join with default separator
195
- - `{items|and}` - Array join with "and"
196
- - `{items.length}` - Array length
197
- - `{data.items[0].value}` - Mixed notation
288
+ - `{name}` Simple variable
289
+ - `{user.name}` Nested object property
290
+ - `{items[0]}` Array index (safe returns empty if out of bounds)
291
+ - `{items}` Array join with default separator
292
+ - `{items|and}` Array join with "and"
293
+ - `{items.length}` Array length
294
+ - `{data.items[0].value}` Mixed notation
198
295
 
199
296
  **Limitations:**
200
297
 
@@ -252,7 +349,7 @@ i18nit uses the browser's built-in `Intl.PluralRules` API to automatically suppo
252
349
  - **Japanese (ja)**: other
253
350
  - And 90+ more languages...
254
351
 
255
- ## Advanced Features
352
+ ## 🔥 Advanced Features
256
353
 
257
354
  ### Fallback Locales
258
355
 
@@ -284,20 +381,39 @@ i18n.t('welcome'); // "Welcome!" (en fallback)
284
381
 
285
382
  ### Async Locale Loading
286
383
 
287
- Load translations on-demand for better performance:
384
+ Load translations on-demand for better performance. Loaders receive the locale as a parameter, allowing you to reuse a single function:
288
385
 
289
386
  ```typescript
387
+ // Define a reusable loader function
388
+ const loadLocale = async (locale: string) => {
389
+ const response = await fetch(`/locales/${locale}.json`);
390
+ return response.json();
391
+ };
392
+
290
393
  const i18n = createI18n({
291
394
  locale: 'en',
292
395
  loaders: {
293
- fr: async () => {
294
- const response = await fetch('/locales/fr.json');
295
- return response.json();
296
- },
297
- de: async () => import('./locales/de.json'),
396
+ fr: loadLocale, // Loader receives 'fr' as parameter
397
+ de: loadLocale, // Loader receives 'de' as parameter
398
+ es: loadLocale, // Loader receives 'es' as parameter
298
399
  },
299
400
  });
300
401
 
402
+ // Load a locale before using it
403
+ await i18n.load('fr');
404
+ i18n.setLocale('fr');
405
+ i18n.t('greeting'); // Uses loaded French messages
406
+
407
+ // Or use dynamic imports
408
+ const importLoader = async (locale: string) => {
409
+ const module = await import(`./locales/${locale}.json`);
410
+ return module.default;
411
+ };
412
+
413
+ i18n.register('it', importLoader);
414
+ await i18n.load('it');
415
+ ```
416
+
301
417
  // Preload at app startup
302
418
  await i18n.loadAll(['en', 'fr', 'de']);
303
419
 
@@ -458,8 +574,8 @@ i18n.t('greeting', { name: 'Bob' }, { locale: 'fr', escape: true }); // With opt
458
574
 
459
575
  **Options:**
460
576
 
461
- - `locale?: string` - Override locale for this translation
462
- - `escape?: boolean` - Override HTML escaping
577
+ - `locale?: string` Override locale for this translation
578
+ - `escape?: boolean` Override HTML escaping
463
579
 
464
580
  ### Locale Management
465
581
 
@@ -713,30 +829,28 @@ const config: I18nConfig = {
713
829
  const i18n = createI18n(config);
714
830
  ```
715
831
 
716
- ## Comparison
832
+ ## 📖 Documentation
717
833
 
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 |
834
+ - [**Full Documentation**](https://helmuthdu.github.io/vielzeug/i18nit)
835
+ - [**Usage Guide**](https://helmuthdu.github.io/vielzeug/i18nit/usage)
836
+ - [**API Reference**](https://helmuthdu.github.io/vielzeug/i18nit/api)
837
+ - [**Examples**](https://helmuthdu.github.io/vielzeug/i18nit/examples)
728
838
 
729
- ## License
839
+ ## 📄 License
730
840
 
731
841
  MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
732
842
 
733
- ## Links
843
+ ## 🤝 Contributing
844
+
845
+ Contributions are welcome! Check our [GitHub repository](https://github.com/helmuthdu/vielzeug).
846
+
847
+ ## 🔗 Links
734
848
 
735
849
  - [GitHub Repository](https://github.com/helmuthdu/vielzeug)
736
- - [Documentation](https://vielzeug.dev)
737
- - [NPM Package](https://www.npmjs.com/package/@vielzeug/i18nit)
850
+ - [Documentation](https://helmuthdu.github.io/vielzeug/deposit)
851
+ - [NPM Package](https://www.npmjs.com/package/@vielzeug/deposit)
738
852
  - [Issue Tracker](https://github.com/helmuthdu/vielzeug/issues)
739
853
 
740
854
  ---
741
855
 
742
- Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem - A collection of type-safe utilities for modern web development.
856
+ Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem A collection of type-safe utilities for modern web development.
package/dist/i18nit.cjs CHANGED
@@ -1,2 +1,2 @@
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;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function g(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 e=t.match(/[^.[\]]+/g)||[];let r=a;for(const s of e){if(r==null||typeof r!="object")return;if(Array.isArray(r)){const n=Number(s);if(Number.isNaN(n)||n<0||n>=r.length)return;r=r[n]}else r=r[s]}return r}function l(a,t,e){if(a.length===0)return"";const r=a.map(String);try{return new Intl.ListFormat(t,{style:"long",type:e}).format(r)}catch{if(r.length===1)return r[0];if(r.length===2){const o=e==="conjunction"?"and":"or";return`${r[0]} ${o} ${r[1]}`}const s=e==="conjunction"?"and":"or",n=r.pop();return`${r.join(", ")} ${s} ${n}`}}function u(a,t,e){return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g,(r,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,e,"conjunction"):n==="or"?l(i,e,"disjunction"):i.map(String).join(n):i.map(String).join(", ");if(typeof i=="number")try{return new Intl.NumberFormat(e).format(i)}catch{return String(i)}return String(i)})}function d(a,t){const e=Math.abs(Math.floor(t));try{return new Intl.PluralRules(a).select(e)}catch{return e===1?"one":"other"}}class f{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[e,r]of Object.entries(t.messages))this.catalogs.set(e,r);if(t.loaders)for(const[e,r]of Object.entries(t.loaders))this.loaders.set(e,r)}setLocale(t){this.locale!==t&&(this.locale=t,this.notifySubscribers())}getLocale(){return this.locale}add(t,e){const r=this.catalogs.get(t)??{};this.catalogs.set(t,{...r,...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 r=(async()=>{try{const s=await e(t);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,r),r}register(t,e){this.loaders.set(t,e)}async hasAsync(t,e){const r=e??this.locale;return!this.catalogs.has(r)&&this.loaders.has(r)&&await this.load(r),this.has(t,r)}async loadAll(t){await Promise.all(t.map(e=>this.load(e)))}t(t,e,r){const s=r?.locale??this.locale,n=r?.escape??this.escape,o=this.findMessage(t,s);if(o===void 0)return t;const c=this.formatMessage(o,e??{},s);return n?g(c):c}number(t,e,r){try{return new Intl.NumberFormat(r??this.locale,e).format(t)}catch{return String(t)}}date(t,e,r){const s=typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(r??this.locale,e).format(s)}catch{return s.toString()}}namespace(t){return{t:(e,r,s)=>this.t(`${t}.${e}`,r,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,e){const r=this.getLocaleChain(e);for(const s of r){const n=this.catalogs.get(s);if(!n)continue;const o=h(n,t);if(o!==void 0&&this.isMessageValue(o))return o}}isMessageValue(t){if(typeof t=="string")return!0;if(typeof t=="object"&&t!==null&&!Array.isArray(t)){const e=t;return"other"in e&&typeof e.other=="string"?Object.values(e).every(r=>typeof r=="string"):!1}return!1}getLocaleChain(t){const e=[t],r=t.split("-")[0];r!==t&&e.push(r);for(const s of this.fallbacks){e.push(s);const n=s.split("-")[0];n!==s&&e.push(n)}return e}formatMessage(t,e,r){if(typeof t=="object"&&"other"in t){const s=Number(e.count??0),n=t;let o;s===0&&n.zero!==void 0?o="zero":o=d(r,s);const c=n[o]??n.other;return u(c,e,r)}return typeof t=="string"?u(t,e,r):""}}function b(a){return new f(a)}exports.I18n=f;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 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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"}
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\n// Support nested message objects with dot notation access\nexport type Messages = {\n [key: string]: MessageValue | Messages;\n};\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, (locale: 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 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\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\nexport class 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, (locale: 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(locale);\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: (locale: Locale) => 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\n // If the value is a nested Messages object (not a MessageValue), return undefined\n // This ensures we only return actual translatable strings, not object containers\n if (value !== undefined && this.isMessageValue(value)) {\n return value as MessageValue;\n }\n }\n\n return undefined;\n }\n\n /**\n * Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object\n */\n private isMessageValue(value: unknown): boolean {\n if (typeof value === 'string') return true;\n\n // Check if it's a PluralMessages object (has 'other' key and all values are strings)\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const obj = value as Record<string, unknown>;\n\n // If it has 'other' key and values are strings, it's a PluralMessages\n if ('other' in obj && typeof obj.other === 'string') {\n return Object.values(obj).every((v) => typeof v === 'string');\n }\n\n // Otherwise it's a nested Messages object\n return false;\n }\n\n return false;\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","v","chain","lang","fallback","fallbackLang","pluralMsg","form","createI18n"],"mappings":"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"}
package/dist/i18nit.js CHANGED
@@ -3,44 +3,44 @@ function f(a) {
3
3
  }
4
4
  function h(a, t) {
5
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)) {
6
+ const e = t.match(/[^.[\]]+/g) || [];
7
+ let r = a;
8
+ for (const s of e) {
9
+ if (r == null || typeof r != "object") return;
10
+ if (Array.isArray(r)) {
11
11
  const n = Number(s);
12
- if (Number.isNaN(n) || n < 0 || n >= e.length)
12
+ if (Number.isNaN(n) || n < 0 || n >= r.length)
13
13
  return;
14
- e = e[n];
14
+ r = r[n];
15
15
  } else
16
- e = e[s];
16
+ r = r[s];
17
17
  }
18
- return e;
18
+ return r;
19
19
  }
20
- function l(a, t, r) {
20
+ function l(a, t, e) {
21
21
  if (a.length === 0) return "";
22
- const e = a.map(String);
22
+ const r = a.map(String);
23
23
  try {
24
- return new Intl.ListFormat(t, { style: "long", type: r }).format(e);
24
+ return new Intl.ListFormat(t, { style: "long", type: e }).format(r);
25
25
  } catch {
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]}`;
26
+ if (r.length === 1) return r[0];
27
+ if (r.length === 2) {
28
+ const o = e === "conjunction" ? "and" : "or";
29
+ return `${r[0]} ${o} ${r[1]}`;
30
30
  }
31
- const s = r === "conjunction" ? "and" : "or", n = e.pop();
32
- return `${e.join(", ")} ${s} ${n}`;
31
+ const s = e === "conjunction" ? "and" : "or", n = r.pop();
32
+ return `${r.join(", ")} ${s} ${n}`;
33
33
  }
34
34
  }
35
- function u(a, t, r) {
36
- return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g, (e, s, n) => {
35
+ function u(a, t, e) {
36
+ return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g, (r, s, n) => {
37
37
  const o = s.endsWith(".length"), c = o ? s.slice(0, -7) : s, i = h(t, c);
38
38
  if (i == null) return "";
39
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(", ");
40
+ return o ? String(i.length) : n !== void 0 ? n === "and" ? l(i, e, "conjunction") : n === "or" ? l(i, e, "disjunction") : i.map(String).join(n) : i.map(String).join(", ");
41
41
  if (typeof i == "number")
42
42
  try {
43
- return new Intl.NumberFormat(r).format(i);
43
+ return new Intl.NumberFormat(e).format(i);
44
44
  } catch {
45
45
  return String(i);
46
46
  }
@@ -48,11 +48,11 @@ function u(a, t, r) {
48
48
  });
49
49
  }
50
50
  function g(a, t) {
51
- const r = Math.abs(Math.floor(t));
51
+ const e = Math.abs(Math.floor(t));
52
52
  try {
53
- return new Intl.PluralRules(a).select(r);
53
+ return new Intl.PluralRules(a).select(e);
54
54
  } catch {
55
- return r === 1 ? "one" : "other";
55
+ return e === 1 ? "one" : "other";
56
56
  }
57
57
  }
58
58
  class d {
@@ -65,11 +65,11 @@ class d {
65
65
  subscribers = /* @__PURE__ */ new Set();
66
66
  constructor(t = {}) {
67
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);
68
+ for (const [e, r] of Object.entries(t.messages))
69
+ this.catalogs.set(e, r);
70
70
  if (t.loaders)
71
- for (const [r, e] of Object.entries(t.loaders))
72
- this.loaders.set(r, e);
71
+ for (const [e, r] of Object.entries(t.loaders))
72
+ this.loaders.set(e, r);
73
73
  }
74
74
  /* -------------------- Locale Management -------------------- */
75
75
  setLocale(t) {
@@ -82,15 +82,15 @@ class d {
82
82
  /**
83
83
  * Adds messages to a locale (merges with existing).
84
84
  */
85
- add(t, r) {
86
- const e = this.catalogs.get(t) ?? {};
87
- this.catalogs.set(t, { ...e, ...r }), this.notifySubscribers();
85
+ add(t, e) {
86
+ const r = this.catalogs.get(t) ?? {};
87
+ this.catalogs.set(t, { ...r, ...e }), this.notifySubscribers();
88
88
  }
89
89
  /**
90
90
  * Sets messages for a locale (replaces existing).
91
91
  */
92
- set(t, r) {
93
- this.catalogs.set(t, r), this.notifySubscribers();
92
+ set(t, e) {
93
+ this.catalogs.set(t, e), this.notifySubscribers();
94
94
  }
95
95
  getMessages(t) {
96
96
  return this.catalogs.get(t);
@@ -98,18 +98,18 @@ class d {
98
98
  hasLocale(t) {
99
99
  return this.catalogs.has(t);
100
100
  }
101
- has(t, r) {
102
- return this.findMessage(t, r ?? this.locale) !== void 0;
101
+ has(t, e) {
102
+ return this.findMessage(t, e ?? this.locale) !== void 0;
103
103
  }
104
104
  /* -------------------- Async Loaders -------------------- */
105
105
  async load(t) {
106
106
  if (this.loading.has(t)) return this.loading.get(t);
107
107
  if (this.catalogs.has(t)) return;
108
- const r = this.loaders.get(t);
109
- if (!r) return;
110
- const e = (async () => {
108
+ const e = this.loaders.get(t);
109
+ if (!e) return;
110
+ const r = (async () => {
111
111
  try {
112
- const s = await r();
112
+ const s = await e(t);
113
113
  this.add(t, s);
114
114
  } catch (s) {
115
115
  throw console.warn(`[I18n] Failed to load locale '${t}':`, s), s;
@@ -117,45 +117,45 @@ class d {
117
117
  this.loading.delete(t);
118
118
  }
119
119
  })();
120
- return this.loading.set(t, e), e;
120
+ return this.loading.set(t, r), r;
121
121
  }
122
- register(t, r) {
123
- this.loaders.set(t, r);
122
+ register(t, e) {
123
+ this.loaders.set(t, e);
124
124
  }
125
- async hasAsync(t, r) {
126
- const e = r ?? this.locale;
127
- return !this.catalogs.has(e) && this.loaders.has(e) && await this.load(e), this.has(t, e);
125
+ async hasAsync(t, e) {
126
+ const r = e ?? this.locale;
127
+ return !this.catalogs.has(r) && this.loaders.has(r) && await this.load(r), this.has(t, r);
128
128
  }
129
129
  /**
130
130
  * Load multiple locales in parallel.
131
131
  * Useful for preloading all needed locales at app startup.
132
132
  */
133
133
  async loadAll(t) {
134
- await Promise.all(t.map((r) => this.load(r)));
134
+ await Promise.all(t.map((e) => this.load(e)));
135
135
  }
136
136
  /* -------------------- Translation -------------------- */
137
137
  /**
138
138
  * Translates a key with optional variables and options.
139
139
  * Synchronous - locale must be loaded first via load() or provided in config.
140
140
  */
141
- t(t, r, e) {
142
- const s = e?.locale ?? this.locale, n = e?.escape ?? this.escape, o = this.findMessage(t, s);
141
+ t(t, e, r) {
142
+ const s = r?.locale ?? this.locale, n = r?.escape ?? this.escape, o = this.findMessage(t, s);
143
143
  if (o === void 0) return t;
144
- const c = this.formatMessage(o, r ?? {}, s);
144
+ const c = this.formatMessage(o, e ?? {}, s);
145
145
  return n ? f(c) : c;
146
146
  }
147
147
  /* -------------------- Formatting Helpers -------------------- */
148
- number(t, r, e) {
148
+ number(t, e, r) {
149
149
  try {
150
- return new Intl.NumberFormat(e ?? this.locale, r).format(t);
150
+ return new Intl.NumberFormat(r ?? this.locale, e).format(t);
151
151
  } catch {
152
152
  return String(t);
153
153
  }
154
154
  }
155
- date(t, r, e) {
155
+ date(t, e, r) {
156
156
  const s = typeof t == "number" ? new Date(t) : t;
157
157
  try {
158
- return new Intl.DateTimeFormat(e ?? this.locale, r).format(s);
158
+ return new Intl.DateTimeFormat(r ?? this.locale, e).format(s);
159
159
  } catch {
160
160
  return s.toString();
161
161
  }
@@ -163,7 +163,7 @@ class d {
163
163
  /* -------------------- Namespaced Translator -------------------- */
164
164
  namespace(t) {
165
165
  return {
166
- t: (r, e, s) => this.t(`${t}.${r}`, e, s)
166
+ t: (e, r, s) => this.t(`${t}.${e}`, r, s)
167
167
  };
168
168
  }
169
169
  /* -------------------- Subscriptions -------------------- */
@@ -183,40 +183,53 @@ class d {
183
183
  }
184
184
  }
185
185
  /* -------------------- Internal Helpers -------------------- */
186
- findMessage(t, r) {
187
- const e = this.getLocaleChain(r);
188
- for (const s of e) {
186
+ findMessage(t, e) {
187
+ const r = this.getLocaleChain(e);
188
+ for (const s of r) {
189
189
  const n = this.catalogs.get(s);
190
190
  if (!n) continue;
191
191
  const o = h(n, t);
192
- if (o !== void 0) return o;
192
+ if (o !== void 0 && this.isMessageValue(o))
193
+ return o;
193
194
  }
194
195
  }
196
+ /**
197
+ * Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object
198
+ */
199
+ isMessageValue(t) {
200
+ if (typeof t == "string") return !0;
201
+ if (typeof t == "object" && t !== null && !Array.isArray(t)) {
202
+ const e = t;
203
+ return "other" in e && typeof e.other == "string" ? Object.values(e).every((r) => typeof r == "string") : !1;
204
+ }
205
+ return !1;
206
+ }
195
207
  getLocaleChain(t) {
196
- const r = [t], e = t.split("-")[0];
197
- e !== t && r.push(e);
208
+ const e = [t], r = t.split("-")[0];
209
+ r !== t && e.push(r);
198
210
  for (const s of this.fallbacks) {
199
- r.push(s);
211
+ e.push(s);
200
212
  const n = s.split("-")[0];
201
- n !== s && r.push(n);
213
+ n !== s && e.push(n);
202
214
  }
203
- return r;
215
+ return e;
204
216
  }
205
- formatMessage(t, r, e) {
217
+ formatMessage(t, e, r) {
206
218
  if (typeof t == "object" && "other" in t) {
207
- const s = Number(r.count ?? 0), n = t;
219
+ const s = Number(e.count ?? 0), n = t;
208
220
  let o;
209
- s === 0 && n.zero !== void 0 ? o = "zero" : o = g(e, s);
221
+ s === 0 && n.zero !== void 0 ? o = "zero" : o = g(r, s);
210
222
  const c = n[o] ?? n.other;
211
- return u(c, r, e);
223
+ return u(c, e, r);
212
224
  }
213
- return typeof t == "string" ? u(t, r, e) : "";
225
+ return typeof t == "string" ? u(t, e, r) : "";
214
226
  }
215
227
  }
216
228
  function b(a) {
217
229
  return new d(a);
218
230
  }
219
231
  export {
232
+ d as I18n,
220
233
  b as createI18n
221
234
  };
222
235
  //# 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 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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"}
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\n// Support nested message objects with dot notation access\nexport type Messages = {\n [key: string]: MessageValue | Messages;\n};\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, (locale: 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 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: -\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\nexport class 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, (locale: 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(locale);\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: (locale: Locale) => 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\n // If the value is a nested Messages object (not a MessageValue), return undefined\n // This ensures we only return actual translatable strings, not object containers\n if (value !== undefined && this.isMessageValue(value)) {\n return value as MessageValue;\n }\n }\n\n return undefined;\n }\n\n /**\n * Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object\n */\n private isMessageValue(value: unknown): boolean {\n if (typeof value === 'string') return true;\n\n // Check if it's a PluralMessages object (has 'other' key and all values are strings)\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const obj = value as Record<string, unknown>;\n\n // If it has 'other' key and values are strings, it's a PluralMessages\n if ('other' in obj && typeof obj.other === 'string') {\n return Object.values(obj).every((v) => typeof v === 'string');\n }\n\n // Otherwise it's a nested Messages object\n return false;\n }\n\n return false;\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","v","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 e=require("./i18nit.cjs");exports.createI18n=e.createI18n;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./i18nit.cjs");exports.I18n=e.I18n;exports.createI18n=e.createI18n;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export declare function createI18n(config?: I18nConfig): I18n;
2
2
 
3
- declare class I18n {
3
+ export declare class I18n {
4
4
  private locale;
5
5
  private fallbacks;
6
6
  private escape;
@@ -23,7 +23,7 @@ declare class I18n {
23
23
  hasLocale(locale: Locale): boolean;
24
24
  has(key: string, locale?: Locale): boolean;
25
25
  load(locale: Locale): Promise<void>;
26
- register(locale: Locale, loader: () => Promise<Messages>): void;
26
+ register(locale: Locale, loader: (locale: Locale) => Promise<Messages>): void;
27
27
  hasAsync(key: string, locale?: Locale): Promise<boolean>;
28
28
  /**
29
29
  * Load multiple locales in parallel.
@@ -43,6 +43,10 @@ declare class I18n {
43
43
  subscribe(handler: (locale: Locale) => void): () => void;
44
44
  private notifySubscribers;
45
45
  private findMessage;
46
+ /**
47
+ * Check if a value is a MessageValue (string or PluralMessages) rather than a nested Messages object
48
+ */
49
+ private isMessageValue;
46
50
  private getLocaleChain;
47
51
  private formatMessage;
48
52
  }
@@ -51,13 +55,15 @@ export declare type I18nConfig = {
51
55
  locale?: Locale;
52
56
  fallback?: Locale | Locale[];
53
57
  messages?: Record<Locale, Messages>;
54
- loaders?: Record<Locale, () => Promise<Messages>>;
58
+ loaders?: Record<Locale, (locale: Locale) => Promise<Messages>>;
55
59
  escape?: boolean;
56
60
  };
57
61
 
58
62
  export declare type Locale = string;
59
63
 
60
- export declare type Messages = Record<string, MessageValue>;
64
+ export declare type Messages = {
65
+ [key: string]: MessageValue | Messages;
66
+ };
61
67
 
62
68
  export declare type MessageValue = string | PluralMessages;
63
69
 
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { createI18n as o } from "./i18nit.js";
1
+ import { I18n as n, createI18n as o } from "./i18nit.js";
2
2
  export {
3
+ n as I18n,
3
4
  o as createI18n
4
5
  };
5
6
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/i18nit",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"