Adding a Locale
ReeWeb ships with English keys and texts. This recipe walks through adding German (de-de) as a second locale, end-to-end.
Step 1: Configure the Locale
Add the new locale to config/supported_locales.ts. A fresh ReeWeb project starts English-only (locales = ["en-us"],
default_locale = "en-us"). Adding German to it gives:
export const locales = ["en-us", "de-de"] as const;
export const active_locales = ["en-us", "de-de"] as const;
// Locales built but kept out of the sitemap, feeds, hreflang and picker.
export const soft_launch_locales: string[] = [];
export const default_locale = "en-us";
export const locale_names: Record<string, string> = {
"en-us": "English",
"de-de": "German",
};
Leave default_locale alone unless you want German served at the site root -
the default locale is the one with no /{locale}/ prefix, and changing it
re-homes every existing URL.
If German should appear in the locale picker, add it to active_locales. If you're building the site in German but don't want it visible yet, add it only to locales - it will render but won't appear in the picker.
Step 2: Add Translation Files
Create a de-de.json in the same directories as your existing translation files. Start with the global file at src/public/de-de.json:
{
"site_name": "Meine Seite",
"nav": {
"home": "Startseite",
"about": "Über uns",
"blog": "Blog",
"contact": "Kontakt"
}
}
Match the key layout of the existing src/public/en-us.json. In particular
site_name is a top-level key, because the layout renders it as
props.site_name; nesting it under ui leaves the German pages showing the
English site name.
Because of the cross-locale fallback in lib/i18n.ts, you only need to provide the keys that differ from other locales. Any key you leave out will inherit from whatever locale has it defined.
Step 3: Add Localized Route Names (Optional)
To translate URL paths for German, route_name is a top-level key in the route directory's own de-de.json - one file
per localized route, not a single nested block in the root file:
// src/public/about/de-de.json
{
"route_name": "ueber-uns"
}
// src/public/contact/de-de.json
{
"route_name": "kontakt"
}
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). Letters NFKD cannot decompose are mapped explicitly, because otherwise they would be dropped rather than simplified: ß→ss, æ→ae, œ→oe, ø→o, ł→l, đ/ð→d, þ→th, ħ→h, ı→i, ŋ→n, ŧ→t, ƶ→z. Anything still outside [a-z0-9_] collapses to a hyphen.
Step 4: Rebuild
bun ssg
The SSG script automatically discovers de-de.json files and renders every page in German. The German version is served at /de-de/:
/de-de/ → German homepage
/de-de/ueber-uns/ → German about page
/de-de/kontakt/ → German contact page
/de-de/blog/ → German blog index
Step 5: Verify
Check that:
- The locale picker shows "German"
- Navigation labels display in German
- All pages are reachable at
/de-de/...paths - Dates format correctly for the
de-delocale - The
hreflanglinks (ifSITE_URLis set) includede-DE(conventional BCP 47 casing - hreflang is presentation output)
Two Ways to Vary a Page by Locale
Once the alternative locale is configured, a given page can differ per locale in one of two ways. Pick per-page, not project-wide.
Option A: Single Template + {locale}.json (default, preferred)
Keep one index.ree and let per-locale JSON files supply the text:
about/
├── en-us.json
├── sl-si.json
├── de-de.json
└── index.ree
<h1>{_ about.title }</h1>
<p>{_ about.intro }</p>
Add the German strings to about/de-de.json:
{
"about": {
"title": "Über uns",
"intro": "Wir sind ein kleines Team..."
}
}
Use this when the layout and structure are the same across locales 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 locale 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: Locale-Variant Template (index.{locale}.ree)
If a page needs different markup in German - not just different text - create a locale-suffixed template file instead (lowercase locale segment):
about/
├── index.de-de.ree ← German-specific markup
├── index.ree ← fallback for other locales
├── en-us.json
├── de-de.json
└── sl-si.json
The resolution chain checked on every template load (pages, layouts, includes, components):
index.{requested_locale}.ree- exact match for the current localeindex.{default_locale}.ree- fallback to the default localeindex.ree- generic fallback
Each variant still reads from {locale}.json for its text via {_ path } tags - the two options aren't mutually exclusive. Use a variant template only for the locales that actually need different markup; every other locale keeps falling back to index.ree.
Pros: full control over markup per locale. 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-de.json. - Reach for Option B only when German (or any locale) 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-de.reevariant, and a variant template still pulls its strings fromde-de.json.