@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.
@@ -1,22 +1,22 @@
1
- import { computed, signal } from "@pyreon/reactivity"
2
- import { interpolate } from "./interpolation"
3
- import { resolvePluralCategory } from "./pluralization"
4
- import type { I18nInstance, I18nOptions, InterpolationValues, TranslationDictionary } from "./types"
1
+ import { computed, signal } from '@pyreon/reactivity'
2
+ import { interpolate } from './interpolation'
3
+ import { resolvePluralCategory } from './pluralization'
4
+ import type { I18nInstance, I18nOptions, InterpolationValues, TranslationDictionary } from './types'
5
5
 
6
6
  /**
7
7
  * Resolve a dot-separated key path in a nested dictionary.
8
8
  * E.g. "user.greeting" → dictionary.user.greeting
9
9
  */
10
10
  function resolveKey(dict: TranslationDictionary, keyPath: string): string | undefined {
11
- const parts = keyPath.split(".")
11
+ const parts = keyPath.split('.')
12
12
  let current: TranslationDictionary | string = dict
13
13
 
14
14
  for (const part of parts) {
15
- if (current == null || typeof current === "string") return undefined
15
+ if (current == null || typeof current === 'string') return undefined
16
16
  current = current[part] as TranslationDictionary | string
17
17
  }
18
18
 
19
- return typeof current === "string" ? current : undefined
19
+ return typeof current === 'string' ? current : undefined
20
20
  }
21
21
 
22
22
  /**
@@ -31,13 +31,13 @@ function nestFlatKeys(messages: TranslationDictionary): TranslationDictionary {
31
31
 
32
32
  for (const key of Object.keys(messages)) {
33
33
  const value = messages[key]
34
- if (key.includes(".") && typeof value === "string") {
34
+ if (key.includes('.') && typeof value === 'string') {
35
35
  hasFlatKeys = true
36
- const parts = key.split(".")
36
+ const parts = key.split('.')
37
37
  let current: TranslationDictionary = result
38
38
  for (let i = 0; i < parts.length - 1; i++) {
39
39
  const part = parts[i] as string
40
- if (!(part in current) || typeof current[part] !== "object") {
40
+ if (!(part in current) || typeof current[part] !== 'object') {
41
41
  current[part] = {}
42
42
  }
43
43
  current = current[part] as TranslationDictionary
@@ -56,13 +56,13 @@ function nestFlatKeys(messages: TranslationDictionary): TranslationDictionary {
56
56
  */
57
57
  function deepMerge(target: TranslationDictionary, source: TranslationDictionary): void {
58
58
  for (const key of Object.keys(source)) {
59
- if (key === "__proto__" || key === "constructor" || key === "prototype") continue
59
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
60
60
  const sourceVal = source[key]
61
61
  const targetVal = target[key]
62
62
  if (
63
- typeof sourceVal === "object" &&
63
+ typeof sourceVal === 'object' &&
64
64
  sourceVal !== null &&
65
- typeof targetVal === "object" &&
65
+ typeof targetVal === 'object' &&
66
66
  targetVal !== null
67
67
  ) {
68
68
  deepMerge(targetVal as TranslationDictionary, sourceVal as TranslationDictionary)
@@ -103,7 +103,7 @@ function deepMerge(target: TranslationDictionary, source: TranslationDictionary)
103
103
  * i18n.t('auth:errors.invalid') // looks up "errors.invalid" in "auth" namespace
104
104
  */
105
105
  export function createI18n(options: I18nOptions): I18nInstance {
106
- const { fallbackLocale, loader, defaultNamespace = "common", pluralRules, onMissingKey } = options
106
+ const { fallbackLocale, loader, defaultNamespace = 'common', pluralRules, onMissingKey } = options
107
107
 
108
108
  // ── Reactive state ──────────────────────────────────────────────────
109
109
 
@@ -172,14 +172,14 @@ export function createI18n(options: I18nOptions): I18nInstance {
172
172
  let namespace = defaultNamespace
173
173
  let keyPath = key
174
174
 
175
- const colonIndex = key.indexOf(":")
175
+ const colonIndex = key.indexOf(':')
176
176
  if (colonIndex > 0) {
177
177
  namespace = key.slice(0, colonIndex)
178
178
  keyPath = key.slice(colonIndex + 1)
179
179
  }
180
180
 
181
181
  // Handle pluralization: if values contain `count`, try plural suffixes
182
- if (values && "count" in values) {
182
+ if (values && 'count' in values) {
183
183
  const count = Number(values.count)
184
184
  const category = resolvePluralCategory(currentLocale, count, pluralRules)
185
185
 
@@ -257,7 +257,7 @@ export function createI18n(options: I18nOptions): I18nInstance {
257
257
 
258
258
  let namespace = defaultNamespace
259
259
  let keyPath = key
260
- const colonIndex = key.indexOf(":")
260
+ const colonIndex = key.indexOf(':')
261
261
  if (colonIndex > 0) {
262
262
  namespace = key.slice(0, colonIndex)
263
263
  keyPath = key.slice(colonIndex + 1)
package/src/devtools.ts CHANGED
@@ -56,7 +56,7 @@ function safeRead(
56
56
  ): unknown {
57
57
  try {
58
58
  const val = obj[key]
59
- return typeof val === "function" ? (val as () => unknown)() : fallback
59
+ return typeof val === 'function' ? (val as () => unknown)() : fallback
60
60
  } catch {
61
61
  return fallback
62
62
  }
@@ -68,12 +68,12 @@ function safeRead(
68
68
  export function getI18nSnapshot(name: string): Record<string, unknown> | undefined {
69
69
  const instance = getI18nInstance(name) as Record<string, unknown> | undefined
70
70
  if (!instance) return undefined
71
- const ns = safeRead(instance, "loadedNamespaces", new Set())
71
+ const ns = safeRead(instance, 'loadedNamespaces', new Set())
72
72
  return {
73
- locale: safeRead(instance, "locale"),
74
- availableLocales: safeRead(instance, "availableLocales", []),
73
+ locale: safeRead(instance, 'locale'),
74
+ availableLocales: safeRead(instance, 'availableLocales', []),
75
75
  loadedNamespaces: ns instanceof Set ? [...ns] : [],
76
- isLoading: safeRead(instance, "isLoading", false),
76
+ isLoading: safeRead(instance, 'isLoading', false),
77
77
  }
78
78
  }
79
79
 
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
- export type { I18nProviderProps } from "./context"
2
- export { I18nContext, I18nProvider, useI18n } from "./context"
3
- export { createI18n } from "./create-i18n"
4
- export { interpolate } from "./interpolation"
5
- export { resolvePluralCategory } from "./pluralization"
6
- export type { TransProps } from "./trans"
7
- export { parseRichText, Trans } from "./trans"
1
+ export type { I18nProviderProps } from './context'
2
+ export { I18nContext, I18nProvider, useI18n } from './context'
3
+ export { createI18n } from './create-i18n'
4
+ export { interpolate } from './interpolation'
5
+ export { resolvePluralCategory } from './pluralization'
6
+ export type { TransProps } from './trans'
7
+ export { parseRichText, Trans } from './trans'
8
8
  export type {
9
9
  I18nInstance,
10
10
  I18nOptions,
@@ -13,4 +13,4 @@ export type {
13
13
  PluralRules,
14
14
  TranslationDictionary,
15
15
  TranslationMessages,
16
- } from "./types"
16
+ } from './types'
@@ -1,4 +1,4 @@
1
- import type { InterpolationValues } from "./types"
1
+ import type { InterpolationValues } from './types'
2
2
 
3
3
  const INTERPOLATION_RE = /\{\{(\s*\w+\s*)\}\}/g
4
4
 
@@ -8,14 +8,14 @@ const INTERPOLATION_RE = /\{\{(\s*\w+\s*)\}\}/g
8
8
  * Unmatched placeholders are left as-is.
9
9
  */
10
10
  export function interpolate(template: string, values?: InterpolationValues): string {
11
- if (!values || !template.includes("{{")) return template
11
+ if (!values || !template.includes('{{')) return template
12
12
  return template.replace(INTERPOLATION_RE, (_, key: string) => {
13
13
  const trimmed = key.trim()
14
14
  const value = values[trimmed]
15
15
  if (value === undefined) return `{{${trimmed}}}`
16
16
  // Safely coerce — guard against malicious toString/valueOf
17
17
  try {
18
- return typeof value === "object" && value !== null ? JSON.stringify(value) : `${value}`
18
+ return typeof value === 'object' && value !== null ? JSON.stringify(value) : `${value}`
19
19
  } catch {
20
20
  return `{{${trimmed}}}`
21
21
  }
@@ -1,4 +1,4 @@
1
- import type { PluralRules } from "./types"
1
+ import type { PluralRules } from './types'
2
2
 
3
3
  /**
4
4
  * Resolve the plural category for a given count and locale.
@@ -17,7 +17,7 @@ export function resolvePluralCategory(
17
17
  }
18
18
 
19
19
  // Use Intl.PluralRules if available
20
- if (typeof Intl !== "undefined" && Intl.PluralRules) {
20
+ if (typeof Intl !== 'undefined' && Intl.PluralRules) {
21
21
  try {
22
22
  const pr = new Intl.PluralRules(locale)
23
23
  return pr.select(count)
@@ -27,5 +27,5 @@ export function resolvePluralCategory(
27
27
  }
28
28
 
29
29
  // Basic fallback
30
- return count === 1 ? "one" : "other"
30
+ return count === 1 ? 'one' : 'other'
31
31
  }
@@ -1,4 +1,4 @@
1
- import { createI18n } from "../create-i18n"
1
+ import { createI18n } from '../create-i18n'
2
2
  import {
3
3
  _resetDevtools,
4
4
  getActiveI18nInstances,
@@ -7,62 +7,62 @@ import {
7
7
  onI18nChange,
8
8
  registerI18n,
9
9
  unregisterI18n,
10
- } from "../devtools"
10
+ } from '../devtools'
11
11
 
12
12
  afterEach(() => _resetDevtools())
13
13
 
14
- describe("i18n devtools", () => {
15
- test("getActiveI18nInstances returns empty initially", () => {
14
+ describe('i18n devtools', () => {
15
+ test('getActiveI18nInstances returns empty initially', () => {
16
16
  expect(getActiveI18nInstances()).toEqual([])
17
17
  })
18
18
 
19
- test("registerI18n makes instance visible", () => {
20
- const i18n = createI18n({ locale: "en", messages: { en: { hi: "Hello" } } })
21
- registerI18n("app", i18n)
22
- expect(getActiveI18nInstances()).toEqual(["app"])
19
+ test('registerI18n makes instance visible', () => {
20
+ const i18n = createI18n({ locale: 'en', messages: { en: { hi: 'Hello' } } })
21
+ registerI18n('app', i18n)
22
+ expect(getActiveI18nInstances()).toEqual(['app'])
23
23
  })
24
24
 
25
- test("getI18nInstance returns the registered instance", () => {
26
- const i18n = createI18n({ locale: "en", messages: { en: { hi: "Hello" } } })
27
- registerI18n("app", i18n)
28
- expect(getI18nInstance("app")).toBe(i18n)
25
+ test('getI18nInstance returns the registered instance', () => {
26
+ const i18n = createI18n({ locale: 'en', messages: { en: { hi: 'Hello' } } })
27
+ registerI18n('app', i18n)
28
+ expect(getI18nInstance('app')).toBe(i18n)
29
29
  })
30
30
 
31
- test("getI18nInstance returns undefined for unregistered name", () => {
32
- expect(getI18nInstance("nope")).toBeUndefined()
31
+ test('getI18nInstance returns undefined for unregistered name', () => {
32
+ expect(getI18nInstance('nope')).toBeUndefined()
33
33
  })
34
34
 
35
- test("unregisterI18n removes the instance", () => {
36
- const i18n = createI18n({ locale: "en" })
37
- registerI18n("app", i18n)
38
- unregisterI18n("app")
35
+ test('unregisterI18n removes the instance', () => {
36
+ const i18n = createI18n({ locale: 'en' })
37
+ registerI18n('app', i18n)
38
+ unregisterI18n('app')
39
39
  expect(getActiveI18nInstances()).toEqual([])
40
40
  })
41
41
 
42
- test("getI18nSnapshot returns current state", () => {
42
+ test('getI18nSnapshot returns current state', () => {
43
43
  const i18n = createI18n({
44
- locale: "en",
45
- messages: { en: { hi: "Hello" }, de: { hi: "Hallo" } },
44
+ locale: 'en',
45
+ messages: { en: { hi: 'Hello' }, de: { hi: 'Hallo' } },
46
46
  })
47
- registerI18n("app", i18n)
48
- const snapshot = getI18nSnapshot("app")
47
+ registerI18n('app', i18n)
48
+ const snapshot = getI18nSnapshot('app')
49
49
  expect(snapshot).toBeDefined()
50
- expect(snapshot!.locale).toBe("en")
51
- expect(snapshot!.availableLocales).toEqual(expect.arrayContaining(["en", "de"]))
50
+ expect(snapshot!.locale).toBe('en')
51
+ expect(snapshot!.availableLocales).toEqual(expect.arrayContaining(['en', 'de']))
52
52
  expect(snapshot!.isLoading).toBe(false)
53
53
  })
54
54
 
55
- test("getI18nSnapshot handles instance with non-function properties", () => {
55
+ test('getI18nSnapshot handles instance with non-function properties', () => {
56
56
  // Register a plain object where properties are NOT functions
57
57
  // This covers the false branches of typeof checks in getI18nSnapshot
58
58
  const plainInstance = {
59
- locale: "not-a-function",
59
+ locale: 'not-a-function',
60
60
  availableLocales: 42,
61
61
  loadedNamespaces: null,
62
62
  isLoading: undefined,
63
63
  }
64
- registerI18n("plain", plainInstance)
65
- const snapshot = getI18nSnapshot("plain")
64
+ registerI18n('plain', plainInstance)
65
+ const snapshot = getI18nSnapshot('plain')
66
66
  expect(snapshot).toBeDefined()
67
67
  expect(snapshot!.locale).toBeUndefined()
68
68
  expect(snapshot!.availableLocales).toEqual([])
@@ -70,56 +70,56 @@ describe("i18n devtools", () => {
70
70
  expect(snapshot!.isLoading).toBe(false)
71
71
  })
72
72
 
73
- test("getI18nSnapshot reflects locale change", () => {
74
- const i18n = createI18n({ locale: "en", messages: { en: {}, de: {} } })
75
- registerI18n("app", i18n)
76
- i18n.locale.set("de")
77
- const snapshot = getI18nSnapshot("app")
78
- expect(snapshot!.locale).toBe("de")
73
+ test('getI18nSnapshot reflects locale change', () => {
74
+ const i18n = createI18n({ locale: 'en', messages: { en: {}, de: {} } })
75
+ registerI18n('app', i18n)
76
+ i18n.locale.set('de')
77
+ const snapshot = getI18nSnapshot('app')
78
+ expect(snapshot!.locale).toBe('de')
79
79
  })
80
80
 
81
- test("getI18nSnapshot returns undefined for unregistered name", () => {
82
- expect(getI18nSnapshot("nope")).toBeUndefined()
81
+ test('getI18nSnapshot returns undefined for unregistered name', () => {
82
+ expect(getI18nSnapshot('nope')).toBeUndefined()
83
83
  })
84
84
 
85
- test("onI18nChange fires on register", () => {
85
+ test('onI18nChange fires on register', () => {
86
86
  const calls: number[] = []
87
87
  const unsub = onI18nChange(() => calls.push(1))
88
88
 
89
- registerI18n("app", createI18n({ locale: "en" }))
89
+ registerI18n('app', createI18n({ locale: 'en' }))
90
90
  expect(calls.length).toBe(1)
91
91
 
92
92
  unsub()
93
93
  })
94
94
 
95
- test("onI18nChange fires on unregister", () => {
96
- registerI18n("app", createI18n({ locale: "en" }))
95
+ test('onI18nChange fires on unregister', () => {
96
+ registerI18n('app', createI18n({ locale: 'en' }))
97
97
 
98
98
  const calls: number[] = []
99
99
  const unsub = onI18nChange(() => calls.push(1))
100
- unregisterI18n("app")
100
+ unregisterI18n('app')
101
101
  expect(calls.length).toBe(1)
102
102
 
103
103
  unsub()
104
104
  })
105
105
 
106
- test("onI18nChange unsubscribe stops notifications", () => {
106
+ test('onI18nChange unsubscribe stops notifications', () => {
107
107
  const calls: number[] = []
108
108
  const unsub = onI18nChange(() => calls.push(1))
109
109
  unsub()
110
110
 
111
- registerI18n("app", createI18n({ locale: "en" }))
111
+ registerI18n('app', createI18n({ locale: 'en' }))
112
112
  expect(calls.length).toBe(0)
113
113
  })
114
114
 
115
- test("multiple instances are tracked", () => {
116
- registerI18n("app", createI18n({ locale: "en" }))
117
- registerI18n("admin", createI18n({ locale: "en" }))
118
- expect(getActiveI18nInstances().sort()).toEqual(["admin", "app"])
115
+ test('multiple instances are tracked', () => {
116
+ registerI18n('app', createI18n({ locale: 'en' }))
117
+ registerI18n('admin', createI18n({ locale: 'en' }))
118
+ expect(getActiveI18nInstances().sort()).toEqual(['admin', 'app'])
119
119
  })
120
120
 
121
- test("getI18nInstance cleans up and returns undefined when WeakRef is dead", () => {
122
- const instance = createI18n({ locale: "en" })
121
+ test('getI18nInstance cleans up and returns undefined when WeakRef is dead', () => {
122
+ const instance = createI18n({ locale: 'en' })
123
123
  const originalWeakRef = globalThis.WeakRef
124
124
  let mockDerefResult: object | undefined = instance
125
125
  const MockWeakRef = class {
@@ -130,18 +130,18 @@ describe("i18n devtools", () => {
130
130
  globalThis.WeakRef = MockWeakRef as any
131
131
 
132
132
  _resetDevtools()
133
- registerI18n("mock-instance", instance)
134
- expect(getI18nInstance("mock-instance")).toBe(instance)
133
+ registerI18n('mock-instance', instance)
134
+ expect(getI18nInstance('mock-instance')).toBe(instance)
135
135
 
136
136
  // Simulate GC
137
137
  mockDerefResult = undefined
138
- expect(getI18nInstance("mock-instance")).toBeUndefined()
138
+ expect(getI18nInstance('mock-instance')).toBeUndefined()
139
139
 
140
140
  globalThis.WeakRef = originalWeakRef
141
141
  })
142
142
 
143
- test("getActiveI18nInstances cleans up garbage-collected WeakRefs", () => {
144
- const instance = createI18n({ locale: "en" })
143
+ test('getActiveI18nInstances cleans up garbage-collected WeakRefs', () => {
144
+ const instance = createI18n({ locale: 'en' })
145
145
  const originalWeakRef = globalThis.WeakRef
146
146
  let mockDerefResult: object | undefined = instance
147
147
  const MockWeakRef = class {
@@ -152,8 +152,8 @@ describe("i18n devtools", () => {
152
152
  globalThis.WeakRef = MockWeakRef as any
153
153
 
154
154
  _resetDevtools()
155
- registerI18n("gc-instance", instance)
156
- expect(getActiveI18nInstances()).toEqual(["gc-instance"])
155
+ registerI18n('gc-instance', instance)
156
+ expect(getActiveI18nInstances()).toEqual(['gc-instance'])
157
157
 
158
158
  // Simulate GC
159
159
  mockDerefResult = undefined