Translations

ReeWeb loads translations from {locale}.json files placed next to your templates. Every page has access to the merged translations for its locale - both the top-level global strings and the page-specific overrides. Missing keys inherit from other locales automatically via a cross-locale 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 locale code. They live alongside the templates they translate:

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

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

Key Organization

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

{
    "site_name": "My Site",
    "nav": {
        "home": "Home",
        "about": "About",
        "blog": "Blog",
        "contact": "Contact"
    },
    "ui": {
        "title": "Welcome"
    }
}

Keys are read in templates with the translation-lookup tags - {_ nav.home }, {_ ui.title }, and so on (see Template Access below). Note that site_name sits at the top level: the shipped layout renders it as props.site_name, so it is read directly rather than through a lookup tag.

The root file loads into the routes namespace, which every page sees as its global scope. Each subdirectory's file loads into a namespace named after the directory, and is merged on top of the global scope for the pages that resolve to that namespace.

Page-Specific Keys

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

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

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

A directory's file is scoped to that directory's index, not to everything under it. A page's namespace is its full dotted path: blog/index.ree is blog, but blog/archive.ree is blog.archive. So blog/en-us.json overrides keys for /blog/ only. To override for a sibling page, nest the strings under that page's filename inside the same file:

{
    "nav": { "blog": "All posts" },
    "archive": {
        "nav": { "blog": "Archive" }
    }
}

Markdown pages are different again: they receive the global routes scope only, with no per-directory or per-page namespace merged on top.

Cross-Locale Fallback

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

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

Exception: The route_name key is never inherited across locales (see Localized Routes). Each locale 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.

The lookup tags work in .ree templates only. A .md file's body is rendered as markdown, not compiled as a Ree template, so a {_ nav.home } written inside markdown reaches the page as that literal text. Put translated copy for a markdown page in per-locale .md variants (post.de-de.md) or move the surrounding markup into a .ree template. 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 {locale}.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-us.json / sl-si.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 locale. A status bar item (🌐 REE: EN-US) shows the currently previewed locale; clicking it opens a picker of the project's supported locales. Choosing one re-renders the ghost text, hover, and default marker in that locale - 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_locale from config/supported_locales.ts, whose supported-locale list also populates the picker.

Lazy-Loading Pattern

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

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