Files
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

164 lines
6.2 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`** — static text in JSX. Use the `message` and `values` props for
interpolation, as in `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>
```
Contextual strings appear in locale catalogs as a group keyed by their
context: `"<context>": { "<message>": "<translation>" }`. Manifest labels are
contextual by construction — each one carries the metadata role it plays
(`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …), so
the same word can translate differently as an object name and as a view name.
## 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",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
Context-free strings are flat entries; contextual strings — including every
manifest label — are nested under their context key.
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.
Manifest labels are compiled on every sync — `twenty dev:build`, `twenty apply`
and the continuous `twenty dev` watch alike — so editing a locale file and
syncing is enough to see the new text. Front component catalogs are baked into
each component bundle at build time, so changing one of those still means
re-running `twenty dev:build` (and redeploying).
## Pulling translations
`twenty pull` writes an application's published translations back into
`locales/`. A published catalog is keyed by message id, so pull recovers the
readable form only for strings whose source it can see: the labels of the
metadata it pulled and the `t()`/`msg()`/`Trans` strings of front components
present in the project. Everything else is kept as is in
`locales/compiled/<locale>.json`, keyed by message id. The build merges those
files into the catalog (an entry in `locales/<locale>.json` wins over a
compiled one with the same id), `dev:translations-extract` leaves them alone,
and every pull rewrites them, so they shrink as more of the app's source lands
in the tree. Do not edit them by hand; translate in `locales/<locale>.json`.
A project without a `locales/` folder says nothing about translations, so a
sync from it leaves whatever the app already published untouched. To remove
published translations on purpose, keep the folder and delete the locale files
inside it: a `locales/` folder that compiles to no locale is an explicit
declaration, and the sync prunes accordingly.
`Trans` text children may span multiple lines — whitespace is collapsed the
same way JSX collapses it, so both of these extract to the key `Welcome back`:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```