# Translations Six languages, one flat JSON file each, and **one copy of each file** — the panel bundles them with esbuild and the Rust side compiles the very same paths in with `include_str!`, so the tray tooltip, the context menu and the Windows notifications cannot drift from the panel. There is no translation table anywhere in the Rust sources. **Corrections are welcome as pull requests.** English and Turkish were written by hand; Chinese, Korean, Russian and Spanish were machine-translated first, exactly as `docs/PROJECT.md` §3 says, and none of the four has been reviewed by a native speaker yet. If something reads badly to you, that is not a nuisance — it is the thing this table is for. ## Status | File | Language | Written by | Reviewed by a speaker | |---|---|---|---| | `en.json` | English | maintainer | — (source language) | | `tr.json` | Türkçe | maintainer | maintainer (native) | | `zh.json` | 中文 (简体) | machine-translated, WP6 and WP7 | not yet | | `ko.json` | 한국어 | machine-translated, WP6 and WP7 | not yet | | `ru.json` | Русский | machine-translated, WP6 and WP7 | not yet | | `es.json` | Español | machine-translated, WP6 and WP7 | not yet | **139 keys per file.** WP7 added thirteen: the settings page's status-line section, all under `settings.statusline.*`. They are the longest sentences in the product — the section has to say that installing nazar-tray does *not* touch Claude Code's settings and that this button does — so they are the first place a translation reads badly. The wrapper's own output shown underneath them is deliberately **not** translated: it is a unified diff of a JSON file and a path to a backup, and a translated diff is a picture of a diff. T-WP12 took one away again: `panel.action.theme` named a footer button that no longer exists, and the theme is now chosen only in the settings, where `settings.theme.*` already named it. T-WP16 added twenty, all under `usage.*`: the third view's tabs, its two numbers, its three footer lines and the six sentences an error can be. Two of them are the same idea said twice on purpose — `usage.row.requests` and `usage.row.events` — because a record that carried usage is a reply on the Claude side and a `token_count` event on the Codex side, and one word over both would be a label that lies about one of them. T-WP17 added `tray.tooltip.week` and forgot to move the number in this paragraph, which is why it said 139 for a while and says 146 now. **T-WP20 added eight and took two away**, on the maintainer's reading of the built panel beside Claude Code's own `/usage`. The four `usage.part.*` keys are the breakdown under the headline — *In*, *Out*, *Cache read*, *Cache write* — which is where the cache is now told from the work, since the headline itself became the four-way sum `/usage` calls a total. That retired `usage.cacheRead`, whose whole job was to keep the cache reads outside it. `usage.cell` and `usage.cell.none` are what a day of the calendar says when it is hovered or focused — a date, a number and the model that led it, or the words for a day with nothing on it — and they replace `usage.bar`, which said only the first two and had no way to say the third. `usage.less` and `usage.more` label the two ends of the heat-map's legend and are the shortest strings in the product; keep them that way, because they sit either side of five small squares on a 360 px panel. **T-WP21 added six and took one away — 151 keys per file**, on the maintainer's second reading of the same view. `usage.range.weeks` and `usage.range.models` are the two new tabs; `usage.range.month` went with the tab it named, because the calendar on *All* draws the month being lived in and eleven more. `usage.span.days7` and `usage.span.days30` are two thirds of the *Models* selector — the third is `usage.range.all`, reused rather than asked for again, so the word above the selector and the word in it agree. `usage.chart.daily` titles the chart, and `usage.detail.week` names what a week's detail is a detail of. **T-WP22 added seven — 158 keys per file.** Five are the settings page's new *Usage history* section (`settings.section.usage`, `settings.usage.perLine`, `settings.usage.history` and a `.help` sentence under each), and two are what the view says when a number is not this application's own: `usage.mode.perLine` — the tag under the headline when the counters are the per-line ones Claude Code's `/usage` shows — and `usage.reported`, which marks a day copied in from Claude Code's own statistics. **T-WP-L2 added two — 160 keys per file.** `tray.hidden.title` and `tray.hidden.body` are the one notification this product shows about **itself** rather than about a quota: on a Linux desktop with no StatusNotifier host — a stock GNOME, most often — the tray icon would be registered and drawn nowhere, so nazar-tray does not register one, says so once, and goes on writing `limits.json`. Two things a translator should know about them. **`GNOME`, `AppIndicator` and `limits.json` are names and are left alone**, like `nazar-tray` and `/usage`; the sentence is about them. And the body is read **in a notification bubble**, which several desktops truncate at two or three lines — so the first clause has to carry the whole message on its own, and everything after it is the explanation for somebody who expands it. **T-WP-L8 added four — 164 keys per file.** `tray.menu.provider` and `tray.menu.noReading` build the live quota rows the Linux tray menu carries, because `tray-icon`'s GTK backend shows no tooltip at all: `{provider} — {reason}` is a label and a clause, and the em dash can become whatever your language uses to join them. `tray.gnome.title` and `tray.gnome.body` are the second notification about this product itself, shown once on a GNOME session — `GNOME`, `Wayland`, `nazar-gnome` and the address are **names**, and the body follows the same first-clause-carries-it rule as `tray.hidden.body` above. Three notes for a translator. **`/usage` is a command name and is left alone**, like `nazar-tray` and `Claude Code`; it is the thing the sentence is about. **The 1.7 in `settings.usage.perLine.help` is a measurement, not a rounding to taste** — it is the ratio between the two counts on the maintainer's machine, and the decimal mark follows the language (`1.7×` in English, `1,7` in Turkish, Russian and Spanish). And **`usage.reported` is read in three places at once** — a hover line on the calendar, the line where a week's breakdown would be, and the tag under a detail's headline — so it wants to be short enough for a 360 px row and to read as a clause rather than a heading. Two things a translator should know about those. **`usage.span.days7` spells its unit out and still needs no plural form**, because the number in it is a *constant*: Russian needs `дней` after 7 and after 30 and never anything else, so the string carries the right form once rather than choosing one at run time. That is why neither is on the frozen counted-key list — they interpolate nothing. And **the share beside a model on the *Models* tab reuses `panel.window.percent`**, the product's one spelling of *a number and a percent sign*; it is the key that already knows Turkish writes `%88` and Korean writes `88%` with no space, and a second key for the same shape would be a second chance to get that wrong. The status lives in this table rather than in a `_meta` key inside the files, and that is a finding rather than a preference. `crates/nazar-tray/src/i18n.rs` parses each file as `BTreeMap`; a nested object makes the **whole file** fail to parse, and the failure is deliberately silent — a damaged translation must not stop the tray from starting — so the language simply stops being offered in the settings and everybody who chose it gets English. Tried, watched happen, reverted. Keep every value a string. ## Fixing a translation 1. Edit the string in `ui/locales/.json`. Nothing else needs to change: the settings list, the tray menu and the notifications all read these files, and none of them has a copy of a word. 2. `cd ui && npm test` — the checks below run there. 3. `cargo test -p nazar-tray` — the Rust half checks the same files from its own side. 4. Open a pull request. Say which language you speak; that is what moves a row in the table above from *not yet* to reviewed. ## The rules a locale file has to keep Each of these is a test in `ui/test/i18n.test.mjs`, so getting one wrong fails the build rather than reaching a user. - **Exactly English's keys.** A missing key fails; so does an invented one. English is the fallback, so a missing key would silently show the English string and never be noticed. - **No empty and no padded values.** A blank string is an invisible label. - **The same placeholders.** `{model}`, `{percent}`, `{time}`, `{age}`, `{days}`, `{hours}`, `{minutes}`, `{seconds}`, `{provider}`, `{window}`. A placeholder with no value is printed as written — `{percent}` on somebody's screen — rather than blanked, so a typo is loud. They may be **reordered** freely; that is most of what translating these strings is. - **Product names are left alone**: `nazar-tray`, `Claude Code`, `Codex`, `Claude`, `Nazar`. A translated brand is the wrong brand. - **Language names stay in their own language.** `settings.language.ko` is `한국어` in all six files, because a picker is read by somebody looking for their own language in it. - **Flat strings only.** See above. ## Numbers, units and plurals **No message in this product needs a plural form, and that is by construction.** Every string that carries a count renders it next to a unit *abbreviation* — `4 d 2 h`, `2 sa 10 dk`, `2 小时 10 分`, `2시간 10분`, `2 ч 10 мин`, `2 h 10 min` — and an abbreviation is the same word after 1 as after 5 in all six languages. That matters most for Russian, which otherwise needs three forms (1, then 2–4, then 5 and up, with 11–14 in neither of the first two). If you ever need a counted **word**, `pluralCategory` and `plural` in `ui/src/i18n.ts` implement exactly that rule. `ui/test/i18n.test.mjs` freezes the list of keys that carry a count, so adding one fails the suite until somebody decides which of the two routes it takes. Percent signs and unit spacing follow the language, not English: Turkish writes `%88`, Chinese and Korean write `88%` with no space, English, Russian and Spanish write `88 %`. **A token count carries no unit at all in these files, and that is the same rule again.** `22.3M` in English is `22,3 Mn` in Turkish, `22,3 млн` in Russian, `2230万` in Chinese and `2230만` in Korean — and every one of those marks comes from `Intl.NumberFormat`'s compact notation in `ui/src/usage.ts`, not from a string here. So there is no `K`, `M` or `B` to translate, no decimal separator to get wrong, and the `{tokens}` in `usage.part.input` or `usage.cell` holds a number that has **already been formatted for the language** — the same thing `{time}` and `{age}` have always held. That is why those keys are not on the frozen counted-key list: what they interpolate is a finished string, not a bare count. ## What is deliberately **not** translated - **Reader error sentences.** A window that could not be read carries the reader's own sentence — which file said what — and the panel prints it verbatim. No translation can know in advance what a malformed log will say. - **Plan names.** `max_20x`, `plus`: shown exactly as the source spelled them. - **Addresses.** `github.com/xfurqan0/nazar-tray`, `docs/limits-contract.md`. - **Anything on the command line.** `--print` emits JSON for a script; `--autostart` answers in English. Standard output is a maintainer's surface, not a user's, and `docs/PROJECT.md` keeps it in English on purpose. - **Theme identifiers.** The theme *names* are translated (`settings.theme.graphite`); the values written into `config.json` are not. ## Writing direction All six languages are left to right, so the panel sets `` and never `dir`. Adding Arabic, Hebrew, Persian or Urdu is more than a JSON file: it needs a direction table in `ui/src/i18n.ts` and an audit of `ui/src/styles.css`, which still uses physical `margin-left` and `text-align: right` in a dozen places. A test in `ui/test/i18n.test.mjs` fails if an RTL tag is added to `LOCALES` before that work is done.