Back to all docs

BeMyWords with Next.js

Integration guide for Next.js applications, covering both the App Router and Pages Router. Uses next-intl for the App Router path (recommended for new apps) and next-i18next for Pages Router.

Shape of the integration:

  1. At build time, fetch translations from BeMyWords into JSON resource files.
  2. Configure the i18n library to load those files server-side (App Router) or pre-generate pages (Pages Router).
  3. Use the library's hooks / server components to render translations.

No runtime BeMyWords dependency — translations are baked in at build time.


Prerequisites

  • A Next.js app (14+ recommended for App Router).
  • A BeMyWords workspace, project, namespace, API key.

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

⚠ Do not prefix BEMYWORDS_API_TOKEN with NEXT_PUBLIC_ — the token must stay server-side.

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, "..", "messages");

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";

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() {
  mkdirSync(OUTPUT_DIR, { recursive: true });

  if (!PROJECT_ID || !API_TOKEN) {
    console.warn("BEMYWORDS_PROJECT_ID or BEMYWORDS_API_TOKEN not set.");
    for (const lang of LANGUAGES) {
      writeFileSync(join(OUTPUT_DIR, `${lang}.json`), "{}\n");
    }
    return;
  }

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

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

Add messages/ to .gitignore if you regenerate every build.

Install:

npm install next-intl

next.config.js:

const createNextIntlPlugin = require("next-intl/plugin");
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");

module.exports = withNextIntl({
  // your existing Next.js config
});

src/i18n/request.ts:

import { getRequestConfig } from "next-intl/server";

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`../../messages/${locale}.json`)).default,
}));

src/app/[locale]/layout.tsx:

import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";

export default async function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Use in a server component:

import { useTranslations } from "next-intl";

export default function Home() {
  const t = useTranslations();
  return <h1>{t("home.hero.title")}</h1>;
}

Use in a client component:

"use client";
import { useTranslations } from "next-intl";

export default function InteractiveBit() {
  const t = useTranslations();
  return <button>{t("common.actions.save")}</button>;
}

BeMyWords returns flat dot-keys ("home.hero.title": "..."). next-intl accepts either flat or nested; no conversion needed.

4. Pages Router setup

Install:

npm install next-i18next react-i18next i18next

next-i18next.config.js:

module.exports = {
  i18n: {
    defaultLocale: "en",
    locales: ["en", "nb"],
  },
  localePath: "./public/locales",
};

Adjust the fetch script to write to public/locales/<lang>/<namespace>.json instead of messages/<lang>.json.

In pages/_app.tsx:

import { appWithTranslation } from "next-i18next";
export default appWithTranslation(MyApp);

In any page:

import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

export async function getStaticProps({ locale }) {
  return {
    props: { ...(await serverSideTranslations(locale, ["app"])) },
  };
}

export default function Home() {
  const { t } = useTranslation();
  return <h1>{t("home.hero.title", "Localization for solo devs and small teams")}</h1>;
}

5. Hook into build

package.json:

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

6. (Optional) Push English to BeMyWords

Same as React + i18next.

7. Vercel / deployment platforms

Set the four BEMYWORDS_* env vars in your deployment provider's project settings. They need to exist at build time only; they're not accessed at runtime.

For Vercel: Project Settings → Environment Variables. Scope to Production + Preview + Development.

Pluralization

next-intl uses ICU message format:

{ "apples": "{count, plural, =0 {no apples} one {1 apple} other {# apples}}" }

t("apples", { count: 3 }) returns "3 apples".

Store these ICU strings verbatim in BeMyWords. Enable ICU placeholder validation in project settings.

next-i18next / i18next uses the suffix convention: apples_one, apples_other.


API reference

See Astro integration guide.

Troubleshooting

App Router: "Unable to find next-intl locale": check that the [locale] dynamic segment in your folder structure matches the locale values returned by i18n config.

Pages Router: translations missing on client-side navigation: ensure you included the namespace in serverSideTranslations() for every page.

Large JSON bundle on client: for App Router, server components render translations server-side — the JSON doesn't ship to the client unless you put it in a client component. Prefer server components where possible.