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

@c9up/rosetta

v0.3.0

Published

Rosetta — dedicated i18n module for the Ream ecosystem

Readme

@c9up/rosetta

Internationalization for Ream applications with an API aligned with @adonisjs/i18n. Rosetta has no runtime npm dependency: message parsing, content negotiation and YAML catalog loading are implemented in the package, while locale data and formatting come from Node's built-in Intl APIs.

Rendering runs on Intl, so CLDR plural rules and number/date formatting track the runtime instead of a table maintained here. Parsing is pure TypeScript: each ICU message is parsed to an AST once per process and cached. The package is platform-independent — no native binary, no build toolchain, and the same behaviour on glibc, musl/Alpine and in the browser.

Requirements

  • Node.js 22 or newer
  • JSON, YAML or YML translation catalogs

Configuration

Register the provider and use the same config/i18n.ts shape as AdonisJS.

// reamrc.ts
export default {
  providers: [() => import('@c9up/rosetta/i18n_provider')],
}
// config/i18n.ts
import { defineConfig, formatters, loaders } from '@c9up/rosetta'

export default defineConfig({
  defaultLocale: 'en',
  formatter: formatters.icu(),
  fallbackLocales: {
    'fr-CH': 'fr',
  },
  loaders: [
    loaders.fs({
      location: new URL('../resources/lang/', import.meta.url),
    }),
  ],
})

The configure export creates this config, a locale-detection middleware and registers the provider when used by Ream's package configurator.

Translations

Catalogs follow the Adonis namespace layout.

resources/lang/
  en/messages.json
  fr/messages.yaml
{
  "greeting": "Hello {name}",
  "items": "{count, plural, one {# item} other {# items}}"
}
import i18nManager from '@c9up/rosetta/services/main'

const i18n = i18nManager.locale('en')
i18n.t('messages.greeting', { name: 'John' })
i18n.t('messages.missing', {}, 'Fallback message')

Supported ICU constructs include interpolation, select, plural, selectordinal, number skeletons, date/time styles, nested messages, offsets and ICU apostrophe escaping. Plural rules and localized number/date output use the runtime CLDR data through Intl.

Formatting

Every Intl-backed helper is available both on a locale-scoped i18n instance and on a standalone Formatter, which is the same class — i18n extends it.

import { Formatter } from '@c9up/rosetta'

const formatter = new Formatter('en-US')
formatter.formatNumber(1234567.89)                    // '1,234,567.89'
formatter.formatCurrency(1234.56, { currency: 'USD' }) // '$1,234.56'
formatter.formatRelativeTime(-1, 'day')                // '1 day ago'
formatter.formatList(['John', 'Jane', 'Bob'])          // 'John, Jane, and Bob'

formatter.switchLocale('fr')
formatter.formatNumber(1234.56)                        // '1 234,56'

formatNumber, formatCurrency, formatDate, formatTime, formatRelativeTime (including 'auto'), formatPlural, formatList and formatDisplayNames all read the locale from the instance.

HTTP, Inker And Validation

@c9up/rosetta/middleware detects the best supported locale from request.languages() or Accept-Language, assigns a request-scoped ctx.i18n instance and shares it with the view layer. Its static registerMessagesProvider method connects a compatible request validator to i18n.createMessagesProvider().

For Ream templates, @c9up/inker is the Edge-equivalent integration. Its provider resolves Rosetta through the rosetta container alias and exposes the canonical t() helper; the middleware writes the negotiated language to ctx.locale, which Inker reads per render. The Edge adapter remains exported from @c9up/rosetta/plugins/edge for Edge-compatible non-Ream hosts.

Typed Translation Keys

Catalogs are JSON/YAML read at runtime, so the compiler cannot see them. generateCatalogTypes closes that gap: it reads your catalogs and emits a declaration that types t() — unknown keys, misspelled variables and wrong variable types all become compile errors.

// scripts/i18n-types.ts
import { writeFileSync } from 'node:fs'
import { generateCatalogTypes } from '@c9up/rosetta'
import i18nManager from '@c9up/rosetta/services/main'

await i18nManager.loadTranslations()
writeFileSync(
  'types/i18n.d.ts',
  generateCatalogTypes(i18nManager.getTranslations(), { referenceLocale: 'en' }),
)

The generated file augments TranslationKeys — the same declaration-merging idiom AdonisJS uses for EventsList:

declare module '@c9up/rosetta' {
  interface TranslationKeys {
    'messages.greeting': { 'name': string | number }
    'messages.items': { 'count': number }
  }
}
i18n.t('messages.greeting', { name: 'Hugo' })  // ok
i18n.t('messages.greetingg', { name: 'Hugo' }) // error: unknown key
i18n.t('messages.greeting', { nam: 'Hugo' })   // error: no such variable
i18n.t('messages.items', { count: '2' })       // error: a plural takes a number
i18n.t('messages.items')                       // error: `count` is required
i18n.t('messages.plain', { count: 1 })         // error: takes no variables

Until you generate that file, TranslationKeys is empty and t() accepts every string exactly as before — opting in is what turns the checks on.

For reference, @adonisjs/[email protected] declares t(identifier: string, data?: Record<string, any>, fallbackMessage?: string), so none of the six lines above is an error there.

Checking Catalogs

Catalogs drift silently: a key lands in en and is forgotten in fr, a variable is renamed in one locale only, a message is edited into invalid ICU. None of it fails until a user with that locale reaches that key. checkCatalogs reconciles every locale against a reference one and reports four kinds of drift — missing keys, orphan keys (with a did you mean for typos), ICU variables that disagree, and messages that no longer parse.

import { checkCatalogs, verifyCatalogs } from '@c9up/rosetta'

const findings = checkCatalogs(i18nManager.getTranslations(), {
  referenceLocale: 'en',
})
// [{ locale: 'fr', key: 'items', kind: 'param-mismatch',
//    detail: "expects n:plural, but 'en' declares count:plural" }]

// Boot guard — warns by default, `mode: 'throw'` to fail fast.
verifyCatalogs(i18nManager.getTranslations(), { referenceLocale: 'en' })

runCatalogCheck(translations, opts) prints the report and returns an exit code, for a CI script. This has no @adonisjs/i18n equivalent — Adonis reports a missing translation at runtime, once the request is already being served.

Reloading

Translations are cached after boot. Reloading is atomic: a loader failure keeps the previous complete catalog active.

await i18nManager.reloadTranslations()

YAML Scope

The dependency-free YAML reader supports the catalog forms used by the package: nested mappings, quoted and unquoted scalar values, comments, and literal or folded block scalars. Unsupported constructs such as sequences, flow mappings, anchors, aliases, tags and merge keys raise an error instead of being silently partially loaded. JSON catalogs have no such YAML-specific restrictions.

Upgrading To 0.2.0

The Rust N-API engine and its binary are gone; the package is pure TypeScript.

  • Remove any pnpm build:napi step from your build or CI. There is no binary and no Rust toolchain to install.
  • isNativeAvailable() is no longer exported. It reported whether the native binary had loaded, which is now always moot.

Nothing else changed: same API, same catalogs, and every message renders exactly as before. Rendering already ran on Intl — the engine only parsed — so this is a packaging change, not a behavioural one. Alpine/musl images, which never had a binary to load, are unaffected.

Entry Points

  • @c9up/rosetta
  • @c9up/rosetta/i18n_provider
  • @c9up/rosetta/factories
  • @c9up/rosetta/middleware
  • @c9up/rosetta/plugins/edge
  • @c9up/rosetta/repl
  • @c9up/rosetta/services/main
  • @c9up/rosetta/types

License

MIT