Translations

Introduction

ReeWeb loads translations from {lang}.json files placed next to your templates. Every page has access to the merged translations for its language - both the top-level global strings and the page-specific overrides. Missing keys inherit from other languages automatically via a cross-language fallback, so you never see a blank label because a translation file was incomplete.

Translation File Layout

Translation files are JSON files named after a language code. They live alongside the templates they translate:

src/public/
├── en.json                    ← English, global scope
├── sl.json                    ← Slovenian, global scope
├── index.ree
├── about/
│   ├── en.json                ← English, /about/ scope
│   ├── sl.json                ← Slovenian, /about/ scope
│   ├── index.ree
│   └── ...
├── blog/
│   ├── en.json                ← English, /blog/ scope
│   └── ...
└── contact/
    ├── en.json
    └── index.ree

The loader (in lib/i18n.ts) walks the entire directory tree, discovers every {lang}.json file, and builds a nested structure per language keyed by directory path.

Key Organization

Keys are namespaced by the directory structure. A global en.json at the root looks like:

{
    "ui": {
        "site_name": "My Site",
        "language_names": {
            "en": "English",
            "sl": "Slovenian"
        }
    },
    "nav": {
        "home": "Home",
        "about": "About",
        "blog": "Blog",
        "contact": "Contact"
    }
}

Keys are read in templates with the translation-lookup tags - {_ ui.site_name }, {_ nav.home }, and so on (see Template Access below). The loader merges each directory's translation file on top of the global scope, so a more specific key overrides a general one.

Page-Specific Keys

A page-level translation file overrides global keys for that section. For example, src/public/blog/en.json:

{
    "nav": {
        "blog": "All posts"
    },
    "blog": {
        "title": "Blog",
        "read_more": "Read more →",
        "published_on": "Published on"
    }
}

In the blog section, {_ nav.blog } resolves to "All posts" instead of "Blog". Keys not overridden - {_ nav.home }, {_ nav.about } - continue to inherit from the global scope.

Cross-Language Fallback

When a key is missing in one language but present in another, the fill_missing() function in lib/i18n.ts copies it from an available source. The rule: all languages fill from all other languages, so a key added to any one {lang}.json file flows to the others automatically.

This means you can build a site in one language first and add translations incrementally - missing keys display the value from whatever language has them, rather than showing nothing.

Exception: The route_name key is never inherited across languages (see Localized Routes). Each language must define its own route names.

Template Access

Translations are exposed under props.translations and read with the translation-lookup tags - {_ path } (escaped), {- path } (raw) and {@ path } (rendered through markdown). path is the dotted key path within the merged namespace:

<h1>{_ ui.site_name }</h1>

<nav>
    <a href="/">{_ nav.home }</a>
    <a href="/about">{_ nav.about }</a>
</nav>

{#if props.translations.blog }
<p>{_ blog.read_more }</p>
{/if}

The tags resolve against props.translations directly - no props. prefix on the path. A key that isn't present renders as its last segment in braces ({_ nav.home }{home}) rather than a blank, so gaps are obvious on the page. Because the path is a plain dotted key (no computed access), reach for {= props.translations[key] } on the rare occasion you need a dynamic key, and use {_ } / {- } / {@ } everywhere else. See Translation Lookups for the full tag reference, including the markdown {@ } tag.

The helpers object also provides nav_label() for nav keys with a fallback, handy when the key is computed:

<a href="/about">{~ nav_label("about") }</a>

Editor Tooling

The ree Templates VS Code extension reads the {lang}.json files next to each template and wires them into the editor, so authoring translations feels like editing typed code - completion, validation, and inline previews right inside the .ree file. It works with no configuration as long as the locale JSON files sit in the same directory as the template.

  • Inline value preview (ghost text). Each resolved tag shows its translated value as dimmed italic text after the tag - {_ ui.site_name } → My Site. The value comes from the default locale (see the setting below).
  • IntelliSense completion. Typing inside a tag ({_ nav.) suggests matching keys from the locale files, with dot-separated partial matching so blog. narrows to keys nested under blog.
  • Hover preview across locales. Hovering a key shows its value in every locale, the default marked with a ⭐, so you can confirm a string is translated everywhere without opening the JSON.
  • Unknown-key validation. A tag whose key exists in no locale file is underlined with a warning (ree-i18n-unknown-key) - the in-editor counterpart to the braced-key marker the renderer shows on the page. Diagnostics refresh automatically when you edit a locale JSON file.
  • Quick Fix to create the key. The warning offers a Quick Fix (Ctrl+.) that inserts the missing key - nested correctly for dotted paths - into each existing locale file (or scaffolds en.json / sl.json if none exist yet), ready to fill in.
  • Go to Definition and Rename. Ctrl+Click a key to jump to its definition in the JSON, or rename it to update the key across every locale file at once.
  • Switch display language. A status bar item (🌐 REE: EN) shows the currently previewed locale; clicking it opens a picker of the project's supported languages. Choosing one re-renders the ghost text, hover, and default marker in that language - handy for eyeballing a page's Slovenian copy without editing anything.

The previewed and default-marked locale is stored in the ree.translation.defaultLocale setting - the status bar picker just writes it for you. When unset, the extension falls back to the project's default_language from config/supported_languages.ts, whose supported-language list also populates the picker.

Lazy-Loading Pattern

Not every language needs every key from the start. Because of the cross-language fallback, you can ship a new language with only the keys that differ from an existing language. The loader will fill the rest from the other language's translations automatically.

This is useful during development - add the language to supported_languages.ts, create a minimal {lang}.json with just the navigation labels, and see the site working immediately. Add more translated text progressively.