Adding, changing or translating interface strings in the SurfSense desktop app (surfsense_local). Use when adding a message to the code, moving hard-coded UI text into messages, translating into every language in LOCALES, or adding a language. Owns the key shape, the tone per language and the glossary.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The desktop app's interface text lives in surfsense_local/frontend/translations/, one ICU MessageFormat file per language. English is the source; every other language in LOCALES is translated from it. How it works: docs/architecture/localization.md.
| File | Holds |
|---|---|
translations/en.json | generated by formatjs extract from every defaultMessage in the code; never edited by hand |
translations/<code>.json (ja, de, …) | {"key": "ICU string"}, flat, sorted, two-space indent, written with the steps below |
glossary.md | terms that stay English, and the chosen term per language |
Only fixed text in the app's code is translated. Never user content, model output, model names or descriptions, file paths, or logs.
<feature>_<surface>_<purpose>, snake_case. <feature> is a folder under frontend/src/features/, or app for the shell. <purpose> is one of _title, _body, _label, _placeholder, _button, _tooltip, _empty, _error, _toast, _aria, _link, _status.sources_delete_confirm_title, not are_you_sure.<feature>_error_<code>.common key.’, never ': ICU uses ' as its escape character.{size, number, ::unit/gigabyte .#}, {percent, number, ::percent} (a fraction), {downloads, number, ::compact-short}. ICU cannot pick a unit, so megabytes and gigabytes are two messages.{count, number}, never a plain {count}, which prints raw digits. Ids and HTTP status codes stay plain: they are not amounts.Date, formatted in the message: {date, date, ::yyyyMMMd}.{name}, {count}. Plurals are ICU: {count, plural, one {# document} other {# documents}}; FormatJS prints # with the language's digit grouping. Never (s).intl.formatMessage({ id: "sources_list_empty", defaultMessage: "No sources yet" }), or intl.formatMessage({ id, defaultMessage }, { name }) with values. The id is literal and explicit. Import intl from @/i18n/intl. Then run pnpm translations in surfsense_local/frontend (or only pnpm translations:extract), which regenerates en.json; commit it with the code."Using <b>{name}</b> via {source}", rendered with intl.formatMessage({ id, defaultMessage }, { b: (chunks) => <span …>{chunks}</span> }). An element can also be a value: { time: <RelativeTime … /> }.intl.formatNumber, formatDate, formatRelativeTime, formatList, formatDisplayName, never a bare Intl.*, toLocale* or a raw {count} in JSX.New UI text, step by step:
intl.formatMessage({ id, defaultMessage }), following the rules above.pnpm translations in surfsense_local/frontend to regenerate en.json.LOCALES with the steps below.en.json and every translated file together.Find the keys. A key needs work in a language when it is missing there, or when its English changed since that language was last translated:
cd surfsense_local/frontend
base=$(git log -1 --format=%H -- translations/ja.json)
git diff "$base" -- translations/en.jsonRepeat for each language. Every added or changed line in that diff is a key to translate.
Read the context. The component that calls it (grep -rn '"<key>"' src). Translate the meaning in that place, not the English words.
Translate. Follow the tone below and glossary.md. Keep every placeholder name, tag name and select branch exactly. Write the plural categories the language has, not the English ones: for example, Japanese only other, German one and other.
Check. From surfsense_local/frontend: pnpm translations:verify (formatjs verify for missing and extra keys and matching placeholders), then node scripts/check_translations.mjs from the repo root. The formatjs-verify and check-translations pre-commit hooks run the same. Stop and fix on any failure; do not commit around it.
translations/<code>.json by translating every key in en.json with the steps above. Do this first: a code in LOCALES without a catalog fails the checks.LOCALES in frontend/src/i18n/locales.ts and electron/src/main/i18n/locales.ts. Nothing else holds a list: intl.ts globs the catalogs, and plural-categories.test.ts and check_translations.mjs read LOCALES.catalogFor() in resolve-locale.ts, which otherwise matches the base language: any Portuguese takes pt-BR, and Chinese takes zh-CN unless the tag asks for Traditional.glossary.md and a line under Tone.pnpm test.A right-to-left language needs more than a catalog: dir on the document, the left and right utility classes replaced with their logical forms, and the directional icons mirrored. None of that is built.
To check layout before any translation exists, pick English (Pseudo-Accents) (en-XA) under Settings › General in pnpm dev. It is longer, accented English; text that stays plain is not in a message.
保存, 削除). 〜してください for instructions. Full-width punctuation (。, 、), no space between Japanese and Latin text unless the product name needs it.다시 시도하세요). Buttons and labels are nouns (저장, 삭제). Standard 띄어쓰기; a particle attaches straight to a Latin name (SurfSense를) and is chosen by how the name is pronounced, but a placeholder that hides the ending takes 을(를).设置, 下载, 文件, 模型). Bare verbs for buttons (保存, 删除); 请 only where the English asks the user to act. Full-width punctuation (,, 。, “”), a half-width space between Chinese and Latin or digits (在 SurfSense 中, 4 GB), none next to full-width punctuation.सहेजें, हटाएं). Sentences end in ।; labels and fragments do not. Everyday technical terms in Devanagari with their nuqta (चैट, मॉडल, सेटिंग्स, फ़ाइल), never a coined Sanskrit equivalent.Speichern, Löschen).video not vídeo, este equipo not ordenador or computadora. Buttons are infinitives (Guardar, Eliminar). Open every question and exclamation (¿, ¡), and quote with «».Enregistrer, Supprimer). A no-break space before :, ;, ?, ! and inside « ».arquivo, baixar, tela, excluir), never European. Buttons are infinitives (Salvar, Excluir).Сохранить, Удалить). Guillemets «» around a quoted interface label. Every plural writes all four categories, each with its own noun form._button or _label has the room its English has. Write ’ for a visible apostrophe, never ', which ICU reads as its escape character.glossary.md in the same change.7fb479c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.