--- name: translating-locales description: Use when filling in or improving a lagging UI translation locale in Trilium (e.g. "Romanian is behind", "bring to 100% coverage", "translate the missing strings"). Covers measuring the gap vs English, drafting translations that preserve i18next placeholders, merging without diff churn, locale grammar rules (Romanian plurals/gender), the source-side pluralization workflow, how Weblate sync actually works (merging is picked up), and validation. --- # Translating / improving a locale in Trilium Trilium's UI is localized with **i18next**. English is the source of truth; other locales are normally crowd-translated via **Hosted Weblate**. This skill is for the *maintainer* case: deliberately filling a locale that lags behind (the recurring one is Romanian, `ro`). Read [romanian.md](romanian.md) for Romanian grammar rules and [scripts.md](scripts.md) for the copy-paste Node snippets used in every step below. ## How Weblate sync works here (merging a PR IS picked up — no separate upload needed) Weblate is linked to this repo with the **GitHub pull-request push method**: `.github/workflows/i18n.yml` triggers on `push` to `weblate:*` branches, and Weblate opens PRs into the repo (the recurring `Translations update from Hosted Weblate (#…)` PRs). The locale `translation.json` / `server.json` files are tracked Weblate **components**. Consequently: - **Merging a PR that edits a locale file IS picked up by Weblate.** On its next pull of the default branch, Weblate imports the new/changed translations into its database — they persist and show up for other contributors. They are **not** discarded or overwritten. With the PR-based push method Weblate never force-pushes the default branch, so the repo is authoritative and Weblate merges *from* it. - A direct-to-repo PR is therefore a fully valid, durable way to land translations. **You do NOT need to separately upload the file into Weblate.** - This is the opposite of arbitrary repo files (README, docs): those aren't part of any Weblate component, so Weblate simply ignores them. - Real (narrow) caveat: a true conflict only arises if the **same key** is edited simultaneously in Weblate (pending, un-pushed) *and* in the repo — then Weblate's merge/rebase conflict handling picks the winner. Filling previously-empty/untranslated strings doesn't conflict. - `CLAUDE.md`'s "only add new keys to `en/translation.json`" rule is about where *new source strings* originate (so they enter the translation pipeline), **not** a prohibition on landing translations for an existing locale via the repo. - Changing an **English source** string (e.g. pluralizing a key) makes Weblate flag the matching translations in *other* locales as needing review and drop orphaned removed-key entries — normal, and it doesn't affect the locale you just filled. ## File locations | Scope | English source | Target locale | |---|---|---| | Client UI | `apps/client/src/translations/en/translation.json` | `apps/client/src/translations//translation.json` | | Server (hidden subtree titles, dialogs, migration/search messages) | `apps/server/src/assets/translations/en/server.json` | `apps/server/src/assets/translations//server.json` | Both client and server have their own EN↔locale pair — check **both**. ## Workflow ### 1. Measure the gap Flatten EN and the locale, then report **missing** keys (not in locale) and **identical-to-EN** keys (present but never translated — excluding proper nouns). See `scripts.md` → *measure*. Romanian has historically sat ~73% client / ~87% server. ### 2. Export the work list Dump `{ key: englishValue }` for every missing-or-identical string to a temp file so you can translate against the real source text. See `scripts.md` → *export*. ### 3. Draft the translations - Match the **terminology and tone already in the file** (probe existing entries first — e.g. RO uses `notiță` for "note", `Anulează` for "Cancel"). Don't invent new terms. - **Keep proper nouns and kept technical terms in English**: ETAPI, MCP, OAuth/OpenID, Markdown, Widget, Mermaid, Bing/Google/Gantt/Kanban/…, and words spelled the same in the target language. - **Preserve every placeholder exactly** (this is the #1 source of bugs): - `{{var}}`, `{{- var}}` (unescaped — keep the hyphen), `{keyword}` (single-brace, e.g. search-engine URLs) - JSX/`` tags: ``, ``, `` - `\n` line breaks, leading/trailing spaces, trailing punctuation - Honor i18next **plural suffixes** (`_one`/`_other`, plus locale-specific like `_few`) — see grammar reference for the target language. ### 4. Merge back — WITHOUT churning the diff - **Do NOT alphabetize.** The EN file is *not* sorted and locale files have their own historical order. Sorting produces a 2500-line diff for a 600-string change. Preserve existing key order; update changed values in place; append genuinely-new keys to the end of their parent object. - **Preserve file formatting:** 2-space indent, **CRLF** line endings, trailing newline. `JSON.stringify(obj, null, 2)` then `.replace(/\n/g,"\r\n") + "\r\n"`. - See `scripts.md` → *merge*. ### 5. Validate - **Placeholder integrity** (programmatic — catches dropped/extra `{{…}}`, tags, `{keyword}`): `scripts.md` → *validate*. Must be 0 errors. - **JSON + duplicate keys:** `pnpm --filter client test src/services/i18n.spec.ts` (the repo's only translation test — checks valid JSON and no duplicate keys; there's **no** locale-parity test and **no** typed-i18next, so adding `_few` not present in EN is fine and won't break typecheck). - Re-run *measure* to confirm 100% (remaining "identical" entries should all be legitimate proper nouns). ## Pluralization is a SOURCE-SIDE decision i18next only pluralizes when **(a)** the translation has plural-suffixed keys **and (b)** the call site passes `{ count }`. So: - You may freely add locale-specific plural categories (e.g. Romanian `_few`) to an existing plural group — EN supplies `_one`/`_other`, the locale supplies `_one`/`_few`/`_other`. - To pluralize a string that EN keeps as a **single form** (e.g. `"{{count}} notes"`), you must: 1. Verify the call site passes `count` (`grep` for the key; look for `t("…", { count })`). If it doesn't, or the key is unused, **don't** pluralize. 2. Convert the **English** key from base → `_one`/`_other` in `en/translation.json` (this is the sanctioned place to edit EN). 3. Add the locale's `_one`/`_few`/`_other`. 4. No code change needed — `t("key", {count})` resolves to the suffixed keys automatically. - ⚠️ **Cross-locale cost:** converting an EN base key to plural removes the base key, so *all other locales* fall back to English for that string until Weblate migrates them. Call this out in the PR. Don't unilaterally edit the other ~36 locale files — let Weblate migrate. - Skip strings i18next can't handle: more than one count-like variable (e.g. `"{{count}} sources from {{sites}} sites"` — only `count` triggers plurals). ## Auditing existing plurals To find plural groups missing a locale-required category (e.g. Romanian `_few`) or the wrong `_other` form, enumerate EN keys with both `_one` and `_other`, then check the locale has all required categories. See `scripts.md` → *audit-plurals*. ## Common pitfalls - Alphabetizing the file (massive diff) — don't. - Writing LF instead of CRLF (whole-file diff) — convert to CRLF. - Dropping a trailing `.` or a placeholder when copying a reviewer's suggestion — diff each suggestion carefully. - Putting server strings in the client file or vice-versa — they're separate namespaces with separate EN sources.