Skip to main content
A block declares the strings it owns, ships catalogs for the locales it supports, and reads translations through 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:
  1. Block instance override — the merchant rewrote this string on this specific block in the editor.
  2. Block default in override mode — the block ships its own default.
  3. Theme override — the theme rewrote this key across all blocks.
  4. Theme default — the theme’s own translation catalog.
If no layer has a value, the resolver returns the key itself.

Declaring locales on a block

A block opts in by adding a locales field to defineBlock — a list of supported codes and an async loader:
A locale file is plain JSON:
Top-level keys are the block defaults. The optional 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 through TranslationContext. 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 reach TranslationContext 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:
The element reads the 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:
From Preact or React (using 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:
The <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: A block can flag a key as "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 writing themeState.locales:
Every block that read a translation through 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 through loadDefaultTranslations — 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 en always. 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 on block.props, wrap the resolve() call so the DOM write doesn’t tangle dependencies with the props read.