@vielzeug/lingua 1.1.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +15 -54
  2. package/dist/_catalog.cjs +1 -1
  3. package/dist/_catalog.cjs.map +1 -1
  4. package/dist/_catalog.d.ts +13 -16
  5. package/dist/_catalog.d.ts.map +1 -1
  6. package/dist/_catalog.js +43 -36
  7. package/dist/_catalog.js.map +1 -1
  8. package/dist/_dev.cjs +1 -1
  9. package/dist/_dev.cjs.map +1 -1
  10. package/dist/_dev.js +3 -11
  11. package/dist/_dev.js.map +1 -1
  12. package/dist/_locale.cjs +2 -0
  13. package/dist/_locale.cjs.map +1 -0
  14. package/dist/_locale.d.ts +5 -0
  15. package/dist/_locale.d.ts.map +1 -0
  16. package/dist/_locale.js +26 -0
  17. package/dist/_locale.js.map +1 -0
  18. package/dist/_resources.cjs +2 -0
  19. package/dist/_resources.cjs.map +1 -0
  20. package/dist/_resources.d.ts +10 -0
  21. package/dist/_resources.d.ts.map +1 -0
  22. package/dist/_resources.js +51 -0
  23. package/dist/_resources.js.map +1 -0
  24. package/dist/_template.cjs +2 -0
  25. package/dist/_template.cjs.map +1 -0
  26. package/dist/_template.d.ts +8 -0
  27. package/dist/_template.d.ts.map +1 -0
  28. package/dist/_template.js +32 -0
  29. package/dist/_template.js.map +1 -0
  30. package/dist/errors.cjs +1 -1
  31. package/dist/errors.cjs.map +1 -1
  32. package/dist/errors.d.ts +6 -18
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +3 -9
  35. package/dist/errors.js.map +1 -1
  36. package/dist/format.cjs +1 -1
  37. package/dist/format.cjs.map +1 -1
  38. package/dist/format.d.ts +0 -9
  39. package/dist/format.d.ts.map +1 -1
  40. package/dist/format.js +10 -12
  41. package/dist/format.js.map +1 -1
  42. package/dist/i18n.cjs +1 -1
  43. package/dist/i18n.cjs.map +1 -1
  44. package/dist/i18n.d.ts +25 -11
  45. package/dist/i18n.d.ts.map +1 -1
  46. package/dist/i18n.js +66 -183
  47. package/dist/i18n.js.map +1 -1
  48. package/dist/index.cjs +1 -1
  49. package/dist/index.d.ts +4 -12
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +4 -4
  52. package/dist/lingua.cjs +1 -1
  53. package/dist/lingua.cjs.map +1 -1
  54. package/dist/lingua.iife.js +1 -1
  55. package/dist/lingua.iife.js.map +1 -1
  56. package/dist/lingua.js +1 -1
  57. package/dist/lingua.js.map +1 -1
  58. package/dist/translator.cjs +2 -0
  59. package/dist/translator.cjs.map +1 -0
  60. package/dist/translator.d.ts +20 -0
  61. package/dist/translator.d.ts.map +1 -0
  62. package/dist/translator.js +61 -0
  63. package/dist/translator.js.map +1 -0
  64. package/dist/types.d.ts +53 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/validate.cjs +1 -1
  67. package/dist/validate.cjs.map +1 -1
  68. package/dist/validate.d.ts +5 -37
  69. package/dist/validate.d.ts.map +1 -1
  70. package/dist/validate.js +21 -52
  71. package/dist/validate.js.map +1 -1
  72. package/package.json +4 -4
  73. package/dist/_catalog-store.cjs +0 -2
  74. package/dist/_catalog-store.cjs.map +0 -1
  75. package/dist/_catalog-store.d.ts +0 -34
  76. package/dist/_catalog-store.d.ts.map +0 -1
  77. package/dist/_catalog-store.js +0 -81
  78. package/dist/_catalog-store.js.map +0 -1
  79. package/dist/_chain.cjs +0 -2
  80. package/dist/_chain.cjs.map +0 -1
  81. package/dist/_chain.d.ts +0 -21
  82. package/dist/_chain.d.ts.map +0 -1
  83. package/dist/_chain.js +0 -45
  84. package/dist/_chain.js.map +0 -1
  85. package/dist/_constants.cjs +0 -2
  86. package/dist/_constants.cjs.map +0 -1
  87. package/dist/_constants.d.ts +0 -3
  88. package/dist/_constants.d.ts.map +0 -1
  89. package/dist/_constants.js +0 -17
  90. package/dist/_constants.js.map +0 -1
  91. package/dist/_namespace-store.cjs +0 -2
  92. package/dist/_namespace-store.cjs.map +0 -1
  93. package/dist/_namespace-store.d.ts +0 -31
  94. package/dist/_namespace-store.d.ts.map +0 -1
  95. package/dist/_namespace-store.js +0 -54
  96. package/dist/_namespace-store.js.map +0 -1
  97. package/dist/_translate.cjs +0 -2
  98. package/dist/_translate.cjs.map +0 -1
  99. package/dist/_translate.d.ts +0 -21
  100. package/dist/_translate.d.ts.map +0 -1
  101. package/dist/_translate.js +0 -54
  102. package/dist/_translate.js.map +0 -1
  103. package/dist/i18n-types.d.ts +0 -261
  104. package/dist/i18n-types.d.ts.map +0 -1
  105. package/dist/template-BZRFdmaQ.cjs +0 -2
  106. package/dist/template-BZRFdmaQ.cjs.map +0 -1
  107. package/dist/template-BihTfBYb.js +0 -2
  108. package/dist/template-BihTfBYb.js.map +0 -1
  109. package/dist/template.cjs +0 -2
  110. package/dist/template.cjs.map +0 -1
  111. package/dist/template.d.ts +0 -15
  112. package/dist/template.d.ts.map +0 -1
  113. package/dist/template.js +0 -49
  114. package/dist/template.js.map +0 -1
  115. package/dist/validate-D1U8TTeT.js +0 -2
  116. package/dist/validate-D1U8TTeT.js.map +0 -1
  117. package/dist/validate-x7lFFyV2.cjs +0 -2
  118. package/dist/validate-x7lFFyV2.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"lingua.js","names":[],"sources":["../src/_bounded-cache.ts","../src/format.ts","../src/_catalog.ts","../src/errors.ts","../src/_catalog-store.ts","../src/_chain.ts","../src/_namespace-store.ts","../src/_translate.ts","../src/i18n.ts"],"sourcesContent":["// Internal — not part of the public API.\n// Minimal bounded cache: evicts the oldest entry (insertion order) once `maxSize` is exceeded.\n// Not LRU — no access-time reordering, only insertion order matters. These caches exist purely\n// to avoid rebuilding cheap-but-not-free values (compiled `Intl` formatters, scoped translator\n// objects) for a bounded number of frequently-reused keys, not to model real recency.\n\nexport function getOrCreate<K, V>(cache: Map<K, V>, key: K, maxSize: number, build: () => V): V {\n if (cache.has(key)) return cache.get(key)!;\n\n const value = build();\n\n if (cache.size >= maxSize) cache.delete(cache.keys().next().value as K);\n\n cache.set(key, value);\n\n return value;\n}\n","/**\n * /lingua/format\n *\n * Standalone Intl formatter factory. Import from `/lingua/format`.\n * Creates formatters for numbers, dates, currencies, lists, durations, and relative time.\n *\n * Pass a static locale string or a reactive locale getter.\n *\n * @example\n * ```ts\n * import { createFormatter } from '@vielzeug/lingua/format';\n *\n * // Static locale\n * const fmt = createFormatter('en-US');\n *\n * // Reactive — always reads the current locale from the i18n instance\n * const fmt = createFormatter(() => i18n.locale);\n *\n * fmt.number(1_234.56);\n * fmt.currency(9.99, 'USD');\n * fmt.date(new Date());\n * fmt.relative(-1, 'day');\n * fmt.list(['A', 'B', 'C']);\n * fmt.duration({ hours: 1, minutes: 30 });\n * ```\n */\n\nimport { getOrCreate } from './_bounded-cache';\n\nconst FORMAT_CACHE_MAX = 128;\n\nexport type DurationValue = Partial<\n Record<\n | 'days'\n | 'hours'\n | 'microseconds'\n | 'milliseconds'\n | 'minutes'\n | 'months'\n | 'nanoseconds'\n | 'seconds'\n | 'weeks'\n | 'years',\n number\n >\n>;\n\nexport type DurationFormatOptions = {\n hours?: '2-digit' | 'numeric';\n microseconds?: 'numeric';\n milliseconds?: 'numeric';\n minutes?: '2-digit' | 'numeric';\n nanoseconds?: 'numeric';\n seconds?: '2-digit' | 'numeric';\n style?: 'digital' | 'long' | 'narrow' | 'short';\n};\n\nexport type ListFormatOptions = {\n style?: 'long' | 'narrow' | 'short';\n type?: 'and' | 'or';\n};\n\nexport type Formatter = {\n /**\n * Clears all cached `Intl` formatter instances. Useful after a locale change on a long-running\n * SSR server, or to reclaim memory.\n *\n * @note This clears the cache for **all callers sharing this formatter instance**. When called on\n * `i18n.fmt`, the cache is cleared for every part of the application that reads `i18n.fmt`.\n * `setLocale()` already calls `clear()` automatically — manual calls are rarely needed.\n */\n clear(): void;\n currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'currency' | 'style'>): string;\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;\n /**\n * Formats a duration using `Intl.DurationFormat` when available, or a compact labeled fallback\n * (e.g. `'1h 30min'`). Returns `''` when every field of `value` is `undefined`.\n *\n * @note The fallback labels (`h`, `min`, `s`, `ms`, etc.) are English-only and are not\n * locale-aware. If `Intl.DurationFormat` is unavailable in your target environment and\n * you need localised duration output, provide your own formatter using the individual\n * duration fields from `DurationValue`.\n */\n duration(value: DurationValue, options?: DurationFormatOptions): string;\n list(value: Array<string | number>, options?: ListFormatOptions): string;\n /** Formats a number with the current locale. */\n number(value: number, options?: Intl.NumberFormatOptions): string;\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;\n};\n\n// ─── Internal ─────────────────────────────────────────────────────────────────\n\nconst DURATION_UNITS: Array<keyof DurationValue> = [\n 'years',\n 'months',\n 'weeks',\n 'days',\n 'hours',\n 'minutes',\n 'seconds',\n 'milliseconds',\n 'microseconds',\n 'nanoseconds',\n];\n\nconst DURATION_LABELS: Record<keyof DurationValue, string> = {\n days: 'd',\n hours: 'h',\n microseconds: 'us',\n milliseconds: 'ms',\n minutes: 'min',\n months: 'mo',\n nanoseconds: 'ns',\n seconds: 's',\n weeks: 'w',\n years: 'y',\n};\n\ntype DurationFormatterCtor = new (\n locale: string,\n options?: DurationFormatOptions,\n) => { format(value: DurationValue): string };\n\nfunction durationLabeled(value: DurationValue): string {\n return DURATION_UNITS.map((unit) => {\n const amount = value[unit];\n\n return typeof amount === 'number' ? `${amount}${DURATION_LABELS[unit]}` : undefined;\n })\n .filter((part): part is string => part !== undefined)\n .join(' ');\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\n/**\n * Creates a formatter bound to a locale. Pass a static locale string or a\n * reactive getter function `() => string` (e.g. `() => i18n.locale`).\n */\nexport function createFormatter(source: string | (() => string)): Formatter {\n const numberCache = new Map<string, Intl.NumberFormat>();\n const currencyCache = new Map<string, Intl.NumberFormat>();\n const dateCache = new Map<string, Intl.DateTimeFormat>();\n const relativeCache = new Map<string, Intl.RelativeTimeFormat>();\n const listCache = new Map<string, Intl.ListFormat>();\n const durationCache = new Map<string, { format(value: DurationValue): string }>();\n\n const getLocale = typeof source === 'string' ? () => source : source;\n\n function cachedKey(locale: string, options?: object): string {\n if (!options) return locale;\n\n const sorted: Record<string, unknown> = {};\n\n for (const key of Object.keys(options as Record<string, unknown>).sort()) {\n sorted[key] = (options as Record<string, unknown>)[key];\n }\n\n try {\n return `${locale}:${JSON.stringify(sorted)}`;\n } catch {\n // Circular references or BigInt values — fall back to locale-only key.\n // Multiple callers with different unserializable options will share the same formatter instance.\n return locale;\n }\n }\n\n return {\n clear() {\n numberCache.clear();\n currencyCache.clear();\n dateCache.clear();\n relativeCache.clear();\n listCache.clear();\n durationCache.clear();\n },\n\n currency(value, currency, options) {\n const locale = getLocale();\n const opts: Intl.NumberFormatOptions = { ...options, currency, style: 'currency' };\n\n return getOrCreate(\n currencyCache,\n cachedKey(locale, opts),\n FORMAT_CACHE_MAX,\n () => new Intl.NumberFormat(locale, opts),\n ).format(value);\n },\n\n date(value, options) {\n const locale = getLocale();\n\n return getOrCreate(\n dateCache,\n cachedKey(locale, options),\n FORMAT_CACHE_MAX,\n () => new Intl.DateTimeFormat(locale, options),\n ).format(value);\n },\n\n duration(value, options) {\n const locale = getLocale();\n const IntlExt = Intl as typeof Intl & { DurationFormat?: DurationFormatterCtor };\n\n if (!IntlExt.DurationFormat) return durationLabeled(value);\n\n if (DURATION_UNITS.every((u) => value[u] === undefined)) return '';\n\n return getOrCreate(\n durationCache,\n cachedKey(locale, options),\n FORMAT_CACHE_MAX,\n () => new IntlExt.DurationFormat!(locale, options),\n ).format(value);\n },\n\n list(value, options) {\n const locale = getLocale();\n const items = value.map((v) => String(v));\n // list() has exactly two string options with small finite value domains.\n // We normalize both to their resolved defaults and build the key directly\n // rather than using cachedKey(). This also prevents cache misses when\n // callers pass { style: 'long', type: 'and' } vs {} (same ListFormat).\n const style = options?.style ?? 'long';\n const type = options?.type === 'or' ? 'disjunction' : 'conjunction';\n\n return getOrCreate(\n listCache,\n cachedKey(locale, { style, type }),\n FORMAT_CACHE_MAX,\n () => new Intl.ListFormat(locale, { style, type }),\n ).format(items);\n },\n\n number(value, options) {\n const locale = getLocale();\n\n return getOrCreate(\n numberCache,\n cachedKey(locale, options),\n FORMAT_CACHE_MAX,\n () => new Intl.NumberFormat(locale, options),\n ).format(value);\n },\n\n relative(value, unit, options) {\n const locale = getLocale();\n\n return getOrCreate(\n relativeCache,\n cachedKey(locale, options),\n FORMAT_CACHE_MAX,\n () => new Intl.RelativeTimeFormat(locale, options),\n ).format(value, unit);\n },\n };\n}\n","// Internal — not part of the public API.\n// Encapsulates the CatalogEntry class, flattenStrings, and related utilities.\n\nimport { UNSAFE_KEYS } from './_constants';\nimport { compileTemplate, type CompiledTemplate, parsePipePlural } from './template';\n\nexport interface Messages {\n [key: string]: string | Messages;\n}\n\nexport type CatalogEntryData = {\n compiled: CompiledTemplate;\n message: string;\n};\n\nexport class CatalogEntry {\n readonly entries = new Map<string, CatalogEntryData>();\n readonly prefixes = new Set<string>();\n\n get(key: string): CatalogEntryData | undefined {\n return this.entries.get(key);\n }\n\n set(key: string, value: string): void {\n this.entries.set(key, { compiled: compileTemplate(value), message: value });\n\n // Populate prefix set for O(1) branch detection in has().\n let dot = key.indexOf('.');\n\n while (dot !== -1) {\n this.prefixes.add(key.slice(0, dot));\n dot = key.indexOf('.', dot + 1);\n }\n }\n\n setAll(flat: Iterable<[string, string]>): void {\n for (const [k, v] of flat) this.set(k, v);\n }\n\n clone(): CatalogEntry {\n const copy = new CatalogEntry();\n\n for (const [k, v] of this.entries) copy.entries.set(k, v);\n for (const p of this.prefixes) copy.prefixes.add(p);\n\n return copy;\n }\n}\n\nexport function flattenStrings(\n messages: Messages,\n result = new Map<string, string>(),\n prefix?: string,\n): Map<string, string> {\n for (const [key, value] of Object.entries(messages)) {\n if (UNSAFE_KEYS.has(key)) continue;\n\n const fullKey = prefix ? `${prefix}.${key}` : key;\n\n if (typeof value === 'string') {\n const plural = parsePipePlural(value);\n\n if (plural) {\n flattenStrings(plural, result, fullKey);\n } else {\n result.set(fullKey, value);\n }\n } else {\n flattenStrings(value as Messages, result, fullKey);\n }\n }\n\n return result;\n}\n","/** Base class for all lingua errors. Use `instanceof LinguaError` or `LinguaError.is()` to catch any lingua-originated error. */\nexport class LinguaError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is LinguaError {\n return err instanceof LinguaError;\n }\n}\n\n/** Thrown when a method is called on a disposed i18n instance. */\nexport class LinguaDisposedError extends LinguaError {\n constructor() {\n super('Operation called on a disposed i18n instance.');\n }\n}\n\n/** Thrown when `tp()` receives a non-finite `count` value. */\nexport class LinguaInvalidCountError extends LinguaError {}\n\n/** Thrown when `vars.count` is set alongside the `count` parameter in `tp()`. */\nexport class LinguaCountInVarsError extends LinguaError {}\n\n/** Thrown when a requested locale has not been registered. */\nexport class LinguaMissingLocaleError extends LinguaError {}\n\n/** Thrown when a locale string is not a valid BCP 47 tag. */\nexport class LinguaInvalidLocaleError extends LinguaError {}\n\n/** Thrown when `loadNamespace()` / `extend()` is called for a namespace that has not been registered with `registerNamespace()`. */\nexport class LinguaNamespaceMissingError extends LinguaError {}\n\n/** Thrown when restoring i18n state whose locale has no catalog. */\nexport class LinguaRestoreError extends LinguaError {}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nexport function checkDisposed(disposed: boolean): void {\n if (disposed) throw new LinguaDisposedError();\n}\n\nexport function checkDisposedAsync(disposed: boolean): Promise<never> | null {\n return disposed ? Promise.reject(new LinguaDisposedError()) : null;\n}\n","// Internal — not part of the public API.\n// Owns all locale → CatalogEntry state, loaders, and in-flight loading tasks.\n\nimport type { NamespaceStore } from './_namespace-store';\n\nimport { CatalogEntry, type Messages, flattenStrings } from './_catalog';\nimport { devOnly, warn } from './_dev';\nimport { LinguaMissingLocaleError, checkDisposed, checkDisposedAsync } from './errors';\n\nexport type Locale = string;\nexport type Loader<M extends Messages = Messages> = () => Promise<M>;\nexport type LocaleSource<M extends Messages = Messages> = M | Loader<M>;\n\n// Dev-only feedback for the same catalog-authoring mistakes `validateCatalog()` (the CI/build\n// tool at `@vielzeug/lingua/validate`) already catches — most projects never wire that up, so\n// this gives the same signal for free, in the console the author is already watching, the\n// moment a full catalog becomes available: at `createI18n({ catalogs })` construction, via\n// `register()`, or once an async loader resolves. Not run for `patch()` — a namespace overlay is\n// expected to add only a handful of keys, not a complete plural set.\n//\n// Dynamic import, not a static one: `validate.ts`'s own docs promise it stays out of production\n// bundles. A static `import { validateCatalog } from './validate'` here would pull it into every\n// consumer's main bundle regardless of `devOnly()`'s runtime check — bundlers only eliminate\n// that dead branch if the consumer's own build defines `__LINGUA_PROD__` as a literal. The\n// dynamic import keeps validate.ts a genuinely separate, lazily-fetched chunk that's never\n// requested at all when `isDev` is false, no consumer bundler config required.\nexport function validateCatalogInDev(loc: Locale, messages: Messages): void {\n devOnly(() => {\n void import('./validate').then(({ validateCatalog }) => {\n for (const w of validateCatalog(messages, loc)) {\n warn(`catalog('${loc}'): missing plural form '${w.form}' for key '${w.key}'.`);\n }\n });\n });\n}\n\nexport type CatalogStore<M extends Messages = Messages> = {\n dispose(): void;\n isLoaded(loc: Locale): boolean;\n isRegistered(loc: Locale): boolean;\n /** All registered locale keys in insertion order. */\n knownLocales(): readonly Locale[];\n /** Callback invoked whenever a catalog changes (drives bump/notify). */\n onChange: ((loc: Locale) => void) | undefined;\n /** Patch a locale catalog with additional flat messages (used by loadNamespace). */\n patch(loc: Locale, messages: Messages): Promise<void>;\n /** Pre-load a locale catalog without switching. */\n preload(loc: Locale): Promise<void>;\n /** Register (or replace) a locale's source. Returns a Promise that resolves when the catalog is loaded. */\n register(loc: Locale, source: LocaleSource<M>, nsStore: NamespaceStore): Promise<void>;\n /** Resolve a compiled CatalogEntry for a locale (undefined if not loaded). */\n resolve(loc: Locale): CatalogEntry | undefined;\n /**\n * Seed the store with pre-compiled entries from a parent fork.\n * Called during fork() — compiled templates are shared by reference.\n */\n seedFrom(entries: ReadonlyMap<Locale, CatalogEntry>, loaderMap: ReadonlyMap<Locale, Loader<M>>): void;\n /** Snapshot of the raw catalogs map — used for SSR serialization (`getState()`) and fork seeding. */\n snapshotCatalogs(): ReadonlyMap<Locale, CatalogEntry>;\n /** Snapshot of the pending (not-yet-resolved) loaders map — used for fork seeding. */\n snapshotLoaders(): ReadonlyMap<Locale, Loader<M>>;\n};\n\nexport function createCatalogStore<M extends Messages = Messages>(disposed: () => boolean): CatalogStore<M> {\n const catalogs = new Map<Locale, CatalogEntry>();\n const loaders = new Map<Locale, Loader<M>>();\n const loadingTasks = new Map<Locale, Promise<void>>();\n const known = new Set<Locale>();\n\n let onChange: ((loc: Locale) => void) | undefined;\n\n const registerRaw = (normalized: Locale, flat: Map<string, string>): void => {\n let entry = catalogs.get(normalized);\n\n if (!entry) {\n entry = new CatalogEntry();\n catalogs.set(normalized, entry);\n }\n\n entry.setAll(flat);\n };\n\n const preload = (loc: Locale): Promise<void> => {\n const early = checkDisposedAsync(disposed());\n\n if (early) return early;\n\n if (catalogs.has(loc)) return Promise.resolve();\n\n const loader = loaders.get(loc);\n\n if (!loader) return Promise.reject(new LinguaMissingLocaleError(`Missing locale source for \"${loc}\".`));\n\n const existing = loadingTasks.get(loc);\n\n if (existing) return existing;\n\n const task = loader().then(\n (messages) => {\n if (loaders.get(loc) !== loader) return;\n\n loaders.delete(loc);\n registerRaw(loc, flattenStrings(messages));\n validateCatalogInDev(loc, messages);\n loadingTasks.delete(loc);\n onChange?.(loc);\n },\n (error: unknown) => {\n if (loaders.get(loc) === loader) loadingTasks.delete(loc);\n\n throw error;\n },\n );\n\n loadingTasks.set(loc, task);\n\n return task;\n };\n\n return {\n dispose() {\n catalogs.clear();\n loaders.clear();\n loadingTasks.clear();\n known.clear();\n onChange = undefined;\n },\n\n isLoaded(loc) {\n return catalogs.has(loc);\n },\n\n isRegistered(loc) {\n return known.has(loc);\n },\n\n knownLocales() {\n return [...known];\n },\n\n get onChange() {\n return onChange;\n },\n\n set onChange(fn: ((loc: Locale) => void) | undefined) {\n onChange = fn;\n },\n\n patch(loc, messages) {\n const early = checkDisposedAsync(disposed());\n\n if (early) return early;\n\n if (loaders.has(loc)) {\n return preload(loc).then(() => {\n if (disposed()) return;\n\n registerRaw(loc, flattenStrings(messages));\n known.add(loc);\n onChange?.(loc);\n });\n }\n\n registerRaw(loc, flattenStrings(messages));\n known.add(loc);\n onChange?.(loc);\n\n return Promise.resolve();\n },\n\n preload,\n\n register(loc, source, nsStore) {\n checkDisposed(disposed());\n\n loadingTasks.delete(loc);\n known.add(loc);\n\n // Clear namespace loaded-markers for this locale so namespaces can be re-applied\n // after catalog replacement.\n const clearedNs = nsStore.clearLocale(loc);\n\n if (clearedNs.length > 0) {\n warn(\n `register('${loc}') cleared loaded namespace markers for: ${clearedNs.map((ns) => `'${ns}'`).join(', ')}. Call loadNamespace() again to reload.`,\n );\n }\n\n let loadPromise: Promise<void>;\n\n if (typeof source === 'function') {\n loaders.set(loc, source as Loader<M>);\n catalogs.delete(loc);\n loadPromise = preload(loc);\n } else {\n loaders.delete(loc);\n catalogs.delete(loc);\n registerRaw(loc, flattenStrings(source as M));\n validateCatalogInDev(loc, source as M);\n loadPromise = Promise.resolve();\n onChange?.(loc);\n }\n\n return loadPromise;\n },\n\n resolve(loc) {\n return catalogs.get(loc);\n },\n\n seedFrom(entries, loaderMap) {\n for (const [loc, entry] of entries) {\n catalogs.set(loc, entry.clone());\n known.add(loc);\n }\n\n for (const [loc, loader] of loaderMap) {\n loaders.set(loc, loader);\n known.add(loc);\n }\n },\n\n snapshotCatalogs() {\n return catalogs as ReadonlyMap<Locale, CatalogEntry>;\n },\n\n snapshotLoaders() {\n return loaders as ReadonlyMap<Locale, Loader<M>>;\n },\n };\n}\n","// Internal — not part of the public API.\n// Locale chain building, canonicalization, and plural-rules selection.\n// All caches are per-instance — no shared module-level state.\n\nimport { LinguaInvalidLocaleError } from './errors';\n\nexport type Locale = string;\n\n// ─── Per-instance caches ──────────────────────────────────────────────────────\n\nexport type LocaleCaches = {\n canon: Map<string, string>;\n chain: Map<string, { chain: Locale[]; set: Set<Locale> }>;\n plural: Map<string, Intl.PluralRules>;\n};\n\nexport function createLocaleCaches(): LocaleCaches {\n return { canon: new Map(), chain: new Map(), plural: new Map() };\n}\n\n// ─── Canon ────────────────────────────────────────────────────────────────────\n\nexport function canon(locale: string, cache: LocaleCaches): string {\n const cached = cache.canon.get(locale);\n\n if (cached !== undefined) return cached;\n\n let canonical: string | undefined;\n\n try {\n [canonical] = Intl.getCanonicalLocales(locale);\n } catch {\n // Invalid BCP 47 tag — canonical stays undefined, guard below throws.\n }\n\n if (!canonical) throw new LinguaInvalidLocaleError(`Invalid BCP 47 locale tag: \"${locale}\".`);\n\n cache.canon.set(locale, canonical);\n\n return canonical;\n}\n\n// ─── Plural form selection ────────────────────────────────────────────────────\n\nexport function selectPluralForm(locale: Locale, count: number, ordinal: boolean, cache: LocaleCaches): string {\n const key = `${locale}:${ordinal ? 'ordinal' : 'cardinal'}`;\n let rules = cache.plural.get(key);\n\n if (!rules) {\n rules = new Intl.PluralRules(locale, { type: ordinal ? 'ordinal' : 'cardinal' });\n cache.plural.set(key, rules);\n }\n\n return rules.select(count);\n}\n\n// ─── Locale chain ─────────────────────────────────────────────────────────────\n\nfunction buildLocaleChainRaw(locale: Locale, fallback: Locale[]): { chain: Locale[]; set: Set<Locale> } {\n const set = new Set<Locale>();\n\n for (const value of [locale, ...fallback]) {\n set.add(value);\n\n const parts = value.split('-');\n\n for (let i = parts.length - 1; i > 0; i--) {\n set.add(parts.slice(0, i).join('-'));\n }\n }\n\n return { chain: [...set], set };\n}\n\n/**\n * Build a locale fallback chain, memoized per `(locale, fallback)` key using a provided cache.\n * The cache is per-instance (lives on LocaleCaches) — no shared module-level state.\n */\nexport function buildLocaleChain(\n locale: Locale,\n fallback: Locale[],\n cache: LocaleCaches,\n): { chain: Locale[]; set: Set<Locale> } {\n const key = fallback.length === 0 ? locale : `${locale}|${fallback.join(',')}`;\n const cached = cache.chain.get(key);\n\n if (cached) return cached;\n\n const result = buildLocaleChainRaw(locale, fallback);\n\n cache.chain.set(key, result);\n\n return result;\n}\n","// Internal — not part of the public API.\n// Owns namespace factory registry, per-locale loaded markers, and in-flight namespace tasks.\n\nimport type { Messages } from './_catalog';\n\nimport { LinguaNamespaceMissingError, checkDisposed, checkDisposedAsync } from './errors';\n\nexport type Locale = string;\nexport type NamespaceFactory<M extends Messages = Messages> = (locale: Locale) => Promise<M>;\n\nexport type NamespaceStore<M extends Messages = Messages> = {\n /**\n * Remove loaded-markers for a locale across all namespaces.\n * Called by CatalogStore.register() when a catalog is replaced.\n * Returns the list of namespace names whose markers were cleared.\n */\n clearLocale(loc: Locale): string[];\n dispose(): void;\n /** Returns true if the namespace has been loaded for the given locale. */\n isLoaded(ns: string, loc: Locale): boolean;\n /** Returns true if the namespace factory is registered. */\n isRegistered(ns: string): boolean;\n /**\n * Load a namespace for the given locale. Deduplicates concurrent and repeated calls.\n * The `patch` callback is responsible for merging the loaded messages into the catalog.\n */\n loadNamespace(ns: string, loc: Locale, patch: (loc: Locale, messages: Messages) => Promise<void>): Promise<void>;\n /** Register a namespace factory. Idempotent — re-registration updates the factory for future loads. */\n registerNamespace(ns: string, factory: NamespaceFactory<M>): void;\n /** Seed from a parent instance (fork). Copies loaded markers; registry is shared by reference. */\n seedFrom(parent: NamespaceStore<M>): void;\n /** Snapshot of loaded markers for fork() */\n snapshotLoaded(): ReadonlyMap<string, ReadonlySet<string>>;\n /** Snapshot of the registry for fork() */\n snapshotRegistry(): ReadonlyMap<string, NamespaceFactory<M>>;\n};\n\nexport function createNamespaceStore<M extends Messages = Messages>(disposed: () => boolean): NamespaceStore<M> {\n const registry = new Map<string, NamespaceFactory<M>>();\n // Two-level: Map<namespace, Set<locale>>\n const loaded = new Map<string, Set<string>>();\n const tasks = new Map<string, Map<string, Promise<void>>>();\n\n return {\n clearLocale(loc) {\n const cleared: string[] = [];\n\n for (const [ns, localeSet] of loaded.entries()) {\n if (localeSet.delete(loc)) cleared.push(ns);\n }\n\n return cleared;\n },\n\n dispose() {\n registry.clear();\n loaded.clear();\n tasks.clear();\n },\n\n isLoaded(ns, loc) {\n return loaded.get(ns)?.has(loc) ?? false;\n },\n\n isRegistered(ns) {\n return registry.has(ns);\n },\n\n loadNamespace(ns, loc, patch) {\n const early = checkDisposedAsync(disposed());\n\n if (early) return early;\n\n const factory = registry.get(ns);\n\n if (!factory) {\n return Promise.reject(\n new LinguaNamespaceMissingError(`Namespace '${ns}' is not registered. Call registerNamespace() first.`),\n );\n }\n\n if (loaded.get(ns)?.has(loc)) return Promise.resolve();\n\n const nsTasks = tasks.get(ns);\n const existing = nsTasks?.get(loc);\n\n if (existing) return existing;\n\n const task = factory(loc)\n .then((messages) => patch(loc, messages as Messages))\n .then(\n () => {\n let localeSet = loaded.get(ns);\n\n if (!localeSet) {\n localeSet = new Set();\n loaded.set(ns, localeSet);\n }\n\n localeSet.add(loc);\n tasks.get(ns)?.delete(loc);\n },\n (err: unknown) => {\n tasks.get(ns)?.delete(loc);\n throw err;\n },\n );\n\n if (!tasks.has(ns)) tasks.set(ns, new Map());\n\n tasks.get(ns)!.set(loc, task);\n\n return task;\n },\n\n registerNamespace(ns, factory) {\n checkDisposed(disposed());\n registry.set(ns, factory);\n },\n\n seedFrom(parent) {\n for (const [ns, factory] of parent.snapshotRegistry()) {\n registry.set(ns, factory);\n }\n\n for (const [ns, localeSet] of parent.snapshotLoaded()) {\n loaded.set(ns, new Set(localeSet));\n }\n },\n\n snapshotLoaded() {\n return loaded as ReadonlyMap<string, ReadonlySet<string>>;\n },\n\n snapshotRegistry() {\n return registry as ReadonlyMap<string, NamespaceFactory<M>>;\n },\n };\n}\n","// Internal — not part of the public API.\n// The translation algorithm: fallback-chain key lookup, plural-form selection, and\n// interpolation. Extracted out of i18n.ts's factory closure so it has its own unit-test seam\n// independent of the full `createI18n()` setup — mirrors `_catalog-store.ts`/`_namespace-store.ts`,\n// which already got this treatment; this was the one piece of comparable complexity left inline.\n\nimport type { CatalogEntry, CatalogEntryData } from './_catalog';\nimport type { LocaleCaches } from './_chain';\nimport type { Locale, TpOptions, TranslateVars } from './i18n-types';\n\nimport { selectPluralForm } from './_chain';\nimport { devOnly, warn } from './_dev';\nimport { LinguaCountInVarsError, LinguaInvalidCountError } from './errors';\nimport { renderTemplate } from './template';\n\n/** Only the read-side of `CatalogStore` this module needs — avoids importing its `M` generic. */\nexport type CatalogResolver = { resolve(loc: Locale): CatalogEntry | undefined };\n\nexport type TranslateContext = {\n readonly caches: LocaleCaches;\n readonly catalogStore: CatalogResolver;\n /** Active fallback chain, most-specific locale first (e.g. `['en-US', 'en']`). */\n readonly chain: readonly Locale[];\n readonly locale: Locale;\n readonly onMissingKey: (key: string, locale: Locale) => string;\n readonly onMissingVar: (varName: string, key: string, locale: Locale) => string;\n};\n\n// Single-pass entry lookup across the active fallback chain.\nfunction findEntry(ctx: TranslateContext, key: string): CatalogEntryData | undefined {\n for (const candidate of ctx.chain) {\n const found = ctx.catalogStore.resolve(candidate)?.get(key);\n\n if (found !== undefined) return found;\n }\n\n return undefined;\n}\n\n// True if `base` exists as a plural branch prefix (e.g. `items` when only `items.one`/\n// `items.other` are registered) anywhere in the fallback chain. Shared by `hasKey()` and\n// `translate()`'s dev-mode miss diagnostic below — both need to distinguish \"key genuinely\n// doesn't exist\" from \"key exists, but only as a plural branch — you want tp(), not t()\".\nfunction isPluralBranch(ctx: TranslateContext, base: string): boolean {\n for (const candidate of ctx.chain) {\n const catalog = ctx.catalogStore.resolve(candidate);\n\n if (catalog?.prefixes.has(base)) return true;\n }\n\n return false;\n}\n\n/** True if `base` exists as a leaf key or a plural branch prefix, anywhere in the fallback chain. */\nexport function hasKey(ctx: TranslateContext, base: string): boolean {\n return findEntry(ctx, base) !== undefined || isPluralBranch(ctx, base);\n}\n\nfunction interpolate(\n ctx: TranslateContext,\n key: string,\n found: CatalogEntryData,\n vars: TranslateVars | undefined,\n): string {\n return renderTemplate(found.compiled, vars, key, ctx.locale, ctx.onMissingVar);\n}\n\nexport function translate(ctx: TranslateContext, key: string, vars?: TranslateVars): string {\n const found = findEntry(ctx, key);\n\n if (!found) {\n // has(key) returns true for a plural-branch-only key (no leaf entry), but t() can't resolve\n // one — the common `if (i18n.has(key)) i18n.t(key)` pattern silently returns the raw key in\n // that case, indistinguishable from a genuinely missing key without this warning.\n devOnly(() => {\n if (isPluralBranch(ctx, key)) {\n warn(\n `t('${key}') resolved to the missing-key fallback, but '${key}' exists as a plural branch — use tp('${key}', count) instead.`,\n );\n }\n });\n\n return ctx.onMissingKey(key, ctx.locale);\n }\n\n return interpolate(ctx, key, found, vars);\n}\n\n// Plural key priority: cardinal zero tries `.zero` override first, then the CLDR form, then\n// `.other` as the final fallback. Ordinal / non-zero skips the `.zero` special case.\nfunction pluralKeyPriority(base: string, form: string, count: number, ordinal: boolean): string[] {\n const keys: string[] = [];\n\n if (!ordinal && count === 0) keys.push(`${base}.zero`);\n\n if (form !== 'zero' || ordinal) keys.push(`${base}.${form}`);\n\n if (form !== 'other') keys.push(`${base}.other`);\n\n return keys;\n}\n\nexport function translatePlural(ctx: TranslateContext, key: string, count: number, options?: TpOptions): string {\n if (!Number.isFinite(count)) {\n throw new LinguaInvalidCountError('`count` must be a finite number.');\n }\n\n const vars = options?.vars;\n const ordinal = options?.ordinal ?? false;\n\n if (vars && Object.hasOwn(vars, 'count')) {\n throw new LinguaCountInVarsError('`tp` does not allow `vars.count`; `count` is injected automatically.');\n }\n\n const mergedVars = vars ? { count, ...vars } : { count };\n\n // Walk the fallback chain locale-by-locale, selecting CLDR plural form using each locale's\n // own rules. This ensures cross-locale fallbacks produce grammatically correct forms.\n for (const candidate of ctx.chain) {\n const catalog = ctx.catalogStore.resolve(candidate);\n\n if (!catalog) continue;\n\n const form = selectPluralForm(candidate, count, ordinal, ctx.caches);\n const keys = pluralKeyPriority(key, form, count, ordinal);\n\n for (const k of keys) {\n const found = catalog.get(k);\n\n if (found !== undefined) return interpolate(ctx, k, found, mergedVars);\n }\n }\n\n return ctx.onMissingKey(key, ctx.locale);\n}\n","import type {\n I18n,\n I18nOptions,\n I18nSnapshot,\n I18nState,\n Locale,\n MessageBranchKeys,\n MessageLeafKeys,\n ScopedI18n,\n SubscribeOptions,\n TpOptions,\n TranslateVars,\n Unsubscribe,\n} from './i18n-types';\n\nimport { getOrCreate } from './_bounded-cache';\nimport { CatalogEntry, type Messages, flattenStrings } from './_catalog';\nimport {\n type CatalogStore,\n type Loader,\n type LocaleSource,\n createCatalogStore,\n validateCatalogInDev,\n} from './_catalog-store';\nimport { type LocaleCaches, buildLocaleChain, canon, createLocaleCaches } from './_chain';\nimport { error as logError } from './_dev';\nimport { type NamespaceFactory, type NamespaceStore, createNamespaceStore } from './_namespace-store';\nimport {\n type TranslateContext,\n hasKey as hasKeyIn,\n translate as translateIn,\n translatePlural as translatePluralIn,\n} from './_translate';\nimport { LinguaDisposedError, LinguaRestoreError, checkDisposed } from './errors';\nimport { type Formatter, createFormatter } from './format';\n\nexport {\n LinguaCountInVarsError,\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCountError,\n LinguaInvalidLocaleError,\n LinguaMissingLocaleError,\n LinguaNamespaceMissingError,\n LinguaRestoreError,\n} from './errors';\n\nexport type {\n I18n,\n I18nOptions,\n I18nSnapshot,\n I18nState,\n Locale,\n MessageBranchKeys,\n MessageLeafKeys,\n ScopedI18n,\n SubscribeOptions,\n TpOptions,\n TranslateVars,\n Unsubscribe,\n} from './i18n-types';\nexport type { Loader, LocaleSource } from './_catalog-store';\nexport type { Messages } from './_catalog';\nexport type { NamespaceFactory } from './_namespace-store';\n\n// Bound for the per-instance `scope()` cache — see its declaration below for why it's bounded.\nconst SCOPE_CACHE_MAX = 128;\n\n// ─── Locale state ─────────────────────────────────────────────────────────────\n// Replaced atomically on every locale change.\n\ntype LocaleState = {\n readonly chain: readonly Locale[];\n readonly chainSet: ReadonlySet<Locale>;\n readonly locale: Locale;\n};\n\nfunction buildState(locale: Locale, fallback: Locale[], caches: LocaleCaches): LocaleState {\n const { chain, set } = buildLocaleChain(locale, fallback, caches);\n\n return { chain, chainSet: set, locale };\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\n/** Overload: explicit type parameter (strict typing) */\nexport function createI18n<M extends Messages>(config: I18nOptions<M>): I18n<M>;\n/** Overload: no type parameter (loose typing, allows heterogeneous catalogs) */\nexport function createI18n(config?: I18nOptions<Messages>): I18n<Messages>;\nexport function createI18n<M extends Messages = Messages>(config?: I18nOptions<M>): I18n<M> {\n return _createI18nImpl<M>(config);\n}\n\n// ─── Internal seed shape ──────────────────────────────────────────────────────\n\ntype I18nSeed<M extends Messages> = {\n catalogStore?: CatalogStore<M>;\n nsStore?: NamespaceStore;\n};\n\nfunction _createI18nImpl<M extends Messages = Messages>(config?: I18nOptions<M>, _seed?: I18nSeed<M>): I18n<M> {\n const cfg: I18nOptions<M> = config ?? {};\n\n // ─── Per-instance caches (no shared module-level state) ───────────────────\n const caches: LocaleCaches = createLocaleCaches();\n\n const canonL = (loc: string) => canon(loc, caches);\n\n const fallback = Array.isArray(cfg.fallback) ? cfg.fallback.map(canonL) : cfg.fallback ? [canonL(cfg.fallback)] : [];\n\n // ─── Disposal ─────────────────────────────────────────────────────────────\n let disposed = false;\n const disposeController = new AbortController();\n\n // ─── Bounded stores ───────────────────────────────────────────────────────\n const catalogStore: CatalogStore<M> = createCatalogStore(() => disposed);\n const nsStore: NamespaceStore = createNamespaceStore(() => disposed);\n\n // ─── Locale state ─────────────────────────────────────────────────────────\n let state: LocaleState = buildState(canonL(cfg.locale ?? 'en'), fallback, caches);\n\n // ─── Subscribers ──────────────────────────────────────────────────────────\n const subscribers = new Set<(snapshot: I18nSnapshot) => void>();\n const subscriptionUnsubscribers = new Set<Unsubscribe>();\n\n const onMissingKey = cfg.onMissingKey ?? ((key: string) => key);\n const onMissingVar = cfg.onMissingVar ?? ((varName: string) => `{${varName}}`);\n const onSubscriberError = cfg.onSubscriberError ?? ((error: unknown) => logError('subscriber error', error));\n\n // ─── Lazy formatter — avoids Intl overhead for SSR forks that only need t(). ──\n let _fmt: Formatter | undefined;\n\n // ─── Scope cache — stable object references per prefix for reactive framework renders. ──\n // Bounded the same way format.ts bounds its Intl-formatter caches: `scope()` is meant for a\n // static, finite set of prefixes (nav sections, form names, ...), but nothing stopped a caller\n // from passing a dynamic per-item prefix (e.g. `scope(`item.${id}`)` in a list render) and\n // growing this unboundedly for the instance's lifetime. Oldest entry evicts past the cap.\n const scopeCache = new Map<string, ScopedI18n>();\n\n const getFormatter = (): Formatter => {\n if (!_fmt) _fmt = createFormatter(() => state.locale);\n\n return _fmt;\n };\n\n // ─── Translate helpers ────────────────────────────────────────────────────\n // The actual algorithm (fallback-chain lookup, plural-form selection, interpolation) lives in\n // `_translate.ts` — this instance just supplies fresh context (current locale/chain) per call.\n\n const translateContext = (): TranslateContext => ({\n caches,\n catalogStore,\n chain: state.chain,\n locale: state.locale,\n onMissingKey,\n onMissingVar,\n });\n\n // Shared by has() and scope().has().\n const hasKey = (base: string): boolean => hasKeyIn(translateContext(), base);\n\n const translate = (key: MessageLeafKeys<M> | (string & {}), vars?: TranslateVars): string =>\n translateIn(translateContext(), String(key), vars);\n\n const translatePlural = (key: MessageBranchKeys<M> | (string & {}), count: number, options?: TpOptions): string =>\n translatePluralIn(translateContext(), String(key), count, options);\n\n // ─── bump() ───────────────────────────────────────────────────────────────\n // Rebuilds the snapshot and notifies all current subscribers.\n\n let snapshot: I18nSnapshot = {\n locale: state.locale,\n t: translate,\n tp: translatePlural,\n };\n\n const bump = (): void => {\n snapshot = { locale: state.locale, t: translate, tp: translatePlural };\n\n const listeners = [...subscribers];\n\n for (const listener of listeners) {\n try {\n listener(snapshot);\n } catch (error) {\n onSubscriberError(error);\n }\n }\n };\n\n // ─── Wire catalog store onChange → bump ───────────────────────────────────\n catalogStore.onChange = (loc: Locale) => {\n if (state.chainSet.has(loc)) bump();\n };\n\n // ─── Seed from parent fork ─────────────────────────────────────────────────\n if (_seed?.catalogStore) {\n catalogStore.seedFrom(_seed.catalogStore.snapshotCatalogs(), _seed.catalogStore.snapshotLoaders());\n }\n\n if (_seed?.nsStore) {\n nsStore.seedFrom(_seed.nsStore);\n }\n\n // ─── Initial catalogs from config ─────────────────────────────────────────\n if (cfg.catalogs) {\n const staticEntries = new Map<Locale, CatalogEntry>();\n const loaderEntries = new Map<Locale, Loader<M>>();\n\n for (const [loc, source] of Object.entries(cfg.catalogs)) {\n const normalized = canonL(loc);\n\n if (typeof source === 'function') {\n loaderEntries.set(normalized, source as Loader<M>);\n } else {\n const entry = new CatalogEntry();\n\n entry.setAll(flattenStrings(source as M));\n staticEntries.set(normalized, entry);\n validateCatalogInDev(normalized, source as M);\n }\n }\n\n // Use seedFrom for zero-overhead init (no bump, no notifications during setup)\n catalogStore.seedFrom(staticEntries, loaderEntries);\n }\n\n // ─── Preload helper ───────────────────────────────────────────────────────\n\n const preload = (loc: Locale): Promise<void> => catalogStore.preload(canonL(loc));\n\n // ─── Monotonic generation counter — last writer wins for concurrent setLocale() ──\n let switchGen = 0;\n\n // ─── Subscribe helper ─────────────────────────────────────────────────────\n\n const subscribeInternal = (callback: (snapshot: I18nSnapshot) => void, options?: SubscribeOptions): Unsubscribe => {\n const signal = options?.signal;\n\n const unsubscribe = (): void => {\n subscribers.delete(callback);\n signal?.removeEventListener('abort', unsubscribe);\n subscriptionUnsubscribers.delete(unsubscribe);\n };\n\n if (disposed) throw new LinguaDisposedError();\n\n if (signal?.aborted) return unsubscribe;\n\n if (options?.immediate === true) {\n try {\n callback(snapshot);\n } catch (error) {\n onSubscriberError(error);\n\n return unsubscribe;\n }\n }\n\n subscribers.add(callback);\n signal?.addEventListener('abort', unsubscribe, { once: true });\n subscriptionUnsubscribers.add(unsubscribe);\n\n return unsubscribe;\n };\n\n // ─── Public object ─────────────────────────────────────────────────────────\n // Named `const` (not a bare `return { ... }`) so `[Symbol.dispose]` below can close over\n // `source` directly instead of `this` — `this` inside an object-literal method is only bound\n // when called as `obj.method()`; detaching it first (`const d = i18n[Symbol.dispose]; d()`,\n // the exact shape most framework cleanup-hook APIs expect) would otherwise throw.\n\n const source: I18n<M> = {\n get disposalSignal(): AbortSignal {\n return disposeController.signal;\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposeController.abort();\n for (const unsubscribe of [...subscriptionUnsubscribers]) unsubscribe();\n subscriptionUnsubscribers.clear();\n subscribers.clear();\n catalogStore.dispose();\n nsStore.dispose();\n scopeCache.clear();\n },\n\n get disposed(): boolean {\n return disposed;\n },\n\n extend(ns: string, factory: NamespaceFactory, loc?: Locale): Promise<void> {\n checkDisposed(disposed);\n\n nsStore.registerNamespace(ns, factory);\n\n const normalized = loc ? canonL(loc) : state.locale;\n\n return nsStore.loadNamespace(ns, normalized, (l, messages) => catalogStore.patch(l, messages));\n },\n\n get fmt(): Formatter {\n return getFormatter();\n },\n\n fork(overrides?: Omit<I18nOptions<M>, 'catalogs'>): I18n<M> {\n return _createI18nImpl(\n {\n fallback: overrides?.fallback ?? (fallback.length > 0 ? fallback : undefined),\n locale: overrides?.locale ?? state.locale,\n onMissingKey: overrides?.onMissingKey ?? cfg.onMissingKey,\n onMissingVar: overrides?.onMissingVar ?? cfg.onMissingVar,\n onSubscriberError: overrides?.onSubscriberError ?? cfg.onSubscriberError,\n },\n {\n catalogStore,\n nsStore,\n },\n );\n },\n\n getSnapshot() {\n return snapshot;\n },\n\n getState(): I18nState {\n const catalogsOut: Record<Locale, Record<string, string>> = {};\n\n for (const [loc, entry] of catalogStore.snapshotCatalogs()) {\n catalogsOut[loc] = Object.fromEntries([...entry.entries.entries()].map(([k, { message }]) => [k, message]));\n }\n\n return { catalogs: catalogsOut, locale: state.locale };\n },\n\n getSupportedLocales(sorted?: boolean): Locale[] {\n const locales = [...catalogStore.knownLocales()];\n\n return sorted === true ? locales.sort() : locales;\n },\n\n has(key: MessageLeafKeys<M> | MessageBranchKeys<M> | (string & {})): boolean {\n return hasKey(String(key));\n },\n\n isLoaded(loc: Locale): boolean {\n try {\n return catalogStore.isLoaded(canonL(loc));\n } catch {\n return false;\n }\n },\n\n isNamespaceLoaded(ns: string, loc?: Locale): boolean {\n if (!loc) return nsStore.isLoaded(ns, state.locale);\n\n try {\n return nsStore.isLoaded(ns, canonL(loc));\n } catch {\n return false;\n }\n },\n\n isNamespaceRegistered(ns: string): boolean {\n return nsStore.isRegistered(ns);\n },\n\n isRegistered(loc: Locale): boolean {\n try {\n return catalogStore.isRegistered(canonL(loc));\n } catch {\n return false;\n }\n },\n\n loadNamespace(ns: string, loc?: Locale): Promise<void> {\n const normalized = loc ? canonL(loc) : state.locale;\n\n return nsStore.loadNamespace(ns, normalized, (l, messages) => catalogStore.patch(l, messages));\n },\n\n get locale(): Locale {\n return state.locale;\n },\n\n preload,\n\n register(loc: Locale, source: LocaleSource<M>): Promise<void> {\n const normalized = canonL(loc);\n\n return catalogStore.register(normalized, source, nsStore);\n },\n\n registerNamespace(ns: string, factory: NamespaceFactory): void {\n nsStore.registerNamespace(ns, factory);\n },\n\n restoreState(st: I18nState): void {\n checkDisposed(disposed);\n\n if (!Object.hasOwn(st.catalogs, st.locale)) {\n throw new LinguaRestoreError(`restoreState: locale \"${st.locale}\" has no catalog in the provided state.`);\n }\n\n const freshEntries = new Map<Locale, CatalogEntry>();\n const knownLocales = catalogStore.knownLocales();\n\n for (const knownLocale of knownLocales) {\n nsStore.clearLocale(knownLocale);\n }\n\n // No validateCatalogInDev() call here, unlike register()/construction/preload(): `state`\n // is already flattened dot-notation (I18nState.catalogs), not the nested Messages shape\n // validateCatalog() expects, and restoreState() exists specifically for cheap SSR\n // hydration of a payload that was already registered — and therefore already\n // validated — once on the system that produced it. Re-validating here would mean\n // un-flattening every restored catalog back into nested form on every hydration, for a\n // check that's already run.\n for (const [loc, flatCatalog] of Object.entries(st.catalogs)) {\n const normalized = canonL(loc);\n const entry = new CatalogEntry();\n\n entry.setAll(Object.entries(flatCatalog));\n freshEntries.set(normalized, entry);\n }\n\n // Dispose and re-seed the catalog store with the restored entries.\n // onChange is re-wired immediately after dispose.\n catalogStore.dispose();\n catalogStore.onChange = (loc: Locale) => {\n if (state.chainSet.has(loc)) bump();\n };\n catalogStore.seedFrom(freshEntries, new Map());\n\n const normalized = canonL(st.locale);\n\n state = buildState(normalized, fallback, caches);\n _fmt?.clear();\n bump();\n },\n\n scope(prefix: MessageBranchKeys<M> | (string & {})): ScopedI18n {\n const pre = String(prefix);\n\n return getOrCreate(scopeCache, pre, SCOPE_CACHE_MAX, () => ({\n get fmt() {\n return getFormatter();\n },\n has: (key) => hasKey(`${pre}.${key}`),\n t: (key, vars?) => translate(`${pre}.${key}`, vars),\n tp: (key, count, options?) => translatePlural(`${pre}.${key}`, count, options),\n }));\n },\n\n async setLocale(next: Locale): Promise<void> {\n checkDisposed(disposed);\n\n const normalized = canonL(next);\n\n if (state.locale === normalized) return;\n\n // Monotonic generation counter — last writer wins.\n const gen = ++switchGen;\n\n await preload(normalized);\n\n if (disposed || switchGen !== gen) return;\n\n state = buildState(normalized, fallback, caches);\n _fmt?.clear();\n bump();\n },\n\n subscribe: subscribeInternal,\n\n [Symbol.dispose](): void {\n source.dispose();\n },\n\n t: translate,\n\n tp: translatePlural,\n };\n\n return source;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lingua.js","names":[],"sources":["../src/errors.ts","../src/_dev.ts","../src/_locale.ts","../src/_template.ts","../src/_catalog.ts","../src/_resources.ts","../src/translator.ts","../src/i18n.ts"],"sourcesContent":["export class LinguaError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(error: unknown): error is LinguaError {\n return error instanceof LinguaError;\n }\n}\n\nexport class LinguaDisposedError extends LinguaError {\n constructor() {\n super('Operation called on a disposed translation store.');\n }\n}\n\nexport class LinguaInvalidCatalogError extends LinguaError {}\nexport class LinguaInvalidLocaleError extends LinguaError {}\nexport class LinguaInvalidPluralCountError extends LinguaError {}\nexport class LinguaInvalidStateError extends LinguaError {}\nexport class LinguaMissingCatalogError extends LinguaError {}\n","const isDev = !(globalThis as { __LINGUA_PROD__?: boolean }).__LINGUA_PROD__;\n\n/** @internal @security Messages may include user-supplied data. */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/lingua] ${msg}`);\n}\n\n/** @internal */\nexport function error(msg: string, ...args: unknown[]): void {\n if (isDev) console.error(`[@vielzeug/lingua] ${msg}`, ...args);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n","import type { Locale } from './types';\n\nimport { LinguaInvalidLocaleError } from './errors';\n\nconst pluralRules = new Map<string, Intl.PluralRules>();\n\nexport function canonicalLocale(locale: string): Locale {\n try {\n const [canonical] = Intl.getCanonicalLocales(locale);\n\n if (canonical) return canonical;\n } catch {\n // Error below names the failed public input.\n }\n\n throw new LinguaInvalidLocaleError(`Invalid BCP 47 locale tag: \"${locale}\".`);\n}\n\nexport function localeChain(locale: Locale, fallback: readonly Locale[]): readonly Locale[] {\n const chain = new Set<Locale>();\n\n for (const candidate of [locale, ...fallback]) {\n const parts = candidate.split('-');\n\n for (let length = parts.length; length > 0; length--) {\n chain.add(parts.slice(0, length).join('-'));\n }\n }\n\n return [...chain];\n}\n\nexport function pluralCategory(locale: Locale, count: number, ordinal: boolean): Intl.LDMLPluralRule {\n const key = `${locale}:${ordinal ? 'ordinal' : 'cardinal'}`;\n let rules = pluralRules.get(key);\n\n if (!rules) {\n rules = new Intl.PluralRules(locale, { type: ordinal ? 'ordinal' : 'cardinal' });\n pluralRules.set(key, rules);\n }\n\n return rules.select(count);\n}\n","export type TemplatePart = string | { readonly value: string };\nexport type Template = readonly TemplatePart[];\n\nconst interpolation = /\\{([\\p{ID_Continue}-]+)\\}/gu;\n\nexport function compileTemplate(value: string): Template {\n const parts: TemplatePart[] = [];\n let offset = 0;\n\n for (const match of value.matchAll(interpolation)) {\n const index = match.index ?? 0;\n\n if (index > offset) parts.push(value.slice(offset, index));\n\n parts.push({ value: match[1] });\n offset = index + match[0].length;\n }\n\n if (offset < value.length) parts.push(value.slice(offset));\n\n return parts;\n}\n\nexport function renderText(\n parts: Template,\n values: Record<string, unknown>,\n missing: (name: string) => string,\n): string {\n return parts\n .map((part) => {\n if (typeof part === 'string') return part;\n\n const value = Object.hasOwn(values, part.value) ? values[part.value] : undefined;\n\n return value == null ? missing(part.value) : String(value);\n })\n .join('');\n}\n\nexport function renderSegments<V>(\n parts: Template,\n values: Record<string, V | number>,\n missing: (name: string) => string,\n): Array<string | number | V> {\n const result: Array<string | number | V> = [];\n\n for (const part of parts) {\n if (typeof part === 'string') {\n if (part !== '') result.push(part);\n\n continue;\n }\n\n result.push(Object.hasOwn(values, part.value) ? values[part.value]! : missing(part.value));\n }\n\n return result;\n}\n","import type { Catalog, CatalogNode, PluralMessage } from './types';\n\nimport { compileTemplate, type Template } from './_template';\nimport { LinguaInvalidCatalogError } from './errors';\n\nconst unsafeKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport type CompiledText = { readonly kind: 'text'; readonly template: Template };\nexport type CompiledPlural = { readonly forms: ReadonlyMap<string, Template>; readonly kind: 'plural' };\nexport type CompiledMessage = CompiledPlural | CompiledText;\nexport type CompiledCatalog = ReadonlyMap<string, CompiledMessage>;\n\nfunction isPluralMessage(node: CatalogNode): node is PluralMessage {\n return typeof node === 'object' && node !== null && Object.hasOwn(node, 'plural');\n}\n\n/** Compile explicit catalog nodes once. Nested objects only group keys; plural intent is never inferred. */\nexport function compileCatalog(catalog: Catalog): CompiledCatalog {\n const messages = new Map<string, CompiledMessage>();\n\n const visit = (node: CatalogNode, prefix: string): void => {\n if (typeof node === 'string') {\n messages.set(prefix, { kind: 'text', template: compileTemplate(node) });\n\n return;\n }\n\n if (typeof node !== 'object' || node === null || Array.isArray(node)) {\n throw new LinguaInvalidCatalogError(`Catalog node \"${prefix}\" must be a string, plural message, or object.`);\n }\n\n if (isPluralMessage(node)) {\n if (typeof node.plural !== 'object' || node.plural === null || Array.isArray(node.plural)) {\n throw new LinguaInvalidCatalogError(`Plural message \"${prefix}\" must provide an object of string forms.`);\n }\n\n const forms = new Map<string, Template>();\n\n for (const [form, template] of Object.entries(node.plural)) {\n if (typeof template !== 'string') {\n throw new LinguaInvalidCatalogError(`Plural form \"${prefix}.${form}\" must be a string.`);\n }\n\n forms.set(form, compileTemplate(template));\n }\n\n messages.set(prefix, { forms, kind: 'plural' });\n\n return;\n }\n\n for (const [key, value] of Object.entries(node)) {\n if (unsafeKeys.has(key)) {\n throw new LinguaInvalidCatalogError(`Catalog key \"${key}\" is reserved.`);\n }\n\n visit(value, prefix ? `${prefix}.${key}` : key);\n }\n };\n\n for (const [key, value] of Object.entries(catalog)) {\n if (unsafeKeys.has(key)) throw new LinguaInvalidCatalogError(`Catalog key \"${key}\" is reserved.`);\n\n visit(value, key);\n }\n\n return messages;\n}\n","import type { Catalog, CatalogSources, TranslationState, LoadedCatalogs, Locale } from './types';\n\nimport { type CompiledCatalog, compileCatalog } from './_catalog';\nimport { canonicalLocale } from './_locale';\nimport { LinguaMissingCatalogError } from './errors';\n\n/** One catalog state machine owns static sources, lazy sources, and in-flight work. */\nexport function createCatalogStore<C extends Catalog>(sources: CatalogSources<C>) {\n const definitions = new Map<Locale, C | (() => Promise<C>)>();\n const loaded = new Map<Locale, { readonly catalog: C; readonly compiled: CompiledCatalog }>();\n const tasks = new Map<Locale, Promise<void>>();\n\n for (const [locale, source] of Object.entries(sources)) definitions.set(canonicalLocale(locale), source);\n\n const add = (locale: Locale, catalog: C): void => {\n loaded.set(locale, { catalog, compiled: compileCatalog(catalog) });\n };\n\n for (const [locale, source] of definitions) {\n if (typeof source !== 'function') add(locale, source);\n }\n\n const load = async (requestedLocale: Locale): Promise<boolean> => {\n const locale = canonicalLocale(requestedLocale);\n\n if (loaded.has(locale)) return false;\n\n const source = definitions.get(locale);\n\n if (!source) throw new LinguaMissingCatalogError(`Catalog has no source for locale \"${locale}\".`);\n\n if (typeof source !== 'function') {\n add(locale, source);\n\n return true;\n }\n\n const existing = tasks.get(locale);\n\n if (existing) {\n await existing;\n\n return false;\n }\n\n const task = source().then(\n (catalog) => {\n add(locale, catalog);\n tasks.delete(locale);\n },\n (error: unknown) => {\n tasks.delete(locale);\n throw error;\n },\n );\n\n tasks.set(locale, task);\n await task;\n\n return true;\n };\n\n return {\n catalogMap(): ReadonlyMap<Locale, CompiledCatalog> {\n return new Map([...loaded].map(([locale, { compiled }]) => [locale, compiled]));\n },\n isLoaded(locale: Locale): boolean {\n return loaded.has(canonicalLocale(locale));\n },\n load,\n state(locale: Locale): TranslationState<C> {\n const catalogs: LoadedCatalogs<C> = {};\n\n for (const [loadedLocale, { catalog }] of loaded) catalogs[loadedLocale] = catalog;\n\n return { catalogs, locale, version: 3 };\n },\n };\n}\n","import type {\n Catalog,\n Catalogs,\n Locale,\n PluralKey,\n PluralOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n} from './types';\n\nimport { type CompiledCatalog, type CompiledMessage, compileCatalog } from './_catalog';\nimport { canonicalLocale, localeChain, pluralCategory } from './_locale';\nimport { renderSegments, renderText, type Template } from './_template';\nimport { LinguaInvalidPluralCountError } from './errors';\n\nexport type Translator<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n segments<V>(key: TextKey<C>, options: TranslateOptions & { values: Record<string, V> }): Array<string | V>;\n segments<V>(key: PluralKey<C>, options: PluralOptions & { values?: Record<string, V> }): Array<string | number | V>;\n segmentsDynamic<V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V>;\n translate(key: TextKey<C>, options?: TranslateOptions): string;\n translate(key: PluralKey<C>, options: PluralOptions): string;\n translateDynamic(key: string, options?: TranslateOptions | PluralOptions): string;\n};\n\ntype ResolvedMessage = { readonly locale: Locale; readonly message: CompiledMessage };\n\nexport function createTranslatorFromCompiled<C extends Catalog>(\n catalogs: ReadonlyMap<Locale, CompiledCatalog>,\n options: TranslatorOptions = {},\n): Translator<C> {\n const locale = canonicalLocale(options.locale ?? 'en');\n const fallback = (\n Array.isArray(options.fallback) ? options.fallback : options.fallback ? [options.fallback] : []\n ).map(canonicalLocale);\n const chain = localeChain(locale, fallback);\n const missingKey = options.onMissingKey ?? ((key: string) => key);\n const missingValue = options.onMissingValue ?? ((name: string) => `{${name}}`);\n\n const resolve = (key: string): ResolvedMessage | undefined => {\n for (const candidate of chain) {\n const message = catalogs.get(candidate)?.get(key);\n\n if (message) return { locale: candidate, message };\n }\n\n return undefined;\n };\n\n const templateFor = (\n key: string,\n options: TranslateOptions | PluralOptions,\n ): { key: string; template: Template } | undefined => {\n const found = resolve(key);\n\n if (!found) return undefined;\n\n if (found.message.kind === 'text') {\n if ('count' in options) return undefined;\n\n return { key, template: found.message.template };\n }\n\n if (!('count' in options) || !Number.isFinite(options.count)) {\n if ('count' in options) throw new LinguaInvalidPluralCountError('`count` must be a finite number.');\n\n return undefined;\n }\n\n const category =\n options.count === 0 && !options.ordinal\n ? 'zero'\n : pluralCategory(found.locale, options.count, options.ordinal ?? false);\n const template = found.message.forms.get(category) ?? found.message.forms.get('other');\n\n return template ? { key, template } : undefined;\n };\n\n const valuesFor = (options: TranslateOptions | PluralOptions): Record<string, unknown> =>\n 'count' in options ? { count: options.count, ...options.values } : (options.values ?? {});\n\n const segmentsDynamic = <V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V> => {\n const found = templateFor(key, options);\n\n if (!found) return [missingKey(key, locale)];\n\n return renderSegments(found.template, valuesFor(options) as Record<string, V | number>, (name) =>\n missingValue(name, found.key, locale),\n );\n };\n\n const translateDynamic = (key: string, options: TranslateOptions | PluralOptions = {}): string => {\n const found = templateFor(key, options);\n\n if (!found) return missingKey(key, locale);\n\n return renderText(found.template, valuesFor(options), (name) => missingValue(name, found.key, locale));\n };\n\n return {\n locale,\n segments(key: string, options: (TranslateOptions | PluralOptions) & { values?: Record<string, unknown> }) {\n return segmentsDynamic(key, options);\n },\n segmentsDynamic,\n translate(key: string, options: TranslateOptions | PluralOptions = {}) {\n return translateDynamic(key, options);\n },\n translateDynamic,\n } as Translator<C>;\n}\n\nexport function createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C> {\n const compiled = new Map<Locale, CompiledCatalog>();\n\n for (const [locale, catalog] of Object.entries(catalogs)) {\n compiled.set(canonicalLocale(locale), compileCatalog(catalog));\n }\n\n return createTranslatorFromCompiled<C>(compiled, options);\n}\n","import type { Catalog, TranslationStoreOptions, TranslationState, Locale, SubscribeOptions } from './types';\n\nimport { error as logError } from './_dev';\nimport { canonicalLocale, localeChain } from './_locale';\nimport { createCatalogStore } from './_resources';\nimport { LinguaDisposedError, LinguaInvalidStateError } from './errors';\nimport { type Translator, createTranslatorFromCompiled } from './translator';\n\nexport type TranslationSnapshot<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n readonly revision: number;\n readonly translator: Translator<C>;\n};\n\nexport type TranslationStore<C extends Catalog = Catalog> = Translator<C> & {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getSnapshot(): TranslationSnapshot<C>;\n isLoaded(options?: { locale?: Locale }): boolean;\n load(options?: { locale?: Locale }): Promise<void>;\n serialize(): TranslationState<C>;\n setLocale(locale: Locale): Promise<void>;\n subscribe(listener: (snapshot: TranslationSnapshot<C>) => void, options?: SubscribeOptions): () => void;\n};\n\nexport function createTranslationStore<C extends Catalog>(options: TranslationStoreOptions<C>): TranslationStore<C> {\n const catalogs = createCatalogStore(options.catalogs);\n const fallback = options.fallback;\n const fallbackLocales = (Array.isArray(fallback) ? fallback : fallback ? [fallback] : []).map(canonicalLocale);\n const controller = new AbortController();\n const subscribers = new Set<(snapshot: TranslationSnapshot<C>) => void>();\n let disposed = false;\n let locale = canonicalLocale(options.locale ?? 'en');\n let revision = 0;\n\n const buildSnapshot = (): TranslationSnapshot<C> => ({\n locale,\n revision,\n translator: createTranslatorFromCompiled<C>(catalogs.catalogMap(), { ...options, fallback, locale }),\n });\n let snapshot = buildSnapshot();\n\n const assertLive = (): void => {\n if (disposed) throw new LinguaDisposedError();\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n subscribers.clear();\n controller.abort();\n };\n\n const dispatch = (listener: (next: TranslationSnapshot<C>) => void): void => {\n try {\n listener(snapshot);\n } catch (error) {\n logError('subscriber error', error);\n }\n };\n\n const notify = (): void => {\n revision++;\n snapshot = buildSnapshot();\n\n for (const listener of [...subscribers]) dispatch(listener);\n };\n\n const relevant = (candidate: Locale): boolean => localeChain(locale, fallbackLocales).includes(candidate);\n\n return {\n get disposalSignal() {\n return controller.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getSnapshot() {\n return snapshot;\n },\n isLoaded(loadOptions) {\n return !disposed && catalogs.isLoaded(loadOptions?.locale ?? locale);\n },\n async load(loadOptions) {\n assertLive();\n\n const targetLocale = canonicalLocale(loadOptions?.locale ?? locale);\n const changed = await catalogs.load(targetLocale);\n\n if (!disposed && changed && relevant(targetLocale)) notify();\n },\n get locale() {\n return locale;\n },\n segments(key: string, translateOptions) {\n return snapshot.translator.segmentsDynamic(key, translateOptions);\n },\n segmentsDynamic(key, translateOptions) {\n return snapshot.translator.segmentsDynamic(key, translateOptions);\n },\n serialize() {\n assertLive();\n\n return catalogs.state(locale);\n },\n async setLocale(nextLocale) {\n assertLive();\n\n const next = canonicalLocale(nextLocale);\n\n if (next === locale) return;\n\n locale = next;\n notify();\n },\n subscribe(listener, subscribeOptions) {\n assertLive();\n\n if (subscribeOptions?.signal?.aborted) return () => {};\n\n const unsubscribe = (): void => {\n subscribers.delete(listener);\n subscribeOptions?.signal?.removeEventListener('abort', unsubscribe);\n };\n\n subscribeOptions?.signal?.addEventListener('abort', unsubscribe, { once: true });\n subscribers.add(listener);\n\n if (subscribeOptions?.immediate) dispatch(listener);\n\n return unsubscribe;\n },\n [Symbol.dispose]: dispose,\n translate(key: string, translateOptions = {}) {\n return snapshot.translator.translateDynamic(key, translateOptions);\n },\n translateDynamic(key, translateOptions) {\n return snapshot.translator.translateDynamic(key, translateOptions);\n },\n } as TranslationStore<C>;\n}\n\nexport function hydrateTranslationStore<C extends Catalog>(\n state: TranslationState<C>,\n options?: Omit<TranslationStoreOptions<C>, 'locale' | 'catalogs'>,\n): TranslationStore<C> {\n if (state.version !== 3) {\n throw new LinguaInvalidStateError(`Unsupported lingua state version: ${String(state.version)}.`);\n }\n\n return createTranslationStore({ ...options, catalogs: state.catalogs, locale: state.locale });\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("./errors.cjs"),t=require("./_locale.cjs"),n=require("./_template.cjs"),r=require("./_catalog.cjs");function i(r,i={}){let a=t.canonicalLocale(i.locale??`en`),o=(Array.isArray(i.fallback)?i.fallback:i.fallback?[i.fallback]:[]).map(t.canonicalLocale),s=t.localeChain(a,o),c=i.onMissingKey??(e=>e),l=i.onMissingValue??(e=>`{${e}}`),u=e=>{for(let t of s){let n=r.get(t)?.get(e);if(n)return{locale:t,message:n}}},d=(n,r)=>{let i=u(n);if(!i)return;if(i.message.kind===`text`)return`count`in r?void 0:{key:n,template:i.message.template};if(!(`count`in r)||!Number.isFinite(r.count)){if(`count`in r)throw new e.LinguaInvalidPluralCountError("`count` must be a finite number.");return}let a=r.count===0&&!r.ordinal?`zero`:t.pluralCategory(i.locale,r.count,r.ordinal??!1),o=i.message.forms.get(a)??i.message.forms.get(`other`);return o?{key:n,template:o}:void 0},f=e=>`count`in e?{count:e.count,...e.values}:e.values??{},p=(e,t)=>{let r=d(e,t);return r?n.renderSegments(r.template,f(t),e=>l(e,r.key,a)):[c(e,a)]},m=(e,t={})=>{let r=d(e,t);return r?n.renderText(r.template,f(t),e=>l(e,r.key,a)):c(e,a)};return{locale:a,segments(e,t){return p(e,t)},segmentsDynamic:p,translate(e,t={}){return m(e,t)},translateDynamic:m}}function a(e,n){let a=new Map;for(let[n,i]of Object.entries(e))a.set(t.canonicalLocale(n),r.compileCatalog(i));return i(a,n)}exports.createTranslator=a,exports.createTranslatorFromCompiled=i;
2
+ //# sourceMappingURL=translator.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.cjs","names":[],"sources":["../src/translator.ts"],"sourcesContent":["import type {\n Catalog,\n Catalogs,\n Locale,\n PluralKey,\n PluralOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n} from './types';\n\nimport { type CompiledCatalog, type CompiledMessage, compileCatalog } from './_catalog';\nimport { canonicalLocale, localeChain, pluralCategory } from './_locale';\nimport { renderSegments, renderText, type Template } from './_template';\nimport { LinguaInvalidPluralCountError } from './errors';\n\nexport type Translator<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n segments<V>(key: TextKey<C>, options: TranslateOptions & { values: Record<string, V> }): Array<string | V>;\n segments<V>(key: PluralKey<C>, options: PluralOptions & { values?: Record<string, V> }): Array<string | number | V>;\n segmentsDynamic<V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V>;\n translate(key: TextKey<C>, options?: TranslateOptions): string;\n translate(key: PluralKey<C>, options: PluralOptions): string;\n translateDynamic(key: string, options?: TranslateOptions | PluralOptions): string;\n};\n\ntype ResolvedMessage = { readonly locale: Locale; readonly message: CompiledMessage };\n\nexport function createTranslatorFromCompiled<C extends Catalog>(\n catalogs: ReadonlyMap<Locale, CompiledCatalog>,\n options: TranslatorOptions = {},\n): Translator<C> {\n const locale = canonicalLocale(options.locale ?? 'en');\n const fallback = (\n Array.isArray(options.fallback) ? options.fallback : options.fallback ? [options.fallback] : []\n ).map(canonicalLocale);\n const chain = localeChain(locale, fallback);\n const missingKey = options.onMissingKey ?? ((key: string) => key);\n const missingValue = options.onMissingValue ?? ((name: string) => `{${name}}`);\n\n const resolve = (key: string): ResolvedMessage | undefined => {\n for (const candidate of chain) {\n const message = catalogs.get(candidate)?.get(key);\n\n if (message) return { locale: candidate, message };\n }\n\n return undefined;\n };\n\n const templateFor = (\n key: string,\n options: TranslateOptions | PluralOptions,\n ): { key: string; template: Template } | undefined => {\n const found = resolve(key);\n\n if (!found) return undefined;\n\n if (found.message.kind === 'text') {\n if ('count' in options) return undefined;\n\n return { key, template: found.message.template };\n }\n\n if (!('count' in options) || !Number.isFinite(options.count)) {\n if ('count' in options) throw new LinguaInvalidPluralCountError('`count` must be a finite number.');\n\n return undefined;\n }\n\n const category =\n options.count === 0 && !options.ordinal\n ? 'zero'\n : pluralCategory(found.locale, options.count, options.ordinal ?? false);\n const template = found.message.forms.get(category) ?? found.message.forms.get('other');\n\n return template ? { key, template } : undefined;\n };\n\n const valuesFor = (options: TranslateOptions | PluralOptions): Record<string, unknown> =>\n 'count' in options ? { count: options.count, ...options.values } : (options.values ?? {});\n\n const segmentsDynamic = <V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V> => {\n const found = templateFor(key, options);\n\n if (!found) return [missingKey(key, locale)];\n\n return renderSegments(found.template, valuesFor(options) as Record<string, V | number>, (name) =>\n missingValue(name, found.key, locale),\n );\n };\n\n const translateDynamic = (key: string, options: TranslateOptions | PluralOptions = {}): string => {\n const found = templateFor(key, options);\n\n if (!found) return missingKey(key, locale);\n\n return renderText(found.template, valuesFor(options), (name) => missingValue(name, found.key, locale));\n };\n\n return {\n locale,\n segments(key: string, options: (TranslateOptions | PluralOptions) & { values?: Record<string, unknown> }) {\n return segmentsDynamic(key, options);\n },\n segmentsDynamic,\n translate(key: string, options: TranslateOptions | PluralOptions = {}) {\n return translateDynamic(key, options);\n },\n translateDynamic,\n } as Translator<C>;\n}\n\nexport function createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C> {\n const compiled = new Map<Locale, CompiledCatalog>();\n\n for (const [locale, catalog] of Object.entries(catalogs)) {\n compiled.set(canonicalLocale(locale), compileCatalog(catalog));\n }\n\n return createTranslatorFromCompiled<C>(compiled, options);\n}\n"],"mappings":"oHA+BA,SAAgB,EACd,EACA,EAA6B,CAAC,EACf,CACf,IAAM,EAAS,EAAA,gBAAgB,EAAQ,QAAU,IAAI,EAC/C,GACJ,MAAM,QAAQ,EAAQ,QAAQ,EAAI,EAAQ,SAAW,EAAQ,SAAW,CAAC,EAAQ,QAAQ,EAAI,CAAC,EAAA,CAC9F,IAAI,EAAA,eAAe,EACf,EAAQ,EAAA,YAAY,EAAQ,CAAQ,EACpC,EAAa,EAAQ,eAAkB,GAAgB,GACvD,EAAe,EAAQ,iBAAoB,GAAiB,IAAI,EAAK,IAErE,EAAW,GAA6C,CAC5D,IAAK,IAAM,KAAa,EAAO,CAC7B,IAAM,EAAU,EAAS,IAAI,CAAS,CAAC,EAAE,IAAI,CAAG,EAEhD,GAAI,EAAS,MAAO,CAAE,OAAQ,EAAW,SAAQ,CACnD,CAGF,EAEM,GACJ,EACA,IACoD,CACpD,IAAM,EAAQ,EAAQ,CAAG,EAEzB,GAAI,CAAC,EAAO,OAEZ,GAAI,EAAM,QAAQ,OAAS,OAGzB,MAFI,UAAW,EAAS,OAEjB,CAAE,MAAK,SAAU,EAAM,QAAQ,QAAS,EAGjD,GAAI,EAAE,UAAW,IAAY,CAAC,OAAO,SAAS,EAAQ,KAAK,EAAG,CAC5D,GAAI,UAAW,EAAS,MAAM,IAAI,EAAA,8BAA8B,kCAAkC,EAElG,MACF,CAEA,IAAM,EACJ,EAAQ,QAAU,GAAK,CAAC,EAAQ,QAC5B,OACA,EAAA,eAAe,EAAM,OAAQ,EAAQ,MAAO,EAAQ,SAAW,EAAK,EACpE,EAAW,EAAM,QAAQ,MAAM,IAAI,CAAQ,GAAK,EAAM,QAAQ,MAAM,IAAI,OAAO,EAErF,OAAO,EAAW,CAAE,MAAK,UAAS,EAAI,IAAA,EACxC,EAEM,EAAa,GACjB,UAAW,EAAU,CAAE,MAAO,EAAQ,MAAO,GAAG,EAAQ,MAAO,EAAK,EAAQ,QAAU,CAAC,EAEnF,GACJ,EACA,IAC+B,CAC/B,IAAM,EAAQ,EAAY,EAAK,CAAO,EAItC,OAFK,EAEE,EAAA,eAAe,EAAM,SAAU,EAAU,CAAO,EAAkC,GACvF,EAAa,EAAM,EAAM,IAAK,CAAM,CACtC,EAJmB,CAAC,EAAW,EAAK,CAAM,CAAC,CAK7C,EAEM,GAAoB,EAAa,EAA4C,CAAC,IAAc,CAChG,IAAM,EAAQ,EAAY,EAAK,CAAO,EAItC,OAFK,EAEE,EAAA,WAAW,EAAM,SAAU,EAAU,CAAO,EAAI,GAAS,EAAa,EAAM,EAAM,IAAK,CAAM,CAAC,EAFlF,EAAW,EAAK,CAAM,CAG3C,EAEA,MAAO,CACL,SACA,SAAS,EAAa,EAAoF,CACxG,OAAO,EAAgB,EAAK,CAAO,CACrC,EACA,kBACA,UAAU,EAAa,EAA4C,CAAC,EAAG,CACrE,OAAO,EAAiB,EAAK,CAAO,CACtC,EACA,kBACF,CACF,CAEA,SAAgB,EAAoC,EAAuB,EAA4C,CACrH,IAAM,EAAW,IAAI,IAErB,IAAK,GAAM,CAAC,EAAQ,KAAY,OAAO,QAAQ,CAAQ,EACrD,EAAS,IAAI,EAAA,gBAAgB,CAAM,EAAG,EAAA,eAAe,CAAO,CAAC,EAG/D,OAAO,EAAgC,EAAU,CAAO,CAC1D"}
@@ -0,0 +1,20 @@
1
+ import type { Catalog, Catalogs, Locale, PluralKey, PluralOptions, TextKey, TranslateOptions, TranslatorOptions } from './types';
2
+ import { type CompiledCatalog } from './_catalog';
3
+ export type Translator<C extends Catalog = Catalog> = {
4
+ readonly locale: Locale;
5
+ segments<V>(key: TextKey<C>, options: TranslateOptions & {
6
+ values: Record<string, V>;
7
+ }): Array<string | V>;
8
+ segments<V>(key: PluralKey<C>, options: PluralOptions & {
9
+ values?: Record<string, V>;
10
+ }): Array<string | number | V>;
11
+ segmentsDynamic<V>(key: string, options: (TranslateOptions | PluralOptions) & {
12
+ values?: Record<string, V>;
13
+ }): Array<string | number | V>;
14
+ translate(key: TextKey<C>, options?: TranslateOptions): string;
15
+ translate(key: PluralKey<C>, options: PluralOptions): string;
16
+ translateDynamic(key: string, options?: TranslateOptions | PluralOptions): string;
17
+ };
18
+ export declare function createTranslatorFromCompiled<C extends Catalog>(catalogs: ReadonlyMap<Locale, CompiledCatalog>, options?: TranslatorOptions): Translator<C>;
19
+ export declare function createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C>;
20
+ //# sourceMappingURL=translator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../src/translator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EACR,MAAM,EACN,SAAS,EACT,aAAa,EACb,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,KAAK,eAAe,EAAwC,MAAM,YAAY,CAAC;AAKxF,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,IAAI;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;KAAE,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3G,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;KAAE,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC;IACpH,eAAe,CAAC,CAAC,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,gBAAgB,GAAG,aAAa,CAAC,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;KAAE,GAC3E,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC;IAC9B,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IAC/D,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAAC;IAC7D,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,aAAa,GAAG,MAAM,CAAC;CACnF,CAAC;AAIF,wBAAgB,4BAA4B,CAAC,CAAC,SAAS,OAAO,EAC5D,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,EAC9C,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CAAC,CAAC,CAAC,CAmFf;AAED,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,UAAU,CAAC,CAAC,CAAC,CAQrH"}
@@ -0,0 +1,61 @@
1
+ import { LinguaInvalidPluralCountError as e } from "./errors.js";
2
+ import { canonicalLocale as t, localeChain as n, pluralCategory as r } from "./_locale.js";
3
+ import { renderSegments as i, renderText as a } from "./_template.js";
4
+ import { compileCatalog as o } from "./_catalog.js";
5
+ //#region src/translator.ts
6
+ function s(o, s = {}) {
7
+ let c = t(s.locale ?? "en"), l = (Array.isArray(s.fallback) ? s.fallback : s.fallback ? [s.fallback] : []).map(t), u = n(c, l), d = s.onMissingKey ?? ((e) => e), f = s.onMissingValue ?? ((e) => `{${e}}`), p = (e) => {
8
+ for (let t of u) {
9
+ let n = o.get(t)?.get(e);
10
+ if (n) return {
11
+ locale: t,
12
+ message: n
13
+ };
14
+ }
15
+ }, m = (t, n) => {
16
+ let i = p(t);
17
+ if (!i) return;
18
+ if (i.message.kind === "text") return "count" in n ? void 0 : {
19
+ key: t,
20
+ template: i.message.template
21
+ };
22
+ if (!("count" in n) || !Number.isFinite(n.count)) {
23
+ if ("count" in n) throw new e("`count` must be a finite number.");
24
+ return;
25
+ }
26
+ let a = n.count === 0 && !n.ordinal ? "zero" : r(i.locale, n.count, n.ordinal ?? !1), o = i.message.forms.get(a) ?? i.message.forms.get("other");
27
+ return o ? {
28
+ key: t,
29
+ template: o
30
+ } : void 0;
31
+ }, h = (e) => "count" in e ? {
32
+ count: e.count,
33
+ ...e.values
34
+ } : e.values ?? {}, g = (e, t) => {
35
+ let n = m(e, t);
36
+ return n ? i(n.template, h(t), (e) => f(e, n.key, c)) : [d(e, c)];
37
+ }, _ = (e, t = {}) => {
38
+ let n = m(e, t);
39
+ return n ? a(n.template, h(t), (e) => f(e, n.key, c)) : d(e, c);
40
+ };
41
+ return {
42
+ locale: c,
43
+ segments(e, t) {
44
+ return g(e, t);
45
+ },
46
+ segmentsDynamic: g,
47
+ translate(e, t = {}) {
48
+ return _(e, t);
49
+ },
50
+ translateDynamic: _
51
+ };
52
+ }
53
+ function c(e, n) {
54
+ let r = /* @__PURE__ */ new Map();
55
+ for (let [n, i] of Object.entries(e)) r.set(t(n), o(i));
56
+ return s(r, n);
57
+ }
58
+ //#endregion
59
+ export { c as createTranslator, s as createTranslatorFromCompiled };
60
+
61
+ //# sourceMappingURL=translator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.js","names":[],"sources":["../src/translator.ts"],"sourcesContent":["import type {\n Catalog,\n Catalogs,\n Locale,\n PluralKey,\n PluralOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n} from './types';\n\nimport { type CompiledCatalog, type CompiledMessage, compileCatalog } from './_catalog';\nimport { canonicalLocale, localeChain, pluralCategory } from './_locale';\nimport { renderSegments, renderText, type Template } from './_template';\nimport { LinguaInvalidPluralCountError } from './errors';\n\nexport type Translator<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n segments<V>(key: TextKey<C>, options: TranslateOptions & { values: Record<string, V> }): Array<string | V>;\n segments<V>(key: PluralKey<C>, options: PluralOptions & { values?: Record<string, V> }): Array<string | number | V>;\n segmentsDynamic<V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V>;\n translate(key: TextKey<C>, options?: TranslateOptions): string;\n translate(key: PluralKey<C>, options: PluralOptions): string;\n translateDynamic(key: string, options?: TranslateOptions | PluralOptions): string;\n};\n\ntype ResolvedMessage = { readonly locale: Locale; readonly message: CompiledMessage };\n\nexport function createTranslatorFromCompiled<C extends Catalog>(\n catalogs: ReadonlyMap<Locale, CompiledCatalog>,\n options: TranslatorOptions = {},\n): Translator<C> {\n const locale = canonicalLocale(options.locale ?? 'en');\n const fallback = (\n Array.isArray(options.fallback) ? options.fallback : options.fallback ? [options.fallback] : []\n ).map(canonicalLocale);\n const chain = localeChain(locale, fallback);\n const missingKey = options.onMissingKey ?? ((key: string) => key);\n const missingValue = options.onMissingValue ?? ((name: string) => `{${name}}`);\n\n const resolve = (key: string): ResolvedMessage | undefined => {\n for (const candidate of chain) {\n const message = catalogs.get(candidate)?.get(key);\n\n if (message) return { locale: candidate, message };\n }\n\n return undefined;\n };\n\n const templateFor = (\n key: string,\n options: TranslateOptions | PluralOptions,\n ): { key: string; template: Template } | undefined => {\n const found = resolve(key);\n\n if (!found) return undefined;\n\n if (found.message.kind === 'text') {\n if ('count' in options) return undefined;\n\n return { key, template: found.message.template };\n }\n\n if (!('count' in options) || !Number.isFinite(options.count)) {\n if ('count' in options) throw new LinguaInvalidPluralCountError('`count` must be a finite number.');\n\n return undefined;\n }\n\n const category =\n options.count === 0 && !options.ordinal\n ? 'zero'\n : pluralCategory(found.locale, options.count, options.ordinal ?? false);\n const template = found.message.forms.get(category) ?? found.message.forms.get('other');\n\n return template ? { key, template } : undefined;\n };\n\n const valuesFor = (options: TranslateOptions | PluralOptions): Record<string, unknown> =>\n 'count' in options ? { count: options.count, ...options.values } : (options.values ?? {});\n\n const segmentsDynamic = <V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V> => {\n const found = templateFor(key, options);\n\n if (!found) return [missingKey(key, locale)];\n\n return renderSegments(found.template, valuesFor(options) as Record<string, V | number>, (name) =>\n missingValue(name, found.key, locale),\n );\n };\n\n const translateDynamic = (key: string, options: TranslateOptions | PluralOptions = {}): string => {\n const found = templateFor(key, options);\n\n if (!found) return missingKey(key, locale);\n\n return renderText(found.template, valuesFor(options), (name) => missingValue(name, found.key, locale));\n };\n\n return {\n locale,\n segments(key: string, options: (TranslateOptions | PluralOptions) & { values?: Record<string, unknown> }) {\n return segmentsDynamic(key, options);\n },\n segmentsDynamic,\n translate(key: string, options: TranslateOptions | PluralOptions = {}) {\n return translateDynamic(key, options);\n },\n translateDynamic,\n } as Translator<C>;\n}\n\nexport function createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C> {\n const compiled = new Map<Locale, CompiledCatalog>();\n\n for (const [locale, catalog] of Object.entries(catalogs)) {\n compiled.set(canonicalLocale(locale), compileCatalog(catalog));\n }\n\n return createTranslatorFromCompiled<C>(compiled, options);\n}\n"],"mappings":";;;;;AA+BA,SAAgB,EACd,GACA,IAA6B,CAAC,GACf;CACf,IAAM,IAAS,EAAgB,EAAQ,UAAU,IAAI,GAC/C,KACJ,MAAM,QAAQ,EAAQ,QAAQ,IAAI,EAAQ,WAAW,EAAQ,WAAW,CAAC,EAAQ,QAAQ,IAAI,CAAC,EAAA,CAC9F,IAAI,CAAe,GACf,IAAQ,EAAY,GAAQ,CAAQ,GACpC,IAAa,EAAQ,kBAAkB,MAAgB,IACvD,IAAe,EAAQ,oBAAoB,MAAiB,IAAI,EAAK,KAErE,KAAW,MAA6C;EAC5D,KAAK,IAAM,KAAa,GAAO;GAC7B,IAAM,IAAU,EAAS,IAAI,CAAS,CAAC,EAAE,IAAI,CAAG;GAEhD,IAAI,GAAS,OAAO;IAAE,QAAQ;IAAW;GAAQ;EACnD;CAGF,GAEM,KACJ,GACA,MACoD;EACpD,IAAM,IAAQ,EAAQ,CAAG;EAEzB,IAAI,CAAC,GAAO;EAEZ,IAAI,EAAM,QAAQ,SAAS,QAGzB,OAFI,WAAW,IAAS,SAEjB;GAAE;GAAK,UAAU,EAAM,QAAQ;EAAS;EAGjD,IAAI,EAAE,WAAW,MAAY,CAAC,OAAO,SAAS,EAAQ,KAAK,GAAG;GAC5D,IAAI,WAAW,GAAS,MAAM,IAAI,EAA8B,kCAAkC;GAElG;EACF;EAEA,IAAM,IACJ,EAAQ,UAAU,KAAK,CAAC,EAAQ,UAC5B,SACA,EAAe,EAAM,QAAQ,EAAQ,OAAO,EAAQ,WAAW,EAAK,GACpE,IAAW,EAAM,QAAQ,MAAM,IAAI,CAAQ,KAAK,EAAM,QAAQ,MAAM,IAAI,OAAO;EAErF,OAAO,IAAW;GAAE;GAAK;EAAS,IAAI,KAAA;CACxC,GAEM,KAAa,MACjB,WAAW,IAAU;EAAE,OAAO,EAAQ;EAAO,GAAG,EAAQ;CAAO,IAAK,EAAQ,UAAU,CAAC,GAEnF,KACJ,GACA,MAC+B;EAC/B,IAAM,IAAQ,EAAY,GAAK,CAAO;EAItC,OAFK,IAEE,EAAe,EAAM,UAAU,EAAU,CAAO,IAAkC,MACvF,EAAa,GAAM,EAAM,KAAK,CAAM,CACtC,IAJmB,CAAC,EAAW,GAAK,CAAM,CAAC;CAK7C,GAEM,KAAoB,GAAa,IAA4C,CAAC,MAAc;EAChG,IAAM,IAAQ,EAAY,GAAK,CAAO;EAItC,OAFK,IAEE,EAAW,EAAM,UAAU,EAAU,CAAO,IAAI,MAAS,EAAa,GAAM,EAAM,KAAK,CAAM,CAAC,IAFlF,EAAW,GAAK,CAAM;CAG3C;CAEA,OAAO;EACL;EACA,SAAS,GAAa,GAAoF;GACxG,OAAO,EAAgB,GAAK,CAAO;EACrC;EACA;EACA,UAAU,GAAa,IAA4C,CAAC,GAAG;GACrE,OAAO,EAAiB,GAAK,CAAO;EACtC;EACA;CACF;AACF;AAEA,SAAgB,EAAoC,GAAuB,GAA4C;CACrH,IAAM,oBAAW,IAAI,IAA6B;CAElD,KAAK,IAAM,CAAC,GAAQ,MAAY,OAAO,QAAQ,CAAQ,GACrD,EAAS,IAAI,EAAgB,CAAM,GAAG,EAAe,CAAO,CAAC;CAG/D,OAAO,EAAgC,GAAU,CAAO;AAC1D"}
@@ -0,0 +1,53 @@
1
+ export type Locale = string;
2
+ export type PluralCategory = Intl.LDMLPluralRule;
3
+ /** Explicit plural message. Grouping objects never have plural semantics. */
4
+ export type PluralMessage = {
5
+ readonly plural: Partial<Record<PluralCategory, string>>;
6
+ };
7
+ export type CatalogNode = Catalog | PluralMessage | string;
8
+ export type Catalog = {
9
+ readonly [key: string]: CatalogNode;
10
+ };
11
+ export type Catalogs<C extends Catalog = Catalog> = Record<Locale, C>;
12
+ export type CatalogLoader<C extends Catalog = Catalog> = () => Promise<C>;
13
+ export type CatalogSource<C extends Catalog = Catalog> = C | CatalogLoader<C>;
14
+ /** Locale declarations may contain static catalogs or lazy loaders. */
15
+ export type CatalogSources<C extends Catalog = Catalog> = Record<Locale, CatalogSource<C>>;
16
+ /** Resolved catalog payload used for SSR serialization and hydration. */
17
+ export type LoadedCatalogs<C extends Catalog = Catalog> = Catalogs<C>;
18
+ export type MessageKey<C, Prefix extends string = '', Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1]> = Depth extends readonly [unknown, ...infer Rest] ? C extends string | PluralMessage ? Prefix : C extends Catalog ? {
19
+ [K in string & keyof C]: MessageKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;
20
+ }[string & keyof C] : never : never;
21
+ export type TextKey<C, Prefix extends string = '', Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1]> = Depth extends readonly [unknown, ...infer Rest] ? C extends string ? Prefix : C extends Catalog ? {
22
+ [K in string & keyof C]: TextKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;
23
+ }[string & keyof C] : never : never;
24
+ export type PluralKey<C, Prefix extends string = '', Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1]> = Depth extends readonly [unknown, ...infer Rest] ? C extends PluralMessage ? Prefix : C extends Catalog ? {
25
+ [K in string & keyof C]: PluralKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;
26
+ }[string & keyof C] : never : never;
27
+ export type Values = Record<string, unknown>;
28
+ export type TranslateOptions = {
29
+ values?: Values;
30
+ };
31
+ export type PluralOptions = TranslateOptions & {
32
+ count: number;
33
+ ordinal?: boolean;
34
+ };
35
+ export type TranslatorOptions = {
36
+ fallback?: Locale | readonly Locale[];
37
+ locale?: Locale;
38
+ onMissingKey?: (key: string, locale: Locale) => string;
39
+ onMissingValue?: (name: string, key: string, locale: Locale) => string;
40
+ };
41
+ export type SubscribeOptions = {
42
+ immediate?: boolean;
43
+ signal?: AbortSignal;
44
+ };
45
+ export type TranslationStoreOptions<C extends Catalog = Catalog> = TranslatorOptions & {
46
+ catalogs: CatalogSources<C>;
47
+ };
48
+ export type TranslationState<C extends Catalog = Catalog> = {
49
+ readonly catalogs: LoadedCatalogs<C>;
50
+ readonly locale: Locale;
51
+ readonly version: 3;
52
+ };
53
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/validate.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_constants.cjs"),t=require("./template.cjs");function n(e){try{let{pluralCategories:t}=new Intl.PluralRules(e).resolvedOptions();return new Set(t)}catch{return new Set([`one`,`other`])}}function r(n,i=``){let a=[];for(let[o,s]of Object.entries(n)){if(e.UNSAFE_KEYS.has(o))continue;let n=i?`${i}.${o}`:o;if(typeof s==`string`){let e=t.parsePipePlural(s);e&&a.push({key:n,obj:e});continue}let c=s;Object.keys(c).some(t=>e.CLDR_FORMS.has(t))?a.push({key:n,obj:c}):a.push(...r(c,n))}return a}var i=new Set([`other`,`two`,`few`,`many`]);function a(e,t){let a=[],o=n(t);for(let{key:n,obj:s}of r(e))for(let e of o)if(!(e in s))a.push({form:e,key:n,locale:t});else if(i.has(e)){let r=s[e];typeof r==`string`&&!r.includes(`{count}`)&&a.push({form:`${e}:missing-count`,key:n,locale:t})}return a}exports.validateCatalog=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_locale.cjs"),t=require("./_catalog.cjs");function n(e){return typeof e==`object`&&!!e&&Object.hasOwn(e,`plural`)}function r(r,i){t.compileCatalog(r);let a=e.canonicalLocale(i),o=new Intl.PluralRules(a).resolvedOptions().pluralCategories,s=[],c=(e,t)=>{if(typeof e!=`string`){if(n(e)){for(let n of o)Object.hasOwn(e.plural,n)||s.push({key:t,locale:a,missing:n});return}for(let[n,r]of Object.entries(e))c(r,t?`${t}.${n}`:n)}};for(let[e,t]of Object.entries(r))c(t,e);return s}exports.validateCatalog=r;
2
2
  //# sourceMappingURL=validate.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.cjs","names":[],"sources":["../src/validate.ts"],"sourcesContent":["/**\n * /lingua/validate\n *\n * Build-time and CI catalog validation utilities.\n * Import from `@vielzeug/lingua/validate`.\n *\n * Not intended for production use — keep this entry out of your production bundle.\n *\n * @example\n * ```ts\n * import { validateCatalog } from '@vielzeug/lingua/validate';\n *\n * const warnings = validateCatalog({ inbox: { one: '...', other: '...' } }, 'ar');\n * // → warnings for 'zero', 'two', 'few', 'many' in Arabic\n * if (warnings.length > 0) throw new Error(`Missing plural forms:\\n${JSON.stringify(warnings, null, 2)}`);\n * ```\n */\n\nimport type { Messages } from './_catalog';\nimport type { Locale } from './_chain';\n\nimport { CLDR_FORMS, UNSAFE_KEYS } from './_constants';\nimport { parsePipePlural } from './template';\n\nexport type ValidationWarning = {\n form: string;\n key: string;\n locale: Locale;\n};\n\n/**\n * Returns the canonical CLDR plural forms expected for the given locale.\n * Uses `Intl.PluralRules#resolvedOptions().pluralCategories` the authoritative source,\n * unlike heuristic sampling.\n */\nfunction getExpectedPluralForms(locale: Locale): Set<string> {\n try {\n const { pluralCategories } = new Intl.PluralRules(locale).resolvedOptions() as {\n pluralCategories: string[];\n };\n\n return new Set(pluralCategories);\n } catch {\n return new Set(['one', 'other']);\n }\n}\n\nfunction findPluralBranches(messages: Messages, prefix = ''): Array<{ key: string; obj: Messages }> {\n const result: Array<{ key: string; obj: Messages }> = [];\n\n for (const [k, v] of Object.entries(messages)) {\n if (UNSAFE_KEYS.has(k)) continue;\n\n const fullKey = prefix ? `${prefix}.${k}` : k;\n\n if (typeof v === 'string') {\n // Expand pipe-plural shorthand so it can be validated like a nested plural branch.\n const expanded = parsePipePlural(v);\n\n if (expanded) result.push({ key: fullKey, obj: expanded });\n\n continue;\n }\n\n const obj = v as Messages;\n const childKeys = Object.keys(obj);\n\n if (childKeys.some((ck) => CLDR_FORMS.has(ck))) {\n result.push({ key: fullKey, obj });\n } else {\n result.push(...findPluralBranches(obj, fullKey));\n }\n }\n\n return result;\n}\n\n// Plural form values that typically reference the count (i.e. non-singleton forms).\n// 'zero' and 'one' often intentionally omit {count} (e.g. 'No messages', 'One message').\nconst FORMS_EXPECTING_COUNT = new Set(['other', 'two', 'few', 'many']);\n\n/**\n * Validates a catalog against the expected CLDR plural forms for the given locale.\n * Returns an array of warnings for:\n * 1. Plural branches missing one or more expected CLDR forms.\n * 2. Plural form templates for `other`, `two`, `few`, or `many` that do not interpolate\n * `{count}` — since `count` is injected automatically by `tp()`, omitting it is almost\n * always a catalog authoring error. Warnings for these use `form: '<form>:missing-count'`.\n *\n * Uses `Intl.PluralRules#resolvedOptions().pluralCategories` — the authoritative CLDR\n * category list, not heuristic sampling.\n *\n * @example\n * const warnings = validateCatalog({ inbox: { one: '...', other: '...' } }, 'ar');\n * // → warnings for missing 'zero', 'two', 'few', 'many' in Arabic\n */\nexport function validateCatalog(messages: Messages, locale: Locale): ValidationWarning[] {\n const warnings: ValidationWarning[] = [];\n const expectedForms = getExpectedPluralForms(locale);\n\n for (const { key, obj } of findPluralBranches(messages)) {\n for (const form of expectedForms) {\n if (!(form in obj)) {\n warnings.push({ form, key, locale });\n } else if (FORMS_EXPECTING_COUNT.has(form)) {\n const template = obj[form];\n\n if (typeof template === 'string' && !template.includes('{count}')) {\n warnings.push({ form: `${form}:missing-count`, key, locale });\n }\n }\n }\n }\n\n return warnings;\n}\n"],"mappings":"mIAmCA,SAAS,EAAuB,EAA6B,CAC3D,GAAI,CACF,GAAM,CAAE,oBAAqB,IAAI,KAAK,YAAY,CAAM,CAAC,CAAC,gBAAgB,EAI1E,OAAO,IAAI,IAAI,CAAgB,CACjC,MAAQ,CACN,OAAO,IAAI,IAAI,CAAC,MAAO,OAAO,CAAC,CACjC,CACF,CAEA,SAAS,EAAmB,EAAoB,EAAS,GAA2C,CAClG,IAAM,EAAgD,CAAC,EAEvD,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,CAAQ,EAAG,CAC7C,GAAI,EAAA,YAAY,IAAI,CAAC,EAAG,SAExB,IAAM,EAAU,EAAS,GAAG,EAAO,GAAG,IAAM,EAE5C,GAAI,OAAO,GAAM,SAAU,CAEzB,IAAM,EAAW,EAAA,gBAAgB,CAAC,EAE9B,GAAU,EAAO,KAAK,CAAE,IAAK,EAAS,IAAK,CAAS,CAAC,EAEzD,QACF,CAEA,IAAM,EAAM,EACM,OAAO,KAAK,CAE1B,CAAA,CAAU,KAAM,GAAO,EAAA,WAAW,IAAI,CAAE,CAAC,EAC3C,EAAO,KAAK,CAAE,IAAK,EAAS,KAAI,CAAC,EAEjC,EAAO,KAAK,GAAG,EAAmB,EAAK,CAAO,CAAC,CAEnD,CAEA,OAAO,CACT,CAIA,IAAM,EAAwB,IAAI,IAAI,CAAC,QAAS,MAAO,MAAO,MAAM,CAAC,EAiBrE,SAAgB,EAAgB,EAAoB,EAAqC,CACvF,IAAM,EAAgC,CAAC,EACjC,EAAgB,EAAuB,CAAM,EAEnD,IAAK,GAAM,CAAE,MAAK,SAAS,EAAmB,CAAQ,EACpD,IAAK,IAAM,KAAQ,EACjB,GAAI,EAAE,KAAQ,GACZ,EAAS,KAAK,CAAE,OAAM,MAAK,QAAO,CAAC,OAC9B,GAAI,EAAsB,IAAI,CAAI,EAAG,CAC1C,IAAM,EAAW,EAAI,GAEjB,OAAO,GAAa,UAAY,CAAC,EAAS,SAAS,SAAS,GAC9D,EAAS,KAAK,CAAE,KAAM,GAAG,EAAK,gBAAiB,MAAK,QAAO,CAAC,CAEhE,CAIJ,OAAO,CACT"}
1
+ {"version":3,"file":"validate.cjs","names":[],"sources":["../src/validate.ts"],"sourcesContent":["import type { Catalog, CatalogNode, Locale, PluralMessage } from './types';\n\nimport { compileCatalog } from './_catalog';\nimport { canonicalLocale } from './_locale';\n\nexport type ValidationIssue = {\n key: string;\n locale: Locale;\n missing: Intl.LDMLPluralRule;\n};\n\nfunction isPluralMessage(node: CatalogNode): node is PluralMessage {\n return typeof node === 'object' && node !== null && Object.hasOwn(node, 'plural');\n}\n\n/** Explicit tooling API. Runtime never loads validation code or emits asynchronous catalog warnings. */\nexport function validateCatalog(catalog: Catalog, locale: Locale): ValidationIssue[] {\n compileCatalog(catalog);\n\n const canonical = canonicalLocale(locale);\n const categories = new Intl.PluralRules(canonical).resolvedOptions().pluralCategories;\n const issues: ValidationIssue[] = [];\n\n const visit = (node: CatalogNode, key: string): void => {\n if (typeof node === 'string') return;\n\n if (isPluralMessage(node)) {\n for (const missing of categories) {\n if (!Object.hasOwn(node.plural, missing)) issues.push({ key, locale: canonical, missing });\n }\n\n return;\n }\n\n for (const [childKey, child] of Object.entries(node)) visit(child, key ? `${key}.${childKey}` : childKey);\n };\n\n for (const [key, node] of Object.entries(catalog)) visit(node, key);\n\n return issues;\n}\n"],"mappings":"gIAWA,SAAS,EAAgB,EAA0C,CACjE,OAAO,OAAO,GAAS,YAAY,GAAiB,OAAO,OAAO,EAAM,QAAQ,CAClF,CAGA,SAAgB,EAAgB,EAAkB,EAAmC,CACnF,EAAA,eAAe,CAAO,EAEtB,IAAM,EAAY,EAAA,gBAAgB,CAAM,EAClC,EAAa,IAAI,KAAK,YAAY,CAAS,CAAC,CAAC,gBAAgB,CAAC,CAAC,iBAC/D,EAA4B,CAAC,EAE7B,GAAS,EAAmB,IAAsB,CAClD,UAAO,GAAS,SAEpB,IAAI,EAAgB,CAAI,EAAG,CACzB,IAAK,IAAM,KAAW,EACf,OAAO,OAAO,EAAK,OAAQ,CAAO,GAAG,EAAO,KAAK,CAAE,MAAK,OAAQ,EAAW,SAAQ,CAAC,EAG3F,MACF,CAEA,IAAK,GAAM,CAAC,EAAU,KAAU,OAAO,QAAQ,CAAI,EAAG,EAAM,EAAO,EAAM,GAAG,EAAI,GAAG,IAAa,CAAQ,CAFxG,CAGF,EAEA,IAAK,GAAM,CAAC,EAAK,KAAS,OAAO,QAAQ,CAAO,EAAG,EAAM,EAAM,CAAG,EAElE,OAAO,CACT"}
@@ -1,41 +1,9 @@
1
- /**
2
- * /lingua/validate
3
- *
4
- * Build-time and CI catalog validation utilities.
5
- * Import from `@vielzeug/lingua/validate`.
6
- *
7
- * Not intended for production use — keep this entry out of your production bundle.
8
- *
9
- * @example
10
- * ```ts
11
- * import { validateCatalog } from '@vielzeug/lingua/validate';
12
- *
13
- * const warnings = validateCatalog({ inbox: { one: '...', other: '...' } }, 'ar');
14
- * // → warnings for 'zero', 'two', 'few', 'many' in Arabic
15
- * if (warnings.length > 0) throw new Error(`Missing plural forms:\n${JSON.stringify(warnings, null, 2)}`);
16
- * ```
17
- */
18
- import type { Messages } from './_catalog';
19
- import type { Locale } from './_chain';
20
- export type ValidationWarning = {
21
- form: string;
1
+ import type { Catalog, Locale } from './types';
2
+ export type ValidationIssue = {
22
3
  key: string;
23
4
  locale: Locale;
5
+ missing: Intl.LDMLPluralRule;
24
6
  };
25
- /**
26
- * Validates a catalog against the expected CLDR plural forms for the given locale.
27
- * Returns an array of warnings for:
28
- * 1. Plural branches missing one or more expected CLDR forms.
29
- * 2. Plural form templates for `other`, `two`, `few`, or `many` that do not interpolate
30
- * `{count}` — since `count` is injected automatically by `tp()`, omitting it is almost
31
- * always a catalog authoring error. Warnings for these use `form: '<form>:missing-count'`.
32
- *
33
- * Uses `Intl.PluralRules#resolvedOptions().pluralCategories` — the authoritative CLDR
34
- * category list, not heuristic sampling.
35
- *
36
- * @example
37
- * const warnings = validateCatalog({ inbox: { one: '...', other: '...' } }, 'ar');
38
- * // → warnings for missing 'zero', 'two', 'few', 'many' in Arabic
39
- */
40
- export declare function validateCatalog(messages: Messages, locale: Locale): ValidationWarning[];
7
+ /** Explicit tooling API. Runtime never loads validation code or emits asynchronous catalog warnings. */
8
+ export declare function validateCatalog(catalog: Catalog, locale: Locale): ValidationIssue[];
41
9
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAKvC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAqDF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAmBvF"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,EAAiB,MAAM,SAAS,CAAC;AAK3E,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,IAAI,CAAC,cAAc,CAAC;CAC9B,CAAC;AAMF,wGAAwG;AACxG,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAwBnF"}
package/dist/validate.js CHANGED
@@ -1,59 +1,28 @@
1
- import { CLDR_FORMS as e, UNSAFE_KEYS as t } from "./_constants.js";
2
- import { parsePipePlural as n } from "./template.js";
1
+ import { canonicalLocale as e } from "./_locale.js";
2
+ import { compileCatalog as t } from "./_catalog.js";
3
3
  //#region src/validate.ts
4
- function r(e) {
5
- try {
6
- let { pluralCategories: t } = new Intl.PluralRules(e).resolvedOptions();
7
- return new Set(t);
8
- } catch {
9
- return /* @__PURE__ */ new Set(["one", "other"]);
10
- }
4
+ function n(e) {
5
+ return typeof e == "object" && !!e && Object.hasOwn(e, "plural");
11
6
  }
12
- function i(r, a = "") {
13
- let o = [];
14
- for (let [s, c] of Object.entries(r)) {
15
- if (t.has(s)) continue;
16
- let r = a ? `${a}.${s}` : s;
17
- if (typeof c == "string") {
18
- let e = n(c);
19
- e && o.push({
20
- key: r,
21
- obj: e
22
- });
23
- continue;
7
+ function r(r, i) {
8
+ t(r);
9
+ let a = e(i), o = new Intl.PluralRules(a).resolvedOptions().pluralCategories, s = [], c = (e, t) => {
10
+ if (typeof e != "string") {
11
+ if (n(e)) {
12
+ for (let n of o) Object.hasOwn(e.plural, n) || s.push({
13
+ key: t,
14
+ locale: a,
15
+ missing: n
16
+ });
17
+ return;
18
+ }
19
+ for (let [n, r] of Object.entries(e)) c(r, t ? `${t}.${n}` : n);
24
20
  }
25
- let l = c;
26
- Object.keys(l).some((t) => e.has(t)) ? o.push({
27
- key: r,
28
- obj: l
29
- }) : o.push(...i(l, r));
30
- }
31
- return o;
32
- }
33
- var a = /* @__PURE__ */ new Set([
34
- "other",
35
- "two",
36
- "few",
37
- "many"
38
- ]);
39
- function o(e, t) {
40
- let n = [], o = r(t);
41
- for (let { key: r, obj: s } of i(e)) for (let e of o) if (!(e in s)) n.push({
42
- form: e,
43
- key: r,
44
- locale: t
45
- });
46
- else if (a.has(e)) {
47
- let i = s[e];
48
- typeof i == "string" && !i.includes("{count}") && n.push({
49
- form: `${e}:missing-count`,
50
- key: r,
51
- locale: t
52
- });
53
- }
54
- return n;
21
+ };
22
+ for (let [e, t] of Object.entries(r)) c(t, e);
23
+ return s;
55
24
  }
56
25
  //#endregion
57
- export { o as validateCatalog };
26
+ export { r as validateCatalog };
58
27
 
59
28
  //# sourceMappingURL=validate.js.map