> ## Documentation Index
> Fetch the complete documentation index at: https://juo.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Localization

> Ship blocks with built-in translations, override them per theme or per block instance.

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:

```ts theme={null}
import { defineBlock } from "@juo/blocks";

export const CheckoutBlock = defineBlock("Checkout", {
  group: "theme",
  schema: { /* ... */ } as const,
  initialValue: () => ({ props: {} }),
  locales: {
    supported: ["en", "de", "nl"],
    async load(locale) {
      // Each file exports a flat object of { key: "translated string" }.
      return (await import(`./locales/${locale}.json`)).default;
    },
  },
  renderer(block) { /* ... */ },
});
```

A locale file is plain JSON:

```json theme={null}
// locales/de.json
{
  "checkout.title": "Zur Kasse",
  "checkout.cta": "Bestellen",
  "overrides": {
    "checkout.cta": "Jetzt bestellen"
  }
}
```

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:

```ts theme={null}
import { defineBlock, effect, untracked, injectContext } from "@juo/blocks";
import { TranslationContext } from "@juo/blocks";

defineBlock("Checkout", {
  // ...
  renderer(block) {
    const el = document.createElement("button");
    const t = injectContext(el, TranslationContext);

    effect(() => {
      // Reading t.locale.value re-runs this effect on locale change
      const _locale = t.locale.value;
      const label = t.resolve(block.type, block.id, "checkout.cta");
      untracked(() => {
        el.textContent = label;
      });
    });

    return el;
  },
});
```

`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](/docs/blocks/frameworks/vue), [React](/docs/blocks/frameworks/react), and [Preact](/docs/blocks/frameworks/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:

```html theme={null}
<juo-text prop="checkout.cta">Place order</juo-text>
```

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:

```ts theme={null}
renderer(block) {
  const btn = document.createElement("button");
  btn.innerHTML = `<juo-text prop="checkout.cta">Place order</juo-text>`;
  return btn;
},
```

From Preact or React (using `h` directly):

```tsx theme={null}
h("juo-text", { prop: "checkout.cta" }, props.cta)
```

### `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:

```html theme={null}
<p><juo-text prop="checkout.subtitle" hide-parent-if-empty>Free shipping on all orders</juo-text></p>
```

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:

| 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. |

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:

```ts theme={null}
import {
  provideContext,
  createTranslationService,
  TranslationContext,
  ThemeStateContext,
  createThemeState,
} from "@juo/blocks";

const themeState = createThemeState({ /* ... */ });
provideContext(root, ThemeStateContext, themeState);

provideContext(
  root,
  TranslationContext,
  createTranslationService(themeState.locales, {
    async loadDefaultTranslations(locale) {
      // Theme-wide catalog, e.g. shipped with the theme package.
      return (await import(`./theme-locales/${locale}.json`)).default;
    },
  }),
);
```

`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`:

```ts theme={null}
import { injectContext, ThemeStateContext } from "@juo/blocks";

const theme = injectContext(el, ThemeStateContext);

theme.locales.setLocale("de");        // triggers catalog reload
console.log(theme.locales.locale.value); // "de"
```

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.
