npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pyreon/i18n

v0.14.0

Published

Reactive internationalization for Pyreon with async namespace loading

Downloads

3,277

Readme

@pyreon/i18n

Reactive internationalization for Pyreon. Async namespace loading, pluralization, interpolation, and rich JSX text.

Install

bun add @pyreon/i18n

Two entry points

| Entry | Use when | Includes | | --------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | @pyreon/i18n | You're building a Pyreon UI and want the JSX components | createI18n, Trans, I18nProvider, useI18n, plus everything below | | @pyreon/i18n/core | You're on a backend / non-Pyreon runtime / don't need JSX | createI18n, interpolate, resolvePluralCategory, types only |

The /core entry has zero JSX dependencies — it only depends on @pyreon/reactivity (which is framework-agnostic). Use it for backend translation pipelines, edge workers, non-Pyreon frontends, or any context where you don't need the <Trans> JSX component.

// Backend / non-JSX usage:
import { createI18n } from '@pyreon/i18n/core'

const i18n = createI18n({ locale: 'en', messages: { en: { hello: 'Hi' } } })
i18n.t('hello') // "Hi"

The main @pyreon/i18n entry includes the same createI18n plus the JSX components — both entries return identical I18nInstance objects, so you can switch entries without changing your code if you decide later to add UI bindings.

Quick Start

import { createI18n } from '@pyreon/i18n'

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  messages: {
    en: {
      greeting: 'Hello, {{name}}!',
      items_one: '{{count}} item',
      items_other: '{{count}} items',
    },
    de: {
      greeting: 'Hallo, {{name}}!',
    },
  },
})

i18n.t('greeting', { name: 'Alice' }) // "Hello, Alice!"
i18n.t('items', { count: 3 }) // "3 items"

i18n.locale.set('de')
i18n.t('greeting', { name: 'Alice' }) // "Hallo, Alice!"
i18n.t('items', { count: 1 }) // "1 item" (fallback to en)

API

createI18n(options)

Create a reactive i18n instance with static messages and/or an async namespace loader.

| Parameter | Type | Description | | -------------------------- | -------------------------------------------------- | ------------------------------------------------------- | | options.locale | string | Initial locale (e.g. "en") | | options.fallbackLocale | string | Locale to try when key is missing in active locale | | options.messages | Record<string, TranslationDictionary> | Static messages keyed by locale | | options.loader | NamespaceLoader | (locale, namespace) => Promise<TranslationDictionary> | | options.defaultNamespace | string | Default namespace for t() (default: "common") | | options.pluralRules | PluralRules | Custom plural rules per locale | | options.onMissingKey | (locale, key, namespace?) => string \| undefined | Missing key handler |

Returns: I18nInstance with:

| Property | Type | Description | | ------------------------------------ | ------------------------------------------------------- | ------------------------------------ | | t(key, values?) | (key: string, values?: InterpolationValues) => string | Translate a key | | locale | Signal<string> | Current locale (reactive, writable) | | loadNamespace(ns, locale?) | (ns: string, locale?: string) => Promise<void> | Load a namespace | | isLoading | Computed<boolean> | Whether any namespace is loading | | loadedNamespaces | Computed<Set<string>> | Namespaces loaded for current locale | | exists(key) | (key: string) => boolean | Check if a key exists | | addMessages(locale, messages, ns?) | Function | Add messages at runtime | | availableLocales | Computed<string[]> | All locales with registered messages |

const i18n = createI18n({
  locale: 'en',
  loader: async (locale, namespace) => {
    const mod = await import(`./locales/${locale}/${namespace}.json`)
    return mod.default
  },
})
await i18n.loadNamespace('auth')
i18n.t('auth:errors.invalid') // namespace:key.path syntax

interpolate(template, values?)

Replace {{key}} placeholders in a string. Supports whitespace inside braces. Unmatched placeholders are left as-is.

| Parameter | Type | Description | | ---------- | --------------------- | --------------------------------------- | | template | string | Template string with {{placeholders}} | | values | InterpolationValues | Key-value pairs for substitution |

Returns: string

interpolate('Hello, {{ name }}!', { name: 'World' })
// "Hello, World!"

resolvePluralCategory(locale, count, customRules?)

Resolve the CLDR plural category for a count. Uses custom rules if provided, then Intl.PluralRules, then a basic one/other fallback.

| Parameter | Type | Description | | ------------- | ------------- | -------------------------------- | | locale | string | Locale code | | count | number | The number to pluralize for | | customRules | PluralRules | Optional custom rules per locale |

Returns: string — one of "zero", "one", "two", "few", "many", "other"

resolvePluralCategory('en', 1) // "one"
resolvePluralCategory('en', 5) // "other"
resolvePluralCategory('ar', 3) // "few" (via Intl.PluralRules)

I18nProvider / useI18n()

Context pattern for providing an i18n instance to the component tree.

// Root:
;<I18nProvider instance={i18n}>
  <App />
</I18nProvider>

// Any descendant:
function Greeting() {
  const { t, locale } = useI18n()
  return () => <h1>{t('greeting', { name: 'World' })}</h1>
}

I18nContext is also exported for advanced usage with useContext directly.

Trans

Rich JSX interpolation component. Resolves {{values}} first, then maps <tag>content</tag> patterns to component functions.

| Parameter | Type | Description | | ------------ | ------------------------------------- | --------------------------------------- | | t | (key, values?) => string | Translation function (from useI18n()) | | i18nKey | string | Translation key | | values | InterpolationValues | Interpolation values | | components | Record<string, (children) => VNode> | Component map for rich tags |

// Translation: "Read our <terms>terms</terms> and <privacy>policy</privacy>"
<Trans
  t={t}
  i18nKey="legal"
  components={{
    terms: (children) => <a href="/terms">{children}</a>,
    privacy: (children) => <a href="/privacy">{children}</a>,
  }}
/>

parseRichText(text)

Parse a string into an array of plain text and { tag, children } segments. Used internally by Trans.

| Parameter | Type | Description | | --------- | -------- | ----------------------------------------- | | text | string | String with <tag>content</tag> patterns |

Returns: (string | { tag: string, children: string })[]

parseRichText('Hello <bold>world</bold>!')
// ["Hello ", { tag: "bold", children: "world" }, "!"]

Patterns

Namespace-Based Loading

Split translations by feature and load them lazily.

const i18n = createI18n({
  locale: 'en',
  loader: (locale, ns) => fetch(`/locales/${locale}/${ns}.json`).then((r) => r.json()),
})

// Load on route entry:
await i18n.loadNamespace('dashboard')
i18n.t('dashboard:widgets.chart')

Runtime Message Addition

Add messages without async loading (e.g. from server-rendered data).

i18n.addMessages('en', { newFeature: 'Try our new feature!' })
i18n.addMessages('en', { errors: { timeout: 'Request timed out' } }, 'api')

Pluralization

Use _one, _other (and _zero, _two, _few, _many for complex locales) suffixes with a count value.

// messages: { items_one: "{{count}} item", items_other: "{{count}} items" }
i18n.t('items', { count: 1 }) // "1 item"
i18n.t('items', { count: 5 }) // "5 items"

Types

| Type | Description | | ----------------------- | ------------------------------------------------------------------------------------ | | I18nInstance | Public API returned by createI18n() | | I18nOptions | Options for createI18n() | | TranslationDictionary | { [key: string]: string \| TranslationDictionary } | | TranslationMessages | Record<string, TranslationDictionary> | | NamespaceLoader | (locale: string, namespace: string) => Promise<TranslationDictionary \| undefined> | | InterpolationValues | Record<string, string \| number> | | PluralRules | Record<string, (count: number) => string> | | I18nProviderProps | Props for I18nProvider: { instance: I18nInstance } | | TransProps | Props for Trans component |

Gotchas

  • t() reads locale reactively — it re-evaluates inside effects and computeds when the locale changes.
  • Concurrent loads for the same locale:namespace are deduplicated — calling loadNamespace("auth") twice returns the same promise.
  • Missing keys return the key string itself as a visual fallback (e.g. "auth:missing.key").
  • The default namespace is "common" — keys without a namespace: prefix look up in the "common" namespace.
  • addMessages deep-merges into existing translations. It does not replace the entire namespace.