Skip to content

Internationalization

umutxyp edited this page Oct 17, 2026 · 1 revision

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.

How a request becomes a page


Supported languages

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

How the language is chosen

src/middleware.js runs on every request except paths that start with _next, api, img, favicon.ico or manifest.json.

  1. If the path already starts with a supported language (/tr or /tr/...), nothing happens.
  2. Otherwise the middleware picks a language:
    1. the locale cookie, if its value is in locales;
    2. else the Accept-Language header of the browser: each entry is cut to its first two letters and lower-cased, and the first one found in locales wins;
    3. else defaultLocale (en).
  3. It redirects to the same path with the language in front: / → /tr, /premium → /tr/premium.

How a page gets its text

  • src/app/[locale]/layout.jsx and every page call getDictionary(locale) from src/i18n/getDictionary.js. The only exception is the 404 page (not-found.jsx), a client component that imports the JSON files directly.
  • getDictionary loads the matching JSON file with a dynamic import(). An unknown code falls back to defaultLocale.
  • layout.jsx exports generateStaticParams() returning one entry per locale, so every page is pre-rendered for every language at build time.
  • The layout sets <html lang="..."> and adds hreflang alternates for all languages to the metadata.

The language switcher

src/components/LanguageSwitcher.jsx, shown in the header (inside the menu on small screens):

  • lists every code from locales with its localeNames label;
  • 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).

Adding a language

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 start

Open /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-BR would never be chosen automatically.

Note: All seven current languages are written left to right, and the layout does not set a dir attribute. A right-to-left language would need dir="rtl" on the <html> element in layout.jsx and a review of the CSS.


Removing a language

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.

Changing the default language

Set defaultLocale in src/i18n/config.js to another code from locales. It is used by the middleware fallback, getDictionary() and the 404 page.


Keeping translations in sync

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

Translation tips

  • Keep placeholders such as {name} in footer.credit exactly 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.

Clone this wiki locally