TranslationContext. The portal switches locales, the theme adds overrides, and a single merchant can rewrite one string on one block instance — all without the block author writing any glue code.
Where translations live
There are four layers of resolution, applied in order until one wins:- Block instance override — the merchant rewrote this string on this specific block in the editor.
- Block default in override mode — the block ships its own default.
- Theme override — the theme rewrote this key across all blocks.
- Theme default — the theme’s own translation catalog.
Declaring locales on a block
A block opts in by adding alocales field to defineBlock — a list of supported codes and an async loader:
overrides object lets a block ship preferred wording for keys it defines — used by the resolver when the theme has no opinion.
Reading translations
Resolve a translation throughTranslationContext. From a vanilla renderer:
resolve(blockType, blockId, key) takes the block’s type and instance id so it can apply the per-instance overrides set by the merchant.
From a framework
Vue, React, and Preact all reachTranslationContext through their useContext — see the Vue, React, and Preact guides.
Using <juo-text> for inline text
For inline translated strings rendered directly in HTML, <juo-text> is a declarative alternative to wiring up TranslationContext by hand. It is a custom element that handles resolution and reactivity internally:
prop attribute as the translation key, walks up the DOM to find the nearest block wrapper (data-block-type / data-block-id), injects TranslationContext, and re-renders whenever the locale changes. The text content is used as a fallback: if the key resolves to itself (nothing matched), the element shows the fallback instead.
From a vanilla renderer:
h directly):
hide-parent-if-empty
Add the hide-parent-if-empty attribute to automatically set hidden on the element’s parent when the resolved text is empty:
<p> is hidden entirely when the merchant has cleared the subtitle — no empty whitespace or layout gap remains.
Resolution order in detail
The translation service walks the catalog from most-specific to most-general:| Step | Source | Wins when |
|---|---|---|
| 1 | catalogs.blocks.overrides[blockId][key] | The merchant overrode this string on this block instance. |
| 2 | catalogs.blocks.defaults[blockType][key] (mode "override") | The block ships a default and prefers it over the theme. |
| 3 | catalogs.theme.overrides[key] | The theme rewrote this key for the whole portal. |
| 4 | catalogs.theme.defaults[key] (or loadDefaultTranslations) | The theme provides a translation. |
| 5 | catalogs.blocks.defaults[blockType][key] (mode "default") | The block has a fallback default. |
| 6 | The key itself | Nothing matched — a developer signal that something’s missing. |
"default" rather than "override" to let the theme’s wording win when both exist — useful for generic strings like "add_to_cart" that the theme is likely to style consistently.
Setting up TranslationContext
The portal creates one translation service per <juo-context-root> and feeds it the theme’s locale state:
themeState.locales exposes locale (a Signal<string>), catalogs (the merged block + theme catalogs for the active locale), and a setLocale setter. Changing the locale reloads catalogs and re-runs every effect() that read t.locale.value.
Switching locales
The portal exposes a locale switcher by reading and writingthemeState.locales:
effect() updates automatically.
Overriding theme strings from a portal
When a portal wants to ship its own translations for keys other blocks own, pass them throughloadDefaultTranslations — the result feeds the theme.defaults layer used by step 4 of the resolver. For per-key forced overrides (step 3), populate catalogs.theme.overrides from the theme state.
Tips
- One key, one block. Namespace keys by block (
checkout.cta,cart.empty). Resolution is global, but lint-style discipline keeps catalogs readable. - Ship
enalways. It’s the safe fallback when a locale file fails to load. - Test against
untracked(). To get a translated string inside a render that already depends onblock.props, wrap theresolve()call so the DOM write doesn’t tangle dependencies with the props read.