Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

3.7 KiB

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, and applies here too.

Reading translations

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.

Never build keys at runtime

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.

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:

{
	"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 }} />

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:

<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.