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:
- At build time, fetch translations from BeMyWords into JSON resource files.
- Configure the i18n library to load those files server-side (App Router) or pre-generate pages (Pages Router).
- 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.
3. App Router setup (recommended)
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
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.