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

@luwiostack/translations

v0.2.0

Published

Lingui-powered translations for React — a language provider, catalog (pre)loading, and a t() helper.

Downloads

289

Readme

@luwiostack/translations

Lingui-powered translations for React. Just translations:

  1. Create a Translations object with createTranslations(catalogRegistry).
  2. Hand it to <Translations translations={translations}> — like <RouterProvider router={…} />.
  3. Reach it with useTranslations() to activate() a language or t() to translate.

Catalogs come from two places: up front with createCatalog() in *.catalog files (discovered by the luwioTranslations Vite plugin and pre-loaded by createTranslations(catalogRegistry)), or at runtime with translations.add() (cached, deduped) — an import, an API, inline messages. Each language must be a valid @luwiostack/language ILanguage, so it's valid by construction. It knows nothing about routing.

Built on Lingui, which is bundled in — you don't install or import @lingui/core or @lingui/react directly. Catalogs are plain objects you manage yourself (see below), not extracted/compiled via the Lingui CLI.

Install

npm install @luwiostack/translations

Create & provide

// translations.ts
import { catalogRegistry, createTranslations } from '@luwiostack/translations'

export const translations = createTranslations(catalogRegistry) // from the registry; add more at runtime
// App.tsx
import { Translations } from '@luwiostack/translations'
import { translations } from './translations'

function App() {
  return (
    <Translations translations={translations}>
      <Site />
    </Translations>
  )
}

Add a catalog — from anywhere

add / activate / isLoaded take an ILanguage (from @luwiostack/language), so a language is always valid. The source accepts messages directly, a dynamic import, an API call, or a promise. It's awaitable, cached and deduped — a language is fetched at most once and never re-loads on switch.

import { Language } from '@luwiostack/language'

const nl = Language.new({ code: 'nl' })
const fr = Language.new({ code: 'fr' })

await translations.add(nl, { greeting: 'Hallo' })                                 // inline messages
await translations.add(fr, () => import('./locales/fr').then((m) => m.messages))  // file import
await translations.add(fr, () => fetch(`/api/i18n/${fr.code}`).then((r) => r.json())) // API

translations.activate(nl) // switch to a loaded language

Combine multiple files

add() takes a single CatalogSource per language — to combine several files (or requests) into one catalog, merge them yourself inside it before returning. A second add() for an already-loaded (or in-flight) language is a no-op, not a merge, so this has to happen in one call:

// Split across files
await translations.add(nl, async () => {
  const [common, page] = await Promise.all([
    import('./locales/nl/common').then((m) => m.messages),
    import('./locales/nl/page').then((m) => m.messages),
  ])
  return { ...common, ...page } // Catalog is a plain object — later keys win
})
// Split across API endpoints
await translations.add(nl, async () => {
  const [common, page] = await Promise.all([
    fetch(`/api/i18n/${nl.code}/common`).then((r) => r.json()),
    fetch(`/api/i18n/${nl.code}/page`).then((r) => r.json()),
  ])
  return { ...common, ...page }
})

Preload

Preloading is just add() without activate() — warm the languages a user is likely to switch to (after first paint, on idle, or on hover) so the switch itself is synchronous, with no fetch and no loading state.

const nl = Language.new({ code: 'nl' })
const fr = Language.new({ code: 'fr' })

// Warm the other languages in the background…
void Promise.all([
  translations.add(nl, () => fetch(`/api/i18n/${nl.code}`).then((r) => r.json())),
  translations.add(fr, () => fetch(`/api/i18n/${fr.code}`).then((r) => r.json())),
])

// …later the switch is instant — already cached:
translations.activate(fr)

Define catalogs up front (registry + Vite)

The runtime add() above is one of two ways to supply a catalog. The other is to define it up front in a *.catalog file with createCatalog — the translations counterpart to createRoute / createApi. createTranslations(catalogRegistry) pre-loads every registered catalog, so a language shipped with the app is ready with no runtime add().

// app/en.catalog.ts
import { Language } from '@luwiostack/language'
import { createCatalog } from '@luwiostack/translations'
import { messages } from './messages'

export const en = createCatalog({ language: Language.new({ code: 'en' }), source: messages.en })

Discover those files automatically with the Vite plugin (like luwioRouter / luwioApi) — no glob in your app:

// vite.config.ts
import { luwioTranslations } from '@luwiostack/translations/vite'
export default defineConfig({ plugins: [luwioTranslations()] }) // scans src for *.catalog.{ts,tsx,…}
// translations.ts
import 'virtual:@luwiostack/translations/catalogs' // every *.catalog file is now registered
import { catalogRegistry, createTranslations } from '@luwiostack/translations'
export const translations = createTranslations(catalogRegistry) // pre-loads the registered catalogs

Reference @luwiostack/translations/vite-client in your vite-env.d.ts for the virtual module's type. Up-front and runtime coexist: ship the default language as a catalog, and still translations.add(language, () => fetch(…)) the rest on switch. Opt out of the shared registry with a different registry to createTranslations (or add an explicit { catalogs: [...] }).

useTranslations

import { useTranslations } from '@luwiostack/translations'

function Greeting() {
  const { t } = useTranslations() // t is pulled out for convenience; re-renders on activate
  return (
    <>
      <h1>{t('greeting')}</h1>
      <p>{t({ key: 'welcome', defaultValue: 'Welcome, {name}' }, { name: 'Gert' })}</p>
    </>
  )
}

useTranslations() returns { translations, t } — destructure t for the common translate case, or reach the whole translations store when you need add() / activate().

With a route (@luwiostack/router)

@luwiostack/translations is routing-agnostic — this is just how an app wires it. @luwiostack/router puts the resolved locale in route context, so a layout route adds + activates the catalog in beforeLoad. Because add is awaited, the route waits for translations; because it's cached, moving between pages in the same language never re-fetches.

import { createRoute } from '@luwiostack/router'
import { Shell } from '../components/Shell'
import { translations } from '../translations'

export default createRoute({
  id: 'shell',
  layout: true,
  component: Shell,
  beforeLoad: async ({ context }) => {
    const language = context.locale.language() // an ILanguage from @luwiostack/language
    await translations.add(language, () => fetch(`/api/i18n/${language.code}`).then((r) => r.json()))
    translations.activate(language)
  },
})

API

  • createTranslations(catalogRegistry, { catalogs? }) → ITranslations — create from a catalog registry (like createRouter(routeRegistry, …)); pre-loads the registered catalogs, plus any passed via options.catalogs
    • add(language, source) — add a catalog for an ILanguage from a CatalogSource (messages · () => import(…) · () => fetch(…) · promise); cached, deduped, awaitable
    • activate(language) — make an ILanguage active
    • isLoaded(language) — whether an ILanguage's catalog is loaded
    • languages — the ILanguage[] added so far
    • t(id, values?) — runtime translate against the active language
    • for(language) → { t } — a translator bound to a specific language, independent of the active one (translate several languages in one pass, e.g. SSR-ing every locale or a locale-aware router building <head> tags per locale)
  • Up-front catalogs (registry): createCatalog({ language, source }) → CatalogEntry (self-registers), catalogRegistry (add / get / all / clear), registerModules(modules) — all on @luwiostack/registry
  • Vite: luwioTranslations({ dir?, suffix?, virtualId? }) (@luwiostack/translations/vite) — scans *.catalog files into virtual:@luwiostack/translations/catalogs; ambient type at @luwiostack/translations/vite-client
  • React: Translations (provider, { translations }), useTranslations() → { translations, t }
  • Types: ITranslations, Translator, CreateTranslationsOptions, CatalogEntry, CatalogSource, Messages