Repository navigation
Internationalization
The site is built for seven languages from day one. There is no i18n library: a small middleware picks the language, and each page reads its text from a JSON dictionary.

| Code | Name in the switcher | Dictionary |
|---|---|---|
en |
English (default) | src/i18n/dictionaries/en.json |
tr |
Türkçe | src/i18n/dictionaries/tr.json |
es |
Español | src/i18n/dictionaries/es.json |
pt |
Português | src/i18n/dictionaries/pt.json |
fr |
Français | src/i18n/dictionaries/fr.json |
de |
Deutsch | src/i18n/dictionaries/de.json |
ru |
Русский | src/i18n/dictionaries/ru.json |
The list is defined in src/i18n/config.js:
export const locales = ["en", "tr", "es", "pt", "fr", "de", "ru"];
export const defaultLocale = "en";src/middleware.js runs on every request except paths that start with _next, api, img, favicon.ico or manifest.json.
- If the path already starts with a supported language (
/tror/tr/...), nothing happens. - Otherwise the middleware picks a language:
- the
localecookie, if its value is inlocales; - else the
Accept-Languageheader of the browser: each entry is cut to its first two letters and lower-cased, and the first one found inlocaleswins; - else
defaultLocale(en).
- the
- It redirects to the same path with the language in front:
/→/tr,/premium→/tr/premium.
-
src/app/[locale]/layout.jsxand every page callgetDictionary(locale)fromsrc/i18n/getDictionary.js. The only exception is the 404 page (not-found.jsx), a client component that imports the JSON files directly. -
getDictionaryloads the matching JSON file with a dynamicimport(). An unknown code falls back todefaultLocale. -
layout.jsxexportsgenerateStaticParams()returning one entry per locale, so every page is pre-rendered for every language at build time. - The layout sets
<html lang="...">and addshreflangalternates for all languages to the metadata.
src/components/LanguageSwitcher.jsx, shown in the header (inside the menu on small screens):
- lists every code from
localeswith itslocaleNameslabel; - on selection, writes
locale=<code>; path=/; max-age=31536000(one year), so the middleware remembers the choice; - replaces the first URL segment and navigates, keeping the rest of the path (
/en/premium→/fr/premium).
This example adds Italian (it). Five files change.
1. Create the dictionary. Copy src/i18n/dictionaries/en.json to src/i18n/dictionaries/it.json and translate every value. Do not rename or remove keys.
2. Register the code and name in src/i18n/config.js:
export const locales = ["en", "tr", "es", "pt", "fr", "de", "ru", "it"];
export const localeNames = {
// ...existing entries
it: "Italiano"
};3. Register the loader in src/i18n/getDictionary.js:
const dictionaries = {
// ...existing entries
it: () => import("./dictionaries/it.json").then((m) => m.default)
};4. Register it in the 404 page. src/app/[locale]/not-found.jsx imports every dictionary directly (the README's list of steps does not mention this file):
import it from "../../i18n/dictionaries/it.json";
const dictionaries = { en, tr, es, pt, fr, de, ru, it };If you skip this step, the not-found page throws an error for the new language, because dictionaries[locale] is undefined and the code reads dict.notFound.
5. Build and check.
npm run build
npm run startOpen /it, /it/commands, /it/premium, /it/partners, /it/tos and /it/privacy. The middleware and the switcher pick up the new code automatically from locales.
Note: Use a two-letter code. The middleware compares only the first two letters of each browser language, so a code such as
pt-BRwould never be chosen automatically.
Note: All seven current languages are written left to right, and the layout does not set a
dirattribute. A right-to-left language would needdir="rtl"on the<html>element inlayout.jsxand a review of the CSS.
Do the same steps in reverse: remove the code from locales and localeNames, the loader from getDictionary.js, the import and map entry from not-found.jsx, and delete the JSON file. Visitors whose cookie still holds the removed code fall back to their browser language or en, because the middleware ignores cookie values that are not in locales.
Set defaultLocale in src/i18n/config.js to another code from locales. It is used by the middleware fallback, getDictionary() and the 404 page.
All seven dictionaries currently have exactly the same keys. When you add text, add it to every file in the same place.
This optional helper is not part of the repository. Save it as check-dictionaries.js in the project root and run node check-dictionaries.js to list missing or extra keys compared to en.json:
const fs = require("fs");
const path = require("path");
const dir = "src/i18n/dictionaries";
const keys = (obj, prefix = "") =>
Object.entries(obj).flatMap(([k, v]) => {
const p = prefix ? `${prefix}.${k}` : k;
return v && typeof v === "object" ? [p, ...keys(v, p)] : [p];
});
const base = new Set(keys(JSON.parse(fs.readFileSync(path.join(dir, "en.json"), "utf8"))));
for (const file of fs.readdirSync(dir).filter((f) => f.endsWith(".json"))) {
const k = new Set(keys(JSON.parse(fs.readFileSync(path.join(dir, file), "utf8"))));
const missing = [...base].filter((x) => !k.has(x));
const extra = [...k].filter((x) => !base.has(x));
console.log(file, missing.length || extra.length ? { missing, extra } : "OK");
}- Keep placeholders such as
{name}infooter.creditexactly as they are. - Command names on the commands page and the partner cards are not in the dictionaries, so they stay the same in every language.
- Some keys exist in the files but are not displayed yet. The list is in Customization.
Beatra Website · MIT License · Made by Umut Bayraktar · Codeshare
Start here
The site
Run it
Community