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 soblog.narrows to keys nested underblog. - 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 scaffoldsen.json/sl.jsonif none exist yet), ready to fill in. - Go to Definition and Rename.
Ctrl+Clicka 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.