# Interface language (i18n)

Navi's web client speaks **en / uk / ru**. The rules themselves live in the
platform handbook ([handbook.gnexus](https://handbook.gnexus) → `10-platform/i18n`);
this file only records how navi follows them and what is still untranslated.

## Where the language comes from

`locale_effective` from `GET /auth/me` — the choice made in Settings
(`navi_users.locale_override`) → the gnexus-auth account locale
(`navi_users.locale`, a straight mirror kept by the `user.profile_updated`
webhook) → `en`.

The picker is the first block of **Settings → Account**
(`webclient/src/components/settings/AccountPanel.vue`) and offers
Auto / English / Українська / Русский — each language named in its own language.
A choice is written with `PATCH /auth/me`, so it follows the account to other
devices and survives logout.

With `NAVI_AUTH_ENABLED=false` there is no account to follow: the picker drops
Auto, the choice is kept in `localStorage` under `navi.locale`, and the same key
is used when the visitor is not signed in.

## The module

`webclient/src/i18n/` — vue-i18n is deliberately not pulled in:

| File | Role |
|---|---|
| `index.js` | `locale` ref, `t(slug, params)`, `setLocale`, `normalizeTag`, `pluralCategory`, and the `navi.locale` storage helpers |
| `messages/en.js`, `uk.js`, `ru.js` | flat dictionaries, the same slugs in all three |

- `t()` interpolates `{param}` and falls back to the **English** string when the
  chosen language has no slug yet (or to the slug itself when nobody has it).
- Plurals are dictionaries (`{ one, few, many, other }`) driven only by
  `params.n`; ru/uk use one/few/many, en uses one/other.
- A slug is `<area>.<name>` and an area is a screen or a component group —
  `settings.*`, `sidebar.*`, `messages.*`, `chat.*`, `artifacts.*`, `ui.*`,
  `profile.*`, `app.*`, `common.*`. Slugs are shared with the other gnexus
  services where they mean the same thing.
- `SUPPORTED_LOCALES` is the single list: adding a language means adding
  `messages/<code>.js` there, plus `navi/locales.py` on the backend (and the
  same slug in `navi_users.locale_override` constraints on the client side).
- Lists of options or nav items must be **computed**, not module-level
  constants, or their labels freeze in the language that was active at import.

The backend side is `navi/locales.py` (`normalize_locale`, `effective_locale`),
mirroring the frontend's normalisation: `en-US`/`en_US` → `en`, anything not
shipped → `None`.

## Translation status

Phase 1 shipped the framework and the language block only. Everything else is
still hard-coded English and is translated area by area, biggest first:

| Area | Strings | Status |
|---|---|---|
| `settings.*` | ~118 | language block only |
| `artifacts.*` | ~45 | pending |
| `sidebar.*` | ~29 | pending |
| `messages.*` | ~27 | pending |
| `chat.*` | ~21 | pending |
| `ui.*` | ~20 | pending |

The remaining strings are counted in templates (text nodes, `placeholder`,
`title`, `aria-label`, `label`, `alt`) plus keyed strings in `<script setup>`.

Backend-generated text (tool results, system prompts, WS event payloads) is
still English and is out of scope for now — the interface language does not
change what the agent answers.
