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

@concordance-wiki/i18n

v1.0.0

Published

Message catalogues of the generated site and their build-time resolution.

Readme


Why

Concordance publishes a static site that speaks the language of your team, with your own words for its labels if you want them. You install @concordance-wiki/concordance for that. This package is the part of it that holds the strings: the message catalogues of the site, en and fr, in ICU MessageFormat, typed by identifier so that a missing variable does not compile, and resolved at build time so that a published page carries final text and no formatting library. Install it alone to build on the engine, or to word a theme component from a plugin.

Quick start

npm install @concordance-wiki/i18n
import { formatMessage, loadCatalogue, textDirection } from "@concordance-wiki/i18n";

const catalogue = loadCatalogue("fr-CA");
formatMessage(catalogue, "entity.mentionsCount", { count: 2 }); // "2 mentions"
formatMessage(catalogue, "site.generatedAt", { date: new Date("2026-03-05T00:00:00Z") }); // "Généré le 5 mars 2026"
textDirection(catalogue.locale); // "ltr", for the dir attribute of the page

An unknown identifier, a missing variable or an extra one does not compile: MessageId is the union of every key of the catalogue and MessageArguments derives the argument types from the declared kinds.

What you get

  • Two complete catalogues, en and fr, picked by language subtag with fallback to the source language: loadCatalogue, resolveLanguage, shippedLanguages.
  • Plurals, numbers and dates done right: formatMessage and formatText apply the plural rules and the Intl formats of the locale.
  • Your labels, checked first: validateLabels, validateOverride, validateOverrides verify the labels block of a theme before any page renders.
  • Formatters for the rest: formatDate, formatDay, formatMonth, formatNumber, formatRelative, textDirection for the values rendered outside a message.
  • Tools for a theme's tests: messageIds, messageArguments, argumentNames, parseMessage, the identifiers, the declared kinds and the ICU parser.

Documentation

Part of Concordance, MIT licence.

Every label of the site comes from a catalogue per language in ICU MessageFormat, stored as the JSON translation platforms exchange, one file per area of the site under messages/<language>/: an area is an identifier prefix (home, entity, search, results, spaces...), messages/en/<area>.json is its source, { "<id>": { "defaultMessage": "...", "description": "..." } }, and messages/<language>/<area>.json its flat translation, { "<id>": "..." }. Every key of a file is under the prefix the file is named after, and the keys are sorted. en and fr ship complete; a test checks that every locale carries every key of the source with the same variables and kinds.

Each area has a module under src/areas/<area>.ts that imports its two files and declares the ICU kind of every argument of every message of the area ({ count: "plural" }, {} for a message without any), checked by satisfies against the keys of the source file. src/areas.ts lists the areas in the order of their prefixes; the catalogue merges them in that order, so the identifiers read in sorted order. Identifiers are typed from that merge (MessageId is the union of the keys of every area), and MessageArguments derives the argument types from the declared kinds: an unknown identifier, a missing variable or an extra one does not compile. A unit test keeps the declared kinds aligned with what the ICU parser reads in the catalogue, and computes the number of messages from the files rather than pinning it.

Adding an area

A new page or part of the site gets its own prefix and its own files: messages/en/<area>.json, messages/fr/<area>.json, src/areas/<area>.ts declaring the arguments, and one line in the AREAS list of src/areas.ts at the place of its prefix. The tests reject a file whose keys leave the prefix or are unsorted, a module whose declared kinds differ from the parsed messages, and an area listed out of order.

Resolution

loadCatalogue picks the shipped catalogue by language subtag (fr-CA uses fr and formats numbers, dates and plurals with fr-CA) and falls back to the source language when a language ships no catalogue (de gets the en messages formatted in en, with catalogue.fallback set). A malformed tag raises a CatalogueError. Dates inside messages are formatted in UTC unless timeZone says otherwise, so that two builds of the same corpus agree.

formatDate, formatNumber and formatRelative wrap Intl.DateTimeFormat, Intl.NumberFormat and Intl.RelativeTimeFormat for the values rendered outside a message; formatRelative picks the largest unit that fits and lets the platform say "yesterday" or "hier". textDirection reads the text information of Intl.Locale (getTextInfo() on engines that have it, the earlier textInfo accessor otherwise) and falls back to the likely script of the locale against a list of right-to-left scripts.

Messages are resolved at build: formatMessage returns final strings and the published HTML contains no message, no catalogue and no formatting library. Nothing in this package touches the DOM or is meant to run in a browser; a test checks that the sources reference no browser global.

Overriding a label

The labels block of theme.yaml overrides any message, by language of a shipped catalogue then by identifier:

labels:
  fr:
    site.home: Début
    entity.mentionsCount: "{count, plural, one {# citation} other {# citations}}"

An override uses the syntax of the message it replaces and must use exactly the variables of the source message. validateLabels(theme.labels) returns the configuration issues of the whole block, one per faulty key with the path labels.<language>.<id>: an unknown identifier, an ICU syntax error with the parser's message, or the missing and unknown variables. The theme validation of the command line is where to call it, so that a bad override is reported with the other theme errors before any rendering; loadCatalogue(locale, { labels }) validates the block it applies as well and throws a CatalogueError carrying the same issues. Dependencies point from this package to core, never the other way, so core only describes the labels key in the theme schema.

Adding a locale

  1. Add messages/<language>/<area>.json for every area, with every key of messages/en/<area>.json, sorted, each value a translation that keeps the variables and kinds of the source message. Plural forms follow the CLDR categories of the language (one, other, ...).
  2. Import each file in the module of its area under src/areas/, add the language to the Area type in src/area.ts and to src/shipped.ts, and to the labels key of theme.schema.json in the core package.
  3. Run the tests: the parity test lists any missing key or differing variable.

Dependencies

  • intl-messageformat 11.2.15 (BSD-3-Clause, FormatJS): formats an ICU message with the plural rules, number and date formats of a locale through the platform Intl objects, without any locale data of its own.
  • @formatjs/icu-messageformat-parser 3.5.18 (MIT, FormatJS): the parser intl-messageformat already uses, needed on its own to read the variables of a message for the parity test and the validation of overrides.