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

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):

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