@vielzeug/i18nit 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 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 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\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
@@ -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
- }
8
- }
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]);
1
+ function f(a) {
2
+ return a.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18
3
  }
19
- function m(i, t) {
20
- if (t in i) return i[t];
4
+ function h(a, t) {
5
+ if (t in a) return a[t];
21
6
  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)
7
+ let r = a;
8
+ for (const s of e) {
9
+ if (r == null || typeof r != "object") return;
10
+ if (Array.isArray(r)) {
11
+ const n = Number(s);
12
+ if (Number.isNaN(n) || n < 0 || n >= r.length)
28
13
  return;
29
- s = s[n];
14
+ r = r[n];
30
15
  } else
31
- s = s[r];
16
+ r = r[s];
32
17
  }
33
- return s;
18
+ return r;
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, e) {
21
+ if (a.length === 0) return "";
22
+ const r = 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: e }).format(r);
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 (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]}`;
45
30
  }
46
- const r = e === "conjunction" ? "and" : "or", n = s.pop();
47
- return `${s.join(", ")} ${r} ${n}`;
31
+ const s = e === "conjunction" ? "and" : "or", n = r.pop();
32
+ return `${r.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, e) {
36
+ return a.replace(/\{([\w.[\]]+)(?:\|([^}]+))?\}/g, (r, 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, e, "conjunction") : n === "or" ? l(i, e, "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(e).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) {
50
+ function g(a, t) {
79
51
  const e = Math.abs(Math.floor(t));
80
52
  try {
81
- return new Intl.PluralRules(i).select(e);
53
+ return new Intl.PluralRules(a).select(e);
82
54
  } catch {
83
55
  return e === 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 [e, r] of Object.entries(t.messages))
69
+ this.catalogs.set(e, r);
100
70
  if (t.loaders)
101
- for (const [e, s] of Object.entries(t.loaders))
102
- this.loaders.set(e, s);
71
+ for (const [e, r] of Object.entries(t.loaders))
72
+ this.loaders.set(e, r);
103
73
  }
104
74
  /* -------------------- Locale Management -------------------- */
105
75
  setLocale(t) {
@@ -113,8 +83,8 @@ class p {
113
83
  * Adds messages to a locale (merges with existing).
114
84
  */
115
85
  add(t, e) {
116
- const s = this.catalogs.get(t) ?? {};
117
- this.catalogs.set(t, { ...s, ...e }), this.notifySubscribers();
86
+ const r = this.catalogs.get(t) ?? {};
87
+ this.catalogs.set(t, { ...r, ...e }), this.notifySubscribers();
118
88
  }
119
89
  /**
120
90
  * Sets messages for a locale (replaces existing).
@@ -137,66 +107,63 @@ class p {
137
107
  if (this.catalogs.has(t)) return;
138
108
  const e = this.loaders.get(t);
139
109
  if (!e) return;
140
- const s = (async () => {
110
+ const r = (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 e(t);
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, r), r;
151
121
  }
152
122
  register(t, e) {
153
123
  this.loaders.set(t, e);
154
124
  }
155
125
  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);
126
+ const r = e ?? this.locale;
127
+ return !this.catalogs.has(r) && this.loaders.has(r) && await this.load(r), this.has(t, r);
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((e) => this.load(e)));
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, e, r) {
142
+ const s = r?.locale ?? this.locale, n = r?.escape ?? this.escape, o = this.findMessage(t, s);
143
+ if (o === void 0) return t;
144
+ const c = this.formatMessage(o, e ?? {}, s);
145
+ return n ? f(c) : c;
178
146
  }
179
147
  /* -------------------- Formatting Helpers -------------------- */
180
- number(t, e, s) {
148
+ number(t, e, r) {
181
149
  try {
182
- return new Intl.NumberFormat(s ?? this.locale, e).format(t);
150
+ return new Intl.NumberFormat(r ?? this.locale, e).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, e, r) {
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(r ?? this.locale, e).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: (e, r, s) => this.t(`${t}.${e}`, r, s)
200
167
  };
201
168
  }
202
169
  /* -------------------- Subscriptions -------------------- */
@@ -217,55 +184,52 @@ class p {
217
184
  }
218
185
  /* -------------------- Internal Helpers -------------------- */
219
186
  findMessage(t, e) {
220
- const s = this.getLocaleChain(e);
221
- for (const r of s) {
222
- const n = this.catalogs.get(r);
187
+ const r = this.getLocaleChain(e);
188
+ for (const s of r) {
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 && this.isMessageValue(o))
193
+ return o;
226
194
  }
227
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
+ }
228
207
  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);
208
+ const e = [t], r = t.split("-")[0];
209
+ r !== t && e.push(r);
210
+ for (const s of this.fallbacks) {
211
+ e.push(s);
212
+ const n = s.split("-")[0];
213
+ n !== s && e.push(n);
235
214
  }
236
215
  return e;
237
216
  }
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
- }
217
+ formatMessage(t, e, r) {
250
218
  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;
219
+ const s = Number(e.count ?? 0), n = t;
220
+ let o;
221
+ s === 0 && n.zero !== void 0 ? o = "zero" : o = g(r, s);
222
+ const c = n[o] ?? n.other;
223
+ return u(c, e, r);
260
224
  }
261
- return "";
225
+ return typeof t == "string" ? u(t, e, r) : "";
262
226
  }
263
227
  }
264
- function w(i) {
265
- return new p(i);
228
+ function b(a) {
229
+ return new d(a);
266
230
  }
267
231
  export {
268
- d as MissingVariableError,
269
- w as createI18n
232
+ d as I18n,
233
+ b as createI18n
270
234
  };
271
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 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\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":"AAkCA,SAASA,EAAWC,GAAqB;AACvC,SAAOA,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAMA,SAASC,EAAYC,GAA8BC,GAAuB;AAExE,MAAIA,KAAQD,EAAK,QAAOA,EAAIC,CAAI;AAGhC,QAAMC,IAAQD,EAAK,MAAM,WAAW,KAAK,CAAA;AACzC,MAAIE,IAAiBH;AAErB,aAAWI,KAAQF,GAAO;AACxB,QAAIC,KAAS,QAAQ,OAAOA,KAAU,SAAU;AAEhD,QAAI,MAAM,QAAQA,CAAK,GAAG;AACxB,YAAME,IAAQ,OAAOD,CAAI;AACzB,UAAI,OAAO,MAAMC,CAAK,KAAKA,IAAQ,KAAKA,KAASF,EAAM;AACrD;AAEF,MAAAA,IAAQA,EAAME,CAAK;AAAA,IACrB;AACE,MAAAF,IAASA,EAAkCC,CAAI;AAAA,EAEnD;AAEA,SAAOD;AACT;AAQA,SAASG,EAAWC,GAAkBC,GAAgBC,GAA6C;AACjG,MAAIF,EAAM,WAAW,EAAG,QAAO;AAE/B,QAAMG,IAAcH,EAAM,IAAI,MAAM;AAEpC,MAAI;AAEF,WADkB,IAAI,KAAK,WAAWC,GAAQ,EAAE,OAAO,QAAQ,MAAAC,GAAM,EACpD,OAAOC,CAAW;AAAA,EACrC,QAAQ;AAEN,QAAIA,EAAY,WAAW,EAAG,QAAOA,EAAY,CAAC;AAClD,QAAIA,EAAY,WAAW,GAAG;AAC5B,YAAMC,IAAYF,MAAS,gBAAgB,QAAQ;AACnD,aAAO,GAAGC,EAAY,CAAC,CAAC,IAAIC,CAAS,IAAID,EAAY,CAAC,CAAC;AAAA,IACzD;AACA,UAAMC,IAAYF,MAAS,gBAAgB,QAAQ,MAC7CG,IAAOF,EAAY,IAAA;AACzB,WAAO,GAAGA,EAAY,KAAK,IAAI,CAAC,IAAIC,CAAS,IAAIC,CAAI;AAAA,EACvD;AACF;AAiBA,SAASC,EAAYC,GAAkBC,GAA+BP,GAAwB;AAE5F,SAAOM,EAAS,QAAQ,kCAAkC,CAACE,GAAQC,GAAaN,MAAuB;AAErG,UAAMO,IAAWD,EAAI,SAAS,SAAS,GACjCE,IAAYD,IAAWD,EAAI,MAAM,GAAG,EAAE,IAAIA,GAE1Cd,IAAQJ,EAAYgB,GAAMI,CAAS;AAGzC,QAAIhB,KAAS,KAAM,QAAO;AAG1B,QAAI,MAAM,QAAQA,CAAK;AACrB,aAAIe,IAAiB,OAAOf,EAAM,MAAM,IAEpCQ,MAAc,SACZA,MAAc,QAAcL,EAAWH,GAAOK,GAAQ,aAAa,IACnEG,MAAc,OAAaL,EAAWH,GAAOK,GAAQ,aAAa,IAC/DL,EAAM,IAAI,MAAM,EAAE,KAAKQ,CAAS,IAGlCR,EAAM,IAAI,MAAM,EAAE,KAAK,IAAI;AAIpC,QAAI,OAAOA,KAAU;AACnB,UAAI;AACF,eAAO,IAAI,KAAK,aAAaK,CAAM,EAAE,OAAOL,CAAK;AAAA,MACnD,QAAQ;AACN,eAAO,OAAOA,CAAK;AAAA,MACrB;AAGF,WAAO,OAAOA,CAAK;AAAA,EACrB,CAAC;AACH;AAQA,SAASiB,EAAcZ,GAAgBa,GAA2B;AAChE,QAAMC,IAAI,KAAK,IAAI,KAAK,MAAMD,CAAK,CAAC;AAEpC,MAAI;AAEF,WADc,IAAI,KAAK,YAAYb,CAAM,EAC5B,OAAOc,CAAC;AAAA,EACvB,QAAQ;AAEN,WAAOA,MAAM,IAAI,QAAQ;AAAA,EAC3B;AACF;AAIO,MAAMC,EAAK;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,+BAAe,IAAA;AAAA,EACf,8BAAc,IAAA;AAAA,EACd,8BAAc,IAAA;AAAA,EACd,kCAAkB,IAAA;AAAA,EAE1B,YAAYC,IAAqB,IAAI;AAKnC,QAJA,KAAK,SAASA,EAAO,UAAU,MAC/B,KAAK,YAAY,MAAM,QAAQA,EAAO,QAAQ,IAAIA,EAAO,WAAWA,EAAO,WAAW,CAACA,EAAO,QAAQ,IAAI,CAAA,GAC1G,KAAK,SAASA,EAAO,UAAU,IAE3BA,EAAO;AACT,iBAAW,CAAChB,GAAQiB,CAAQ,KAAK,OAAO,QAAQD,EAAO,QAAQ;AAC7D,aAAK,SAAS,IAAIhB,GAAQiB,CAAQ;AAItC,QAAID,EAAO;AACT,iBAAW,CAAChB,GAAQkB,CAAM,KAAK,OAAO,QAAQF,EAAO,OAAO;AAC1D,aAAK,QAAQ,IAAIhB,GAAQkB,CAAM;AAAA,EAGrC;AAAA;AAAA,EAIA,UAAUlB,GAAsB;AAC9B,IAAI,KAAK,WAAWA,MACpB,KAAK,SAASA,GACd,KAAK,kBAAA;AAAA,EACP;AAAA,EAEA,YAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAIA,GAAgBiB,GAA0B;AAC5C,UAAME,IAAW,KAAK,SAAS,IAAInB,CAAM,KAAK,CAAA;AAC9C,SAAK,SAAS,IAAIA,GAAQ,EAAE,GAAGmB,GAAU,GAAGF,GAAU,GACtD,KAAK,kBAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA,EAKA,IAAIjB,GAAgBiB,GAA0B;AAC5C,SAAK,SAAS,IAAIjB,GAAQiB,CAAQ,GAClC,KAAK,kBAAA;AAAA,EACP;AAAA,EAEA,YAAYjB,GAAsC;AAChD,WAAO,KAAK,SAAS,IAAIA,CAAM;AAAA,EACjC;AAAA,EAEA,UAAUA,GAAyB;AACjC,WAAO,KAAK,SAAS,IAAIA,CAAM;AAAA,EACjC;AAAA,EAEA,IAAIS,GAAaT,GAA0B;AACzC,WAAO,KAAK,YAAYS,GAAKT,KAAU,KAAK,MAAM,MAAM;AAAA,EAC1D;AAAA;AAAA,EAIA,MAAM,KAAKA,GAA+B;AAExC,QAAI,KAAK,QAAQ,IAAIA,CAAM,EAAG,QAAO,KAAK,QAAQ,IAAIA,CAAM;AAG5D,QAAI,KAAK,SAAS,IAAIA,CAAM,EAAG;AAE/B,UAAMkB,IAAS,KAAK,QAAQ,IAAIlB,CAAM;AACtC,QAAI,CAACkB,EAAQ;AAEb,UAAME,KAAW,YAAY;AAC3B,UAAI;AACF,cAAMH,IAAW,MAAMC,EAAOlB,CAAM;AACpC,aAAK,IAAIA,GAAQiB,CAAQ;AAAA,MAC3B,SAASI,GAAO;AACd,sBAAQ,KAAK,iCAAiCrB,CAAM,MAAMqB,CAAK,GACzDA;AAAA,MACR,UAAA;AACE,aAAK,QAAQ,OAAOrB,CAAM;AAAA,MAC5B;AAAA,IACF,GAAA;AAEA,gBAAK,QAAQ,IAAIA,GAAQoB,CAAO,GACzBA;AAAA,EACT;AAAA,EAEA,SAASpB,GAAgBkB,GAAqD;AAC5E,SAAK,QAAQ,IAAIlB,GAAQkB,CAAM;AAAA,EACjC;AAAA,EAEA,MAAM,SAAST,GAAaT,GAAmC;AAC7D,UAAMsB,IAAetB,KAAU,KAAK;AAEpC,WAAI,CAAC,KAAK,SAAS,IAAIsB,CAAY,KAAK,KAAK,QAAQ,IAAIA,CAAY,KACnE,MAAM,KAAK,KAAKA,CAAY,GAGvB,KAAK,IAAIb,GAAKa,CAAY;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAQC,GAAkC;AAC9C,UAAM,QAAQ,IAAIA,EAAQ,IAAI,CAACvB,MAAW,KAAK,KAAKA,CAAM,CAAC,CAAC;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,EAAES,GAAaF,GAAgCiB,GAAoC;AACjF,UAAMF,IAAeE,GAAS,UAAU,KAAK,QACvCC,IAAeD,GAAS,UAAU,KAAK,QAEvCE,IAAU,KAAK,YAAYjB,GAAKa,CAAY;AAClD,QAAII,MAAY,OAAW,QAAOjB;AAElC,UAAMkB,IAAS,KAAK,cAAcD,GAASnB,KAAQ,CAAA,GAAIe,CAAY;AACnE,WAAOG,IAAepC,EAAWsC,CAAM,IAAIA;AAAA,EAC7C;AAAA;AAAA,EAIA,OAAOhC,GAAe6B,GAAoCxB,GAAyB;AACjF,QAAI;AACF,aAAO,IAAI,KAAK,aAAaA,KAAU,KAAK,QAAQwB,CAAO,EAAE,OAAO7B,CAAK;AAAA,IAC3E,QAAQ;AACN,aAAO,OAAOA,CAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,KAAKA,GAAsB6B,GAAsCxB,GAAyB;AACxF,UAAM4B,IAAO,OAAOjC,KAAU,WAAW,IAAI,KAAKA,CAAK,IAAIA;AAC3D,QAAI;AACF,aAAO,IAAI,KAAK,eAAeK,KAAU,KAAK,QAAQwB,CAAO,EAAE,OAAOI,CAAI;AAAA,IAC5E,QAAQ;AACN,aAAOA,EAAK,SAAA;AAAA,IACd;AAAA,EACF;AAAA;AAAA,EAIA,UAAUC,GAAY;AACpB,WAAO;AAAA,MACL,GAAG,CAACpB,GAAaF,GAAgCiB,MAC/C,KAAK,EAAE,GAAGK,CAAE,IAAIpB,CAAG,IAAIF,GAAMiB,CAAO;AAAA,IAAA;AAAA,EAE1C;AAAA;AAAA,EAIA,UAAUM,GAA+C;AACvD,SAAK,YAAY,IAAIA,CAAO;AAG5B,QAAI;AACF,MAAAA,EAAQ,KAAK,MAAM;AAAA,IACrB,QAAQ;AAAA,IAER;AAEA,WAAO,MAAM,KAAK,YAAY,OAAOA,CAAO;AAAA,EAC9C;AAAA,EAEQ,oBAA0B;AAChC,eAAWA,KAAW,KAAK;AACzB,UAAI;AACF,QAAAA,EAAQ,KAAK,MAAM;AAAA,MACrB,QAAQ;AAAA,MAER;AAAA,EAEJ;AAAA;AAAA,EAIQ,YAAYrB,GAAaT,GAA0C;AACzE,UAAMuB,IAAU,KAAK,eAAevB,CAAM;AAE1C,eAAW+B,KAAOR,GAAS;AACzB,YAAMN,IAAW,KAAK,SAAS,IAAIc,CAAG;AACtC,UAAI,CAACd,EAAU;AAEf,YAAMtB,IAAQJ,EAAY0B,GAAUR,CAAG;AAIvC,UAAId,MAAU,UAAa,KAAK,eAAeA,CAAK;AAClD,eAAOA;AAAA,IAEX;AAAA,EAGF;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAeA,GAAyB;AAC9C,QAAI,OAAOA,KAAU,SAAU,QAAO;AAGtC,QAAI,OAAOA,KAAU,YAAYA,MAAU,QAAQ,CAAC,MAAM,QAAQA,CAAK,GAAG;AACxE,YAAMH,IAAMG;AAGZ,aAAI,WAAWH,KAAO,OAAOA,EAAI,SAAU,WAClC,OAAO,OAAOA,CAAG,EAAE,MAAM,CAACwC,MAAM,OAAOA,KAAM,QAAQ,IAIvD;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAehC,GAA0B;AAC/C,UAAMiC,IAAkB,CAACjC,CAAM,GAGzBkC,IAAOlC,EAAO,MAAM,GAAG,EAAE,CAAC;AAChC,IAAIkC,MAASlC,KAAQiC,EAAM,KAAKC,CAAI;AAGpC,eAAWC,KAAY,KAAK,WAAW;AACrC,MAAAF,EAAM,KAAKE,CAAQ;AACnB,YAAMC,IAAeD,EAAS,MAAM,GAAG,EAAE,CAAC;AAC1C,MAAIC,MAAiBD,KAAUF,EAAM,KAAKG,CAAY;AAAA,IACxD;AAEA,WAAOH;AAAA,EACT;AAAA,EAEQ,cAAcP,GAAuBnB,GAA+BP,GAAwB;AAElG,QAAI,OAAO0B,KAAY,YAAY,WAAWA,GAAS;AACrD,YAAMb,IAAQ,OAAON,EAAK,SAAS,CAAC,GAC9B8B,IAAYX;AAGlB,UAAIY;AACJ,MAAIzB,MAAU,KAAKwB,EAAU,SAAS,SACpCC,IAAO,SAEPA,IAAO1B,EAAcZ,GAAQa,CAAK;AAGpC,YAAMP,IAAW+B,EAAUC,CAAI,KAAKD,EAAU;AAC9C,aAAOhC,EAAYC,GAAUC,GAAMP,CAAM;AAAA,IAC3C;AAGA,WAAI,OAAO0B,KAAY,WACdrB,EAAYqB,GAASnB,GAAMP,CAAM,IAGnC;AAAA,EACT;AACF;AAIO,SAASuC,EAAWvB,GAA2B;AACpD,SAAO,IAAID,EAAKC,CAAM;AACxB;"}
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.I18n=e.I18n;exports.createI18n=e.createI18n;
2
2
  //# sourceMappingURL=index.cjs.map