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 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-us.json/sl-si.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 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.