@pyreon/i18n 0.11.4 → 0.11.6

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
@@ -11,29 +11,29 @@ bun add @pyreon/i18n
11
11
  ## Quick Start
12
12
 
13
13
  ```ts
14
- import { createI18n } from "@pyreon/i18n"
14
+ import { createI18n } from '@pyreon/i18n'
15
15
 
16
16
  const i18n = createI18n({
17
- locale: "en",
18
- fallbackLocale: "en",
17
+ locale: 'en',
18
+ fallbackLocale: 'en',
19
19
  messages: {
20
20
  en: {
21
- greeting: "Hello, {{name}}!",
22
- items_one: "{{count}} item",
23
- items_other: "{{count}} items",
21
+ greeting: 'Hello, {{name}}!',
22
+ items_one: '{{count}} item',
23
+ items_other: '{{count}} items',
24
24
  },
25
25
  de: {
26
- greeting: "Hallo, {{name}}!",
26
+ greeting: 'Hallo, {{name}}!',
27
27
  },
28
28
  },
29
29
  })
30
30
 
31
- i18n.t("greeting", { name: "Alice" }) // "Hello, Alice!"
32
- i18n.t("items", { count: 3 }) // "3 items"
31
+ i18n.t('greeting', { name: 'Alice' }) // "Hello, Alice!"
32
+ i18n.t('items', { count: 3 }) // "3 items"
33
33
 
34
- i18n.locale.set("de")
35
- i18n.t("greeting", { name: "Alice" }) // "Hallo, Alice!"
36
- i18n.t("items", { count: 1 }) // "1 item" (fallback to en)
34
+ i18n.locale.set('de')
35
+ i18n.t('greeting', { name: 'Alice' }) // "Hallo, Alice!"
36
+ i18n.t('items', { count: 1 }) // "1 item" (fallback to en)
37
37
  ```
38
38
 
39
39
  ## API
@@ -42,54 +42,54 @@ i18n.t("items", { count: 1 }) // "1 item" (fallback to en)
42
42
 
43
43
  Create a reactive i18n instance with static messages and/or an async namespace loader.
44
44
 
45
- | Parameter | Type | Description |
46
- | --- | --- | --- |
47
- | `options.locale` | `string` | Initial locale (e.g. `"en"`) |
48
- | `options.fallbackLocale` | `string` | Locale to try when key is missing in active locale |
49
- | `options.messages` | `Record<string, TranslationDictionary>` | Static messages keyed by locale |
50
- | `options.loader` | `NamespaceLoader` | `(locale, namespace) => Promise<TranslationDictionary>` |
51
- | `options.defaultNamespace` | `string` | Default namespace for `t()` (default: `"common"`) |
52
- | `options.pluralRules` | `PluralRules` | Custom plural rules per locale |
53
- | `options.onMissingKey` | `(locale, key, namespace?) => string \| undefined` | Missing key handler |
45
+ | Parameter | Type | Description |
46
+ | -------------------------- | -------------------------------------------------- | ------------------------------------------------------- |
47
+ | `options.locale` | `string` | Initial locale (e.g. `"en"`) |
48
+ | `options.fallbackLocale` | `string` | Locale to try when key is missing in active locale |
49
+ | `options.messages` | `Record<string, TranslationDictionary>` | Static messages keyed by locale |
50
+ | `options.loader` | `NamespaceLoader` | `(locale, namespace) => Promise<TranslationDictionary>` |
51
+ | `options.defaultNamespace` | `string` | Default namespace for `t()` (default: `"common"`) |
52
+ | `options.pluralRules` | `PluralRules` | Custom plural rules per locale |
53
+ | `options.onMissingKey` | `(locale, key, namespace?) => string \| undefined` | Missing key handler |
54
54
 
55
55
  **Returns:** `I18nInstance` with:
56
56
 
57
- | Property | Type | Description |
58
- | --- | --- | --- |
59
- | `t(key, values?)` | `(key: string, values?: InterpolationValues) => string` | Translate a key |
60
- | `locale` | `Signal<string>` | Current locale (reactive, writable) |
61
- | `loadNamespace(ns, locale?)` | `(ns: string, locale?: string) => Promise<void>` | Load a namespace |
62
- | `isLoading` | `Computed<boolean>` | Whether any namespace is loading |
63
- | `loadedNamespaces` | `Computed<Set<string>>` | Namespaces loaded for current locale |
64
- | `exists(key)` | `(key: string) => boolean` | Check if a key exists |
65
- | `addMessages(locale, messages, ns?)` | `Function` | Add messages at runtime |
66
- | `availableLocales` | `Computed<string[]>` | All locales with registered messages |
57
+ | Property | Type | Description |
58
+ | ------------------------------------ | ------------------------------------------------------- | ------------------------------------ |
59
+ | `t(key, values?)` | `(key: string, values?: InterpolationValues) => string` | Translate a key |
60
+ | `locale` | `Signal<string>` | Current locale (reactive, writable) |
61
+ | `loadNamespace(ns, locale?)` | `(ns: string, locale?: string) => Promise<void>` | Load a namespace |
62
+ | `isLoading` | `Computed<boolean>` | Whether any namespace is loading |
63
+ | `loadedNamespaces` | `Computed<Set<string>>` | Namespaces loaded for current locale |
64
+ | `exists(key)` | `(key: string) => boolean` | Check if a key exists |
65
+ | `addMessages(locale, messages, ns?)` | `Function` | Add messages at runtime |
66
+ | `availableLocales` | `Computed<string[]>` | All locales with registered messages |
67
67
 
68
68
  ```ts
69
69
  const i18n = createI18n({
70
- locale: "en",
70
+ locale: 'en',
71
71
  loader: async (locale, namespace) => {
72
72
  const mod = await import(`./locales/${locale}/${namespace}.json`)
73
73
  return mod.default
74
74
  },
75
75
  })
76
- await i18n.loadNamespace("auth")
77
- i18n.t("auth:errors.invalid") // namespace:key.path syntax
76
+ await i18n.loadNamespace('auth')
77
+ i18n.t('auth:errors.invalid') // namespace:key.path syntax
78
78
  ```
79
79
 
80
80
  ### `interpolate(template, values?)`
81
81
 
82
82
  Replace `{{key}}` placeholders in a string. Supports whitespace inside braces. Unmatched placeholders are left as-is.
83
83
 
84
- | Parameter | Type | Description |
85
- | --- | --- | --- |
86
- | `template` | `string` | Template string with `{{placeholders}}` |
87
- | `values` | `InterpolationValues` | Key-value pairs for substitution |
84
+ | Parameter | Type | Description |
85
+ | ---------- | --------------------- | --------------------------------------- |
86
+ | `template` | `string` | Template string with `{{placeholders}}` |
87
+ | `values` | `InterpolationValues` | Key-value pairs for substitution |
88
88
 
89
89
  **Returns:** `string`
90
90
 
91
91
  ```ts
92
- interpolate("Hello, {{ name }}!", { name: "World" })
92
+ interpolate('Hello, {{ name }}!', { name: 'World' })
93
93
  // "Hello, World!"
94
94
  ```
95
95
 
@@ -97,18 +97,18 @@ interpolate("Hello, {{ name }}!", { name: "World" })
97
97
 
98
98
  Resolve the CLDR plural category for a count. Uses custom rules if provided, then `Intl.PluralRules`, then a basic `one`/`other` fallback.
99
99
 
100
- | Parameter | Type | Description |
101
- | --- | --- | --- |
102
- | `locale` | `string` | Locale code |
103
- | `count` | `number` | The number to pluralize for |
100
+ | Parameter | Type | Description |
101
+ | ------------- | ------------- | -------------------------------- |
102
+ | `locale` | `string` | Locale code |
103
+ | `count` | `number` | The number to pluralize for |
104
104
  | `customRules` | `PluralRules` | Optional custom rules per locale |
105
105
 
106
106
  **Returns:** `string` — one of `"zero"`, `"one"`, `"two"`, `"few"`, `"many"`, `"other"`
107
107
 
108
108
  ```ts
109
- resolvePluralCategory("en", 1) // "one"
110
- resolvePluralCategory("en", 5) // "other"
111
- resolvePluralCategory("ar", 3) // "few" (via Intl.PluralRules)
109
+ resolvePluralCategory('en', 1) // "one"
110
+ resolvePluralCategory('en', 5) // "other"
111
+ resolvePluralCategory('ar', 3) // "few" (via Intl.PluralRules)
112
112
  ```
113
113
 
114
114
  ### `I18nProvider` / `useI18n()`
@@ -117,14 +117,14 @@ Context pattern for providing an i18n instance to the component tree.
117
117
 
118
118
  ```tsx
119
119
  // Root:
120
- <I18nProvider instance={i18n}>
120
+ ;<I18nProvider instance={i18n}>
121
121
  <App />
122
122
  </I18nProvider>
123
123
 
124
124
  // Any descendant:
125
125
  function Greeting() {
126
126
  const { t, locale } = useI18n()
127
- return () => <h1>{t("greeting", { name: "World" })}</h1>
127
+ return () => <h1>{t('greeting', { name: 'World' })}</h1>
128
128
  }
129
129
  ```
130
130
 
@@ -134,12 +134,12 @@ function Greeting() {
134
134
 
135
135
  Rich JSX interpolation component. Resolves `{{values}}` first, then maps `<tag>content</tag>` patterns to component functions.
136
136
 
137
- | Parameter | Type | Description |
138
- | --- | --- | --- |
139
- | `t` | `(key, values?) => string` | Translation function (from `useI18n()`) |
140
- | `i18nKey` | `string` | Translation key |
141
- | `values` | `InterpolationValues` | Interpolation values |
142
- | `components` | `Record<string, (children) => VNode>` | Component map for rich tags |
137
+ | Parameter | Type | Description |
138
+ | ------------ | ------------------------------------- | --------------------------------------- |
139
+ | `t` | `(key, values?) => string` | Translation function (from `useI18n()`) |
140
+ | `i18nKey` | `string` | Translation key |
141
+ | `values` | `InterpolationValues` | Interpolation values |
142
+ | `components` | `Record<string, (children) => VNode>` | Component map for rich tags |
143
143
 
144
144
  ```tsx
145
145
  // Translation: "Read our <terms>terms</terms> and <privacy>policy</privacy>"
@@ -157,14 +157,14 @@ Rich JSX interpolation component. Resolves `{{values}}` first, then maps `<tag>c
157
157
 
158
158
  Parse a string into an array of plain text and `{ tag, children }` segments. Used internally by `Trans`.
159
159
 
160
- | Parameter | Type | Description |
161
- | --- | --- | --- |
162
- | `text` | `string` | String with `<tag>content</tag>` patterns |
160
+ | Parameter | Type | Description |
161
+ | --------- | -------- | ----------------------------------------- |
162
+ | `text` | `string` | String with `<tag>content</tag>` patterns |
163
163
 
164
164
  **Returns:** `(string | { tag: string, children: string })[]`
165
165
 
166
166
  ```ts
167
- parseRichText("Hello <bold>world</bold>!")
167
+ parseRichText('Hello <bold>world</bold>!')
168
168
  // ["Hello ", { tag: "bold", children: "world" }, "!"]
169
169
  ```
170
170
 
@@ -176,13 +176,13 @@ Split translations by feature and load them lazily.
176
176
 
177
177
  ```ts
178
178
  const i18n = createI18n({
179
- locale: "en",
180
- loader: (locale, ns) => fetch(`/locales/${locale}/${ns}.json`).then(r => r.json()),
179
+ locale: 'en',
180
+ loader: (locale, ns) => fetch(`/locales/${locale}/${ns}.json`).then((r) => r.json()),
181
181
  })
182
182
 
183
183
  // Load on route entry:
184
- await i18n.loadNamespace("dashboard")
185
- i18n.t("dashboard:widgets.chart")
184
+ await i18n.loadNamespace('dashboard')
185
+ i18n.t('dashboard:widgets.chart')
186
186
  ```
187
187
 
188
188
  ### Runtime Message Addition
@@ -190,8 +190,8 @@ i18n.t("dashboard:widgets.chart")
190
190
  Add messages without async loading (e.g. from server-rendered data).
191
191
 
192
192
  ```ts
193
- i18n.addMessages("en", { newFeature: "Try our new feature!" })
194
- i18n.addMessages("en", { errors: { timeout: "Request timed out" } }, "api")
193
+ i18n.addMessages('en', { newFeature: 'Try our new feature!' })
194
+ i18n.addMessages('en', { errors: { timeout: 'Request timed out' } }, 'api')
195
195
  ```
196
196
 
197
197
  ### Pluralization
@@ -200,23 +200,23 @@ Use `_one`, `_other` (and `_zero`, `_two`, `_few`, `_many` for complex locales)
200
200
 
201
201
  ```ts
202
202
  // messages: { items_one: "{{count}} item", items_other: "{{count}} items" }
203
- i18n.t("items", { count: 1 }) // "1 item"
204
- i18n.t("items", { count: 5 }) // "5 items"
203
+ i18n.t('items', { count: 1 }) // "1 item"
204
+ i18n.t('items', { count: 5 }) // "5 items"
205
205
  ```
206
206
 
207
207
  ## Types
208
208
 
209
- | Type | Description |
210
- | --- | --- |
211
- | `I18nInstance` | Public API returned by `createI18n()` |
212
- | `I18nOptions` | Options for `createI18n()` |
213
- | `TranslationDictionary` | `{ [key: string]: string \| TranslationDictionary }` |
214
- | `TranslationMessages` | `Record<string, TranslationDictionary>` |
215
- | `NamespaceLoader` | `(locale: string, namespace: string) => Promise<TranslationDictionary \| undefined>` |
216
- | `InterpolationValues` | `Record<string, string \| number>` |
217
- | `PluralRules` | `Record<string, (count: number) => string>` |
218
- | `I18nProviderProps` | Props for `I18nProvider`: `{ instance: I18nInstance }` |
219
- | `TransProps` | Props for `Trans` component |
209
+ | Type | Description |
210
+ | ----------------------- | ------------------------------------------------------------------------------------ |
211
+ | `I18nInstance` | Public API returned by `createI18n()` |
212
+ | `I18nOptions` | Options for `createI18n()` |
213
+ | `TranslationDictionary` | `{ [key: string]: string \| TranslationDictionary }` |
214
+ | `TranslationMessages` | `Record<string, TranslationDictionary>` |
215
+ | `NamespaceLoader` | `(locale: string, namespace: string) => Promise<TranslationDictionary \| undefined>` |
216
+ | `InterpolationValues` | `Record<string, string \| number>` |
217
+ | `PluralRules` | `Record<string, (count: number) => string>` |
218
+ | `I18nProviderProps` | Props for `I18nProvider`: `{ instance: I18nInstance }` |
219
+ | `TransProps` | Props for `Trans` component |
220
220
 
221
221
  ## Gotchas
222
222
 
package/lib/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","names":[],"sources":["../src/interpolation.ts","../src/pluralization.ts","../src/create-i18n.ts"],"sourcesContent":["import type { InterpolationValues } from \"./types\"\n\nconst INTERPOLATION_RE = /\\{\\{(\\s*\\w+\\s*)\\}\\}/g\n\n/**\n * Replace `{{key}}` placeholders in a string with values from the given record.\n * Supports optional whitespace inside braces: `{{ name }}` works too.\n * Unmatched placeholders are left as-is.\n */\nexport function interpolate(template: string, values?: InterpolationValues): string {\n if (!values || !template.includes(\"{{\")) return template\n return template.replace(INTERPOLATION_RE, (_, key: string) => {\n const trimmed = key.trim()\n const value = values[trimmed]\n if (value === undefined) return `{{${trimmed}}}`\n // Safely coerce — guard against malicious toString/valueOf\n try {\n return typeof value === \"object\" && value !== null ? JSON.stringify(value) : `${value}`\n } catch {\n return `{{${trimmed}}}`\n }\n })\n}\n","import type { PluralRules } from \"./types\"\n\n/**\n * Resolve the plural category for a given count and locale.\n *\n * Uses custom rules if provided, otherwise falls back to `Intl.PluralRules`.\n * Returns CLDR plural categories: \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\".\n */\nexport function resolvePluralCategory(\n locale: string,\n count: number,\n customRules?: PluralRules,\n): string {\n // Custom rules take priority\n if (customRules?.[locale]) {\n return customRules[locale](count)\n }\n\n // Use Intl.PluralRules if available\n if (typeof Intl !== \"undefined\" && Intl.PluralRules) {\n try {\n const pr = new Intl.PluralRules(locale)\n return pr.select(count)\n } catch {\n // Invalid locale — fall through\n }\n }\n\n // Basic fallback\n return count === 1 ? \"one\" : \"other\"\n}\n","import { computed, signal } from \"@pyreon/reactivity\"\nimport { interpolate } from \"./interpolation\"\nimport { resolvePluralCategory } from \"./pluralization\"\nimport type { I18nInstance, I18nOptions, InterpolationValues, TranslationDictionary } from \"./types\"\n\n/**\n * Resolve a dot-separated key path in a nested dictionary.\n * E.g. \"user.greeting\" → dictionary.user.greeting\n */\nfunction resolveKey(dict: TranslationDictionary, keyPath: string): string | undefined {\n const parts = keyPath.split(\".\")\n let current: TranslationDictionary | string = dict\n\n for (const part of parts) {\n if (current == null || typeof current === \"string\") return undefined\n current = current[part] as TranslationDictionary | string\n }\n\n return typeof current === \"string\" ? current : undefined\n}\n\n/**\n * Convert flat dotted keys into nested objects.\n * `{ 'section.title': 'Report' }` → `{ section: { title: 'Report' } }`\n * Keys that don't contain dots are passed through as-is.\n * Already-nested objects are preserved — only string values with dotted keys are expanded.\n */\nfunction nestFlatKeys(messages: TranslationDictionary): TranslationDictionary {\n const result: TranslationDictionary = {}\n let hasFlatKeys = false\n\n for (const key of Object.keys(messages)) {\n const value = messages[key]\n if (key.includes(\".\") && typeof value === \"string\") {\n hasFlatKeys = true\n const parts = key.split(\".\")\n let current: TranslationDictionary = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] as string\n if (!(part in current) || typeof current[part] !== \"object\") {\n current[part] = {}\n }\n current = current[part] as TranslationDictionary\n }\n current[parts[parts.length - 1] as string] = value\n } else if (value !== undefined) {\n result[key] = value\n }\n }\n\n return hasFlatKeys ? result : messages\n}\n\n/**\n * Deep-merge source into target (mutates target).\n */\nfunction deepMerge(target: TranslationDictionary, source: TranslationDictionary): void {\n for (const key of Object.keys(source)) {\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") continue\n const sourceVal = source[key]\n const targetVal = target[key]\n if (\n typeof sourceVal === \"object\" &&\n sourceVal !== null &&\n typeof targetVal === \"object\" &&\n targetVal !== null\n ) {\n deepMerge(targetVal as TranslationDictionary, sourceVal as TranslationDictionary)\n } else {\n target[key] = sourceVal!\n }\n }\n}\n\n/**\n * Create a reactive i18n instance.\n *\n * @example\n * const i18n = createI18n({\n * locale: 'en',\n * fallbackLocale: 'en',\n * messages: {\n * en: { greeting: 'Hello {{name}}!' },\n * de: { greeting: 'Hallo {{name}}!' },\n * },\n * })\n *\n * // Reactive translation — re-evaluates on locale change\n * i18n.t('greeting', { name: 'Alice' }) // \"Hello Alice!\"\n * i18n.locale.set('de')\n * i18n.t('greeting', { name: 'Alice' }) // \"Hallo Alice!\"\n *\n * @example\n * // Async namespace loading\n * const i18n = createI18n({\n * locale: 'en',\n * loader: async (locale, namespace) => {\n * const mod = await import(`./locales/${locale}/${namespace}.json`)\n * return mod.default\n * },\n * })\n * await i18n.loadNamespace('auth')\n * i18n.t('auth:errors.invalid') // looks up \"errors.invalid\" in \"auth\" namespace\n */\nexport function createI18n(options: I18nOptions): I18nInstance {\n const { fallbackLocale, loader, defaultNamespace = \"common\", pluralRules, onMissingKey } = options\n\n // ── Reactive state ──────────────────────────────────────────────────\n\n const locale = signal(options.locale)\n\n // Internal store: locale → namespace → dictionary\n // We use a version counter to trigger reactive updates when messages change,\n // since the store is mutated in place (Object.is would skip same-reference sets).\n const store = new Map<string, Map<string, TranslationDictionary>>()\n const storeVersion = signal(0)\n\n // Loading state\n const pendingLoads = signal(0)\n const loadedNsVersion = signal(0)\n\n // In-flight load promises — deduplicates concurrent loads for the same locale:namespace\n const pendingPromises = new Map<string, Promise<void>>()\n\n const isLoading = computed(() => pendingLoads() > 0)\n const loadedNamespaces = computed(() => {\n loadedNsVersion()\n const currentLocale = locale()\n const nsMap = store.get(currentLocale)\n return new Set(nsMap ? nsMap.keys() : [])\n })\n const availableLocales = computed(() => {\n storeVersion() // subscribe to store changes\n return [...store.keys()]\n })\n\n // ── Initialize static messages ──────────────────────────────────────\n\n if (options.messages) {\n for (const [loc, dict] of Object.entries(options.messages)) {\n const nsMap = new Map<string, TranslationDictionary>()\n nsMap.set(defaultNamespace, dict)\n store.set(loc, nsMap)\n }\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n function getNamespaceMap(loc: string): Map<string, TranslationDictionary> {\n let nsMap = store.get(loc)\n if (!nsMap) {\n nsMap = new Map()\n store.set(loc, nsMap)\n }\n return nsMap\n }\n\n function lookupKey(loc: string, namespace: string, keyPath: string): string | undefined {\n const nsMap = store.get(loc)\n if (!nsMap) return undefined\n const dict = nsMap.get(namespace)\n if (!dict) return undefined\n return resolveKey(dict, keyPath)\n }\n\n function resolveTranslation(key: string, values?: InterpolationValues): string {\n // Subscribe to reactive dependencies\n const currentLocale = locale()\n storeVersion()\n\n // Parse key: \"namespace:key.path\" or just \"key.path\"\n let namespace = defaultNamespace\n let keyPath = key\n\n const colonIndex = key.indexOf(\":\")\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n // Handle pluralization: if values contain `count`, try plural suffixes\n if (values && \"count\" in values) {\n const count = Number(values.count)\n const category = resolvePluralCategory(currentLocale, count, pluralRules)\n\n // Try exact form first (e.g. \"items_one\"), then fall back to base key\n const pluralKey = `${keyPath}_${category}`\n const pluralResult =\n lookupKey(currentLocale, namespace, pluralKey) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, pluralKey) : undefined)\n\n if (pluralResult) {\n return interpolate(pluralResult, values)\n }\n }\n\n // Standard lookup: current locale → fallback locale\n const result =\n lookupKey(currentLocale, namespace, keyPath) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) : undefined)\n\n if (result !== undefined) {\n return interpolate(result, values)\n }\n\n // Missing key handler\n if (onMissingKey) {\n const custom = onMissingKey(currentLocale, key, namespace)\n if (custom !== undefined) return custom!\n }\n\n // Return the key itself as a visual fallback\n return key\n }\n\n // ── Public API ──────────────────────────────────────────────────────\n\n const t = (key: string, values?: InterpolationValues): string => {\n return resolveTranslation(key, values)\n }\n\n const loadNamespace = async (namespace: string, loc?: string): Promise<void> => {\n if (!loader) return\n\n const targetLocale = loc ?? locale.peek()\n const cacheKey = `${targetLocale}:${namespace}`\n const nsMap = getNamespaceMap(targetLocale)\n\n // Skip if already loaded\n if (nsMap.has(namespace)) return\n\n // Deduplicate concurrent loads for the same locale:namespace\n const existing = pendingPromises.get(cacheKey)\n if (existing) return existing\n\n pendingLoads.update((n) => n + 1)\n\n const promise = loader(targetLocale, namespace)\n .then((dict) => {\n if (dict) {\n nsMap.set(namespace, dict)\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n })\n .finally(() => {\n pendingPromises.delete(cacheKey)\n pendingLoads.update((n) => n - 1)\n })\n\n pendingPromises.set(cacheKey, promise)\n return promise\n }\n\n const exists = (key: string): boolean => {\n const currentLocale = locale.peek()\n\n let namespace = defaultNamespace\n let keyPath = key\n const colonIndex = key.indexOf(\":\")\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n return (\n lookupKey(currentLocale, namespace, keyPath) !== undefined ||\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) !== undefined : false)\n )\n }\n\n const addMessages = (loc: string, messages: TranslationDictionary, namespace?: string): void => {\n const ns = namespace ?? defaultNamespace\n const nsMap = getNamespaceMap(loc)\n const nested = nestFlatKeys(messages)\n const existing = nsMap.get(ns)\n\n if (existing) {\n deepMerge(existing, nested)\n } else {\n // Deep-clone to prevent external mutation from corrupting the store\n const cloned: TranslationDictionary = {}\n deepMerge(cloned, nested)\n nsMap.set(ns, cloned)\n }\n\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n\n return {\n t,\n locale,\n loadNamespace,\n isLoading,\n loadedNamespaces,\n exists,\n addMessages,\n availableLocales,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"core.js","names":[],"sources":["../src/interpolation.ts","../src/pluralization.ts","../src/create-i18n.ts"],"sourcesContent":["import type { InterpolationValues } from './types'\n\nconst INTERPOLATION_RE = /\\{\\{(\\s*\\w+\\s*)\\}\\}/g\n\n/**\n * Replace `{{key}}` placeholders in a string with values from the given record.\n * Supports optional whitespace inside braces: `{{ name }}` works too.\n * Unmatched placeholders are left as-is.\n */\nexport function interpolate(template: string, values?: InterpolationValues): string {\n if (!values || !template.includes('{{')) return template\n return template.replace(INTERPOLATION_RE, (_, key: string) => {\n const trimmed = key.trim()\n const value = values[trimmed]\n if (value === undefined) return `{{${trimmed}}}`\n // Safely coerce — guard against malicious toString/valueOf\n try {\n return typeof value === 'object' && value !== null ? JSON.stringify(value) : `${value}`\n } catch {\n return `{{${trimmed}}}`\n }\n })\n}\n","import type { PluralRules } from './types'\n\n/**\n * Resolve the plural category for a given count and locale.\n *\n * Uses custom rules if provided, otherwise falls back to `Intl.PluralRules`.\n * Returns CLDR plural categories: \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\".\n */\nexport function resolvePluralCategory(\n locale: string,\n count: number,\n customRules?: PluralRules,\n): string {\n // Custom rules take priority\n if (customRules?.[locale]) {\n return customRules[locale](count)\n }\n\n // Use Intl.PluralRules if available\n if (typeof Intl !== 'undefined' && Intl.PluralRules) {\n try {\n const pr = new Intl.PluralRules(locale)\n return pr.select(count)\n } catch {\n // Invalid locale — fall through\n }\n }\n\n // Basic fallback\n return count === 1 ? 'one' : 'other'\n}\n","import { computed, signal } from '@pyreon/reactivity'\nimport { interpolate } from './interpolation'\nimport { resolvePluralCategory } from './pluralization'\nimport type { I18nInstance, I18nOptions, InterpolationValues, TranslationDictionary } from './types'\n\n/**\n * Resolve a dot-separated key path in a nested dictionary.\n * E.g. \"user.greeting\" → dictionary.user.greeting\n */\nfunction resolveKey(dict: TranslationDictionary, keyPath: string): string | undefined {\n const parts = keyPath.split('.')\n let current: TranslationDictionary | string = dict\n\n for (const part of parts) {\n if (current == null || typeof current === 'string') return undefined\n current = current[part] as TranslationDictionary | string\n }\n\n return typeof current === 'string' ? current : undefined\n}\n\n/**\n * Convert flat dotted keys into nested objects.\n * `{ 'section.title': 'Report' }` → `{ section: { title: 'Report' } }`\n * Keys that don't contain dots are passed through as-is.\n * Already-nested objects are preserved — only string values with dotted keys are expanded.\n */\nfunction nestFlatKeys(messages: TranslationDictionary): TranslationDictionary {\n const result: TranslationDictionary = {}\n let hasFlatKeys = false\n\n for (const key of Object.keys(messages)) {\n const value = messages[key]\n if (key.includes('.') && typeof value === 'string') {\n hasFlatKeys = true\n const parts = key.split('.')\n let current: TranslationDictionary = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] as string\n if (!(part in current) || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part] as TranslationDictionary\n }\n current[parts[parts.length - 1] as string] = value\n } else if (value !== undefined) {\n result[key] = value\n }\n }\n\n return hasFlatKeys ? result : messages\n}\n\n/**\n * Deep-merge source into target (mutates target).\n */\nfunction deepMerge(target: TranslationDictionary, source: TranslationDictionary): void {\n for (const key of Object.keys(source)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue\n const sourceVal = source[key]\n const targetVal = target[key]\n if (\n typeof sourceVal === 'object' &&\n sourceVal !== null &&\n typeof targetVal === 'object' &&\n targetVal !== null\n ) {\n deepMerge(targetVal as TranslationDictionary, sourceVal as TranslationDictionary)\n } else {\n target[key] = sourceVal!\n }\n }\n}\n\n/**\n * Create a reactive i18n instance.\n *\n * @example\n * const i18n = createI18n({\n * locale: 'en',\n * fallbackLocale: 'en',\n * messages: {\n * en: { greeting: 'Hello {{name}}!' },\n * de: { greeting: 'Hallo {{name}}!' },\n * },\n * })\n *\n * // Reactive translation — re-evaluates on locale change\n * i18n.t('greeting', { name: 'Alice' }) // \"Hello Alice!\"\n * i18n.locale.set('de')\n * i18n.t('greeting', { name: 'Alice' }) // \"Hallo Alice!\"\n *\n * @example\n * // Async namespace loading\n * const i18n = createI18n({\n * locale: 'en',\n * loader: async (locale, namespace) => {\n * const mod = await import(`./locales/${locale}/${namespace}.json`)\n * return mod.default\n * },\n * })\n * await i18n.loadNamespace('auth')\n * i18n.t('auth:errors.invalid') // looks up \"errors.invalid\" in \"auth\" namespace\n */\nexport function createI18n(options: I18nOptions): I18nInstance {\n const { fallbackLocale, loader, defaultNamespace = 'common', pluralRules, onMissingKey } = options\n\n // ── Reactive state ──────────────────────────────────────────────────\n\n const locale = signal(options.locale)\n\n // Internal store: locale → namespace → dictionary\n // We use a version counter to trigger reactive updates when messages change,\n // since the store is mutated in place (Object.is would skip same-reference sets).\n const store = new Map<string, Map<string, TranslationDictionary>>()\n const storeVersion = signal(0)\n\n // Loading state\n const pendingLoads = signal(0)\n const loadedNsVersion = signal(0)\n\n // In-flight load promises — deduplicates concurrent loads for the same locale:namespace\n const pendingPromises = new Map<string, Promise<void>>()\n\n const isLoading = computed(() => pendingLoads() > 0)\n const loadedNamespaces = computed(() => {\n loadedNsVersion()\n const currentLocale = locale()\n const nsMap = store.get(currentLocale)\n return new Set(nsMap ? nsMap.keys() : [])\n })\n const availableLocales = computed(() => {\n storeVersion() // subscribe to store changes\n return [...store.keys()]\n })\n\n // ── Initialize static messages ──────────────────────────────────────\n\n if (options.messages) {\n for (const [loc, dict] of Object.entries(options.messages)) {\n const nsMap = new Map<string, TranslationDictionary>()\n nsMap.set(defaultNamespace, dict)\n store.set(loc, nsMap)\n }\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n function getNamespaceMap(loc: string): Map<string, TranslationDictionary> {\n let nsMap = store.get(loc)\n if (!nsMap) {\n nsMap = new Map()\n store.set(loc, nsMap)\n }\n return nsMap\n }\n\n function lookupKey(loc: string, namespace: string, keyPath: string): string | undefined {\n const nsMap = store.get(loc)\n if (!nsMap) return undefined\n const dict = nsMap.get(namespace)\n if (!dict) return undefined\n return resolveKey(dict, keyPath)\n }\n\n function resolveTranslation(key: string, values?: InterpolationValues): string {\n // Subscribe to reactive dependencies\n const currentLocale = locale()\n storeVersion()\n\n // Parse key: \"namespace:key.path\" or just \"key.path\"\n let namespace = defaultNamespace\n let keyPath = key\n\n const colonIndex = key.indexOf(':')\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n // Handle pluralization: if values contain `count`, try plural suffixes\n if (values && 'count' in values) {\n const count = Number(values.count)\n const category = resolvePluralCategory(currentLocale, count, pluralRules)\n\n // Try exact form first (e.g. \"items_one\"), then fall back to base key\n const pluralKey = `${keyPath}_${category}`\n const pluralResult =\n lookupKey(currentLocale, namespace, pluralKey) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, pluralKey) : undefined)\n\n if (pluralResult) {\n return interpolate(pluralResult, values)\n }\n }\n\n // Standard lookup: current locale → fallback locale\n const result =\n lookupKey(currentLocale, namespace, keyPath) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) : undefined)\n\n if (result !== undefined) {\n return interpolate(result, values)\n }\n\n // Missing key handler\n if (onMissingKey) {\n const custom = onMissingKey(currentLocale, key, namespace)\n if (custom !== undefined) return custom!\n }\n\n // Return the key itself as a visual fallback\n return key\n }\n\n // ── Public API ──────────────────────────────────────────────────────\n\n const t = (key: string, values?: InterpolationValues): string => {\n return resolveTranslation(key, values)\n }\n\n const loadNamespace = async (namespace: string, loc?: string): Promise<void> => {\n if (!loader) return\n\n const targetLocale = loc ?? locale.peek()\n const cacheKey = `${targetLocale}:${namespace}`\n const nsMap = getNamespaceMap(targetLocale)\n\n // Skip if already loaded\n if (nsMap.has(namespace)) return\n\n // Deduplicate concurrent loads for the same locale:namespace\n const existing = pendingPromises.get(cacheKey)\n if (existing) return existing\n\n pendingLoads.update((n) => n + 1)\n\n const promise = loader(targetLocale, namespace)\n .then((dict) => {\n if (dict) {\n nsMap.set(namespace, dict)\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n })\n .finally(() => {\n pendingPromises.delete(cacheKey)\n pendingLoads.update((n) => n - 1)\n })\n\n pendingPromises.set(cacheKey, promise)\n return promise\n }\n\n const exists = (key: string): boolean => {\n const currentLocale = locale.peek()\n\n let namespace = defaultNamespace\n let keyPath = key\n const colonIndex = key.indexOf(':')\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n return (\n lookupKey(currentLocale, namespace, keyPath) !== undefined ||\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) !== undefined : false)\n )\n }\n\n const addMessages = (loc: string, messages: TranslationDictionary, namespace?: string): void => {\n const ns = namespace ?? defaultNamespace\n const nsMap = getNamespaceMap(loc)\n const nested = nestFlatKeys(messages)\n const existing = nsMap.get(ns)\n\n if (existing) {\n deepMerge(existing, nested)\n } else {\n // Deep-clone to prevent external mutation from corrupting the store\n const cloned: TranslationDictionary = {}\n deepMerge(cloned, nested)\n nsMap.set(ns, cloned)\n }\n\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n\n return {\n t,\n locale,\n loadNamespace,\n isLoading,\n loadedNamespaces,\n exists,\n addMessages,\n availableLocales,\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @pyreon/i18n devtools introspection API.\n * Import: `import { ... } from \"@pyreon/i18n/devtools\"`\n */\n\nconst _activeInstances = new Map<string, WeakRef<object>>()\nconst _listeners = new Set<() => void>()\n\nfunction _notify(): void {\n for (const listener of _listeners) listener()\n}\n\n/**\n * Register an i18n instance for devtools inspection.\n *\n * @example\n * const i18n = createI18n({ ... })\n * registerI18n(\"app\", i18n)\n */\nexport function registerI18n(name: string, instance: object): void {\n _activeInstances.set(name, new WeakRef(instance))\n _notify()\n}\n\n/** Unregister an i18n instance. */\nexport function unregisterI18n(name: string): void {\n _activeInstances.delete(name)\n _notify()\n}\n\n/** Get all registered i18n instance names. Cleans up garbage-collected instances. */\nexport function getActiveI18nInstances(): string[] {\n for (const [name, ref] of _activeInstances) {\n if (ref.deref() === undefined) _activeInstances.delete(name)\n }\n return [..._activeInstances.keys()]\n}\n\n/** Get an i18n instance by name (or undefined if GC'd or not registered). */\nexport function getI18nInstance(name: string): object | undefined {\n const ref = _activeInstances.get(name)\n if (!ref) return undefined\n const instance = ref.deref()\n if (!instance) {\n _activeInstances.delete(name)\n return undefined\n }\n return instance\n}\n\n/** Safely read a property that may be a signal (callable). */\nfunction safeRead(\n obj: Record<string, unknown>,\n key: string,\n fallback: unknown = undefined,\n): unknown {\n try {\n const val = obj[key]\n return typeof val === \"function\" ? (val as () => unknown)() : fallback\n } catch {\n return fallback\n }\n}\n\n/**\n * Get a snapshot of an i18n instance's state.\n */\nexport function getI18nSnapshot(name: string): Record<string, unknown> | undefined {\n const instance = getI18nInstance(name) as Record<string, unknown> | undefined\n if (!instance) return undefined\n const ns = safeRead(instance, \"loadedNamespaces\", new Set())\n return {\n locale: safeRead(instance, \"locale\"),\n availableLocales: safeRead(instance, \"availableLocales\", []),\n loadedNamespaces: ns instanceof Set ? [...ns] : [],\n isLoading: safeRead(instance, \"isLoading\", false),\n }\n}\n\n/** Subscribe to i18n registry changes. Returns unsubscribe function. */\nexport function onI18nChange(listener: () => void): () => void {\n _listeners.add(listener)\n return () => {\n _listeners.delete(listener)\n }\n}\n\n/** @internal — reset devtools registry (for tests). */\nexport function _resetDevtools(): void {\n _activeInstances.clear()\n _listeners.clear()\n}\n"],"mappings":";;;;;AAKA,MAAM,mCAAmB,IAAI,KAA8B;AAC3D,MAAM,6BAAa,IAAI,KAAiB;AAExC,SAAS,UAAgB;AACvB,MAAK,MAAM,YAAY,WAAY,WAAU;;;;;;;;;AAU/C,SAAgB,aAAa,MAAc,UAAwB;AACjE,kBAAiB,IAAI,MAAM,IAAI,QAAQ,SAAS,CAAC;AACjD,UAAS;;;AAIX,SAAgB,eAAe,MAAoB;AACjD,kBAAiB,OAAO,KAAK;AAC7B,UAAS;;;AAIX,SAAgB,yBAAmC;AACjD,MAAK,MAAM,CAAC,MAAM,QAAQ,iBACxB,KAAI,IAAI,OAAO,KAAK,OAAW,kBAAiB,OAAO,KAAK;AAE9D,QAAO,CAAC,GAAG,iBAAiB,MAAM,CAAC;;;AAIrC,SAAgB,gBAAgB,MAAkC;CAChE,MAAM,MAAM,iBAAiB,IAAI,KAAK;AACtC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,WAAW,IAAI,OAAO;AAC5B,KAAI,CAAC,UAAU;AACb,mBAAiB,OAAO,KAAK;AAC7B;;AAEF,QAAO;;;AAIT,SAAS,SACP,KACA,KACA,WAAoB,QACX;AACT,KAAI;EACF,MAAM,MAAM,IAAI;AAChB,SAAO,OAAO,QAAQ,aAAc,KAAuB,GAAG;SACxD;AACN,SAAO;;;;;;AAOX,SAAgB,gBAAgB,MAAmD;CACjF,MAAM,WAAW,gBAAgB,KAAK;AACtC,KAAI,CAAC,SAAU,QAAO;CACtB,MAAM,KAAK,SAAS,UAAU,oCAAoB,IAAI,KAAK,CAAC;AAC5D,QAAO;EACL,QAAQ,SAAS,UAAU,SAAS;EACpC,kBAAkB,SAAS,UAAU,oBAAoB,EAAE,CAAC;EAC5D,kBAAkB,cAAc,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE;EAClD,WAAW,SAAS,UAAU,aAAa,MAAM;EAClD;;;AAIH,SAAgB,aAAa,UAAkC;AAC7D,YAAW,IAAI,SAAS;AACxB,cAAa;AACX,aAAW,OAAO,SAAS;;;;AAK/B,SAAgB,iBAAuB;AACrC,kBAAiB,OAAO;AACxB,YAAW,OAAO"}
1
+ {"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @pyreon/i18n devtools introspection API.\n * Import: `import { ... } from \"@pyreon/i18n/devtools\"`\n */\n\nconst _activeInstances = new Map<string, WeakRef<object>>()\nconst _listeners = new Set<() => void>()\n\nfunction _notify(): void {\n for (const listener of _listeners) listener()\n}\n\n/**\n * Register an i18n instance for devtools inspection.\n *\n * @example\n * const i18n = createI18n({ ... })\n * registerI18n(\"app\", i18n)\n */\nexport function registerI18n(name: string, instance: object): void {\n _activeInstances.set(name, new WeakRef(instance))\n _notify()\n}\n\n/** Unregister an i18n instance. */\nexport function unregisterI18n(name: string): void {\n _activeInstances.delete(name)\n _notify()\n}\n\n/** Get all registered i18n instance names. Cleans up garbage-collected instances. */\nexport function getActiveI18nInstances(): string[] {\n for (const [name, ref] of _activeInstances) {\n if (ref.deref() === undefined) _activeInstances.delete(name)\n }\n return [..._activeInstances.keys()]\n}\n\n/** Get an i18n instance by name (or undefined if GC'd or not registered). */\nexport function getI18nInstance(name: string): object | undefined {\n const ref = _activeInstances.get(name)\n if (!ref) return undefined\n const instance = ref.deref()\n if (!instance) {\n _activeInstances.delete(name)\n return undefined\n }\n return instance\n}\n\n/** Safely read a property that may be a signal (callable). */\nfunction safeRead(\n obj: Record<string, unknown>,\n key: string,\n fallback: unknown = undefined,\n): unknown {\n try {\n const val = obj[key]\n return typeof val === 'function' ? (val as () => unknown)() : fallback\n } catch {\n return fallback\n }\n}\n\n/**\n * Get a snapshot of an i18n instance's state.\n */\nexport function getI18nSnapshot(name: string): Record<string, unknown> | undefined {\n const instance = getI18nInstance(name) as Record<string, unknown> | undefined\n if (!instance) return undefined\n const ns = safeRead(instance, 'loadedNamespaces', new Set())\n return {\n locale: safeRead(instance, 'locale'),\n availableLocales: safeRead(instance, 'availableLocales', []),\n loadedNamespaces: ns instanceof Set ? [...ns] : [],\n isLoading: safeRead(instance, 'isLoading', false),\n }\n}\n\n/** Subscribe to i18n registry changes. Returns unsubscribe function. */\nexport function onI18nChange(listener: () => void): () => void {\n _listeners.add(listener)\n return () => {\n _listeners.delete(listener)\n }\n}\n\n/** @internal — reset devtools registry (for tests). */\nexport function _resetDevtools(): void {\n _activeInstances.clear()\n _listeners.clear()\n}\n"],"mappings":";;;;;AAKA,MAAM,mCAAmB,IAAI,KAA8B;AAC3D,MAAM,6BAAa,IAAI,KAAiB;AAExC,SAAS,UAAgB;AACvB,MAAK,MAAM,YAAY,WAAY,WAAU;;;;;;;;;AAU/C,SAAgB,aAAa,MAAc,UAAwB;AACjE,kBAAiB,IAAI,MAAM,IAAI,QAAQ,SAAS,CAAC;AACjD,UAAS;;;AAIX,SAAgB,eAAe,MAAoB;AACjD,kBAAiB,OAAO,KAAK;AAC7B,UAAS;;;AAIX,SAAgB,yBAAmC;AACjD,MAAK,MAAM,CAAC,MAAM,QAAQ,iBACxB,KAAI,IAAI,OAAO,KAAK,OAAW,kBAAiB,OAAO,KAAK;AAE9D,QAAO,CAAC,GAAG,iBAAiB,MAAM,CAAC;;;AAIrC,SAAgB,gBAAgB,MAAkC;CAChE,MAAM,MAAM,iBAAiB,IAAI,KAAK;AACtC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,WAAW,IAAI,OAAO;AAC5B,KAAI,CAAC,UAAU;AACb,mBAAiB,OAAO,KAAK;AAC7B;;AAEF,QAAO;;;AAIT,SAAS,SACP,KACA,KACA,WAAoB,QACX;AACT,KAAI;EACF,MAAM,MAAM,IAAI;AAChB,SAAO,OAAO,QAAQ,aAAc,KAAuB,GAAG;SACxD;AACN,SAAO;;;;;;AAOX,SAAgB,gBAAgB,MAAmD;CACjF,MAAM,WAAW,gBAAgB,KAAK;AACtC,KAAI,CAAC,SAAU,QAAO;CACtB,MAAM,KAAK,SAAS,UAAU,oCAAoB,IAAI,KAAK,CAAC;AAC5D,QAAO;EACL,QAAQ,SAAS,UAAU,SAAS;EACpC,kBAAkB,SAAS,UAAU,oBAAoB,EAAE,CAAC;EAC5D,kBAAkB,cAAc,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE;EAClD,WAAW,SAAS,UAAU,aAAa,MAAM;EAClD;;;AAIH,SAAgB,aAAa,UAAkC;AAC7D,YAAW,IAAI,SAAS;AACxB,cAAa;AACX,aAAW,OAAO,SAAS;;;;AAK/B,SAAgB,iBAAuB;AACrC,kBAAiB,OAAO;AACxB,YAAW,OAAO"}
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/interpolation.ts","../src/pluralization.ts","../src/create-i18n.ts","../src/trans.tsx"],"sourcesContent":["import type { Props, VNode, VNodeChild } from \"@pyreon/core\"\nimport { createContext, provide, useContext } from \"@pyreon/core\"\nimport type { I18nInstance } from \"./types\"\n\nexport const I18nContext = createContext<I18nInstance | null>(null)\n\nexport interface I18nProviderProps extends Props {\n instance: I18nInstance\n children?: VNodeChild\n}\n\n/**\n * Provide an i18n instance to the component tree.\n *\n * @example\n * const i18n = createI18n({ locale: 'en', messages: { en: { hello: 'Hello' } } })\n *\n * // In JSX:\n * <I18nProvider instance={i18n}>\n * <App />\n * </I18nProvider>\n */\nexport function I18nProvider(props: I18nProviderProps): VNode {\n provide(I18nContext, props.instance)\n\n const ch = props.children\n return (typeof ch === \"function\" ? (ch as () => VNodeChild)() : ch) as VNode\n}\n\n/**\n * Access the i18n instance from the nearest I18nProvider.\n * Must be called within a component tree wrapped by I18nProvider.\n *\n * @example\n * function Greeting() {\n * const { t, locale } = useI18n()\n * return <h1>{t('greeting', { name: 'World' })}</h1>\n * }\n */\nexport function useI18n(): I18nInstance {\n const instance = useContext(I18nContext)\n if (!instance) {\n throw new Error(\"[@pyreon/i18n] useI18n() must be used within an <I18nProvider>.\")\n }\n return instance\n}\n","import type { InterpolationValues } from \"./types\"\n\nconst INTERPOLATION_RE = /\\{\\{(\\s*\\w+\\s*)\\}\\}/g\n\n/**\n * Replace `{{key}}` placeholders in a string with values from the given record.\n * Supports optional whitespace inside braces: `{{ name }}` works too.\n * Unmatched placeholders are left as-is.\n */\nexport function interpolate(template: string, values?: InterpolationValues): string {\n if (!values || !template.includes(\"{{\")) return template\n return template.replace(INTERPOLATION_RE, (_, key: string) => {\n const trimmed = key.trim()\n const value = values[trimmed]\n if (value === undefined) return `{{${trimmed}}}`\n // Safely coerce — guard against malicious toString/valueOf\n try {\n return typeof value === \"object\" && value !== null ? JSON.stringify(value) : `${value}`\n } catch {\n return `{{${trimmed}}}`\n }\n })\n}\n","import type { PluralRules } from \"./types\"\n\n/**\n * Resolve the plural category for a given count and locale.\n *\n * Uses custom rules if provided, otherwise falls back to `Intl.PluralRules`.\n * Returns CLDR plural categories: \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\".\n */\nexport function resolvePluralCategory(\n locale: string,\n count: number,\n customRules?: PluralRules,\n): string {\n // Custom rules take priority\n if (customRules?.[locale]) {\n return customRules[locale](count)\n }\n\n // Use Intl.PluralRules if available\n if (typeof Intl !== \"undefined\" && Intl.PluralRules) {\n try {\n const pr = new Intl.PluralRules(locale)\n return pr.select(count)\n } catch {\n // Invalid locale — fall through\n }\n }\n\n // Basic fallback\n return count === 1 ? \"one\" : \"other\"\n}\n","import { computed, signal } from \"@pyreon/reactivity\"\nimport { interpolate } from \"./interpolation\"\nimport { resolvePluralCategory } from \"./pluralization\"\nimport type { I18nInstance, I18nOptions, InterpolationValues, TranslationDictionary } from \"./types\"\n\n/**\n * Resolve a dot-separated key path in a nested dictionary.\n * E.g. \"user.greeting\" → dictionary.user.greeting\n */\nfunction resolveKey(dict: TranslationDictionary, keyPath: string): string | undefined {\n const parts = keyPath.split(\".\")\n let current: TranslationDictionary | string = dict\n\n for (const part of parts) {\n if (current == null || typeof current === \"string\") return undefined\n current = current[part] as TranslationDictionary | string\n }\n\n return typeof current === \"string\" ? current : undefined\n}\n\n/**\n * Convert flat dotted keys into nested objects.\n * `{ 'section.title': 'Report' }` → `{ section: { title: 'Report' } }`\n * Keys that don't contain dots are passed through as-is.\n * Already-nested objects are preserved — only string values with dotted keys are expanded.\n */\nfunction nestFlatKeys(messages: TranslationDictionary): TranslationDictionary {\n const result: TranslationDictionary = {}\n let hasFlatKeys = false\n\n for (const key of Object.keys(messages)) {\n const value = messages[key]\n if (key.includes(\".\") && typeof value === \"string\") {\n hasFlatKeys = true\n const parts = key.split(\".\")\n let current: TranslationDictionary = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] as string\n if (!(part in current) || typeof current[part] !== \"object\") {\n current[part] = {}\n }\n current = current[part] as TranslationDictionary\n }\n current[parts[parts.length - 1] as string] = value\n } else if (value !== undefined) {\n result[key] = value\n }\n }\n\n return hasFlatKeys ? result : messages\n}\n\n/**\n * Deep-merge source into target (mutates target).\n */\nfunction deepMerge(target: TranslationDictionary, source: TranslationDictionary): void {\n for (const key of Object.keys(source)) {\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") continue\n const sourceVal = source[key]\n const targetVal = target[key]\n if (\n typeof sourceVal === \"object\" &&\n sourceVal !== null &&\n typeof targetVal === \"object\" &&\n targetVal !== null\n ) {\n deepMerge(targetVal as TranslationDictionary, sourceVal as TranslationDictionary)\n } else {\n target[key] = sourceVal!\n }\n }\n}\n\n/**\n * Create a reactive i18n instance.\n *\n * @example\n * const i18n = createI18n({\n * locale: 'en',\n * fallbackLocale: 'en',\n * messages: {\n * en: { greeting: 'Hello {{name}}!' },\n * de: { greeting: 'Hallo {{name}}!' },\n * },\n * })\n *\n * // Reactive translation — re-evaluates on locale change\n * i18n.t('greeting', { name: 'Alice' }) // \"Hello Alice!\"\n * i18n.locale.set('de')\n * i18n.t('greeting', { name: 'Alice' }) // \"Hallo Alice!\"\n *\n * @example\n * // Async namespace loading\n * const i18n = createI18n({\n * locale: 'en',\n * loader: async (locale, namespace) => {\n * const mod = await import(`./locales/${locale}/${namespace}.json`)\n * return mod.default\n * },\n * })\n * await i18n.loadNamespace('auth')\n * i18n.t('auth:errors.invalid') // looks up \"errors.invalid\" in \"auth\" namespace\n */\nexport function createI18n(options: I18nOptions): I18nInstance {\n const { fallbackLocale, loader, defaultNamespace = \"common\", pluralRules, onMissingKey } = options\n\n // ── Reactive state ──────────────────────────────────────────────────\n\n const locale = signal(options.locale)\n\n // Internal store: locale → namespace → dictionary\n // We use a version counter to trigger reactive updates when messages change,\n // since the store is mutated in place (Object.is would skip same-reference sets).\n const store = new Map<string, Map<string, TranslationDictionary>>()\n const storeVersion = signal(0)\n\n // Loading state\n const pendingLoads = signal(0)\n const loadedNsVersion = signal(0)\n\n // In-flight load promises — deduplicates concurrent loads for the same locale:namespace\n const pendingPromises = new Map<string, Promise<void>>()\n\n const isLoading = computed(() => pendingLoads() > 0)\n const loadedNamespaces = computed(() => {\n loadedNsVersion()\n const currentLocale = locale()\n const nsMap = store.get(currentLocale)\n return new Set(nsMap ? nsMap.keys() : [])\n })\n const availableLocales = computed(() => {\n storeVersion() // subscribe to store changes\n return [...store.keys()]\n })\n\n // ── Initialize static messages ──────────────────────────────────────\n\n if (options.messages) {\n for (const [loc, dict] of Object.entries(options.messages)) {\n const nsMap = new Map<string, TranslationDictionary>()\n nsMap.set(defaultNamespace, dict)\n store.set(loc, nsMap)\n }\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n function getNamespaceMap(loc: string): Map<string, TranslationDictionary> {\n let nsMap = store.get(loc)\n if (!nsMap) {\n nsMap = new Map()\n store.set(loc, nsMap)\n }\n return nsMap\n }\n\n function lookupKey(loc: string, namespace: string, keyPath: string): string | undefined {\n const nsMap = store.get(loc)\n if (!nsMap) return undefined\n const dict = nsMap.get(namespace)\n if (!dict) return undefined\n return resolveKey(dict, keyPath)\n }\n\n function resolveTranslation(key: string, values?: InterpolationValues): string {\n // Subscribe to reactive dependencies\n const currentLocale = locale()\n storeVersion()\n\n // Parse key: \"namespace:key.path\" or just \"key.path\"\n let namespace = defaultNamespace\n let keyPath = key\n\n const colonIndex = key.indexOf(\":\")\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n // Handle pluralization: if values contain `count`, try plural suffixes\n if (values && \"count\" in values) {\n const count = Number(values.count)\n const category = resolvePluralCategory(currentLocale, count, pluralRules)\n\n // Try exact form first (e.g. \"items_one\"), then fall back to base key\n const pluralKey = `${keyPath}_${category}`\n const pluralResult =\n lookupKey(currentLocale, namespace, pluralKey) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, pluralKey) : undefined)\n\n if (pluralResult) {\n return interpolate(pluralResult, values)\n }\n }\n\n // Standard lookup: current locale → fallback locale\n const result =\n lookupKey(currentLocale, namespace, keyPath) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) : undefined)\n\n if (result !== undefined) {\n return interpolate(result, values)\n }\n\n // Missing key handler\n if (onMissingKey) {\n const custom = onMissingKey(currentLocale, key, namespace)\n if (custom !== undefined) return custom!\n }\n\n // Return the key itself as a visual fallback\n return key\n }\n\n // ── Public API ──────────────────────────────────────────────────────\n\n const t = (key: string, values?: InterpolationValues): string => {\n return resolveTranslation(key, values)\n }\n\n const loadNamespace = async (namespace: string, loc?: string): Promise<void> => {\n if (!loader) return\n\n const targetLocale = loc ?? locale.peek()\n const cacheKey = `${targetLocale}:${namespace}`\n const nsMap = getNamespaceMap(targetLocale)\n\n // Skip if already loaded\n if (nsMap.has(namespace)) return\n\n // Deduplicate concurrent loads for the same locale:namespace\n const existing = pendingPromises.get(cacheKey)\n if (existing) return existing\n\n pendingLoads.update((n) => n + 1)\n\n const promise = loader(targetLocale, namespace)\n .then((dict) => {\n if (dict) {\n nsMap.set(namespace, dict)\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n })\n .finally(() => {\n pendingPromises.delete(cacheKey)\n pendingLoads.update((n) => n - 1)\n })\n\n pendingPromises.set(cacheKey, promise)\n return promise\n }\n\n const exists = (key: string): boolean => {\n const currentLocale = locale.peek()\n\n let namespace = defaultNamespace\n let keyPath = key\n const colonIndex = key.indexOf(\":\")\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n return (\n lookupKey(currentLocale, namespace, keyPath) !== undefined ||\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) !== undefined : false)\n )\n }\n\n const addMessages = (loc: string, messages: TranslationDictionary, namespace?: string): void => {\n const ns = namespace ?? defaultNamespace\n const nsMap = getNamespaceMap(loc)\n const nested = nestFlatKeys(messages)\n const existing = nsMap.get(ns)\n\n if (existing) {\n deepMerge(existing, nested)\n } else {\n // Deep-clone to prevent external mutation from corrupting the store\n const cloned: TranslationDictionary = {}\n deepMerge(cloned, nested)\n nsMap.set(ns, cloned)\n }\n\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n\n return {\n t,\n locale,\n loadNamespace,\n isLoading,\n loadedNamespaces,\n exists,\n addMessages,\n availableLocales,\n }\n}\n","import type { Props, VNode } from \"@pyreon/core\"\nimport type { InterpolationValues } from \"./types\"\n\nconst TAG_RE = /<(\\w+)>([^<]*)<\\/\\1>/g\n\ninterface RichPart {\n tag: string\n children: string\n}\n\n/**\n * Parse a translated string into an array of plain text and rich tag segments.\n *\n * @example\n * parseRichText(\"Hello <bold>world</bold>, click <link>here</link>\")\n * // → [\"Hello \", { tag: \"bold\", children: \"world\" }, \", click \", { tag: \"link\", children: \"here\" }]\n */\nexport function parseRichText(text: string): (string | RichPart)[] {\n const parts: (string | RichPart)[] = []\n let lastIndex = 0\n\n for (const match of text.matchAll(TAG_RE)) {\n const before = text.slice(lastIndex, match.index)\n if (before) parts.push(before)\n parts.push({ tag: match[1]!, children: match[2]! })\n lastIndex = match.index! + match[0].length\n }\n\n const after = text.slice(lastIndex)\n if (after) parts.push(after)\n\n return parts\n}\n\nexport interface TransProps extends Props {\n /** Translation key (supports namespace:key syntax). */\n i18nKey: string\n /** Interpolation values for {{placeholder}} syntax. */\n values?: InterpolationValues\n /**\n * Component map for rich interpolation.\n * Keys match tag names in the translation string.\n * Values are component functions: `(children: any) => VNode`\n *\n * @example\n * // Translation: \"Read the <terms>terms</terms> and <privacy>policy</privacy>\"\n * components={{\n * terms: (children) => <a href=\"/terms\">{children}</a>,\n * privacy: (children) => <a href=\"/privacy\">{children}</a>,\n * }}\n */\n components?: Record<string, (children: any) => any>\n /**\n * The i18n instance's `t` function.\n * Can be obtained from `useI18n()` or passed directly.\n */\n t: (key: string, values?: InterpolationValues) => string\n}\n\n/**\n * Rich JSX interpolation component for translations.\n *\n * Allows embedding JSX components within translated strings using XML-like tags.\n * The `t` function resolves the translation and interpolates `{{values}}` first,\n * then `<tag>content</tag>` patterns are mapped to the provided components.\n *\n * @example\n * // Translation: \"You have <bold>{{count}}</bold> unread messages\"\n * const { t } = useI18n()\n * <Trans\n * t={t}\n * i18nKey=\"messages.unread\"\n * values={{ count: 5 }}\n * components={{\n * bold: (children) => <strong>{children}</strong>,\n * }}\n * />\n * // Renders: You have <strong>5</strong> unread messages\n *\n * @example\n * // Translation: \"Read our <terms>terms of service</terms> and <privacy>privacy policy</privacy>\"\n * <Trans\n * t={t}\n * i18nKey=\"legal\"\n * components={{\n * terms: (children) => <a href=\"/terms\">{children}</a>,\n * privacy: (children) => <a href=\"/privacy\">{children}</a>,\n * }}\n * />\n */\nexport function Trans(props: TransProps): VNode | string {\n const translated = props.t(props.i18nKey, props.values)\n\n if (!props.components) return translated\n\n const parts = parseRichText(translated)\n\n // If the result is a single plain string, return it directly\n if (parts.length === 1 && typeof parts[0] === \"string\") return parts[0]\n\n const children = parts.map((part) => {\n if (typeof part === \"string\") return part\n const component = props.components![part.tag]\n // Unmatched tags: render children as plain text (no raw HTML markup)\n if (!component) return part.children\n return component(part.children)\n })\n\n return <>{children}</>\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/interpolation.ts","../src/pluralization.ts","../src/create-i18n.ts","../src/trans.tsx"],"sourcesContent":["import type { Props, VNode, VNodeChild } from '@pyreon/core'\nimport { createContext, provide, useContext } from '@pyreon/core'\nimport type { I18nInstance } from './types'\n\nexport const I18nContext = createContext<I18nInstance | null>(null)\n\nexport interface I18nProviderProps extends Props {\n instance: I18nInstance\n children?: VNodeChild\n}\n\n/**\n * Provide an i18n instance to the component tree.\n *\n * @example\n * const i18n = createI18n({ locale: 'en', messages: { en: { hello: 'Hello' } } })\n *\n * // In JSX:\n * <I18nProvider instance={i18n}>\n * <App />\n * </I18nProvider>\n */\nexport function I18nProvider(props: I18nProviderProps): VNode {\n provide(I18nContext, props.instance)\n\n const ch = props.children\n return (typeof ch === 'function' ? (ch as () => VNodeChild)() : ch) as VNode\n}\n\n/**\n * Access the i18n instance from the nearest I18nProvider.\n * Must be called within a component tree wrapped by I18nProvider.\n *\n * @example\n * function Greeting() {\n * const { t, locale } = useI18n()\n * return <h1>{t('greeting', { name: 'World' })}</h1>\n * }\n */\nexport function useI18n(): I18nInstance {\n const instance = useContext(I18nContext)\n if (!instance) {\n throw new Error('[@pyreon/i18n] useI18n() must be used within an <I18nProvider>.')\n }\n return instance\n}\n","import type { InterpolationValues } from './types'\n\nconst INTERPOLATION_RE = /\\{\\{(\\s*\\w+\\s*)\\}\\}/g\n\n/**\n * Replace `{{key}}` placeholders in a string with values from the given record.\n * Supports optional whitespace inside braces: `{{ name }}` works too.\n * Unmatched placeholders are left as-is.\n */\nexport function interpolate(template: string, values?: InterpolationValues): string {\n if (!values || !template.includes('{{')) return template\n return template.replace(INTERPOLATION_RE, (_, key: string) => {\n const trimmed = key.trim()\n const value = values[trimmed]\n if (value === undefined) return `{{${trimmed}}}`\n // Safely coerce — guard against malicious toString/valueOf\n try {\n return typeof value === 'object' && value !== null ? JSON.stringify(value) : `${value}`\n } catch {\n return `{{${trimmed}}}`\n }\n })\n}\n","import type { PluralRules } from './types'\n\n/**\n * Resolve the plural category for a given count and locale.\n *\n * Uses custom rules if provided, otherwise falls back to `Intl.PluralRules`.\n * Returns CLDR plural categories: \"zero\", \"one\", \"two\", \"few\", \"many\", \"other\".\n */\nexport function resolvePluralCategory(\n locale: string,\n count: number,\n customRules?: PluralRules,\n): string {\n // Custom rules take priority\n if (customRules?.[locale]) {\n return customRules[locale](count)\n }\n\n // Use Intl.PluralRules if available\n if (typeof Intl !== 'undefined' && Intl.PluralRules) {\n try {\n const pr = new Intl.PluralRules(locale)\n return pr.select(count)\n } catch {\n // Invalid locale — fall through\n }\n }\n\n // Basic fallback\n return count === 1 ? 'one' : 'other'\n}\n","import { computed, signal } from '@pyreon/reactivity'\nimport { interpolate } from './interpolation'\nimport { resolvePluralCategory } from './pluralization'\nimport type { I18nInstance, I18nOptions, InterpolationValues, TranslationDictionary } from './types'\n\n/**\n * Resolve a dot-separated key path in a nested dictionary.\n * E.g. \"user.greeting\" → dictionary.user.greeting\n */\nfunction resolveKey(dict: TranslationDictionary, keyPath: string): string | undefined {\n const parts = keyPath.split('.')\n let current: TranslationDictionary | string = dict\n\n for (const part of parts) {\n if (current == null || typeof current === 'string') return undefined\n current = current[part] as TranslationDictionary | string\n }\n\n return typeof current === 'string' ? current : undefined\n}\n\n/**\n * Convert flat dotted keys into nested objects.\n * `{ 'section.title': 'Report' }` → `{ section: { title: 'Report' } }`\n * Keys that don't contain dots are passed through as-is.\n * Already-nested objects are preserved — only string values with dotted keys are expanded.\n */\nfunction nestFlatKeys(messages: TranslationDictionary): TranslationDictionary {\n const result: TranslationDictionary = {}\n let hasFlatKeys = false\n\n for (const key of Object.keys(messages)) {\n const value = messages[key]\n if (key.includes('.') && typeof value === 'string') {\n hasFlatKeys = true\n const parts = key.split('.')\n let current: TranslationDictionary = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] as string\n if (!(part in current) || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part] as TranslationDictionary\n }\n current[parts[parts.length - 1] as string] = value\n } else if (value !== undefined) {\n result[key] = value\n }\n }\n\n return hasFlatKeys ? result : messages\n}\n\n/**\n * Deep-merge source into target (mutates target).\n */\nfunction deepMerge(target: TranslationDictionary, source: TranslationDictionary): void {\n for (const key of Object.keys(source)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue\n const sourceVal = source[key]\n const targetVal = target[key]\n if (\n typeof sourceVal === 'object' &&\n sourceVal !== null &&\n typeof targetVal === 'object' &&\n targetVal !== null\n ) {\n deepMerge(targetVal as TranslationDictionary, sourceVal as TranslationDictionary)\n } else {\n target[key] = sourceVal!\n }\n }\n}\n\n/**\n * Create a reactive i18n instance.\n *\n * @example\n * const i18n = createI18n({\n * locale: 'en',\n * fallbackLocale: 'en',\n * messages: {\n * en: { greeting: 'Hello {{name}}!' },\n * de: { greeting: 'Hallo {{name}}!' },\n * },\n * })\n *\n * // Reactive translation — re-evaluates on locale change\n * i18n.t('greeting', { name: 'Alice' }) // \"Hello Alice!\"\n * i18n.locale.set('de')\n * i18n.t('greeting', { name: 'Alice' }) // \"Hallo Alice!\"\n *\n * @example\n * // Async namespace loading\n * const i18n = createI18n({\n * locale: 'en',\n * loader: async (locale, namespace) => {\n * const mod = await import(`./locales/${locale}/${namespace}.json`)\n * return mod.default\n * },\n * })\n * await i18n.loadNamespace('auth')\n * i18n.t('auth:errors.invalid') // looks up \"errors.invalid\" in \"auth\" namespace\n */\nexport function createI18n(options: I18nOptions): I18nInstance {\n const { fallbackLocale, loader, defaultNamespace = 'common', pluralRules, onMissingKey } = options\n\n // ── Reactive state ──────────────────────────────────────────────────\n\n const locale = signal(options.locale)\n\n // Internal store: locale → namespace → dictionary\n // We use a version counter to trigger reactive updates when messages change,\n // since the store is mutated in place (Object.is would skip same-reference sets).\n const store = new Map<string, Map<string, TranslationDictionary>>()\n const storeVersion = signal(0)\n\n // Loading state\n const pendingLoads = signal(0)\n const loadedNsVersion = signal(0)\n\n // In-flight load promises — deduplicates concurrent loads for the same locale:namespace\n const pendingPromises = new Map<string, Promise<void>>()\n\n const isLoading = computed(() => pendingLoads() > 0)\n const loadedNamespaces = computed(() => {\n loadedNsVersion()\n const currentLocale = locale()\n const nsMap = store.get(currentLocale)\n return new Set(nsMap ? nsMap.keys() : [])\n })\n const availableLocales = computed(() => {\n storeVersion() // subscribe to store changes\n return [...store.keys()]\n })\n\n // ── Initialize static messages ──────────────────────────────────────\n\n if (options.messages) {\n for (const [loc, dict] of Object.entries(options.messages)) {\n const nsMap = new Map<string, TranslationDictionary>()\n nsMap.set(defaultNamespace, dict)\n store.set(loc, nsMap)\n }\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n function getNamespaceMap(loc: string): Map<string, TranslationDictionary> {\n let nsMap = store.get(loc)\n if (!nsMap) {\n nsMap = new Map()\n store.set(loc, nsMap)\n }\n return nsMap\n }\n\n function lookupKey(loc: string, namespace: string, keyPath: string): string | undefined {\n const nsMap = store.get(loc)\n if (!nsMap) return undefined\n const dict = nsMap.get(namespace)\n if (!dict) return undefined\n return resolveKey(dict, keyPath)\n }\n\n function resolveTranslation(key: string, values?: InterpolationValues): string {\n // Subscribe to reactive dependencies\n const currentLocale = locale()\n storeVersion()\n\n // Parse key: \"namespace:key.path\" or just \"key.path\"\n let namespace = defaultNamespace\n let keyPath = key\n\n const colonIndex = key.indexOf(':')\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n // Handle pluralization: if values contain `count`, try plural suffixes\n if (values && 'count' in values) {\n const count = Number(values.count)\n const category = resolvePluralCategory(currentLocale, count, pluralRules)\n\n // Try exact form first (e.g. \"items_one\"), then fall back to base key\n const pluralKey = `${keyPath}_${category}`\n const pluralResult =\n lookupKey(currentLocale, namespace, pluralKey) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, pluralKey) : undefined)\n\n if (pluralResult) {\n return interpolate(pluralResult, values)\n }\n }\n\n // Standard lookup: current locale → fallback locale\n const result =\n lookupKey(currentLocale, namespace, keyPath) ??\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) : undefined)\n\n if (result !== undefined) {\n return interpolate(result, values)\n }\n\n // Missing key handler\n if (onMissingKey) {\n const custom = onMissingKey(currentLocale, key, namespace)\n if (custom !== undefined) return custom!\n }\n\n // Return the key itself as a visual fallback\n return key\n }\n\n // ── Public API ──────────────────────────────────────────────────────\n\n const t = (key: string, values?: InterpolationValues): string => {\n return resolveTranslation(key, values)\n }\n\n const loadNamespace = async (namespace: string, loc?: string): Promise<void> => {\n if (!loader) return\n\n const targetLocale = loc ?? locale.peek()\n const cacheKey = `${targetLocale}:${namespace}`\n const nsMap = getNamespaceMap(targetLocale)\n\n // Skip if already loaded\n if (nsMap.has(namespace)) return\n\n // Deduplicate concurrent loads for the same locale:namespace\n const existing = pendingPromises.get(cacheKey)\n if (existing) return existing\n\n pendingLoads.update((n) => n + 1)\n\n const promise = loader(targetLocale, namespace)\n .then((dict) => {\n if (dict) {\n nsMap.set(namespace, dict)\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n })\n .finally(() => {\n pendingPromises.delete(cacheKey)\n pendingLoads.update((n) => n - 1)\n })\n\n pendingPromises.set(cacheKey, promise)\n return promise\n }\n\n const exists = (key: string): boolean => {\n const currentLocale = locale.peek()\n\n let namespace = defaultNamespace\n let keyPath = key\n const colonIndex = key.indexOf(':')\n if (colonIndex > 0) {\n namespace = key.slice(0, colonIndex)\n keyPath = key.slice(colonIndex + 1)\n }\n\n return (\n lookupKey(currentLocale, namespace, keyPath) !== undefined ||\n (fallbackLocale ? lookupKey(fallbackLocale, namespace, keyPath) !== undefined : false)\n )\n }\n\n const addMessages = (loc: string, messages: TranslationDictionary, namespace?: string): void => {\n const ns = namespace ?? defaultNamespace\n const nsMap = getNamespaceMap(loc)\n const nested = nestFlatKeys(messages)\n const existing = nsMap.get(ns)\n\n if (existing) {\n deepMerge(existing, nested)\n } else {\n // Deep-clone to prevent external mutation from corrupting the store\n const cloned: TranslationDictionary = {}\n deepMerge(cloned, nested)\n nsMap.set(ns, cloned)\n }\n\n storeVersion.update((n) => n + 1)\n loadedNsVersion.update((n) => n + 1)\n }\n\n return {\n t,\n locale,\n loadNamespace,\n isLoading,\n loadedNamespaces,\n exists,\n addMessages,\n availableLocales,\n }\n}\n","import type { Props, VNode } from '@pyreon/core'\nimport type { InterpolationValues } from './types'\n\nconst TAG_RE = /<(\\w+)>([^<]*)<\\/\\1>/g\n\ninterface RichPart {\n tag: string\n children: string\n}\n\n/**\n * Parse a translated string into an array of plain text and rich tag segments.\n *\n * @example\n * parseRichText(\"Hello <bold>world</bold>, click <link>here</link>\")\n * // → [\"Hello \", { tag: \"bold\", children: \"world\" }, \", click \", { tag: \"link\", children: \"here\" }]\n */\nexport function parseRichText(text: string): (string | RichPart)[] {\n const parts: (string | RichPart)[] = []\n let lastIndex = 0\n\n for (const match of text.matchAll(TAG_RE)) {\n const before = text.slice(lastIndex, match.index)\n if (before) parts.push(before)\n parts.push({ tag: match[1]!, children: match[2]! })\n lastIndex = match.index! + match[0].length\n }\n\n const after = text.slice(lastIndex)\n if (after) parts.push(after)\n\n return parts\n}\n\nexport interface TransProps extends Props {\n /** Translation key (supports namespace:key syntax). */\n i18nKey: string\n /** Interpolation values for {{placeholder}} syntax. */\n values?: InterpolationValues\n /**\n * Component map for rich interpolation.\n * Keys match tag names in the translation string.\n * Values are component functions: `(children: any) => VNode`\n *\n * @example\n * // Translation: \"Read the <terms>terms</terms> and <privacy>policy</privacy>\"\n * components={{\n * terms: (children) => <a href=\"/terms\">{children}</a>,\n * privacy: (children) => <a href=\"/privacy\">{children}</a>,\n * }}\n */\n components?: Record<string, (children: any) => any>\n /**\n * The i18n instance's `t` function.\n * Can be obtained from `useI18n()` or passed directly.\n */\n t: (key: string, values?: InterpolationValues) => string\n}\n\n/**\n * Rich JSX interpolation component for translations.\n *\n * Allows embedding JSX components within translated strings using XML-like tags.\n * The `t` function resolves the translation and interpolates `{{values}}` first,\n * then `<tag>content</tag>` patterns are mapped to the provided components.\n *\n * @example\n * // Translation: \"You have <bold>{{count}}</bold> unread messages\"\n * const { t } = useI18n()\n * <Trans\n * t={t}\n * i18nKey=\"messages.unread\"\n * values={{ count: 5 }}\n * components={{\n * bold: (children) => <strong>{children}</strong>,\n * }}\n * />\n * // Renders: You have <strong>5</strong> unread messages\n *\n * @example\n * // Translation: \"Read our <terms>terms of service</terms> and <privacy>privacy policy</privacy>\"\n * <Trans\n * t={t}\n * i18nKey=\"legal\"\n * components={{\n * terms: (children) => <a href=\"/terms\">{children}</a>,\n * privacy: (children) => <a href=\"/privacy\">{children}</a>,\n * }}\n * />\n */\nexport function Trans(props: TransProps): VNode | string {\n const translated = props.t(props.i18nKey, props.values)\n\n if (!props.components) return translated\n\n const parts = parseRichText(translated)\n\n // If the result is a single plain string, return it directly\n if (parts.length === 1 && typeof parts[0] === 'string') return parts[0]\n\n const children = parts.map((part) => {\n if (typeof part === 'string') return part\n const component = props.components![part.tag]\n // Unmatched tags: render children as plain text (no raw HTML markup)\n if (!component) return part.children\n return component(part.children)\n })\n\n return <>{children}</>\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,20 +1,17 @@
1
1
  {
2
2
  "name": "@pyreon/i18n",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Reactive internationalization for Pyreon with async namespace loading",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/i18n#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/fundamentals/i18n"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/i18n#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
15
  "files": [
19
16
  "lib",
20
17
  "src",
@@ -43,21 +40,24 @@
43
40
  "types": "./lib/types/devtools.d.ts"
44
41
  }
45
42
  },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
46
  "scripts": {
47
47
  "build": "vl_rolldown_build",
48
48
  "dev": "vl_rolldown_build-watch",
49
49
  "test": "vitest run",
50
50
  "typecheck": "tsc --noEmit",
51
- "lint": "biome check ."
52
- },
53
- "peerDependencies": {
54
- "@pyreon/core": "^0.11.4",
55
- "@pyreon/reactivity": "^0.11.4"
51
+ "lint": "oxlint ."
56
52
  },
57
53
  "devDependencies": {
58
54
  "@happy-dom/global-registrator": "^20.8.3",
59
- "@pyreon/core": "^0.11.4",
60
- "@pyreon/reactivity": "^0.11.4",
61
- "@pyreon/runtime-dom": "^0.11.4"
55
+ "@pyreon/core": "^0.11.6",
56
+ "@pyreon/reactivity": "^0.11.6",
57
+ "@pyreon/runtime-dom": "^0.11.6"
58
+ },
59
+ "peerDependencies": {
60
+ "@pyreon/core": "^0.11.6",
61
+ "@pyreon/reactivity": "^0.11.6"
62
62
  }
63
63
  }
package/src/context.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { Props, VNode, VNodeChild } from "@pyreon/core"
2
- import { createContext, provide, useContext } from "@pyreon/core"
3
- import type { I18nInstance } from "./types"
1
+ import type { Props, VNode, VNodeChild } from '@pyreon/core'
2
+ import { createContext, provide, useContext } from '@pyreon/core'
3
+ import type { I18nInstance } from './types'
4
4
 
5
5
  export const I18nContext = createContext<I18nInstance | null>(null)
6
6
 
@@ -24,7 +24,7 @@ export function I18nProvider(props: I18nProviderProps): VNode {
24
24
  provide(I18nContext, props.instance)
25
25
 
26
26
  const ch = props.children
27
- return (typeof ch === "function" ? (ch as () => VNodeChild)() : ch) as VNode
27
+ return (typeof ch === 'function' ? (ch as () => VNodeChild)() : ch) as VNode
28
28
  }
29
29
 
30
30
  /**
@@ -40,7 +40,7 @@ export function I18nProvider(props: I18nProviderProps): VNode {
40
40
  export function useI18n(): I18nInstance {
41
41
  const instance = useContext(I18nContext)
42
42
  if (!instance) {
43
- throw new Error("[@pyreon/i18n] useI18n() must be used within an <I18nProvider>.")
43
+ throw new Error('[@pyreon/i18n] useI18n() must be used within an <I18nProvider>.')
44
44
  }
45
45
  return instance
46
46
  }
package/src/core.ts CHANGED
@@ -8,9 +8,9 @@
8
8
  * import { createI18n, interpolate } from "@pyreon/i18n/core"
9
9
  * ```
10
10
  */
11
- export { createI18n } from "./create-i18n"
12
- export { interpolate } from "./interpolation"
13
- export { resolvePluralCategory } from "./pluralization"
11
+ export { createI18n } from './create-i18n'
12
+ export { interpolate } from './interpolation'
13
+ export { resolvePluralCategory } from './pluralization'
14
14
  export type {
15
15
  I18nInstance,
16
16
  I18nOptions,
@@ -19,4 +19,4 @@ export type {
19
19
  PluralRules,
20
20
  TranslationDictionary,
21
21
  TranslationMessages,
22
- } from "./types"
22
+ } from './types'