Files
twenty/packages/twenty-docs/developers/extend/apps/translations/overview.mdx
T
Félix Malfait 55ed4b7adb feat(sdk): translate front-component strings with t()/Trans/useTranslate (#22301)
## What

Lets app **front components** localize the strings they render,
extending the
existing application-translation pipeline (which today only covers
manifest
labels) to component source. App authors mark strings with a small,
familiar
API; the build extracts and bakes them; the runtime resolves them for
the
user's locale.

```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';

<Trans>Loading postcard…</Trans>
<Trans context="card-title">Untitled</Trans>            // disambiguation
const empty = t('No content yet…');                     // works outside JSX
<p>{t('Saved {count} cards', { count })}</p>            // interpolation
const STATUSES = [{ id: 'draft', label: msg('Draft') }]; // lazy descriptor
```

## How

- **Runtime** (`twenty-sdk/front-component`): `t()` (eager, usable
anywhere —
event handlers, helpers, module scope), `msg()` (lazy descriptor),
`<Trans>`
(reactive JSX), `useTranslate()` / `useLocale()`. Source-string
fallback,
`{name}` interpolation, and `context` disambiguation. No build-time
macro —
  these are plain runtime functions.
- **Extraction**: a `ts-morph` scan collects `t()`/`msg()`/`<Trans>`
strings
from component source into the same `locales/*.json` catalogs the
manifest
  pipeline already writes (`twenty dev:translations-extract`).
- **Delivery**: `twenty dev:build` bakes the compiled per-locale catalog
into
each front-component bundle via an esbuild banner, so the runtime
resolves
with **no server or renderer changes**. Locale comes from the execution
  context that already flows to the worker.

The catalog key and `generateMessageId` hashing are shared between the
node
extractor and the browser runtime; `<Trans>` text whitespace is
normalized
identically on both sides so multi-line elements resolve.

## Design notes

- Reuses the existing `extract → compile → manifest.translations`
contract and
`generateMessageId`, so component strings flow through the same
machinery as
  manifest labels.
- Self-contained in `twenty-sdk` + a shared pure helper; the server is
untouched.

## Scope / follow-ups

- `twenty dev` (watch) does not bake catalogs yet — preview shows source
strings; use `twenty dev:build` (documented). Wiring the watcher is a
follow-up.
- Usage is documented in twenty-docs under **Apps → Translations**
  (`developers/extend/apps/translations`).

## Tests

Unit tests for the catalog-key/interpolation helpers, the runtime
resolver
(hit/miss/context/fallback/interpolation), and the ts-morph extractor
(static `t`/`msg`/`<Trans>`, dynamic-skip, dedup, multi-line
whitespace), plus a
compile test for context→messageId. Verified with an adversarial review
pass.

https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA

---
_Generated by [Claude
Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22301?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-01 18:50:35 +02:00

126 lines
4.4 KiB
Plaintext

---
title: Translations
description: Ship your app in multiple languages — translate manifest labels and UI strings through one locales catalog.
icon: "language"
---
Twenty apps are authored in **English**: the strings in your source and manifest
are the English source text, `en` is the source locale you translate *from*, and
any locale left untranslated falls back to it.
Your app has two kinds of translatable text, and both flow through the same
`locales/` catalog:
- **Manifest labels** — object and field names, view titles, menu items, and
other strings declared in your app's metadata.
- **Front-component strings** — the UI text your React front components render.
You mark the translatable strings, extract them into per-locale catalogs,
translate those catalogs, and the build serves the right language for the
current user — no extra wiring.
## Marking front-component strings
Import the translation helpers from `twenty-sdk/front-component`:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### When to use which
- **`<Trans>…</Trans>`** — static text in JSX. Use the `message` and `values`
props for interpolation (`<Trans message="Hi {name}" values={{ name }} />`);
interpolating directly in the children is not statically extractable.
- **`useTranslate().t`** — dynamic strings inside a component. Re-renders when the
user switches language. Prefer this inside render.
- **`t(...)`** (imported directly) — eager translation usable **anywhere**,
including event handlers, helpers, and module scope — not only inside render.
- **`msg(...)`** — a lazy descriptor for strings declared as data (constants,
config). Resolve it later with `t(descriptor)`.
### Context
Pass `context` to disambiguate identical source strings that translate
differently:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
## Extracting and translating
Run the extract command from your app directory:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
Extraction collects both your manifest labels and the `t()`/`msg()`/`<Trans>`
strings from your front-component source into `locales/<locale>.json`, keyed by
source string. Fill in the translations:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées"
}
```
Placeholders like `{name}` are substituted at runtime — keep them in the
translation. Any string left empty falls back to the source text.
## How it runs
`twenty dev:build` compiles the catalogs and serves the right language for the
current user: manifest labels are resolved server-side, and front-component
catalogs are baked into each component bundle. At runtime a component reads the
locale from its execution context (the host's current language) and resolves
each string against its catalog, falling back to the source when a translation
is missing. Switching language in the host re-renders `<Trans>` and
`useTranslate().t` strings live.
Because catalogs are compiled at build time, updating a translation means
re-running `twenty dev:build` (and redeploying), the same as any other change.
<Note>
Translations are compiled by `twenty dev:build` (and `twenty apply`). The
continuous `twenty dev` watch shows source strings, so test localized output
with a one-off build.
</Note>
`<Trans>` text children may span multiple lines — whitespace is collapsed the
same way JSX collapses it, so `<Trans>Welcome\n back</Trans>` and the extracted
key both become `Welcome back`.