# Translations (i18n) in the client
Client-side conventions only. How keys are stored, named, and written — naming, interpolation, plurals, formatters, what the linter enforces — is in [docs/i18n.md](../i18n.md), and applies here too.
## Reading translations
Use `useTranslation` from `react-i18next`:
```tsx
import { useTranslation } from 'react-i18next';
const DeleteRoomButton = () => {
const { t } = useTranslation();
return ;
};
```
❌ Not `useTranslation` from `@rocket.chat/ui-contexts` — it is `@deprecated` and re-renders every consumer of the translation context on language change. ~125 client modules still import it against ~890 for the `react-i18next` one; don't add to the count.
[`TranslationProvider`](../../apps/meteor/client/providers/TranslationProvider.tsx) bundles `en` statically and sets it as `fallbackLng`, so English keys resolve immediately. Only a non-English active locale is fetched over HTTP (`partialBundledLanguages: true`); until it arrives, and for any key it omits, the English string is used. The server, by contrast, holds all 68 locales in memory.
## Never build keys at runtime
`t()` does not reject unknown keys (see [docs/i18n.md](../i18n.md#types-are-generated-from-en)), so a computed key has nothing checking it — and it makes the key invisible to anyone grepping for it.
❌ Incorrect:
```tsx
{t(`App_status_${instance.status}` as TranslationKey)}
```
✅ Correct — map the value to a literal key:
```tsx
const APP_STATUS_LABELS = {
installed: 'App_status_installed',
updating: 'App_status_updating',
} as const satisfies Record;
{t(APP_STATUS_LABELS[instance.status])}
```
The map is the thing that tells you a key is missing when a new status is added, and `satisfies` is what makes the map itself checkable. `as TranslationKey` appears about 70 times in the client; treat every one as debt rather than precedent.
## Rich text: the `Trans` component
When a sentence contains a link, bold run, or nested element, don't split it into several keys. Use `Trans` and put the markup in the translation:
```json
{
"Unique_ID_change_detected_learn_more_link": "Read the documentation before proceeding."
}
```
```tsx
import { Trans } from 'react-i18next';
}} />;
```
Name your tags. Indexed placeholders (`<1>`, `<3>`) appear in older keys and are effectively unreadable for a translator, who can't tell what index 3 wraps:
❌ Avoid:
```json
{ "Limits_reached": "Your workspace reached the <1>{{val}}1> limit. <3>Manage subscription3>." }
```
✅ Prefer:
```json
{ "Limits_reached": "Your workspace reached the {{val}} limit. Manage subscription." }
```
The base locale is currently split about evenly between the two (~50 values each), so this is a direction, not an established majority.
Pass interpolation values through `values` and `t` options through `tOptions`:
```tsx
```
## Interpolated values are not HTML-escaped
`escapeValue` is `false`. That is safe only because React escapes strings when it renders them as text nodes — it is not a property of the translation itself.
So if a translated string is injected as HTML, sanitize it, as the two existing call sites do:
```tsx
```
Prefer `Trans` over injected HTML wherever possible — it needs no sanitizing because the components are real React elements, not parsed markup.