@vielzeug/codex 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export {\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCatalogError,\n LinguaInvalidLocaleError,\n LinguaInvalidPluralCountError,\n LinguaInvalidStateError,\n LinguaMissingCatalogError,\n} from './errors';\nexport {\n createTranslationStore,\n hydrateTranslationStore,\n type TranslationSnapshot,\n type TranslationStore,\n} from './i18n';\nexport { createTranslator, type Translator } from './translator';\nexport type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';\n",
2
+ "apiSource": "export {\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCatalogError,\n LinguaInvalidLocaleError,\n LinguaInvalidPluralCountError,\n LinguaInvalidStateError,\n LinguaMissingCatalogError,\n} from './errors';\nexport {\n createTranslationStore,\n hydrateTranslationStore,\n type TranslationSnapshot,\n type TranslationStore,\n} from './i18n';\nexport { createCatalogTranslator, createTranslator, type Translator } from './translator';\nexport type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Lingua — Explicit localization for TypeScript\ndescription: Framework-neutral locale catalogs, typed translations, and explicit plural messages.\npackage: lingua\ncategory: i18n\nkeywords: [internationalization, translations, pluralization, locale, i18n, catalog-loading]\nrelated: [ripple, wayfinder, courier]\nexports: [createTranslationStore, createTranslator, hydrateTranslationStore, LinguaError, LinguaDisposedError, LinguaInvalidCatalogError, LinguaInvalidLocaleError, LinguaInvalidPluralCountError, LinguaInvalidStateError, LinguaMissingCatalogError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"lingua\" />\n\n## Why Lingua?\n\nLingua separates immutable translation from mutable locale state. Use one catalog per locale, then select static or stateful API from whether locale can change.\n\n```ts\n// Before\nconst message = catalogs[locale]?.inbox?.[count === 1 ? 'one' : 'other'] ?? 'inbox';\n\n// After\nconst output = i18n.translate('inbox', { count });\n```\n\n| Feature | Lingua | i18next | FormatJS |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"lingua\" type=\"size\" /> | Varies by selected modules | Varies by selected modules |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Explicit plural catalog nodes | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Convention/config dependent | ICU-message dependent |\n| Declared lazy locale catalogs | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Plugin/config dependent | Application-defined |\n| Immutable locale snapshots | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | Application-defined |\n\n<div class=\"decision-callout\">\n\n**Use Lingua when** you need a compact TypeScript runtime with explicit catalog structure, deterministic fallback, and framework-neutral subscriptions.\n\n**Consider i18next or FormatJS when** you need their plugin ecosystems, message extraction pipelines, or framework-specific integrations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/lingua\n```\n\n```sh [npm]\nnpm install @vielzeug/lingua\n```\n\n```sh [yarn]\nyarn add @vielzeug/lingua\n```\n\n:::\n\n## Quick Start\n\nCreate locale store with static catalogs, then dispose it when owner ends.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n de: { inbox: { plural: { one: 'Eine Nachricht', other: '{count} Nachrichten' } } },\n en: { inbox: { plural: { one: 'One message', other: '{count} messages' } } },\n },\n locale: 'en',\n});\n\ntry {\n console.log(i18n.translate('inbox', { count: 3 }));\n await i18n.setLocale('de');\n console.log(i18n.translate('inbox', { count: 1 }));\n} finally {\n i18n.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createTranslator()` compiles immutable multi-locale catalogs.\n- `createTranslationStore()` manages locale changes and declared catalogs.\n- `translate()` renders text and plural messages through explicit catalog nodes.\n- `translateDynamic()` makes runtime-key lookup explicit.\n- `load()` deduplicates lazy catalog loading per locale.\n- `getSnapshot()` and `subscribe()` expose immutable translator revisions.\n- `serialize()` and `hydrateTranslationStore()` transfer resolved SSR catalogs.\n- `createFormatter()` and `validateCatalog()` remain isolated subpath tools.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](../ripple/index.md) adapts Lingua snapshots into reactive application state.\n- [Courier](../courier/index.md) can fetch locale catalogs before passing them to Lingua loaders.\n- [Wayfinder](../wayfinder/index.md) can drive locale selection from route state.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Lingua — API Reference\ndescription: Complete API reference for @vielzeug/lingua.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createTranslator()` | Compile immutable locale catalogs | Sync | Locale is fixed for translator lifetime |\n| `createTranslationStore()` | Create mutable locale and catalog store | Sync | Load lazy locale explicitly |\n| `hydrateTranslationStore()` | Create store from serialized loaded catalogs | Sync | Serialized state never includes loaders |\n| `createFormatter()` | Format Intl values from `/format` | Sync | Import from subpath |\n| `validateCatalog()` | Check explicit plural forms from `/validate` | Sync | Import from subpath |\n| `LinguaError` | Base class for Lingua errors | Sync | Use `LinguaError.is()` for broad narrowing |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/lingua` | Translation factories, state types, and Lingua errors |\n| `@vielzeug/lingua/format` | `createFormatter()` and formatter types |\n| `@vielzeug/lingua/validate` | `validateCatalog()` and `ValidationIssue` |\n\n## Translation Factories\n\n### createTranslator\n\n```ts\nfunction createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C>;\n```\n\nCompiles locale catalogs and returns immutable translator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalogs` | `Catalogs<C>` | Locale-keyed catalog objects |\n| `options` | `TranslatorOptions` | Locale, fallback chain, and missing-message handlers |\n\n**Returns:** `Translator<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `translate` | `(textKey, options?)` or `(pluralKey, { count, ordinal?, values? })` | Rendered string |\n| `translateDynamic` | `(key, options?)` | Rendered string for runtime key |\n| `segments` | `(textKey, { values })` or `(pluralKey, { count, ordinal?, values? })` | String and typed-value segments |\n| `segmentsDynamic` | `(key, options)` | Segments for runtime key |\n| `locale` | `Locale` | Resolved active locale |\n\n---\n\n### createTranslationStore\n\n```ts\nfunction createTranslationStore<C extends Catalog>(options: TranslationStoreOptions<C>): TranslationStore<C>;\n```\n\nCreates catalog store, current locale state, and immutable translator snapshots.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.catalogs` | `CatalogSources<C>` | Static catalogs or lazy locale loaders |\n| `options.locale` | `Locale` | Initial locale; defaults to `en` |\n| `options.fallback` | `Locale \\| readonly Locale[]` | Fallback locale chain |\n| `options.onMissingKey` | `(key, locale) => string` | Missing-message handler |\n| `options.onMissingValue` | `(name, key, locale) => string` | Missing-interpolation handler |\n\n**Returns:** `TranslationStore<C>`, with every `Translator<C>` method plus lifecycle methods.\n\n**Example:**\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst translations = createTranslationStore({\n catalogs: { en: { title: 'Home' }, fr: { title: 'Accueil' } },\n locale: 'en',\n});\n\nawait translations.setLocale('fr');\ntranslations.translate('title');\n```\n\n| Method or property | Signature | Returns |\n| --- | --- | --- |\n| `translate` | Translator method | Rendered string |\n| `segments` | Translator method | String and typed-value segments |\n| `load` | `({ locale? })` | `Promise<void>` after catalog resolution |\n| `setLocale` | `(locale)` | `Promise<void>` after locale commit; never loads implicitly |\n| `isLoaded` | `({ locale? })` | `boolean` |\n| `getSnapshot` | `()` | `TranslationSnapshot<C>` |\n| `subscribe` | `(listener, { immediate?, signal? })` | Unsubscribe function |\n| `serialize` | `()` | Loader-free `TranslationState<C>` |\n| `dispose` | `()` | `void` |\n| `locale` | `Locale` | Current canonical locale |\n| `disposed` | `boolean` | Disposal state |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal |\n| `[Symbol.dispose]` | `()` | Delegates to `dispose()` |\n\n---\n\n### hydrateTranslationStore\n\n```ts\nfunction hydrateTranslationStore<C extends Catalog>(\n state: TranslationState<C>,\n options?: Omit<TranslationStoreOptions<C>, 'locale' | 'catalogs'>,\n): TranslationStore<C>;\n```\n\nCreates translation store from SSR state payload containing resolved raw catalogs.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `state` | `TranslationState<C>` | Version `3`, active locale, and loader-free catalogs |\n| `options` | `Omit<TranslationStoreOptions<C>, 'locale' \\| 'catalogs'>` | Fallback and missing-message handlers |\n\n**Returns:** `TranslationStore<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst server = createTranslationStore({ catalogs: { en: { title: 'Home' } }, locale: 'en' });\nconst client = hydrateTranslationStore(server.serialize());\n\nclient.translate('title');\n```\n\n## Migration from 1.x\n\n| Before | After |\n| --- | --- |\n| `createI18n(options)` | `createTranslationStore({ catalogs, ...options })` |\n| `hydrateI18n(state)` | `hydrateTranslationStore(state)` |\n| `I18n` / `I18nSnapshot` | `TranslationStore` / `TranslationSnapshot` |\n| `I18nState` version `2` | `TranslationState` version `3` |\n| Named resources and namespaces | One catalog source per locale |\n| `LinguaMissingResourceError` | `LinguaMissingCatalogError` |\n\n## Formatting and Validation\n\n### createFormatter\n\n```ts\nfunction createFormatter(source: string | (() => string)): Formatter;\n```\n\nCreates cached Intl formatters using static locale or locale getter.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `string \\| (() => string)` | Static locale or locale getter |\n\n**Returns:** `Formatter`.\n\n**Example:**\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\n\nconst formatter = createFormatter('en-US');\nformatter.currency(19.99, 'USD');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `number` | `(value, options?)` | `string` |\n| `currency` | `(value, currency, options?)` | `string` |\n| `date` | `(value, options?)` | `string` |\n| `relative` | `(value, unit, options?)` | `string` |\n| `list` | `(value, options?)` | `string` |\n| `duration` | `(value, options?)` | `string` |\n\n### validateCatalog\n\n```ts\nfunction validateCatalog(catalog: Catalog, locale: Locale): ValidationIssue[];\n```\n\nValidates explicit plural messages against locale plural categories after catalog structural validation.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalog` | `Catalog` | Explicit catalog to validate |\n| `locale` | `Locale` | BCP 47 locale tag |\n\n**Returns:** `ValidationIssue[]`.\n\n**Example:**\n\n```ts\nimport { validateCatalog } from '@vielzeug/lingua/validate';\n\nvalidateCatalog({ inbox: { plural: { one: 'One message' } } }, 'en');\n```\n\n## Types\n\n```ts\ntype Locale = string;\ntype PluralCategory = Intl.LDMLPluralRule;\ntype PluralMessage = { readonly plural: Partial<Record<PluralCategory, string>> };\ntype CatalogNode = Catalog | PluralMessage | string;\ntype Catalog = { readonly [key: string]: CatalogNode };\ntype Catalogs<C extends Catalog = Catalog> = Record<Locale, C>;\ntype CatalogLoader<C extends Catalog = Catalog> = () => Promise<C>;\ntype CatalogSource<C extends Catalog = Catalog> = C | CatalogLoader<C>;\ntype CatalogSources<C extends Catalog = Catalog> = Record<Locale, CatalogSource<C>>;\ntype LoadedCatalogs<C extends Catalog = Catalog> = Catalogs<C>;\n\ntype TranslationStoreOptions<C extends Catalog = Catalog> = TranslatorOptions & {\n catalogs: CatalogSources<C>;\n};\n\ntype TranslationState<C extends Catalog = Catalog> = {\n readonly catalogs: LoadedCatalogs<C>;\n readonly locale: Locale;\n readonly version: 3;\n};\n\ntype TranslationSnapshot<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n readonly revision: number;\n readonly translator: Translator<C>;\n};\n\ntype TranslationStore<C extends Catalog = Catalog> = Translator<C> & {\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 [Symbol.dispose](): void;\n};\n```\n\n```ts\ntype Values = Record<string, unknown>;\ntype TranslateOptions = { values?: Values };\ntype PluralOptions = TranslateOptions & { count: number; ordinal?: boolean };\ntype TranslatorOptions = {\n fallback?: Locale | readonly Locale[];\n locale?: Locale;\n onMissingKey?: (key: string, locale: Locale) => string;\n onMissingValue?: (name: string, key: string, locale: Locale) => string;\n};\ntype SubscribeOptions = { immediate?: boolean; signal?: AbortSignal };\n\ntype DurationValue = Partial<Record<\n 'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'months' | 'nanoseconds' | 'seconds' | 'weeks' | 'years',\n number\n>>;\n\ntype 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\ntype ListFormatOptions = { style?: 'long' | 'narrow' | 'short'; type?: 'and' | 'or' };\n\ntype Formatter = {\n currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'currency' | 'style'>): string;\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;\n duration(value: DurationValue, options?: DurationFormatOptions): string;\n list(value: Array<string | number>, options?: ListFormatOptions): string;\n number(value: number, options?: Intl.NumberFormatOptions): string;\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;\n};\n\ntype ValidationIssue = { key: string; locale: Locale; missing: Intl.LDMLPluralRule };\n```\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `LinguaDisposedError` | State mutation or subscription after `dispose()` |\n| `LinguaInvalidCatalogError` | Invalid catalog node or reserved key |\n| `LinguaInvalidLocaleError` | Invalid BCP 47 locale tag |\n| `LinguaInvalidPluralCountError` | Non-finite plural count |\n| `LinguaInvalidStateError` | Unsupported serialized state version |\n| `LinguaMissingCatalogError` | Catalog has no source for requested locale |\n",
6
- "usage": "---\ntitle: Lingua — Usage Guide\ndescription: Translate explicit catalogs, load lazy locales, and connect locale snapshots to UI state.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate i18n store from locale-keyed catalogs. Strings are text messages; plural messages use `{ plural: ... }`.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: {\n greeting: 'Hello, {name}!',\n inbox: { plural: { one: 'One message', other: '{count} messages' } },\n },\n },\n locale: 'en',\n});\n\nconsole.log(i18n.translate('greeting', { values: { name: 'Ada' } }));\nconsole.log(i18n.translate('inbox', { count: 3 }));\n```\n\nCall `dispose()` when store belongs to temporary request, test, or route owner.\n\n## Define Explicit Catalogs\n\nUse nested objects only to group keys. A plural message always has `plural`, so regular objects containing `one` or `other` remain groups.\n\n```ts\nconst catalog = {\n account: {\n greeting: 'Hello, {name}!',\n unread: { plural: { one: 'One unread message', other: '{count} unread messages' } },\n },\n};\n```\n\nUse `{ values }` for text replacements. Pass `count` at top level for plural selection; Lingua injects it into selected template.\n\n## Render Framework Content\n\nUse `segments()` when replacements are framework nodes, links, or other values that must not be stringified.\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { error: 'Try {retry} or {support}.' } },\n { locale: 'en' },\n);\n\nconst retry = { href: '/retry', label: 'retry' };\nconst support = { href: '/support', label: 'support' };\n\nconsole.log(translator.segments('error', { values: { retry, support } }));\n```\n\nRender returned array with framework fragment or list primitive.\n\n## Use Static Catalogs\n\nUse `createTranslator()` when catalog data and locale selection are fixed for translator lifetime.\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\n## Load Catalogs and Switch Locales\n\nDeclare one static catalog or lazy loader per locale. Switch locale, then load it explicitly when source is lazy.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: { navigation: { settings: 'Settings' } },\n fr: async () => ({ navigation: { settings: 'Réglages' } }),\n },\n locale: 'en',\n});\n\nawait i18n.setLocale('fr');\nawait i18n.load();\nconsole.log(i18n.translate('navigation.settings'));\n```\n\nConcurrent loads for same locale share work. `setLocale()` never triggers hidden loads.\n\n## Subscribe to Immutable Snapshots\n\nSubscribe when UI state must change with locale or loaded active/fallback catalog. Every callback receives snapshot containing translator for that revision.\n\n```ts\nconst unsubscribe = i18n.subscribe(\n ({ locale, translator }) => {\n console.log(locale, translator.translate('navigation.settings'));\n },\n { immediate: true },\n);\n\nunsubscribe();\n```\n\nPass `{ signal }` when an `AbortController` owns subscription lifetime.\n\n## SSR State\n\nSerialize only resolved catalogs on server, then hydrate client store from payload.\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst serverTranslationStore = createTranslationStore({\n catalogs: { en: { title: 'Server title' } },\n locale: 'en',\n});\n\nconst state = serverTranslationStore.serialize();\nconst clientTranslationStore = hydrateTranslationStore(state, { fallback: 'en' });\n\nconsole.log(clientTranslationStore.translate('title'));\nserverTranslationStore.dispose();\nclientTranslationStore.dispose();\n```\n\nState contains raw loaded catalogs. It never contains loader functions.\n\n## Formatting and Validation\n\nImport formatting and catalog validation from dedicated subpaths to keep translation state focused.\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\nimport { validateCatalog } from '@vielzeug/lingua/validate';\n\nconst formatter = createFormatter('en-US');\nconst catalog = { inbox: { plural: { one: 'One message', other: '{count} messages' } } };\n\nconsole.log(formatter.currency(19.99, 'USD'));\nconsole.log(validateCatalog(catalog, 'en'));\n```\n\n## Framework Integration\n\nAdapt `getSnapshot()` and `subscribe()` to framework state primitive.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n return useSyncExternalStore(\n (notify) => i18n.subscribe(() => notify()),\n () => i18n.getSnapshot().translator,\n () => i18n.getSnapshot().translator,\n );\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n const snapshot = shallowRef(i18n.getSnapshot());\n const unsubscribe = i18n.subscribe((next) => {\n snapshot.value = next;\n });\n\n onUnmounted(unsubscribe);\n return snapshot;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function translatorStore(i18n: TranslationStore) {\n return readable(i18n.getSnapshot().translator, (set) => i18n.subscribe(({ translator }) => set(translator)));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nBridge Lingua subscriptions into Ripple through Flux when templates need reactive locale reads.\n\n```ts\nimport { stream } from '@vielzeug/flux';\nimport { toSignal } from '@vielzeug/flux/ripple';\nimport { computed } from '@vielzeug/ripple';\n\nconst localeBinding = toSignal(\n stream<string>((observer) => {\n observer.next(i18n.locale);\n return i18n.subscribe(({ locale }) => observer.next(locale));\n }),\n { initial: i18n.locale },\n);\n\nexport const locale = computed(() => localeBinding.value);\n```\n\nUse Courier loaders when locale catalogs come from HTTP rather than bundled modules; pass each loader to `catalogs`.\n\n## Best Practices\n\n- Define plural messages with `{ plural: ... }`.\n- Keep every locale catalog complete for required keys.\n- Use `translateDynamic()` only for runtime-generated keys.\n- Load a lazy catalog before rendering it.\n- Render `segments()` values with framework-native fragment support.\n- Keep loader functions out of SSR payloads.\n- Pass an `AbortSignal` to subscriptions owned by component or request.\n- Dispose temporary stores after requests, tests, and route lifetimes.\n",
4
+ "index": "---\ntitle: Lingua — Explicit localization for TypeScript\ndescription: Framework-neutral locale catalogs, typed translations, and explicit plural messages.\npackage: lingua\ncategory: i18n\nkeywords: [internationalization, translations, pluralization, locale, i18n, catalog-loading]\nrelated: [ripple, wayfinder, courier]\nexports: [createCatalogTranslator, createTranslationStore, createTranslator, hydrateTranslationStore, LinguaError, LinguaDisposedError, LinguaInvalidCatalogError, LinguaInvalidLocaleError, LinguaInvalidPluralCountError, LinguaInvalidStateError, LinguaMissingCatalogError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"lingua\" />\n\n## Why Lingua?\n\nLingua separates immutable translation from mutable locale state. Use one catalog per locale, then select static or stateful API from whether locale can change.\n\n```ts\n// Before\nconst message = catalogs[locale]?.inbox?.[count === 1 ? 'one' : 'other'] ?? 'inbox';\n\n// After\nconst output = i18n.translate('inbox', { count });\n```\n\n| Feature | Lingua | i18next | FormatJS |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"lingua\" type=\"size\" /> | Varies by selected modules | Varies by selected modules |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Explicit plural catalog nodes | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Convention/config dependent | ICU-message dependent |\n| Declared lazy locale catalogs | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Plugin/config dependent | Application-defined |\n| Immutable locale snapshots | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | Application-defined |\n\n<div class=\"decision-callout\">\n\n**Use Lingua when** you need a compact TypeScript runtime with explicit catalog structure, deterministic fallback, and framework-neutral subscriptions.\n\n**Consider i18next or FormatJS when** you need their plugin ecosystems, message extraction pipelines, or framework-specific integrations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/lingua\n```\n\n```sh [npm]\nnpm install @vielzeug/lingua\n```\n\n```sh [yarn]\nyarn add @vielzeug/lingua\n```\n\n:::\n\n## Quick Start\n\nCreate locale store with static catalogs, then dispose it when owner ends.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n de: { inbox: { plural: { one: 'Eine Nachricht', other: '{count} Nachrichten' } } },\n en: { inbox: { plural: { one: 'One message', other: '{count} messages' } } },\n },\n locale: 'en',\n});\n\ntry {\n console.log(i18n.translate('inbox', { count: 3 }));\n await i18n.setLocale('de');\n console.log(i18n.translate('inbox', { count: 1 }));\n} finally {\n i18n.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createCatalogTranslator()` compiles one immutable fixed-locale catalog.\n- `createTranslator()` compiles immutable locale-keyed catalogs.\n- `createTranslationStore()` manages locale changes and declared catalogs.\n- `translate()` renders text and plural messages through explicit catalog nodes.\n- `translateDynamic()` makes runtime-key lookup explicit.\n- `load()` deduplicates lazy catalog loading per locale.\n- `getSnapshot()` and `subscribe()` expose immutable translator revisions.\n- `serialize()` and `hydrateTranslationStore()` transfer resolved SSR catalogs.\n- `createFormatter()` and `validateCatalog()` remain isolated subpath tools.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](../ripple/index.md) adapts Lingua snapshots into reactive application state.\n- [Courier](../courier/index.md) can fetch locale catalogs before passing them to Lingua loaders.\n- [Wayfinder](../wayfinder/index.md) can drive locale selection from route state.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Lingua — API Reference\ndescription: Complete API reference for @vielzeug/lingua.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCatalogTranslator()` | Compile one immutable locale catalog | Sync | No fallback locales |\n| `createTranslator()` | Compile immutable locale catalogs | Sync | Locale is fixed for translator lifetime |\n| `createTranslationStore()` | Create mutable locale and catalog store | Sync | Load lazy locale explicitly |\n| `hydrateTranslationStore()` | Create store from serialized loaded catalogs | Sync | Serialized state never includes loaders |\n| `createFormatter()` | Format Intl values from `/format` | Sync | Import from subpath |\n| `validateCatalog()` | Check explicit plural forms from `/validate` | Sync | Import from subpath |\n| `LinguaError` | Base class for Lingua errors | Sync | Use `LinguaError.is()` for broad narrowing |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/lingua` | Translation factories, state types, and Lingua errors |\n| `@vielzeug/lingua/format` | `createFormatter()` and formatter types |\n| `@vielzeug/lingua/validate` | `validateCatalog()` and `ValidationIssue` |\n\n## Translation Factories\n\n### createCatalogTranslator\n\n```ts\nfunction createCatalogTranslator<C extends Catalog>(\n catalog: C,\n options?: CatalogTranslatorOptions,\n): Translator<C>;\n```\n\nCompiles one catalog and returns an immutable fixed-locale translator. Locale defaults to `en` and controls plural selection and diagnostics.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalog` | `C` | One catalog containing only messages and grouping objects |\n| `options` | `CatalogTranslatorOptions` | Locale and missing-message handlers; fallback is unavailable |\n\n**Returns:** `Translator<C>`.\n\n**Example:**\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator(\n { save: 'Enregistrer' },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n---\n\n### createTranslator\n\n```ts\nfunction createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C>;\n```\n\nCompiles locale catalogs and returns immutable translator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalogs` | `Catalogs<C>` | Locale-keyed catalog objects |\n| `options` | `TranslatorOptions` | Locale, fallback chain, and missing-message handlers |\n\n**Returns:** `Translator<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `translate` | `(textKey, options?)` or `(pluralKey, { count, ordinal?, values? })` | Rendered string |\n| `translateDynamic` | `(key, options?)` | Rendered string for runtime key |\n| `segments` | `(textKey, { values })` or `(pluralKey, { count, ordinal?, values? })` | String and typed-value segments |\n| `segmentsDynamic` | `(key, options)` | Segments for runtime key |\n| `locale` | `Locale` | Resolved active locale |\n\n---\n\n### createTranslationStore\n\n```ts\nfunction createTranslationStore<C extends Catalog>(options: TranslationStoreOptions<C>): TranslationStore<C>;\n```\n\nCreates catalog store, current locale state, and immutable translator snapshots.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.catalogs` | `CatalogSources<C>` | Static catalogs or lazy locale loaders |\n| `options.locale` | `Locale` | Initial locale; defaults to `en` |\n| `options.fallback` | `Locale \\| readonly Locale[]` | Fallback locale chain |\n| `options.onMissingKey` | `(key, locale) => string` | Missing-message handler |\n| `options.onMissingValue` | `(name, key, locale) => string` | Missing-interpolation handler |\n\n**Returns:** `TranslationStore<C>`, with every `Translator<C>` method plus lifecycle methods.\n\n**Example:**\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst translations = createTranslationStore({\n catalogs: { en: { title: 'Home' }, fr: { title: 'Accueil' } },\n locale: 'en',\n});\n\nawait translations.setLocale('fr');\ntranslations.translate('title');\n```\n\n| Method or property | Signature | Returns |\n| --- | --- | --- |\n| `translate` | Translator method | Rendered string |\n| `segments` | Translator method | String and typed-value segments |\n| `load` | `({ locale? })` | `Promise<void>` after catalog resolution |\n| `setLocale` | `(locale)` | `Promise<void>` after locale commit; never loads implicitly |\n| `isLoaded` | `({ locale? })` | `boolean` |\n| `getSnapshot` | `()` | `TranslationSnapshot<C>` |\n| `subscribe` | `(listener, { immediate?, signal? })` | Unsubscribe function |\n| `serialize` | `()` | Loader-free `TranslationState<C>` |\n| `dispose` | `()` | `void` |\n| `locale` | `Locale` | Current canonical locale |\n| `disposed` | `boolean` | Disposal state |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal |\n| `[Symbol.dispose]` | `()` | Delegates to `dispose()` |\n\n---\n\n### hydrateTranslationStore\n\n```ts\nfunction hydrateTranslationStore<C extends Catalog>(\n state: TranslationState<C>,\n options?: Omit<TranslationStoreOptions<C>, 'locale' | 'catalogs'>,\n): TranslationStore<C>;\n```\n\nCreates translation store from SSR state payload containing resolved raw catalogs.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `state` | `TranslationState<C>` | Version `3`, active locale, and loader-free catalogs |\n| `options` | `Omit<TranslationStoreOptions<C>, 'locale' \\| 'catalogs'>` | Fallback and missing-message handlers |\n\n**Returns:** `TranslationStore<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst server = createTranslationStore({ catalogs: { en: { title: 'Home' } }, locale: 'en' });\nconst client = hydrateTranslationStore(server.serialize());\n\nclient.translate('title');\n```\n\n## Migration from 1.x\n\n| Before | After |\n| --- | --- |\n| `createI18n(options)` | `createTranslationStore({ catalogs, ...options })` |\n| `hydrateI18n(state)` | `hydrateTranslationStore(state)` |\n| `I18n` / `I18nSnapshot` | `TranslationStore` / `TranslationSnapshot` |\n| `I18nState` version `2` | `TranslationState` version `3` |\n| Named resources and namespaces | One catalog source per locale |\n| `LinguaMissingResourceError` | `LinguaMissingCatalogError` |\n\n## Formatting and Validation\n\n### createFormatter\n\n```ts\nfunction createFormatter(source: string | (() => string)): Formatter;\n```\n\nCreates cached Intl formatters using static locale or locale getter.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `string \\| (() => string)` | Static locale or locale getter |\n\n**Returns:** `Formatter`.\n\n**Example:**\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\n\nconst formatter = createFormatter('en-US');\nformatter.currency(19.99, 'USD');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `number` | `(value, options?)` | `string` |\n| `currency` | `(value, currency, options?)` | `string` |\n| `date` | `(value, options?)` | `string` |\n| `relative` | `(value, unit, options?)` | `string` |\n| `list` | `(value, options?)` | `string` |\n| `duration` | `(value, options?)` | `string` |\n\n### validateCatalog\n\n```ts\nfunction validateCatalog(catalog: Catalog, locale: Locale): ValidationIssue[];\n```\n\nValidates explicit plural messages against locale plural categories after catalog structural validation.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalog` | `Catalog` | Explicit catalog to validate |\n| `locale` | `Locale` | BCP 47 locale tag |\n\n**Returns:** `ValidationIssue[]`.\n\n**Example:**\n\n```ts\nimport { validateCatalog } from '@vielzeug/lingua/validate';\n\nvalidateCatalog({ inbox: { plural: { one: 'One message' } } }, 'en');\n```\n\n## Types\n\n```ts\ntype Locale = string;\ntype PluralCategory = Intl.LDMLPluralRule;\ntype PluralMessage = { readonly plural: Partial<Record<PluralCategory, string>> };\ntype CatalogNode = Catalog | PluralMessage | string;\ntype Catalog = { readonly [key: string]: CatalogNode };\ntype Catalogs<C extends Catalog = Catalog> = Record<Locale, C>;\ntype CatalogTranslatorOptions = Omit<TranslatorOptions, 'fallback'>;\ntype CatalogLoader<C extends Catalog = Catalog> = () => Promise<C>;\ntype CatalogSource<C extends Catalog = Catalog> = C | CatalogLoader<C>;\ntype CatalogSources<C extends Catalog = Catalog> = Record<Locale, CatalogSource<C>>;\ntype LoadedCatalogs<C extends Catalog = Catalog> = Catalogs<C>;\n\ntype TranslationStoreOptions<C extends Catalog = Catalog> = TranslatorOptions & {\n catalogs: CatalogSources<C>;\n};\n\ntype TranslationState<C extends Catalog = Catalog> = {\n readonly catalogs: LoadedCatalogs<C>;\n readonly locale: Locale;\n readonly version: 3;\n};\n\ntype TranslationSnapshot<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n readonly revision: number;\n readonly translator: Translator<C>;\n};\n\ntype TranslationStore<C extends Catalog = Catalog> = Translator<C> & {\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 [Symbol.dispose](): void;\n};\n```\n\n```ts\ntype Values = Record<string, unknown>;\ntype TranslateOptions = { values?: Values };\ntype PluralOptions = TranslateOptions & { count: number; ordinal?: boolean };\ntype TranslatorOptions = {\n fallback?: Locale | readonly Locale[];\n locale?: Locale;\n onMissingKey?: (key: string, locale: Locale) => string;\n onMissingValue?: (name: string, key: string, locale: Locale) => string;\n};\ntype SubscribeOptions = { immediate?: boolean; signal?: AbortSignal };\n\ntype DurationValue = Partial<Record<\n 'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'months' | 'nanoseconds' | 'seconds' | 'weeks' | 'years',\n number\n>>;\n\ntype 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\ntype ListFormatOptions = { style?: 'long' | 'narrow' | 'short'; type?: 'and' | 'or' };\n\ntype Formatter = {\n currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'currency' | 'style'>): string;\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;\n duration(value: DurationValue, options?: DurationFormatOptions): string;\n list(value: Array<string | number>, options?: ListFormatOptions): string;\n number(value: number, options?: Intl.NumberFormatOptions): string;\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;\n};\n\ntype ValidationIssue = { key: string; locale: Locale; missing: Intl.LDMLPluralRule };\n```\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `LinguaDisposedError` | State mutation or subscription after `dispose()` |\n| `LinguaInvalidCatalogError` | Invalid catalog node or reserved key |\n| `LinguaInvalidLocaleError` | Invalid BCP 47 locale tag |\n| `LinguaInvalidPluralCountError` | Non-finite plural count |\n| `LinguaInvalidStateError` | Unsupported serialized state version |\n| `LinguaMissingCatalogError` | Catalog has no source for requested locale |\n",
6
+ "usage": "---\ntitle: Lingua — Usage Guide\ndescription: Translate explicit catalogs, load lazy locales, and connect locale snapshots to UI state.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate i18n store from locale-keyed catalogs. Strings are text messages; plural messages use `{ plural: ... }`.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: {\n greeting: 'Hello, {name}!',\n inbox: { plural: { one: 'One message', other: '{count} messages' } },\n },\n },\n locale: 'en',\n});\n\nconsole.log(i18n.translate('greeting', { values: { name: 'Ada' } }));\nconsole.log(i18n.translate('inbox', { count: 3 }));\n```\n\nCall `dispose()` when store belongs to temporary request, test, or route owner.\n\n## Define Explicit Catalogs\n\nUse nested objects only to group keys. A plural message always has `plural`, so regular objects containing `one` or `other` remain groups.\n\n```ts\nconst catalog = {\n account: {\n greeting: 'Hello, {name}!',\n unread: { plural: { one: 'One unread message', other: '{count} unread messages' } },\n },\n};\n```\n\nUse `{ values }` for text replacements. Pass `count` at top level for plural selection; Lingua injects it into selected template. Absent replacements render as `{name}` by default. `segments()` preserves an own `undefined` or `null` value; omit property to receive `{name}`.\n\nCatalogs contain strings, grouping objects, and explicit `{ plural: ... }` messages only. Keep application data outside catalog, then translate display labels while constructing it.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst messages = {\n status: { blocked: 'Blocked', done: 'Done', inProgress: 'In progress' },\n};\nconst statusDefinitions = [\n { labelKey: 'status.inProgress', value: 'in-progress' },\n { labelKey: 'status.blocked', value: 'blocked' },\n { labelKey: 'status.done', value: 'done' },\n] as const;\nconst translator = createCatalogTranslator(messages);\nconst statusOptions = statusDefinitions.map(({ labelKey, value }) => ({ label: translator.translate(labelKey), value }));\n```\n\n## Render Framework Content\n\nUse `segments()` when replacements are framework nodes, links, or other values that must not be stringified.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator({ error: 'Try {retry} or {support}.' });\n\nconst retry = { href: '/retry', label: 'retry' };\nconst support = { href: '/support', label: 'support' };\n\nconsole.log(translator.segments('error', { values: { retry, support } }));\n```\n\nRender returned array with framework fragment or list primitive. Give UI values consumer-owned keys before passing them to `segments()`; Lingua preserves value identity and never clones or mutates them.\n\n## Use Static Catalogs\n\nUse `createCatalogTranslator()` when one catalog and locale stay fixed for translator lifetime. It defaults locale to `en`; pass `locale` when plural rules or diagnostics need another locale. Lingua snapshots catalog messages during construction. Do not mutate source catalog objects afterward.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator(\n { save: 'Enregistrer' },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\nUse `createTranslator()` when fixed translation requires locale-keyed catalogs and fallback resolution.\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\n## Load Catalogs and Switch Locales\n\nDeclare one static catalog or lazy loader per locale. Switch locale, then load it explicitly when source is lazy.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: { navigation: { settings: 'Settings' } },\n fr: async () => ({ navigation: { settings: 'Réglages' } }),\n },\n locale: 'en',\n});\n\nawait i18n.setLocale('fr');\nawait i18n.load();\nconsole.log(i18n.translate('navigation.settings'));\n```\n\nConcurrent loads for same locale share work. `setLocale()` never triggers hidden loads.\n\n## Subscribe to Immutable Snapshots\n\nSubscribe when UI state must change with locale or loaded active/fallback catalog. Every callback receives snapshot containing translator for that revision.\n\n```ts\nconst unsubscribe = i18n.subscribe(\n ({ locale, translator }) => {\n console.log(locale, translator.translate('navigation.settings'));\n },\n { immediate: true },\n);\n\nunsubscribe();\n```\n\nPass `{ signal }` when an `AbortController` owns subscription lifetime.\n\n## SSR State\n\nSerialize resolved catalogs on server, then hydrate client store from same payload. `getSnapshot()` stays referentially stable until store revision changes, so use same hydrated store throughout initial client render.\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst serverTranslationStore = createTranslationStore({\n catalogs: { en: { title: 'Server title' } },\n locale: 'en',\n});\n\nconst state = serverTranslationStore.serialize();\nconst clientTranslationStore = hydrateTranslationStore(state, { fallback: 'en' });\n\nconsole.log(clientTranslationStore.translate('title'));\nserverTranslationStore.dispose();\nclientTranslationStore.dispose();\n```\n\nState contains raw loaded catalogs. It never contains loader functions.\n\n## Formatting and Validation\n\nImport formatting and catalog validation from dedicated subpaths to keep translation state focused.\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\nimport { validateCatalog } from '@vielzeug/lingua/validate';\n\nconst formatter = createFormatter('en-US');\nconst catalog = { inbox: { plural: { one: 'One message', other: '{count} messages' } } };\n\nconsole.log(formatter.currency(19.99, 'USD'));\nconsole.log(validateCatalog(catalog, 'en'));\n```\n\n## Framework Integration\n\nPass stable `getSnapshot()` and `subscribe()` methods to framework state primitives. For SSR, create client store from same serialized state used by server before calling `useSyncExternalStore`.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n const snapshot = useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n\n return snapshot.translator;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n const snapshot = shallowRef(i18n.getSnapshot());\n const unsubscribe = i18n.subscribe((next) => {\n snapshot.value = next;\n });\n\n onUnmounted(unsubscribe);\n return snapshot;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function translatorStore(i18n: TranslationStore) {\n return readable(i18n.getSnapshot().translator, (set) => i18n.subscribe(({ translator }) => set(translator)));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nBridge Lingua subscriptions into Ripple through Flux when templates need reactive locale reads.\n\n```ts\nimport { stream } from '@vielzeug/flux';\nimport { toSignal } from '@vielzeug/flux/ripple';\nimport { computed } from '@vielzeug/ripple';\n\nconst localeBinding = toSignal(\n stream<string>((observer) => {\n observer.next(i18n.locale);\n return i18n.subscribe(({ locale }) => observer.next(locale));\n }),\n { initial: i18n.locale },\n);\n\nexport const locale = computed(() => localeBinding.value);\n```\n\nUse Courier loaders when locale catalogs come from HTTP rather than bundled modules; pass each loader to `catalogs`.\n\n## Best Practices\n\n- Define plural messages with `{ plural: ... }` and no sibling metadata.\n- Keep arrays and application metadata outside catalogs.\n- Treat source catalog objects as immutable after construction.\n- Use `translateDynamic()` only for runtime-generated keys.\n- Load a lazy catalog before rendering it.\n- Give UI values keys before passing them to `segments()`.\n- Keep loader functions out of SSR payloads.\n- Dispose temporary stores after requests, tests, and route lifetimes.\n",
7
7
  "examples": "---\ntitle: Lingua — Examples\ndescription: Focused examples for explicit catalogs and locale resources.\n---\n\n- [Static Translator](./examples/static-translator.md)\n- [Lazy Locale Catalog](./examples/feature-resources.md)\n- [SSR Hydration](./examples/ssr-hydration.md)\n"
8
8
  },
9
9
  "examples": [
@@ -14,12 +14,12 @@
14
14
  },
15
15
  {
16
16
  "id": "rich-segments",
17
- "code": "import { createTranslator } from '@vielzeug/lingua'\n\n// segments() preserves components, nodes, or other non-string replacements.\nconst translator = createTranslator({\n en: { error: 'Try {retry} or {support}.' },\n}, { locale: 'en' })\n\nconst retry = { label: 'retry', href: '/retry' }\nconst support = { label: 'support', href: '/support' }\nconst result = translator.segments('error', { values: { retry, support } })\n\nconsole.log(result)\nconsole.log(result.map((part) => typeof part === 'string' ? part : part.label).join(''))",
17
+ "code": "import { createCatalogTranslator } from '@vielzeug/lingua'\n\n// segments() preserves components, nodes, or other non-string replacements.\nconst translator = createCatalogTranslator({\n error: 'Try {retry} or {support}.',\n})\n\nconst retry = { label: 'retry', href: '/retry' }\nconst support = { label: 'support', href: '/support' }\nconst result = translator.segments('error', { values: { retry, support } })\n\nconsole.log(result)\nconsole.log(result.map((part) => typeof part === 'string' ? part : part.label).join(''))",
18
18
  "name": "Rich Segments"
19
19
  },
20
20
  {
21
21
  "id": "static-translator",
22
- "code": "import { createTranslator } from '@vielzeug/lingua'\n\n// Immutable translator: explicit text and plural catalog nodes.\nconst translator = createTranslator({\n en: {\n greeting: 'Hello, {name}!',\n inbox: { plural: { one: 'One message', other: '{count} messages' } },\n },\n fr: {\n greeting: 'Bonjour, {name} !',\n inbox: { plural: { one: 'Un message', other: '{count} messages' } },\n },\n}, { locale: 'fr' })\n\nconsole.log(translator.translate('greeting', { values: { name: 'Ada' } }))\nconsole.log(translator.translate('inbox', { count: 3 }))",
22
+ "code": "import { createCatalogTranslator } from '@vielzeug/lingua'\n\n// Immutable translator: explicit text and plural catalog nodes.\nconst translator = createCatalogTranslator({\n greeting: 'Bonjour, {name} !',\n inbox: { plural: { one: 'Un message', other: '{count} messages' } },\n}, { locale: 'fr' })\n\nconsole.log(translator.translate('greeting', { values: { name: 'Ada' } }))\nconsole.log(translator.translate('inbox', { count: 3 }))",
23
23
  "name": "Static Translator"
24
24
  },
25
25
  {
@@ -40,27 +40,29 @@
40
40
  "hydrateTranslationStore": "export {\n createTranslationStore,\n hydrateTranslationStore,\n type TranslationSnapshot,\n type TranslationStore,\n} from './i18n';",
41
41
  "TranslationSnapshot": "export {\n createTranslationStore,\n hydrateTranslationStore,\n type TranslationSnapshot,\n type TranslationStore,\n} from './i18n';",
42
42
  "TranslationStore": "export {\n createTranslationStore,\n hydrateTranslationStore,\n type TranslationSnapshot,\n type TranslationStore,\n} from './i18n';",
43
- "createTranslator": "export { createTranslator, type Translator } from './translator';",
44
- "Translator": "export { createTranslator, type Translator } from './translator';",
45
- "Catalog": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
46
- "CatalogLoader": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
47
- "CatalogNode": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
48
- "Catalogs": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
49
- "CatalogSource": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
50
- "CatalogSources": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
51
- "TranslationState": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
52
- "TranslationStoreOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
53
- "LoadedCatalogs": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
54
- "Locale": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
55
- "MessageKey": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
56
- "PluralCategory": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
57
- "PluralKey": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
58
- "PluralMessage": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
59
- "PluralOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
60
- "SubscribeOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
61
- "TextKey": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
62
- "TranslateOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
63
- "TranslatorOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
64
- "Values": "export type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';"
43
+ "createCatalogTranslator": "export { createCatalogTranslator, createTranslator, type Translator } from './translator';",
44
+ "createTranslator": "export { createCatalogTranslator, createTranslator, type Translator } from './translator';",
45
+ "Translator": "export { createCatalogTranslator, createTranslator, type Translator } from './translator';",
46
+ "Catalog": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
47
+ "CatalogLoader": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
48
+ "CatalogTranslatorOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
49
+ "CatalogNode": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
50
+ "Catalogs": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
51
+ "CatalogSource": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
52
+ "CatalogSources": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
53
+ "TranslationState": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
54
+ "TranslationStoreOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
55
+ "LoadedCatalogs": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
56
+ "Locale": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
57
+ "MessageKey": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
58
+ "PluralCategory": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
59
+ "PluralKey": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
60
+ "PluralMessage": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
61
+ "PluralOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
62
+ "SubscribeOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
63
+ "TextKey": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
64
+ "TranslateOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
65
+ "TranslatorOptions": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';",
66
+ "Values": "export type {\n Catalog,\n CatalogLoader,\n CatalogTranslatorOptions,\n CatalogNode,\n Catalogs,\n CatalogSource,\n CatalogSources,\n TranslationState,\n TranslationStoreOptions,\n LoadedCatalogs,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslatorOptions,\n Values,\n} from './types';"
65
67
  }
66
68
  }
@@ -3,7 +3,7 @@
3
3
  "docs": {
4
4
  "index": "---\ntitle: Vault — Typed browser storage\ndescription: Typed browser storage with portable keys, TTL, observation, and explicit IndexedDB transactions.\npackage: vault\ncategory: Storage\nkeywords: [storage, indexeddb, localstorage, sessionstorage, ttl, browser]\nrelated: [courier, forge, ripple]\nexports: [createMemory, createLocalStorage, createSessionStorage, createIndexedDB, table, ttl, scheduleExpiredPrune, defineMigration, isExpired]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"vault\" />\n\n## Why Vault?\n\nVault gives browser storage one typed schema while keeping backend guarantees explicit. Use `VaultStore` for portable CRUD and observation; choose IndexedDB only when you need atomic transactions or cursor iteration.\n\n```ts\n// Before\nlocalStorage.setItem('theme', JSON.stringify({ value: 'dark' }));\nconst theme = JSON.parse(localStorage.getItem('theme') ?? '{}').value;\n\n// After\nawait store.put('preferences', { id: 'theme', value: 'dark' });\nconst theme = await store.get('preferences', 'theme');\n```\n\n| Feature | Vault | Raw Web Storage | Dexie |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"vault\" type=\"size\" /> | Browser built-in | Extra dependency |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed schema and keys | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Portable Memory/Web Storage API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | IndexedDB only |\n| Explicit atomic transactions | IndexedDB capability | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Vault when** you need typed browser persistence with one portable CRUD API and explicit IndexedDB-only capabilities.\n\n**Consider raw Web Storage when** you only persist one or two unstructured values. **Consider Dexie when** you need a broader IndexedDB ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/vault\n```\n\n```sh [npm]\nnpm install @vielzeug/vault\n```\n\n```sh [yarn]\nyarn add @vielzeug/vault\n```\n\n:::\n\n## Quick Start\n\nDefine a schema, create a portable store, and dispose it with its owner.\n\n```ts\nimport { createLocalStorage, table } from '@vielzeug/vault';\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<{ id: string; theme: 'dark' | 'light' }>('id') },\n});\n\ntry {\n await store.put('preferences', { id: 'theme', theme: 'dark' });\n console.log(await store.get('preferences', 'theme'));\n} finally {\n await store.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `table()` defines typed records with portable string or number keys.\n- `createMemory()`, `createLocalStorage()`, and `createSessionStorage()` return portable `VaultStore` instances.\n- `observe()` emits current and changed table snapshots.\n- `ttl` creates validated expiration durations.\n- `createIndexedDB()` returns `IndexedDbVaultStore` with `batch()` and `iterate()`.\n- `defineMigration()` handles IndexedDB schema upgrades.\n- `scheduleExpiredPrune()` removes stale TTL entries on an owned schedule.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Forge](../forge/index.md) saves and restores form drafts through Vault stores.\n- [Ripple](../ripple/index.md) owns application state that can persist through Vault.\n- [Courier](../courier/index.md) can populate persistent cache data.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Vault — API Reference\ndescription: Typed browser storage with portable keys and capability-specific stores.\n---\n\n[[toc]]\n\n## API Overview\n\nVault has a portable `VaultStore<S>` and an IndexedDB-only `IndexedDbVaultStore<S>`.\n\n## Package Entry Point\n\n```ts\nimport { createIndexedDB, createLocalStorage, createMemory, createSessionStorage, table, ttl } from '@vielzeug/vault';\n```\n\n## Factories\n\n```ts\ncreateMemory({ schema, name? }): VaultStore<S>\ncreateLocalStorage({ name, schema }): VaultStore<S>\ncreateSessionStorage({ name, schema }): VaultStore<S>\ncreateIndexedDB({ name, schema, version?, migrate? }): IndexedDbVaultStore<S>\n```\n\nAll factories use the same fixed storage envelope: `{ value, expiresAt? }`. Primary-key fields must be `string` or finite `number`; Vault's tagged encoding preserves the distinction between `1` and `'1'` in every adapter.\n\n## VaultStore\n\nThe portable store supports CRUD (`get`, `put`, `getAll`, `putAll`, `delete`, `deleteMany`, `clear`), key and entry reads, `update`, `upsert`, `getOrDefault`, `query`, TTL pruning, `debug`, and lifecycle disposal.\n\n```ts\nconst stop = store.observe('users', (users) => render(users), { immediate: true });\nstop();\n```\n\n`observe()` is the only reactivity API. It emits the current table snapshot by default, then emits after mutations. Pass `{ immediate: false }` to wait for the first mutation or `{ signal }` for AbortSignal-owned cleanup.\n\n## IndexedDbVaultStore\n\n`createIndexedDB` adds cursor iteration and atomic transactions:\n\n```ts\nawait db.batch(['users'], async (tx) => {\n await tx.put('users', { id: 1, name: 'Ada' });\n await tx.delete('users', 2);\n});\n\nfor await (const user of db.iterate('users')) console.log(user);\n```\n\n`batch()` is unavailable on memory and Web Storage stores because only IndexedDB can guarantee atomic commits.\n\n## Schema and TTL\n\n```ts\nimport { table, ttl } from '@vielzeug/vault';\n\ntype Session = { id: string; userId: string };\nconst schema = { sessions: table<Session>('id').ttl(ttl.hours(1)).index('userId') };\n```\n\n`ttl.ms`, `seconds`, `minutes`, `hours`, and `days` return branded positive durations. IndexedDB indexes always target `value.<field>` because the storage envelope is fixed.\n\n## Migration from Vault 1\n\n`Adapter`, `MemoryAdapter`, `IndexedDbAdapter`, codecs, `watch`, `observeMany`, constructor signals, and `toReadableStream` are removed. Use `VaultStore`, `IndexedDbVaultStore`, fixed envelopes, and `observe()` instead. Existing custom-encoded data must be migrated or cleared before use with this release.\n",
6
- "usage": "---\ntitle: Vault — Usage Guide\ndescription: Persist typed browser data, observe table snapshots, and use IndexedDB transactions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a portable store with one schema and write a typed row.\n\n```ts\nimport { createLocalStorage, table } from '@vielzeug/vault';\n\ninterface Preference {\n id: string;\n theme: 'dark' | 'light';\n}\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Portable Store\n\nMemory, LocalStorage, and SessionStorage return `VaultStore`. They share portable string/number keys, fixed `{ value, expiresAt? }` envelopes, CRUD methods, queries, TTL, and `observe()`.\n\nUse a new storage name when upgrading from Vault 1. Old key and envelope formats are not read by Vault 2.\n\n```ts\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n```\n\n## Read and Change Records\n\nUse `update()` for an existing row and `upsert()` when the row may not exist.\n\n```ts\nconst updated = await store.update('preferences', 'theme', { theme: 'light' });\n\nawait store.upsert('preferences', 'locale', (current) => ({\n id: 'locale',\n theme: current?.theme ?? 'dark',\n}));\n\nconsole.log(updated);\n```\n\n`update()` returns `undefined` for a missing key. `upsert()` always writes the record returned by its callback.\n\n## Use TTL and Pruning\n\nUse `ttl.*` helpers for expiring rows. Schedule pruning when stale rows can accumulate without reads.\n\n```ts\nimport { scheduleExpiredPrune, ttl } from '@vielzeug/vault';\n\nawait store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));\nconst stopPrune = scheduleExpiredPrune(store, { interval: ttl.hours(6), signal: store.disposalSignal });\n\nstopPrune();\n```\n\n## Observe a Table\n\nUse `observe()` for current and future snapshots. Tie subscription lifetime to an `AbortSignal` when a component or request owns it.\n\n```ts\nconst controller = new AbortController();\n\nstore.observe('preferences', (preferences) => {\n console.log(preferences);\n}, { signal: controller.signal });\n\ncontroller.abort();\n```\n\n## Use IndexedDB for Atomic Work\n\nChoose IndexedDB when multiple writes must commit together or when you need cursor iteration.\n\n```ts\nimport { createIndexedDB, table } from '@vielzeug/vault';\n\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nOnly await `tx.*` operations inside a batch callback. Do not await timers, fetches, or other external asynchronous work; IndexedDB can commit an inactive transaction.\n\n## Handle Schema Migrations\n\nDeclare indexes in schema. Use `migrate` only for version upgrades and mirror Vault’s fixed `value.<field>` index path.\n\n```ts\nimport { createIndexedDB, table, type MigrationFn } from '@vielzeug/vault';\n\nconst schema = { users: table<{ id: number; name: string }>('id').index('name') };\nconst migrate: MigrationFn = ({ db, oldVersion, tx }) => {\n if (oldVersion < 2 && db.objectStoreNames.contains('users')) {\n tx.objectStore('users').createIndex('name', 'value.name');\n }\n};\n\ncreateIndexedDB({ name: 'app-v2', migrate, schema, version: 2 });\n```\n\n## Upgrade from Vault 1\n\n- Replace `Adapter` with `VaultStore`.\n- Replace `IndexedDbAdapter` with `IndexedDbVaultStore`.\n- Replace `watch`, `observeMany`, signals, and streams with per-table `observe()`.\n- Remove codecs and versioned codecs. Start a new storage namespace or migrate data outside Vault before construction.\n- Move atomic code to `createIndexedDB().batch()`.\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const [rows, setRows] = useState<RecordOf<S, K>[]>([]);\n\n useEffect(() => store.observe(table, setRows), [store, table]);\n return rows;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const rows = shallowRef<RecordOf<S, K>[]>([]);\n const stop = store.observe(table, (next) => (rows.value = next));\n\n onUnmounted(stop);\n return rows;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n return readable<RecordOf<S, K>[]>([], (set) => store.observe(table, set));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple signals as application state and persist selected changes through Vault writes.\n\n## Best Practices\n\n- Define one schema per storage namespace.\n- Use string or finite-number primary keys only.\n- Choose a new namespace for Vault 1 storage unless you migrate it yourself.\n- Use `observe()` for table snapshots.\n- Use IndexedDB for atomic work.\n- Keep external asynchronous work outside `batch()` callbacks.\n- Use `ttl.*` instead of raw durations.\n- Dispose stores when their owner ends.\n",
6
+ "usage": "---\ntitle: Vault — Usage Guide\ndescription: Persist typed browser data, observe table snapshots, and use IndexedDB transactions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a portable store with one schema and write a typed row.\n\n```ts\nimport { createLocalStorage, table } from '@vielzeug/vault';\n\ninterface Preference {\n id: string;\n theme: 'dark' | 'light';\n}\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Portable Store\n\nMemory, LocalStorage, and SessionStorage return `VaultStore`. They share portable string/number keys, fixed `{ value, expiresAt? }` envelopes, CRUD methods, queries, TTL, and `observe()`.\n\nUse a new storage name when upgrading from Vault 1. Old key and envelope formats are not read by Vault 2.\n\n```ts\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n```\n\n## Read and Change Records\n\nUse `update()` for an existing row and `upsert()` when the row may not exist.\n\n```ts\nconst updated = await store.update('preferences', 'theme', { theme: 'light' });\n\nawait store.upsert('preferences', 'locale', (current) => ({\n id: 'locale',\n theme: current?.theme ?? 'dark',\n}));\n\nconsole.log(updated);\n```\n\n`update()` returns `undefined` for a missing key. `upsert()` always writes the record returned by its callback.\n\n## Use TTL and Pruning\n\nUse `ttl.*` helpers for expiring rows. Schedule pruning when stale rows can accumulate without reads.\n\n```ts\nimport { scheduleExpiredPrune, ttl } from '@vielzeug/vault';\n\nawait store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));\nconst stopPrune = scheduleExpiredPrune(store, { interval: ttl.hours(6), signal: store.disposalSignal });\n\nstopPrune();\n```\n\n## Observe a Table\n\nUse `observe()` for current and future snapshots. Tie subscription lifetime to an `AbortSignal` when a component or request owns it.\n\n```ts\nconst controller = new AbortController();\n\nstore.observe('preferences', (preferences) => {\n console.log(preferences);\n}, { signal: controller.signal });\n\ncontroller.abort();\n```\n\n## Use IndexedDB for Atomic Work\n\nChoose IndexedDB when multiple writes must commit together or when you need cursor iteration.\n\n```ts\nimport { createIndexedDB, table } from '@vielzeug/vault';\n\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nOnly await `tx.*` operations inside a batch callback. Do not await timers, fetches, or other external asynchronous work; IndexedDB can commit an inactive transaction.\n\n## Handle Schema Migrations\n\nDeclare indexes in schema. Use `migrate` only for version upgrades and mirror Vault’s fixed `value.<field>` index path.\n\n```ts\nimport { createIndexedDB, table, type MigrationFn } from '@vielzeug/vault';\n\nconst schema = { users: table<{ id: number; name: string }>('id').index('name') };\nconst migrate: MigrationFn = ({ db, oldVersion, tx }) => {\n if (oldVersion < 2 && db.objectStoreNames.contains('users')) {\n tx.objectStore('users').createIndex('name', 'value.name');\n }\n};\n\ncreateIndexedDB({ name: 'app-v2', migrate, schema, version: 2 });\n```\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const [rows, setRows] = useState<RecordOf<S, K>[]>([]);\n\n useEffect(() => store.observe(table, setRows), [store, table]);\n return rows;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const rows = shallowRef<RecordOf<S, K>[]>([]);\n const stop = store.observe(table, (next) => (rows.value = next));\n\n onUnmounted(stop);\n return rows;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n return readable<RecordOf<S, K>[]>([], (set) => store.observe(table, set));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple signals as application state and persist selected changes through Vault writes.\n\n## Best Practices\n\n- Define one schema per storage namespace.\n- Use string or finite-number primary keys only.\n- Choose a new namespace for Vault 1 storage unless you migrate it yourself.\n- Use `observe()` for table snapshots.\n- Use IndexedDB for atomic work.\n- Keep external asynchronous work outside `batch()` callbacks.\n- Use `ttl.*` instead of raw durations.\n- Dispose stores when their owner ends.\n",
7
7
  "examples": "---\ntitle: Vault — Examples\ndescription: Portable storage, observation, and IndexedDB transactions.\n---\n\n- [CRUD](./examples/crud.md)\n- [TTL](./examples/ttl.md)\n- [Querying](./examples/querying.md)\n- [Reactive observation](./examples/reactive.md)\n- [IndexedDB iteration](./examples/iterate.md)\n- [IndexedDB batch transactions](./examples/batch.md)\n- [Plugin validation](./examples/plugins.md)\n"
8
8
  },
9
9
  "examples": [