---
title: Translations
description: Every word of the app in every language it speaks, edited live in the page by translators working together.
section: Frontend
order: 4
---

# Translations

<p class="lead">An app's words live in <code>i18n.json</code>, one entry per phrase with each language beside it. Translators change them in the admin UI, or on the page itself, and every open page shows the change without a reload.</p>

## The file

```json title="app/i18n.json"
{
  "locales": ["en", "fr", "de"],
  "classes": { "en": "classes", "fr": "classes", "de": "klassen" },
  "welcome": { "en": "welcome to {{app}}, {{name}}", "fr": "bienvenue sur {{app}}, {{name}}" }
}
```

- Keys are lower case.
- `{{name}}` is filled from the values given. `{{app}}` is always the app's name.
- A missing language falls back to English. A key missing from English too shows as `?[key.locale]`, so the gap is visible rather than blank.

## In the app

```tsx title="app/i18n.ts"
import { createI18n } from "sluurp/i18n";
import table from "./i18n.json" with { type: "json" };

export const i18n = createI18n({ table, appName: "MikroSchool" });
export const t = i18n.t;
i18n.connect(sluurp);   // live updates
```

```tsx title="app/header.tsx"
<h1>{() => t("classes|title")}</h1>          // "Classes"
<p>{() => t("welcome", { name: me.first_name })}</p>
```

- `|title`, `|upper` and `|lower` change the case, so "classes" and "Classes" don't need two entries.
- The arrow keeps a string live: switching language with `i18n.setLocale("fr")`, or a translator's change, redraws it. `t("x")` without the arrow is read once.
- The server serves `i18n.json` with the published changes already laid over it, so the import is always the current words.

## Translating

- **In the admin UI,** under **Translations**, every key with a field per language.
- **On the page:** a translator switches translate mode on, clicks any text, and edits it where it's shown. Clicks go to the editor, not the page, so a link doesn't navigate.
- **Together:** a translation session keeps drafts that only its translators see. Each person's cursor is outlined in their colour, typing shows to the others as it happens, and the session is published in one step when it's ready.
- **With a link:** a session's link lets someone without an account join as a guest, to edit but not publish.

Only people with the `translator` role may translate. The [UI kit's](/docs/ui-kit) own words (Close, Search…, the months of the calendar) come translated already. `sluurp i18n` lists keys that nothing uses any more.
