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

@serenis/i18n

v0.22.0

Published

Internationalization for Serenis apps. The library owns two concerns:

Readme

@serenis/i18n

Internationalization for Serenis apps. The library owns two concerns:

  • Translations — a hierarchical system for global and scoped copy.
  • Language management — the user's language preference, persisted and shared across every Serenis app.

Translations

This library creates a hierarchical translation system that lets you maintain both global translations that are available throughout your application and independent scoped translations (e.g. for the domains) that can override or extend the global ones. This allows to split translations into smaller files, making it easier to manage and maintain them in addition to optimizing code splitting. It works by nesting react contexts: translations are first looked up in the closest context, then fall back to parent contexts if not found.

Usage

import { I18nProvider, Translation } from '@serenis/i18n'
export { default as globalTranslations } from './global/en.json'
export { default as domainTranslations } from './domains/foo/i18n/en.json'

const App = () => {
  return (
    <I18nProvider translations={globalTranslations}>
      <GlobalComponent />
      <Domain />
    </I18nProvider>
  )
}

const GlobalComponent = () => {
  const translate = useTranslate()

  return (
    <div>
      <h1>
        <Tranlation id="serenis" />
      </h1>
    </div>
  )
}

const Domain = () => {
  return (
    <I18nProvider translations={domainTranslations}>
      <DomainComponent />
    </I18nProvider>
  )
}

const DomainComponent = () => {
  const translate = useTranslate()

  return (
    <div>
      <h1>
        <Tranlation id="nutritionPlan.title" />
      </h1>
      <button type="button">
        <Tranlation id="global.save" />
      </button>
    </div>
  )
}

Typing the translation keys

The library uses TypeScript type augmentation to type the translation keys. You can provide the keys you need by extending the Translation interface:

import { type default as en } from '../src/i18n/en.json'

declare global {
  namespace I18n {
    interface Translation {
      // Extend the keys with the ones from the json translation file
      id: keyof typeof en
    }
  }
}

Language management

The library also owns the user's language preference, shared across every Serenis app through a cookie (srns_lang) scoped to serenis.it. The cookie is the single source of truth: apps mount LanguageProvider and read or change the language through useLanguage — persistence and cross-app sharing come for free.

import { LanguageProvider, useLanguage } from '@serenis/i18n'

const App = () => {
  return (
    <LanguageProvider>
      <LanguageSwitcher />
    </LanguageProvider>
  )
}

const LanguageSwitcher = () => {
  const { language, setLanguage } = useLanguage()

  return (
    <button type="button" onClick={() => setLanguage(language === 'it' ? 'en' : 'it')}>
      {language}
    </button>
  )
}

Seeding the first render

initialLanguage seeds the first render with values the provider cannot read on its own: a query parameter at the entry point, or the cookie forwarded by an SSR framework. Unsupported values are ignored, and the resolved language is persisted back to the cookie after mount.

<LanguageProvider initialLanguage={new URLSearchParams(window.location.search).get('lng')}>
  <App />
</LanguageProvider>

Reacting to language changes

onLanguageChange is called with the resolved language after mount and again on every change. Use it for side effects that must follow the language, like a date library locale. Keep its reference stable: an inline arrow re-triggers it on every render.

const syncDateLocale = (language: Language) => {
  setDefaultOptions({ locale: locales[language] })
}

const App = () => {
  return (
    <LanguageProvider onLanguageChange={syncDateLocale}>
      <Content />
    </LanguageProvider>
  )
}

Reading and writing outside React

readLanguage and writeLanguage expose the language to non-React environments — a server component, a proxy — through transport functions the consumer supplies. The library owns the cookie name, the value validation, the default, and the scope; the consumer only provides how to read or write a cookie in its environment. They are published from the React-free @serenis/i18n/server entry point, so importing them into a server component or proxy does not pull React in.

readLanguage returns null when no language cookie is set; pass { fallback: true } to get the default language instead — useful when a concrete value is required, e.g. an <html lang> attribute.

import { readLanguage, writeLanguage } from '@serenis/i18n/server'

// Next.js server component: read the language, defaulting when none is set
const cookieStore = await cookies()
const language = readLanguage((name) => cookieStore.get(name)?.value, { fallback: true })

// Next.js proxy: normalize a query parameter into the shared cookie
writeLanguage(
  request.nextUrl.searchParams.get('lng'),
  (cookie) => response.cookies.set(cookie),
  request.nextUrl.hostname,
)