55ed4b7adb
## 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">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
---------
Co-authored-by: github-actions <github-actions@twenty.com>
126 lines
4.4 KiB
Plaintext
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`.
|