Back to all docs

BeMyWords with React + i18next

Integration guide for React applications using i18next / react-i18next.

Shape of the integration:

  1. At build time, fetch translations from BeMyWords into JSON resource files.
  2. Configure i18next to load those resource files at app startup.
  3. Use useTranslation() / t() as normal.

No runtime dependency on BeMyWords.


Prerequisites

  • A React app (Create React App, Vite, etc.) with i18next + react-i18next installed, or ready to install.
  • A BeMyWords workspace, project, and namespace.
  • A BeMyWords API key.
npm install i18next react-i18next i18next-browser-languagedetector

1. Environment variables

.env.local:

BEMYWORDS_BASE_URL=https://www.bemywords.no
BEMYWORDS_PROJECT_ID=<your-project-uuid>
BEMYWORDS_API_TOKEN=<your-api-token>
BEMYWORDS_NAMESPACE=app

The BEMYWORDS_API_TOKEN is used only at build time, not shipped to the browser. Keep it out of NEXT_PUBLIC_* / VITE_* / any client-exposed prefix.

2. Fetch script

scripts/fetch-translations.mjs:

import { writeFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = join(__dirname, "..", "public", "locales");

const BASE_URL = process.env.BEMYWORDS_BASE_URL || "https://www.bemywords.no";
const PROJECT_ID = process.env.BEMYWORDS_PROJECT_ID;
const API_TOKEN = process.env.BEMYWORDS_API_TOKEN;
const NAMESPACE = process.env.BEMYWORDS_NAMESPACE || "app";

// Languages your app supports
const LANGUAGES = ["en", "nb"];

async function fetchLanguage(lang) {
  const url = `${BASE_URL}/api/${PROJECT_ID}/latest/${lang}/${NAMESPACE}`;
  const res = await fetch(url, {
    headers: { Authorization: `Token token=${API_TOKEN}` },
  });
  if (!res.ok) throw new Error(`${lang}: HTTP ${res.status}`);
  return res.json();
}

async function main() {
  if (!PROJECT_ID || !API_TOKEN) {
    console.warn("BEMYWORDS_PROJECT_ID or BEMYWORDS_API_TOKEN not set.");
    return;
  }

  for (const lang of LANGUAGES) {
    const data = await fetchLanguage(lang);
    const langDir = join(OUTPUT_DIR, lang);
    mkdirSync(langDir, { recursive: true });
    writeFileSync(
      join(langDir, `${NAMESPACE}.json`),
      JSON.stringify(data, null, 2) + "\n"
    );
    console.log(`  [${lang}] ${Object.keys(data).length} keys`);
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

i18next's HTTP backend expects files at /locales/<lang>/<namespace>.json — we write them there directly.

3. Configure i18next

src/i18n.ts:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(HttpBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    supportedLngs: ["en", "nb"],
    defaultNS: "app",
    ns: ["app"],
    backend: {
      loadPath: "/locales/{{lng}}/{{ns}}.json",
    },
    interpolation: { escapeValue: false },
  });

export default i18n;

Import this in your app entry point (main.tsx / index.tsx):

import "./i18n";

4. Use in components

import { useTranslation } from "react-i18next";

export function HeroSection() {
  const { t } = useTranslation();

  return (
    <>
      <h1>{t("home.hero.title", "Localization for solo devs and small teams")}</h1>
      <p>{t("home.hero.subtitle", "Cheap, simple, developer-first.")}</p>
    </>
  );
}

The second argument to t() is the default value — shown if the key is missing in the current language. Keep this as your English master.

5. Hook into build

package.json:

{
  "scripts": {
    "predev": "node scripts/fetch-translations.mjs",
    "prebuild": "node scripts/fetch-translations.mjs",
    "dev": "vite",
    "build": "vite build"
  }
}

The pre- hooks run automatically before dev/build.

6. (Optional) Push English to BeMyWords

Extract your t() calls' default values or maintain an en.json manually, then:

const url = `${BASE_URL}/api/overwrite/${PROJECT_ID}/latest/en/${NAMESPACE}`;
await fetch(url, {
  method: "PUT",
  headers: {
    Authorization: `Token token=${API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ translations: englishMaster }),
});

Extraction tools like i18next-parser can produce en.json from your source code automatically.

Pluralization and interpolation

i18next supports CLDR plurals out of the box. Key names for plurals follow i18next conventions:

{
  "apples_one":   "1 apple",
  "apples_other": "{{count}} apples"
}

Use t("apples", { count: 3 }) — i18next picks the right plural form.

Interpolation uses {{name}}:

{ "greeting": "Hello, {{name}}!" }

Store these strings as-is in BeMyWords. BeMyWords preserves placeholders verbatim. Use placeholder validation in BeMyWords project settings to catch broken translations.


API reference

See Astro integration guide — the API is framework-agnostic.

Troubleshooting

Translations show as raw keys (home.hero.title) in the browser: i18next couldn't load the JSON. Check DevTools Network tab for /locales/en/app.json — it should return 200. Re-run the fetch script if needed.

NEXT_PUBLIC_BEMYWORDS_* exposes your API token: don't use NEXT_PUBLIC_ / VITE_ prefix for BEMYWORDS_API_TOKEN. The fetch script runs in Node at build time and has access to non-public env vars.

Pluralization doesn't kick in: i18next v21+ uses suffix _one, _other, _few, _many, _zero. Older versions used _plural. Check your i18next version and plural suffix convention.