Internationalization & Localization#
Element Web and matrix-react-sdk share a common i18n infrastructure built around the counterpart library with a custom wrapper layer. Translation strings are managed via Localazy, synced automatically, and contributed by the community. A known open issue causes browser translation engines to corrupt displayed message text when the UI is already rendered in a non-English language.
Core Infrastructure#
The primary i18n module in matrix-react-sdk is src/languageHandler.tsx. It configures counterpart with a pipe key separator and English as the fallback locale , and exports the following key functions:
| Function | Purpose |
|---|---|
_t(key, vars?) | Translate a string; main call site for all UI strings |
_td(key) | Mark a string for extraction without translating (for module-level constants) |
_tDom(key) | Like _t(), but wraps fallback translations with lang attributes for accessibility |
setLanguage(langs) | Async; loads and activates a language, falls back to English if unavailable |
getUserLanguage() | Reads user preference from settings or browser |
registerCustomTranslations() | Injects module-level or remote-URL translations (cached for 5 min) |
Language JSON files are loaded at runtime via getLanguageRetry() (3 retries), registered with counterpart, and persisted to SettingsStore . A webpack plugin (I18nWebpackPlugin) hashes language files at build time and generates a languages.json manifest for runtime discovery.
String Files and Key Conventions#
The source of truth for all strings is src/i18n/strings/en_EN.json. All other language files are managed exclusively via Localazy — do not edit them directly in the repo.
Key naming conventions :
action_prefix for verb/action strings (e.g.,action_send)common_for non-verb reusable stringsa11y_for accessibility-only strings- Nest logically; avoid
.,|, or spaces in key names
To add a new string :
- Use
_t("my_key")(or_td()+ deferred_t()) in your component. - Run
pnpm i18nto auto-populate the key inen_EN.json. - Write the English text in
en_EN.jsonand open a PR.
Changing the meaning of an existing string requires renaming the key so that Localazy flags all translations for re-review .
Translation Contribution Workflow#
Element uses Localazy (not Weblate) as its community translation platform . The pipeline is:
- Developers merge changes to
en_EN.jsonvia PR. - Localazy picks up the new/changed strings automatically.
- Community translators submit translations at localazy.com/p/element-web .
- Translations are pulled automatically 3× per week and appear on
develop.element.iowithin a few days .
The localazy.json at the repo root configures upload/download paths for three translation targets: element-web, element-desktop, and shared-components . Community coordination happens in #element-translations:matrix.org .
Translators should preserve %(something)s placeholders (variable substitutions) and <link>…</link> markup tags exactly as they appear .
Docs: docs/translating.md (community) · docs/translating-dev.md (developer guide)
Known Issue: Hardcoded lang="en" Triggers Browser Translation#
Issue: #34124 — The root <html> element in apps/web/src/vector/index.html has lang="en" hardcoded:
<html lang="en" style="height: 100%;">
This attribute is never updated at runtime, regardless of the user's selected UI language.
Impact: When Element is rendered in a non-English language (e.g., German), browsers with automatic translation enabled treat the page as English and offer — or automatically apply — translation. If the user accepts :
- The browser's translation engine mutates DOM text nodes, including message bodies in the timeline.
- The sender sees different words than were typed and sent (e.g., "Du lernst es bald" → "Du lernst ist kahl").
- Messages may spuriously appear as "(edited)" due to DOM manipulation — not an actual server-side edit.
- Recipients are unaffected; only the local DOM is corrupted.
- Element Desktop is unaffected (no browser translation engine).
This is easily misreported as a message-editing or sync bug .
Fix direction: The lang attribute on <html> should be updated dynamically to match the active locale whenever setLanguage() completes — see languageHandler.tsx#setLanguage. No fix has been merged as of this writing.
Related: Issue #34302 (German disk-full warning text) is a separate downstream consequence of translated UI strings expressing diagnostic certainty differently across locales, not caused by browser translation.