Client-side conventions only. How keys are stored, named, and written — naming, interpolation, plurals, formatters, what the linter enforces — is in docs/i18n.md, and applies here too.
Use useTranslation from react-i18next:
import { useTranslation } from 'react-i18next';
const DeleteRoomButton = () => {
const { t } = useTranslation();
return <Button danger>{t('Delete_room')}</Button>;
};❌ 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 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.
t() does not reject unknown keys (see docs/i18n.md), so a computed key has nothing checking it — and it makes the key invisible to anyone grepping for it.
❌ Incorrect:
{t(`App_status_${instance.status}` as TranslationKey)}✅ Correct — map the value to a literal key:
const APP_STATUS_LABELS = {
installed: 'App_status_installed',
updating: 'App_status_updating',
} as const satisfies Record<AppStatus, RocketchatI18nKeys>;
{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.
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:
{
"Unique_ID_change_detected_learn_more_link": "Read the <a>documentation</a> before proceeding."
}import { Trans } from 'react-i18next';
<Trans i18nKey='Unique_ID_change_detected_learn_more_link' components={{ a: <ExternalLink to={links.go.fingerPrintChangedFaq} /> }} />;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:
{ "Limits_reached": "Your workspace reached the <1>{{val}}</1> limit. <3>Manage subscription</3>." }✅ Prefer:
{ "Limits_reached": "Your workspace reached the <bold>{{val}}</bold> limit. <link>Manage subscription</link>." }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:
<Trans i18nKey='Airgapped_workspace_warning' values={{ remainingDays }} />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:
<FieldHint dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t(provider.description)) }} />Prefer Trans over injected HTML wherever possible — it needs no sanitizing because the components are real React elements, not parsed markup.