Adding a Language
Introduction
ReeWeb ships with English keys and texts. This recipe walks through adding German (de) as a third language, end-to-end.
Step 1: Configure the Language
Add the new language to config/supported_languages.ts:
export const languages = ["en", "sl", "de"] as const;
export const active_languages = ["sl", "en", "de"] as const;
export const default_language = "sl";
export const language_names: Record<string, string> = {
en: "English",
sl: "Slovenian",
de: "German",
};
export const language_locales: Record<string, string> = {
en: "en-US",
sl: "sl-SI",
de: "de-DE",
};
If German should appear in the language switcher, add it to active_languages. If you're building the site in German but don't want it visible yet, add it only to languages - it will render but won't appear in the switcher.
Step 2: Add Translation Files
Create a de.json in the same directories as your existing translation files. Start with the global file at src/public/de.json:
{
"ui": {
"site_name": "Meine Seite",
"language_names": {
"en": "Englisch",
"sl": "Slowenisch",
"de": "Deutsch"
}
},
"nav": {
"home": "Startseite",
"about": "Über uns",
"blog": "Blog",
"contact": "Kontakt"
}
}
Because of the cross-language fallback in lib/i18n.ts, you only need to provide the keys that differ from other languages. Any key you leave out will inherit from whatever language has it defined.
Step 3: Add Localized Route Names (Optional)
To translate URL paths for German:
{
"about": { "route_name": "ueber-uns" },
"contact": { "route_name": "kontakt" },
"blog": { "route_name": "blog" }
}
The slugify() function in lib/route_aliases.ts handles the transliteration - it uses NFKD normalization to decompose characters and then strips combining diacritics, so "ü" becomes "u" (the diaeresis is stripped). Explicit exceptions apply: "ß" becomes "ss", "æ" becomes "ae", "œ" becomes "oe".
Step 4: Rebuild
bun ssg
The SSG script automatically discovers de.json files and renders every page in German. The German version is served at /de/:
/de/ → German homepage
/de/ueber-uns/ → German about page
/de/kontakt/ → German contact page
/de/blog/ → German blog index
Step 5: Verify
Check that:
- The language switcher shows "German"
- Navigation labels display in German
- All pages are reachable at
/de/...paths - Dates format correctly for the
de-DElocale - The
hreflanglinks (ifSITE_URLis set) includede
Two Ways to Vary a Page by Language
Once alternative language is configured, a given page can differ per language in one of two ways. Pick per-page, not project-wide.
Option A: Single Template + {lang}.json (default, preferred)
Keep one index.ree and let per-language JSON files supply the text:
about/
├── en.json
├── sl.json
├── de.json
└── index.ree
<h1>{_ about.title }</h1>
<p>{_ about.intro }</p>
Add the German strings to about/de.json:
{
"about": {
"title": "Über uns",
"intro": "Wir sind ein kleines Team..."
}
}
Use this when the layout and structure are the same across languages and only the copy changes - which is true for the vast majority of pages. See Translations for key organization, fallback behavior, and the {_ } / {- } / {@ } lookup tags.
Pros: one template to maintain, changes to markup apply to every language automatically, missing keys fall back instead of breaking the build. Cons: doesn't work if German needs different markup, not just different text (e.g. a different image layout, an extra section, reordered content).
Option B: Language-Variant Template (index.{lang}.ree)
If a page needs different markup in German - not just different text - create a language-specific template file instead:
about/
├── index.de.ree ← German-specific markup
├── index.ree ← fallback for other languages
├── en.json
├── de.json
└── sl.json
The resolution chain checked on every template load (pages, layouts, includes, components):
index.{requested_lang}.ree- exact match for the current languageindex.{default_language}.ree- fallback to the default languageindex.ree- generic fallback
Each variant still reads from {lang}.json for its text via {_ path } tags - the two options aren't mutually exclusive. Use a variant template only for the languages that actually need different markup; every other language keeps falling back to index.ree.
Pros: full control over markup per language. Cons: duplicated structure to keep in sync across variants; a markup change usually needs to be applied to every variant file by hand.
Which One to Use
- Default to Option A. Start with a single
index.reeand addde.json. - Reach for Option B only when German (or any language) genuinely needs different HTML structure - not as a first move for "the text is longer" or "word order differs," which
{_ }tags handle fine. - The two compose: a project can have some pages on Option A and others with an
index.de.reevariant, and a variant template still pulls its strings fromde.json.