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

react-translate-kit

v0.1.0

Published

Drop-in translation plugin for React

Downloads

7

Readme

react-translate-kit

A drop-in translation plugin for React, Next.js, and anything React-based. Define your translator once in a module, import it anywhere — no provider wrapping required — and get a t() hook plus a customizable <LanguageToggle> component.

Installation

npm install react-translate-kit

React 17 or newer is required.

How translation works

The primary way to translate content is through a translation provider — a connection to a real translation API (DeepL, Google, MyMemory, LibreTranslate, or your own backend). Point createTranslator at a provider and your copy is translated automatically as languages change, with no need to hand-author every string in every language up front.

For parts of the app where you want exact, hand-written wording per language — a hero headline, legal copy, brand voice — you can customize the output by authoring an i18n key-based dictionary. Keys you define in translations always win over the provider; the provider only fills in what you haven't written yourself:

  • dot-notation for nested keys ("nav.home")
  • {placeholder} interpolation (t("greeting", { name: "Rohan" }))
  • automatic fallback to fallbackLanguage when a key is missing

Switching language changes which dictionary is read and re-renders subscribed components. Combine the two freely: let the provider handle the bulk of your UI copy, and override specific keys wherever precise wording matters.

Translation providers (the primary way to translate)

Configure a translate function and the translator calls a translation API for any key that isn't explicitly authored in the active language's dictionary:

import { createTranslator, myMemoryProvider } from "react-translate-kit";

export const i18n = createTranslator({
  translations,
  defaultLanguage: "en",
  autoTranslate: { translate: myMemoryProvider() },
});

How it behaves:

  • t() stays synchronous: it returns the fallback-language text immediately, fires the API request in the background, and re-renders subscribed components with the translated text when it arrives.
  • Results are cached in memory and localStorage (rtk-mt:<lang>:<key>:<hash of the source text>), so each cache key and source text is normally translated only once per browser. Set autoTranslate.cachePrefix: false for memory-only caching.
  • Editing your copy invalidates its cache. The key includes a fingerprint of the source text, so shipping a reworded string re-translates it instead of serving the translation of the wording you deleted. For keyed t() translations, the superseded entry is removed when the new one is written.
  • Duplicate in-flight requests are deduped; API failures log a warning and leave the fallback text in place.
  • Never runs during SSR — the client picks it up after hydration.

Built-in providers (all proper translation APIs)

| Provider | Service | Key needed | | --- | --- | --- | | myMemoryProvider() | MyMemory (translated.net) | No — free; pass { email } to raise the quota | | deeplProvider({ apiKey }) | DeepL | Yes | | googleProvider({ apiKey }) | Google Cloud Translation v2 | Yes | | libreTranslateProvider({ url }) | LibreTranslate (self-hosted/open-source) | Instance-dependent |

import { deeplProvider } from "react-translate-kit";

createTranslator({
  translations,
  autoTranslate: {
    // Your backend route adds the DeepL API key before forwarding the request.
    translate: deeplProvider({ endpoint: "/api/translate" }),
  },
});

Security note: never ship a paid API key to the browser. The direct form below is for server-only code. In browser code, use an endpoint override that points to a backend route which adds the key server-side, or write your own TranslateFn:

// Server-only code
deeplProvider({ apiKey: process.env.DEEPL_KEY! });
import type { TranslateFn } from "react-translate-kit";

const viaMyBackend: TranslateFn = async (text, from, to) => {
  const res = await fetch("/api/translate", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text, from, to }), // your server calls DeepL/Google
  });
  return (await res.json()).translation;
};

createTranslator({ translations, autoTranslate: { translate: viaMyBackend } });

Caveat: machine translation runs on the raw template, so {placeholders} usually survive but review high-stakes copy for anything customer-facing or high-stakes — that's what the key-based dictionary below is for.

Zero-integration mode: <TranslateDOM>

Wrap your app once and every string inside it is translated — no t() calls, no keys, no changes to existing components:

<TranslateDOM> can use dictionary translations by itself, but it needs an autoTranslate provider to translate text that is not in the dictionary.

import { TranslateDOM } from "react-translate-kit";
import "./i18n";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <TranslateDOM>
      <App />
    </TranslateDOM>
  </StrictMode>
);

<h1>Welcome</h1> written as plain JSX becomes <h1>Bienvenido</h1> when the language changes. Nothing else in your codebase has to know i18n exists.

Your dictionary still wins. <TranslateDOM> resolves the same way t() does — customization first, API second. When a string it finds in the DOM matches a value you authored in the source language, it traces it back to that key and uses your translation for the active language, without calling the API at all:

translations: {
  en: { hero: { title: "Welcome" } },
  fr: { hero: { title: "Bienvenue" } },
}
<h1>Welcome</h1>   // renders "Bienvenue" — your wording, no request

So you can wrap everything in plain JSX and override any string later by adding a dictionary entry, without touching the component. Strings nobody authored fall through to the provider as usual. When two keys share the same source text, the first wins, so overriding it covers every occurrence.

The exception is interpolation: an entry reading "You have {count} messages" can never match a rendered "You have 3 messages", and DOM translation would request every value separately. Keep anything with a placeholder on t(), where the template itself is translated once.

Why it works at the DOM layer. React gives a parent no access to a child's rendered output — a wrapper receives <App /> as an unrendered element, so it can't see the "Welcome" inside some component's <h1>. <TranslateDOM> therefore runs one level lower: a MutationObserver over its subtree, walking real text nodes and rewriting them in place. It re-sweeps on every re-render, route change, language switch, and incoming API result, and it remembers each node's source text separately from what it wrote, so switching back and forth never round-trips machine translation through itself.

What it skips automatically: <script>, <style>, <noscript>, <code>, <pre>, <kbd>, <samp>, <var>, <textarea>, <input>, <select>, <option>, <svg>, plus anything marked data-no-translate, translate="no", or .notranslate. Strings without letters (numbers, punctuation) never hit the API. <LanguageToggle> opts itself out, so language names stay in their own language.

<p data-no-translate>Stays in the source language.</p>

<TranslateDOM
  skipTags={["FIGCAPTION"]}
  skipSelector=".user-generated"
  skip={(text) => text.startsWith("@")}
>
  <App />
</TranslateDOM>

Trade-offs. Translations land after a paint, so there's a brief flash of the source language; it never runs during SSR; and it translates indiscriminately until you opt regions out. For copy that must be exact, author a dictionary key and use t() — dictionary entries always beat the provider.

Customizing with the i18n key-based dictionary

Provider translation covers the general case; dictionaries are how you pin down exact wording for specific keys. Anything you author here takes precedence over the provider, so use it wherever phrasing needs to be exact.

This matters more than it sounds: machine translation picks a sense without context. "Welcome" came back from MyMemory as Accueil (French for a reception desk) — correct-ish, wrong register for a hero headline. Pin it:

createTranslator({
  translations: {
    en: { hero: { title: "Welcome" } },
    fr: { hero: { title: "Bienvenue" } }, // wins over the provider
  },
  autoTranslate: { translate: myMemoryProvider() },
});

Quick start (dictionary only, no provider)

1. Create the translator once, in a module:

// src/i18n.ts
import { createTranslator } from "react-translate-kit";

export const i18n = createTranslator({
  translations: {
    en: { nav: { home: "Home" }, greeting: "Welcome back, {name}!" },
    es: { nav: { home: "Inicio" }, greeting: "¡Bienvenido de nuevo, {name}!" },
  },
  defaultLanguage: "en",
  languageLabels: { en: "English", es: "Español" },
});

2. Use it from any page or component:

import { useTranslation, LanguageToggle } from "react-translate-kit";
import "../i18n"; // ensure the translator module is loaded

function Header() {
  const { t } = useTranslation(); // finds the global translator automatically
  return (
    <header>
      <a href="/">{t("nav.home")}</a>
      <p>{t("greeting", { name: "Rohan" })}</p>
      <LanguageToggle />
    </header>
  );
}

The first createTranslator() call becomes the app-wide default that useTranslation(), <LanguageToggle>, and <Trans> resolve to. State lives outside React (hooks subscribe via useSyncExternalStore), so every page, route, modal, or portal shares the same language with zero wrapping.

You can also use it outside React entirely — i18n.t("nav.home"), i18n.setLanguage("es"), and i18n.subscribe(cb) work in plain JS (event handlers, analytics, document.title, etc.).

Next.js

Works with both routers. The published bundle carries a "use client" banner, so importing from server components in the App Router automatically treats it as a client module.

// app/lib/i18n.ts
import { createTranslator } from "react-translate-kit";
export const i18n = createTranslator({ ... });

// app/components/Header.tsx
"use client";
import { useTranslation, LanguageToggle } from "react-translate-kit";
import "@/lib/i18n";

export function Header() {
  const { t } = useTranslation();
  return <header>{t("nav.home")} <LanguageToggle variant="select" /></header>;
}

Language state is client-side (it depends on localStorage / navigator), so use it from client components. On the server it renders defaultLanguage and hydrates to the user's stored choice on the client.

Scoping a translator

Everything resolves to the app-wide translator from createTranslator() by default. To run a second one — an embedded widget with its own languages, a preview pane, a test — pass it explicitly; every entry point takes a translator argument:

const widgetI18n = createTranslator({
  translations: widgetCopy,
  persistKey: false,          // don't fight the host app over stored language
  detectBrowserLanguage: false,
});

const { t } = useTranslation(widgetI18n);

<TranslateDOM translator={widgetI18n}><Widget /></TranslateDOM>
<LanguageToggle translator={widgetI18n} />
<Trans i18nKey="title" translator={widgetI18n} />

Resolution order: explicit translator argument → app-wide default.

Customizing the toggle

Three built-in variants:

<LanguageToggle variant="buttons" />  {/* pill per language (default) */}
<LanguageToggle variant="switch" />   {/* one button that cycles — great for 2 languages */}
<LanguageToggle variant="select" />   {/* native dropdown — great for many languages */}

Style it:

<LanguageToggle
  className="my-toggle"
  activeStyle={{ background: "#4f46e5", color: "#fff" }}
  renderLabel={(code, isActive) => <Flag code={code} dimmed={!isActive} />}
  languages={["en", "es"]} // restrict/order which languages appear
/>

Or replace the UI entirely with a render prop:

<LanguageToggle>
  {({ language, languages, setLanguage, getLabel }) => (
    <MyDropdown
      value={language}
      options={languages.map((l) => ({ value: l, label: getLabel(l) }))}
      onChange={setLanguage}
    />
  )}
</LanguageToggle>

Config options (createTranslator)

| Option | Default | Description | | --- | --- | --- | | translations | (required) | { [lang]: { key: "text", nested: { key: "text" } } } | | defaultLanguage | first language | Used when nothing is stored/detected; also the SSR language | | fallbackLanguage | defaultLanguage | Used when a key is missing in the active language | | persistKey | "rtk-language" | localStorage key; pass false to disable persistence | | detectBrowserLanguage | true | Match navigator.languages on first visit | | languageLabels | — | Display names, e.g. { en: "English" } | | onLanguageChange | — | Callback fired on every switch | | autoTranslate | — | { translate, cachePrefix? } — API fallback for missing keys |

API surface

  • createTranslator(config) → Translator (first one becomes the app-wide default)
  • Translator: t(), translateText(), language, languages, setLanguage(), toggleLanguage(), getLabel(), subscribe(), addTranslations()
  • useTranslation(translator?) → { t, language, languages, setLanguage, toggleLanguage, getLabel }
  • <TranslateDOM> — translate a whole subtree, no t() calls required
  • <Trans i18nKey="..." /> — the JSX form of t()
  • <LanguageToggle> — three variants plus a render prop
  • setDefaultTranslator(t) / getDefaultTranslator()

Every component and hook accepts an optional translator to scope it.

Choosing between them

| | Keys? | Scope | Use for | | --- | --- | --- | --- | | <TranslateDOM> | No | Whole subtree | Static copy — the default. Zero refactor. | | t() / <Trans> | Yes | One string | Interpolation, and anything that must be exact |

Both resolve the same way — an authored dictionary entry wins, the API is only consulted for text nobody customized — and share one cache and one language state.

Reach for t() whenever a string has a {placeholder}: the template is translated once, whereas DOM translation would treat every rendered value as a new string and request each one separately.

Development

npm install && (cd demo && npm install)
npm run dev    # demo at http://localhost:5173 — two pages, no provider
npm run build  # publishable package in dist/ (ESM + CJS + types)
src/    the library      demo/    Vite demo app      dist/    build output