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

@canonical/i18n-core

v0.33.0

Published

Framework-agnostic internationalization core (native Intl): locale negotiation, message catalogs, translators, memoized formatters, and a reactive locale source.

Readme

@canonical/i18n-core

Framework-agnostic internationalization for the Pragma design system, built on the native Intl API. Zero runtime dependencies.

i18n-core owns everything that is not framework reactivity — locale negotiation, message catalogs and translation, memoized formatters, and a live locale source. React, Svelte, and Lit bindings stay thin and share this one engine.

Installation

bun add @canonical/i18n-core

Configuration

Locales are declared explicitly as plain data — no magic discovery.

import type { I18nConfig } from "@canonical/i18n-core";

const config: I18nConfig = {
  locales: ["en", "fr", "ar"],
  defaultLocale: "en",
  rtlLocales: ["ar"],
};

Locale negotiation (pure, SSR-safe)

import { directionOf, negotiateLocale } from "@canonical/i18n-core";

const locale = negotiateLocale(config, {
  cookieHeader: request.headers.get("cookie"),
  acceptLanguage: request.headers.get("accept-language"),
});
const dir = directionOf(config, locale); // "ltr" | "rtl"

An explicit cookie wins, then Accept-Language negotiation, then the default locale. It takes raw header strings and returns a tag, so it behaves the same on the server and the client.

Messages

import { createTranslator, mergeCatalogs } from "@canonical/i18n-core";

const t = createTranslator("en", {
  "nav.home": "Home",
  greeting: "Hello, {name}!",
  items: { one: "{count} item", other: "{count} items" },
});

t("greeting", { name: "Ada" }); // "Hello, Ada!"
t("items", { count: 3 }); // "3 items" — via Intl.PluralRules

mergeCatalogs(base, overrides) layers app messages over a component's built-in defaults. Resolution never throws: a missing key returns the key itself, an absent placeholder is left verbatim.

Formatting

import { createFormatters } from "@canonical/i18n-core";

const f = createFormatters("en-US");
f.number(1234.5); // "1,234.5"
f.currency(1234.5, "USD"); // "$1,234.50"
f.relativeTime(-3, "day", { numeric: "auto" }); // "3 days ago"
f.list(["A", "B", "C"]); // "A, B, and C"

Formatters memoize their Intl instances by options, since constructing them is comparatively expensive.

Reactive locale source

The cross-framework runtime channel. subscribe follows the Svelte store contract — it calls back immediately and on every change — which also satisfies React's useSyncExternalStore and a Lit reactive controller. One object drives every framework. In the browser it persists the choice to a cookie and reflects <html lang dir>; on the server those effects are inert.

import { createLocaleSource } from "@canonical/i18n-core";

const source = createLocaleSource(config, { initial: locale });
const unsubscribe = source.subscribe((next) => render(next));
source.set("ar"); // notifies subscribers; persists + reflects dir="rtl"

On the server, create one source per request — the React/Svelte/Lit bindings already do this. A module-level singleton shared across concurrent requests leaks locale state between them.

Server-side rendering

The source can only set <html lang dir> from the client. For a correct first paint and assistive technology, render <html lang dir> on the server from the negotiated locale with documentAttrs:

import { documentAttrs, negotiateLocale } from "@canonical/i18n-core";

const locale = negotiateLocale(config, {
  cookieHeader: request.headers.get("cookie"),
  acceptLanguage: request.headers.get("accept-language"),
});
const { lang, dir } = documentAttrs(config, locale);
// render: <html lang={lang} dir={dir}> … </html>

Pass the same locale to the client (createLocaleSource({ initial: locale })) so the server and first client render agree.

Right-to-left & accessibility

directionOf resolves writing direction from rtlLocales (base languages); a sensible default set is ["ar", "he", "fa", "ur"]. Flipping dir is necessary but not sufficient — author UI with CSS logical properties (margin-inline-start, inset-inline, text-align: start) and mirror directional icons so RTL is structural, not decorative.

Values in config.locales must be valid BCP-47 tags — they are reflected to <html lang> and passed to Intl. A missing message key is returned verbatim ("nav.home"), which is visible and read aloud, so treat catalog completeness as an accessibility concern. t() returns plain strings that the framework escapes on render; never pipe its output into dangerouslySetInnerHTML / unsafeHTML / {@html}.

License

LGPL-3.0