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-translation-bridge

v1.0.0

Published

Capture browser-translated text (Google Translate, Edge Translate, etc.) into React state so layout-recomputing components stop infinite-looping.

Readme

react-translation-bridge

Capture browser-translated text (Google Translate, Edge Translate, Safari/Firefox built-ins) into React state, so layout-recomputing components like react-truncate-markup, react-virtualized, and chart libraries don't infinite-loop.

npm license

The bug

Some components re-measure the DOM after every render: react-truncate-markup, react-virtualized, react-textfit, several charting libs. They assume the DOM still matches what React rendered. Google Translate breaks that assumption. It mutates text nodes in place, which triggers a re-measure, which causes the component to re-render the original text, which Google Translate mutates again. The page hangs.

See react-truncate-markup#65 and facebook/react#11538 for the long-running discussions.

// Hangs when Google Translate is on
<TruncateMarkup lines={2}>
  <div>{longText}</div>
</TruncateMarkup>

Why existing solutions don't fix it

| Approach | Problem | |---|---| | Outgoing translators (react-google-translate, translate-mutation-observer) | Wrong direction. They translate from React, not into it. | | Crash-prevention monkey-patches and lint rules | Stops the loop, but leaves the text untranslated. | | <html lang> boolean detection | Tells you whether translation is on, not what each string became. | | class="notranslate" everywhere | Disables translation entirely, which defeats the point. |

This package takes a different angle. It wraps the protected subtree in notranslate so the browser leaves it alone. Then it renders invisible decoy spans outside that subtree, watches them with MutationObserver, and feeds the translated text back into your component as React state.

Install

npm install react-translation-bridge

React 16.8+ is required (peer dependency).

Quickstart

import { TranslationBridge, useTranslationBridge } from "react-translation-bridge";

const Greeting = () => {
  const { hello } = useTranslationBridge();
  return <p>{hello}</p>;
};

export const App = () => (
  <TranslationBridge labels={{ hello: "Hello, world!" }}>
    <Greeting />
  </TranslationBridge>
);

Turn on Google Translate. hello becomes "Ahoj, světe!" (or whatever language you picked), <Greeting> re-renders normally, no infinite loop.

For typed access to your labels, pass a type argument to the hook:

type AppLabels = { hello: string };

const Greeting = () => {
  const { hello } = useTranslationBridge<AppLabels>();
  return <p>{hello}</p>;
};

API

<TranslationBridge>

interface TranslationBridgeProps {
  labels: Record<string, string>;
  children: React.ReactNode;
}

Pass your strings to labels as { key: originalText }. The keys are arbitrary; the values are the source strings you want translated. children is rendered inside a notranslate wrapper so the browser translator skips it, and one invisible decoy <span> per label sits outside that wrapper where the translator can pick it up.

useTranslationBridge()

const labels: Record<string, string> = useTranslationBridge();
// or, with a typed shape:
const labels = useTranslationBridge<{ hello: string }>();

Returns the current translated values, keyed by the same keys you passed to labels. Starts equal to the input and updates as the browser translator mutates the decoys.

The optional generic is a type assertion at the call site. TypeScript can't cross-check it against the <TranslationBridge labels={...}> prop because the link runs through Context, so make sure the shape matches what you pass in.

Example: react-truncate-markup

import TruncateMarkup from "react-truncate-markup";
import {
  TranslationBridge,
  useTranslationBridge,
} from "react-translation-bridge";

const TruncatedBody = () => {
  const { body } = useTranslationBridge();
  return (
    <TruncateMarkup lines={3}>
      <div>{body}</div>
    </TruncateMarkup>
  );
};

export const Article = ({ body }: { body: string }) => (
  <TranslationBridge labels={{ body }}>
    <TruncatedBody />
  </TranslationBridge>
);

The same shape works for react-virtualized row renderers and chart axis labels.

How it works

┌─────────────────────────────────────────────────────────────┐
│  <TranslationBridge labels={{ hello: "Hello" }}>            │
│                                                             │
│    ┌──────────────────────────┐                             │
│    │ aria-hidden decoy region │   ← browser translates THIS │
│    │   <span>Hello</span>     │     (visible to translator, │
│    │                          │      invisible to user)     │
│    └──────────────────────────┘                             │
│                │ MutationObserver                           │
│                ▼                                            │
│    React state: { hello: "Ahoj" }                           │
│                │ Context                                    │
│                ▼                                            │
│    ┌──────────────────────────┐                             │
│    │ <div class="notranslate" │   ← browser SKIPS this      │
│    │      translate="no">     │     (React owns it)         │
│    │   children               │                             │
│    └──────────────────────────┘                             │
│                                                             │
│  </TranslationBridge>                                       │
└─────────────────────────────────────────────────────────────┘

Browser support and caveats

Chrome Translate, Edge Translate, Safari Translate (iOS and macOS), and Firefox full-page translation all work. When the user turns translation off, browsers usually restore the original text on the decoys, so state reverts on its own.

The library is SSR-safe. The MutationObserver setup runs only in useEffect, and the effect short-circuits when the constructor is undefined.

A decoy is one node per labels entry, so keep labels at the granularity you actually need. Splitting too finely (one decoy per word, say) defeats the translator's sentence-level context. When the content of the labels prop changes, the bridge briefly returns the new original values until the translator re-mutates the new decoys; most translators catch up within a few hundred milliseconds. Inline labels={{...}} objects are fine because the component compares structurally, not by reference.

License

MIT. See LICENSE.