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

next-react-intl

v0.1.1

Published

react-intl for the Next.js App Router — server + client glue, no build plugin required. Resolves FormatJS hashed message IDs at runtime so you don't need the ABI-fragile @swc/plugin-formatjs.

Readme

next-react-intl

react-intl for the Next.js App Router — no build plugin required.

react-intl/FormatJS is great, but using it with the App Router means re-solving the same two problems in every project:

  1. No Server Component story. useIntl needs React context, so it can't run in RSC. You end up hand-rolling a getIntl() for the server and an IntlProvider for the client every time.
  2. The extracted-key workflow fights Next. FormatJS's best feature — author an inline defaultMessage, get a hashed id injected at build time — normally needs a Babel plugin. Next uses SWC. The @swc/plugin-formatjs WASM plugin exists but is ABI-locked to a specific Next/swc_core version, so a Next bump can silently break your build.

next-react-intl solves both:

  • App Router glue, server + client, from one config.
  • It computes the FormatJS message id at runtime instead of at build time — byte-for-byte the same hash @formatjs/cli writes into your catalog (verified by test). So you keep the extract → compile workflow with no SWC/Babel plugin, and it works under Turbopack and any bundler. If the build plugin is present, ids are already injected and the runtime hashing is skipped — it composes, it doesn't conflict.

The id hash is sha512 over defaultMessage (+ #description), base64, first 6 chars — memoized, so each unique message hashes once.

Install

npm install next-react-intl react-intl @formatjs/intl
# and, for extracting/compiling catalogs:
npm install -D @formatjs/cli

Peer deps: react >=18, react-intl >=6, @formatjs/intl >=2, next >=14 (optional — only the cookieLocaleResolver helper imports next/headers).

This package is ESM-only ("type": "module") — which is the norm for the App Router. Import it; don't require() it.

Quickstart

1. Configure once

// src/i18n.ts  (server module — no "use client")
import { createI18n, cookieLocaleResolver } from "next-react-intl/server"

const SUPPORTED = ["en", "es"] as const

export const { getIntl, getProviderProps, I18nProvider, resolveLocale } = createI18n({
  // The language your inline defaultMessages are authored in (react-intl's fallback).
  sourceLocale: "en",
  // Where the active locale comes from — app-owned, a single source of truth.
  // Use the helper, or pass any () => string | Promise<string>.
  resolveLocale: cookieLocaleResolver({
    cookieName: "locale",
    supportedLocales: SUPPORTED,
    defaultLocale: "es",
  }),
  // Load the compiled catalog for a locale (see the catalog workflow below).
  loadMessages: async (locale) => (await import(`./i18n/compiled/${locale}.json`)).default,
})

2. Mount the provider in your root layout (Server Component)

// src/app/layout.tsx
import { I18nProvider, getProviderProps, resolveLocale } from "@/i18n"

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const locale = await resolveLocale()
  const { messages } = await getProviderProps()
  return (
    <html lang={locale}>
      <body>
        <I18nProvider locale={locale} messages={messages}>
          {children}
        </I18nProvider>
      </body>
    </html>
  )
}

3. Use it — Server Components

import { getIntl } from "@/i18n"

export default async function AccountPage() {
  const intl = await getIntl()
  return <h1>{intl.formatMessage({ defaultMessage: "Account" })}</h1>
}

4. Use it — Client Components

Import the hook/component from the client entry (next-react-intl), not from your server config module — that keeps server code out of the client bundle. They read the active locale and id pattern from the provider you mounted in the layout.

"use client"
import { useIntl, FormattedMessage } from "next-react-intl"

export function CartButton({ count }: { count: number }) {
  const intl = useIntl()
  return (
    <button aria-label={intl.formatMessage({ defaultMessage: "Cart ({count})" }, { count })}>
      <FormattedMessage defaultMessage="Cart ({count})" values={{ count }} />
    </button>
  )
}

No ids anywhere — just defaultMessage. ICU syntax ({count}, plurals, etc.) works as usual.

The catalog workflow

English (defaultMessage) is canonical and lives inline in your source. The other locales are translated JSON catalogs, keyed by the hashed id. Drive it with @formatjs/cli:

// package.json
{
  "scripts": {
    // 1. Scan source → English catalog (hashed ids). The id pattern MUST match
    //    next-react-intl's default.
    "i18n:extract": "formatjs extract 'src/**/*.{ts,tsx}' --ignore='**/*.d.ts' --out-file lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
    // 2. Mirror lang/en.json → lang/locales/<locale>.json and translate each value
    //    (keep the same keys). One file per non-source locale.
    // 3. Compile to the runtime catalogs your loadMessages() imports.
    "i18n:compile-en": "formatjs compile lang/en.json --out-file src/i18n/compiled/en.json",
    "i18n:compile-others": "formatjs compile-folder lang/locales src/i18n/compiled"
  }
}

The compiled src/i18n/compiled/*.json files are what loadMessages returns at runtime — commit them.

If you change idInterpolationPattern in createI18n, pass the same --id-interpolation-pattern to formatjs extract. The default ([sha512:contenthash:base64:6]) matches FormatJS's own default, so you usually don't touch it.

API

next-react-intl/server

  • createI18n(config){ getIntl, getProviderProps, resolveLocale, I18nProvider, useIntl, FormattedMessage }. One-stop wiring from a single config.
  • createGetIntl(config)() => Promise<IntlShape>. Just the RSC getIntl if you don't want the rest.
  • cookieLocaleResolver({ cookieName, supportedLocales, defaultLocale }) → a resolveLocale backed by a cookie (reads next/headers). Optional — pass any resolver you like.

config (I18nConfig): sourceLocale, loadMessages, resolveLocale, optional idInterpolationPattern, optional onError (default ignores MISSING_TRANSLATION and logs the rest).

next-react-intl (client)

  • <I18nProvider locale messages [defaultLocale] [idInterpolationPattern] [onError]> — wraps react-intl's IntlProvider and publishes the id pattern via context.
  • useIntl() — like react-intl's, but formatMessage auto-injects the id.
  • <FormattedMessage defaultMessage ... /> — like react-intl's, but id is computed when absent.

next-react-intl/hash

  • messageId(descriptor, pattern?) / withId(descriptor, pattern?) — the runtime id computation, if you need it directly. DEFAULT_ID_INTERPOLATION_PATTERN.

Locale routing

This package only resolves and applies a locale; it's agnostic about where the locale comes from. Cookie-based (the cookieLocaleResolver) or a custom resolver reading a URL segment / header both work — keep the locale in one place so you don't end up with two diverging stores.

Scope (v0)

Runtime glue only. It does not wrap @formatjs/cli in a custom binary — you call extract/compile yourself (snippets above). A compatibility checker for the SWC plugin and a CLI wrapper are possible future additions.

License

MIT