Languages
Neens ships its interface in eight languages, and every one of them is fully translated — not “mostly”. This page covers how to choose yours, how an API client asks for a language, and what stays in English on purpose.
At a glance
| Where | Topbar language switcher, or Settings → Account → Language |
| Key API routes | PUT /auth/locale (persist your choice), Accept-Language header (per request) |
| Languages | en es fr de it pt ar ja |
| Scope | Per user. Your language never changes what anyone else sees |
| Falls back to | English — for content Neens does not own, and for the handful of API messages that are not in the translated set (see What stays in English) |
Supported languages
| Code | Language | Direction |
|---|---|---|
en | English | left-to-right |
es | Español | left-to-right |
fr | Français | left-to-right |
de | Deutsch | left-to-right |
it | Italiano | left-to-right |
pt | Português | left-to-right |
ar | العربية | right-to-left |
ja | 日本語 | left-to-right |
Picking Arabic sets the document direction to RTL, so text, input fields, and the browser’s own scrollbars and text selection follow Arabic reading order.
Arabic layout is not yet mirrored. The direction flips, but the app’s spacing and alignment
still use physical left/right utilities rather than CSS logical properties, so paddings,
margins, and column alignment stay in their left-to-right positions. Arabic is fully translated
and usable; the layout pass is outstanding.
Choose your language
Before you sign in
The sign-in page has no language switcher. Neens guesses from your browser’s Accept-Language, so
the sign-in form, error messages, and the password rules usually already speak your language — but
it is a guess, not a choice, and there is no way to override it until you are signed in.
After you sign in
Use the topbar language switcher, or Settings → Account → Language → Display language. The
switch is immediate — no reload — and is saved to your profile (PUT /auth/locale), so it follows
you to any browser or device you sign in from.
Your first sign-in stores whatever you were reading
If your profile has no language stored yet, Neens back-fills the one the interface is currently using — your explicit pick if you made one, otherwise the browser guess. From then on the stored value wins everywhere, on every device, until you change it in Settings.
Because the first sign-in promotes a browser guess into a stored preference, a user whose browser is set to a language they do not want should change it in Settings → Account; clearing browser state will not do it.
Ask for a language from an API client
Every JSON API route honours the standard Accept-Language request header. The web app stamps
your active language on every call it makes; your own scripts and integrations can do the same.
# German error copy from a deliberately unauthenticated call
curl -s https://app.neens.ai/api/auth/me \
-H 'Accept-Language: de'
# {"detail":"Nicht authentifiziert."}
# Same call, Japanese
curl -s https://app.neens.ai/api/auth/me \
-H 'Accept-Language: ja'
# {"detail":"認証されていません。"}Header handling follows the spec, so you can pass a browser’s header through unchanged:
| You send | Neens uses | Why |
|---|---|---|
de | de | exact match |
pt-BR | pt | region is dropped when the base language is supported |
fr-CA, fr;q=0.9, en;q=0.5 | fr | highest-quality supported entry wins |
nl | en | unsupported → English, never an error |
| (no header) | en | — |
Accept-Language affects the response copy, never the data. It does not filter, translate, or
reorder your traces, scores, or any other stored record.
What gets translated
| Surface | Translated? | Notes |
|---|---|---|
| The whole web interface | Yes, completely | Every navigation item, button, table header, tooltip, empty state, and error banner |
| API error messages | High-traffic messages | The curated catalogue covers authentication, permission, and validation errors. A message outside it stays readable English rather than blank |
| Transactional emails | Yes | Invitations, activation, password changed/reset, and failed-sign-in alerts, sent in the recipient’s stored language |
| The digest email | No — English | Localizing the digest body is deferred; only the greeting uses your name |
| Right-to-left text direction | Yes (ar) | The document direction flips. Layout mirroring is still outstanding — see the note above |
| Your own data | No — it’s yours | Trace content, session text, agent and judge names, dataset rows, annotations |
| LLM-generated text | Follows your LLM | Cluster labels, remediation drafts, and narrative summaries are written by the LLM connection you configured, from your own trace data |
What stays in English
Some strings are identical in every language on purpose, and translating them would make the product harder to use:
- The product name —
Neens. - Acronyms borrowed unchanged across all eight languages —
API,SDK,LLM,URL,JSON,CSV,HTTP,IP,PR,CI,MCP,SSO,TOTP. - Metric and verdict tokens that must match what the API returns and the CI runners print —
pass^k,PASS,FAIL,p50/p95/p99. - Code you would copy-paste — field names, file names, env var names, header names
(
X-Neens-Project-Id), span attributes (neens.*), key prefixes (nk_live_…), and model ids such asclaude-opus-5. - Vendor names — GitHub, Slack, ClickHouse, Postgres, Langfuse, Zendesk, and the rest.
The judgement calls — a word that happens to be identical in one language, a label that stays an
acronym — are recorded one by one with a written reason. The purely mechanical cases (a bare
number, a URL, an identifier like user_id) are recognised automatically and deliberately are
not listed, so the recorded list stays short enough to actually read.
How completeness is guaranteed
A missing translation is easy to catch. A translation that is silently the English string is not: the language file has the key, everything loads, and only a reader who speaks the language notices. That is exactly what happened to one of our pre-production screens, whose sign-in hints sat in English across all seven non-English languages while every automated check reported green.
Neens now blocks both failure modes before a change can ship:
- Every language carries every key. A key that exists in English and is missing elsewhere fails the build.
- No language may carry the English string. If a value is byte-identical to the English one, it
fails the build — unless it is on the short, reasoned list above, or it is structurally
untranslatable (a bare number, a URL, an identifier like
user_id, a{{placeholder}}). - Every language carries every plural form it needs. English has two (
one/other); Arabic has six and the Romance languages have three. A language missing one of its own forms falls back to English for exactly the counts in that form — for Arabic that was 0 and 2–99 — so the build now checks each language against its own CLDR rules, not against English’s. - Nothing can be deferred quietly. The list of accepted exceptions can only shrink automatically; adding to it takes a deliberate, reviewed edit.
So “eight languages” means the same thing for the eighth language as for the first.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Interface is English after signing in on a new device | Your profile preference is set, but you had picked a different language on that browser before signing in | Signing in applies your stored profile language; switch from the topbar if you want to change it |
| A specific error message is English while the UI is translated | The message is outside the curated API catalogue, or it embeds live values (an id, a limit) | Nothing to fix — the message stays readable rather than blank. Report it if it is a common one |
| Emails arrive in English but the UI is translated | Emails use your stored profile language; a browser-only choice never reached the server | Sign in and pick your language from Settings → Account so it is persisted |
| Cluster labels and remediation text are English | They are generated by your configured LLM from your trace data, not shipped copy | Nothing to fix in Neens; the language follows the model and the data |
| Arabic reads right-to-left but the layout is not mirrored | Known limitation — the interface still uses physical left/right spacing | Nothing to do yet; the text direction is correct and the layout pass is tracked separately |
Related: Account settings · LLM connections