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 for Romanian grammar rules and 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 toen/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/<locale>/translation.json |
| Server (hidden subtree titles, dialogs, migration/search messages) | apps/server/src/assets/translations/en/server.json |
apps/server/src/assets/translations/<locale>/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/
<Trans>tags:<buildRevision />,<Note/>,<code>…</code> \nline 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_fewnot 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:- Verify the call site passes
count(grepfor the key; look fort("…", { count })). If it doesn't, or the key is unused, don't pluralize. - Convert the English key from base →
_one/_otherinen/translation.json(this is the sanctioned place to edit EN). - Add the locale's
_one/_few/_other. - No code change needed —
t("key", {count})resolves to the suffixed keys automatically.
- Verify the call site passes
- ⚠️ 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"— onlycounttriggers 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.