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/locale

v0.1.0

Published

Simple, predictable locale management for React — locale, country & language data with a typed domain model.

Readme

@luwiostack/locale

Simple, predictable locale management for React. Ties a language and a country together into an active locale, exposed through a provider + hook. It composes @luwiostack/country and @luwiostack/language (installed automatically), so a locale is valid whenever its language and country are each known.

Part of Luwio — standalone, but pairs well with the other @luwiostack/* packages.

Ported and restructured from @tacky-org/locale.

Install

npm install @luwiostack/locale

Pulls in @luwiostack/country and @luwiostack/language automatically. React 18+ is a peer dependency (the domain layer works without React too).

React usage

import { Locale, useLocale } from '@luwiostack/locale/react'

function App() {
  return (
    <Locale locale={Locale.new({ languageOrLocale: 'nl-BE' })}>
      <Info />
    </Locale>
  )
}

function Info() {
  const { locale } = useLocale()
  // locale is the active Locale — the same object Locale.new() returns.
  return (
    <p>
      {locale.language().machine_name} in {locale.country().code} ({locale.code})
      — dial {locale.country().dialing_code}
    </p>
  )
}

Give <Locale> a locale you control. For untrusted values — from the URL, storage or an API — resolve them first with Locale.resolve: it maps the value onto your supported set (falling back via the required * catch-all) so an unsupported locale can't crash the app. A locale that still can't be resolved throws while rendering; useLocale throws if used outside a provider.

<Locale> mirrors lang/dir onto <html> through the nested <Language> provider, but that's a client-side effect — server-rendered HTML is still missing them on the first byte. Render the root with htmlAttrs(locale) in your server entry to ship them from the start instead:

import { htmlAttrs, Locale } from '@luwiostack/locale'

const html = renderToString(
  <html {...htmlAttrs(Locale.new({ languageOrLocale: 'ar-EG' }))}>
    {/* → <html lang="ar" dir="rtl"> */}
    <body>{app}</body>
  </html>,
)

Domain model (no React required)

A Locale resolves its language(), country() and continent() as domain objects, so you rarely need anything else. Country, Language and Continent come from @luwiostack/country and @luwiostack/language — @luwiostack/locale composes them as dependencies but does not re-export them, so import those from their own packages when you want to build one directly.

import { Currency } from '@luwiostack/currency'
import { Locale } from '@luwiostack/locale'

const locale = Locale.new({ languageOrLocale: 'nl-BE' })
locale.language().machine_name  // 'dutch'
locale.country().alpha3         // 'BEL'
locale.country().borders()      // Countries → FR, DE, LU, NL
Currency.of(locale.country()).symbol // '€' — throws for a currency-less country (e.g. Antarctica)
locale.continent().machine_name // 'europe'
locale.languages().toArray().map((l) => l.machine_name) // ['dutch', 'french', 'german'] — spoken in BE

// A locale is valid when its language and country are each known — no "listed pair"
// requirement. Unknown parts throw:
Locale.new({ languageOrLocale: 'en', country: 'BE' })  // 'en-BE' (both parts exist)
Locale.new({ languageOrLocale: 'zz', country: 'BE' })  // throws — 'zz' isn't a language

Locale-aware sorting

Plain Array.prototype.sort() is case-sensitive binary order ('Zebra' sorts before 'apple'), which is rarely what you want for user-facing lists. LocaleCollator.new(locale) binds an Intl.Collator to a locale once — a shortcut for new Intl.Collator(locale.code), memoized for the collator's lifetime — then sort() as many lists as you like without repeating the locale. It's a separate formatter, not a method on Locale (this repo keeps behavior off domain objects and in dedicated formatters):

import { Locale, LocaleCollator } from '@luwiostack/locale'

const locale = Locale.new({ languageOrLocale: 'nl-BE' })
const collator = LocaleCollator.new(locale)

collator.sort(['Zebra', 'apple', 'Mango']) // ['apple', 'Mango', 'Zebra']

// Sort by a string derived from each item — e.g. translated names, not English machine_names:
collator.sort(countries.toArray(), (c) => t(c.machine_name))

// Any Intl.CollatorOptions works too, e.g. natural ordering for numbered strings:
collator.sort(['item2', 'item10', 'item1'], { numeric: true }) // ['item1', 'item2', 'item10']

In a component, skip the LocaleCollator.new(locale) step — useLocale() already bootstraps one from the active locale and hands it back as sort, memoized so its identity is stable across renders:

function CountryList() {
  const { sort } = useLocale()
  return (
    <ul>
      {sort(Countries.all().toArray(), (c) => t(c.machine_name)).map((c) => (
        <li key={c.code}>{t(c.machine_name)}</li>
      ))}
    </ul>
  )
}

Locale resolution

Locale.resolve maps a detected locale onto the ones your app supports, with exact → override → wildcard → same-language → catch-all fallback:

import { Locale } from '@luwiostack/locale'

Locale.resolve({
  detected: Locale.system, // the runtime's detected locale
  supported: ['nl-BE', 'fr-FR', 'en-US'], // optional — omit to accept any valid locale
  overrides: { 'en-*': 'en-US', '*': 'nl-BE' }, // '*' catch-all is required
})

// No `supported` list → the whole dataset: any valid locale is returned as-is,
// only an unknown/missing one falls through to the overrides + '*' catch-all.
Locale.resolve({ detected: Locale.system, overrides: { '*': 'nl-BE' } })

Locale routing

With @luwiostack/router, useRouteLocale() always returns a locale — the one in the URL, or the router's configured default when the URL has none — so you never handle null. Resolve it onto what you support, then hand the result to <Locale>:

import { Locale } from '@luwiostack/locale/react'
import { useRouteLocale } from '@luwiostack/router'

const SUPPORTED = ['nl-BE', 'fr-FR', 'en-US']
const OVERRIDES = { 'en-*': 'en-US', '*': 'nl-BE' } // '*' catch-all → default

function App() {
  const { locale } = useRouteLocale() // always a locale (route's, or the router default)
  const active = Locale.resolve({ detected: locale, supported: SUPPORTED, overrides: OVERRIDES })
  return (
    <Locale locale={active}>
      <Site />
    </Locale>
  )
}

An unsupported /pt-PT falls to the * catch-all (default); everything else resolves through the usual rules (same-language → per-pattern → catch-all). No crash, no manual null handling.

Structure

src/
├── domain/     Locale, system-locale (→ Locale.system)
├── utils/      resolve-locale (→ Locale.resolve), normalizeLocale, matchLocalePattern,
│               create-locale (→ Locale.new), from-intl-locale (→ Locale.fromIntlLocale), htmlAttrs
├── formatters/ LocaleCollator (→ .new(locale).sort())
├── react/      LocaleContext, Locale, useLocale
└── types.ts    ILocale, LocaleOverrides

Country, language and continent data live in @luwiostack/country / @luwiostack/language.

API surface

  • React: Locale, useLocale (returns { locale, sort }; locale is the active Locale, same as Locale.new() → .code, .language(), .languages(), .country(), .continent(), .toIntlLocale(); sort is a LocaleCollator bound to locale, memoized)
  • Factory: Locale.new, Locale.tryNew, Locale.resolve, Locale.tryResolve, Locale.system, Locale.fromIntlLocale (build from a native Intl.Locale instead of a string)
  • Formatters: LocaleCollator.new(locale) → { sort(items, by?, options?) } — locale-aware sort via a memoized Intl.Collator
  • Utils: normalizeLocale, matchLocalePattern, htmlAttrs (<html lang dir> attributes for SSR)
  • Types: ILocale, LocaleOverrides

Country / Language / Continent and their types are not re-exported — they're internal dependencies. Import them from @luwiostack/country / @luwiostack/language when you need them directly.