> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/features/multilingual-ui.md).

# Multilingual UI

The ODD Platform UI ships with **seven locale translations** loaded at SPA bootstrap. Users switch the active locale from the **user-account menu** in the top-right of the toolbar; the choice persists per browser device in `localStorage`. The platform's API surface remains English-only — the localisation applies to the UI shell (toolbar labels, page titles, button captions, form field labels, error messages); operator-authored content (entity descriptions, glossary terms, custom-metadata field names, namespaces, tag names) is rendered verbatim regardless of the active locale.

## Supported locales

The platform's translation table currently includes seven entries, sourced from `odd-platform-ui/src/locales/translations/`:

| Code | Language             |
| ---- | -------------------- |
| `en` | English (default)    |
| `es` | Spanish              |
| `ch` | Chinese              |
| `fr` | French               |
| `ua` | Ukrainian            |
| `hy` | Armenian             |
| `br` | Brazilian Portuguese |

The default is `en` — a fresh browser session with no prior locale selection lands on English. The `br` locale (Brazilian Portuguese) ships from platform release 0.28.0 — a community contribution ([`odd-platform#1564`](https://github.com/opendatadiscovery/odd-platform/pull/1564)); deployments on earlier releases offer the original six.

## How users switch

The active locale is changed from the **user-account menu** in the top-right of the toolbar — the area showing your username next to a dropdown arrow. Click it to open the account dropdown (the same menu that holds **Logout**), then click the **Select language** entry — it shows the current language and a chevron. That opens a **Select language** dialog with one searchable entry per supported locale. Picking a locale takes effect immediately on the current page; subsequent SPA navigation continues in the chosen locale.

## Persistence model — per-device, not per-user-account

The chosen locale is stored in the browser's `localStorage` under the key `i18nextLng`. The implications operators should know:

* **Per browser device.** The locale preference is bound to the browser profile, not to the user's catalog account — signing in from a different browser, a different machine, or an incognito session starts from the default (`en`) until the user picks again.
* **Survives logout and re-login.** The `localStorage` value is not cleared by sign-out; the next sign-in from the same browser resumes the prior locale choice.
* **No server-side propagation.** The platform does not record the user's locale preference on the server, does not honour an `Accept-Language` HTTP header, and does not push the choice to other devices the same user is signed in on.

If the deployment serves a user population that uses multiple devices and expects a single, account-bound locale choice — for example a company-managed account propagating across the user's laptop and phone — this is a known absence; the per-device localStorage model is the only persistence path today.

## Known caveat — how missing translations behave (and the 0.28.0 catch-up)

Each locale's translation table uses the English text itself as the lookup key (for example `"Activity": "Activity"`). When a key a component asks for is missing from the active locale, i18next falls back to **English** (the `fallbackLng: 'en'` setting) — the English label renders with no warning and no visible marker. A locale can therefore look fully translated and still show English wherever its table trails the code.

**As of 0.28.0 every shipped locale table is complete.** Earlier releases carried a real gap: a June 2026 sweep found code-referenced keys (about 70, growing to 84 as more UI surfaces landed) absent from *every* non-English catalog — confirmation dialogs, the Data Quality dashboard headlines, lookup-table and query-example labels, the activity-feed actor tooltips — so a non-English locale showed English in those spots. [`odd-platform#1748`](https://github.com/opendatadiscovery/odd-platform/issues/1748) fixed the first slice (the three top-level navigation tabs — Data Quality, Data Modelling, Master Data — which had been absent from every bundle); [`odd-platform#1751`](https://github.com/opendatadiscovery/odd-platform/issues/1751) completed the rest, translating the remaining 84 keys into all six non-English catalogs (`es`, `ch`, `fr`, `ua`, `hy`) and the seventh, `br` — which had separately trailed by eight keys, including the **Master Data** tab. **All seven catalogs (`en`, `es`, `ch`, `fr`, `ua`, `hy`, `br`) now carry the same key set.** A separate gap was also closed in 0.28.0: a number of UI strings were hardcoded in the source with no translation lookup at all (so they showed English under every locale, independent of the catalogs) — these were wrapped so they pass through the translation layer and were translated across all seven locales. Three automated build checks now keep this from regressing: a key-parity check (every locale must carry the English key set), a no-untranslated-string check (a new user-facing label that skips the translation layer fails the build — covering both JSX text and component attributes such as placeholders and labels), and — added in 0.29.0 — a symbolic-key value check. So the gap should not silently reopen as new screens are built.

The third check guards a subtler defect than a missing key. Because the keys *are* the English text, a translator working from the key list alone can mistranslate the one **symbolic** key — `main search placeholder`, whose English value is the home-page search hint (`Search data tables, feature group, jobs and ML models via keywords`) rather than the key itself. All six non-English catalogs had rendered it as a literal gloss of the key (`"main search space"` / `"main search pointer"` / the i18n word "placeholder" in Chinese), so a non-English operator saw meaningless text on the first surface they meet — the home-page catalog search box. The 0.29.0 fix translated the hint into every locale, and the new check now requires every non-English value of a symbolic key to be a translated hint, not a literal gloss of the key ([`odd-platform#1776`](https://github.com/opendatadiscovery/odd-platform/issues/1776)).

These bulk catch-up translations were contributed in one pass; native-speaker refinements to any string are welcome — correcting an existing translation is the same workflow as adding a new locale (see [How to contribute a new locale](#how-to-contribute-a-new-locale) below), applied to the value already in the JSON.

The fallback **mechanism** remains by design: a future code change that introduces a new `t('...')` string renders the English label under any locale whose table has not yet caught up. For completeness audits, treat the translation-table JSON files as the source of truth, not the running UI — because a missing key shows the English text, an English-locale UI always looks complete. On releases that predate 0.28.0, the gaps described above are present.

## How to contribute a new locale

Adding an eighth locale (or any subsequent locale) is a contributor-facing workflow on the UI repository ([`odd-platform`](https://github.com/opendatadiscovery/odd-platform)). The shape:

1. Create a new translation file at `odd-platform-ui/src/locales/translations/{code}.json` (where `{code}` is the locale code — typically the 2-letter ISO 639-1 code, or the project's own short code such as `ch` for Chinese or `br` for Brazilian Portuguese). The file mirrors the structure of the existing translation files; the simplest start is to copy `en.json` and translate each value.
2. Register the locale in the bootstrap module at `odd-platform-ui/src/locales/i18n.ts` — add the import and add the entry to the `resources` map (and, to keep it consistent with the others, add the code to the `fallbackLng` array). The `resources` map is what populates the picker, so this step is what makes the locale selectable.
3. Add the new code to **both** maps in `odd-platform-ui/src/lib/constants.ts` — `LANGUAGES_MAP` (code → display name, shown in the picker) and `LANG_TO_COUNTRY_CODE_MAP` (code → flag country code). These are required: the **Select language** dialog renders each row by looking the code up in these maps, and a code that is missing from `LANGUAGES_MAP` will break the picker. The `Lang` type is derived from `LANGUAGES_MAP`, so adding the key here is also what makes the rest of the code recognise the new locale.
4. Open a pull request against the UI repository. The contribution does not require platform-API changes, and the **Select language** dialog needs no edits — it builds its list from the registered locales automatically. [`odd-platform#1564`](https://github.com/opendatadiscovery/odd-platform/pull/1564), which added Brazilian Portuguese, is a complete worked example of exactly this shape — three files, no other changes.

The build-and-run workflow for the platform (including the frontend UI) is documented at [Build and run ODD Platform](/developer-guides/build-and-run/build-and-run-odd-platform.md).

## Where to next

* [Management](/features/management.md) — the operator surface inside the platform UI; the localisation applies to the Management section's tab labels and form controls.
* [Main Concepts → Terms & Aliases](/introduction/main-concepts.md#terms-and-aliases) — the alias registry where "Multilingual UI", "i18n", "internationalisation", and "locale switching" point at this page.
