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-de locale
  • The hreflang links (if SITE_URL is set) include de-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):

  1. index.{requested_locale}.ree - exact match for the current locale
  2. index.{default_locale}.ree - fallback to the default locale
  3. index.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.ree and add de-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.ree variant, and a variant template still pulls its strings from de-de.json.