Skip to main content

add-ui-string

Add or change any text a player can see, in every locale the game ships in. Covers the full procedure — en.json first, real translations for the rest, INTENTIONAL_SAME.json only where a term genuinely does not translate, then the two vitest gates. Use for frontend strings and for Rust-side message keys.

معلومات المصدر

المستودع
openfootmanager/openfootmanager
آخر نشاط في المصدر
٣ أكتوبر ٢٠٢٦ في ١٥:٣٠
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٬١٤٧
التفرعات
١٩٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
add-ui-string
description
Add or change any text a player can see, in every locale the game ships in. Covers the full procedure — en.json first, real translations for the rest, INTENTIONAL_SAME.json only where a term genuinely does not translate, then the two vitest gates. Use for frontend strings and for Rust-side message keys.
when_to_use
Adding a label, button, tooltip, error message, news headline, inbox message, aria-label, or any other user-visible text. Also when changing existing wording, renaming a translation key, or when localeCoverage.test.ts or frontendKeyCoverage.test.ts fails.
argument-hint
[what the string says or the key you are adding]
allowed-tools
Read, Edit, Write, Grep, Glob, Bash(npm exec --no -- vitest run src/i18n), Bash(npm exec --no -- vitest run src/utils), Bash(npm run audit:i18n)
# Adding a user-facing string Follow the root Code quality section; use `i18n-auditor` for translation review and `ui-accessibility-reviewer` for changed accessible names. `/write-tests` and `ofm-test-reviewer` provide scenario and regression evidence; locale coverage alone does not prove wiring. OpenFoot Manager ships in **12 locales**. A string that exists only in English is a broken build, not a TODO. This is the project's most frequently violated rule, so follow the steps in order and finish with the tests. ## First: will a player read it? That is the test, and it is narrower than "a human will read it". If the answer is yes — anywhere in the UI, or a backend error the UI surfaces — this skill applies and the string needs all 12 locales. If the only reader is an **AI agent** (the MCP server's tool output) or a **modder at a terminal** (`ofm-cli` output and its scaffold templates, `docs/`), it stays **English** and this skill does not apply. The boundary inside the MCP server: its own private errors reach only the agent, through `tools.rs::err_result`. But an MCP function that propagates an error from a shared `application::` service which also backs a Tauri command (such as `application::live_match::*`) must keep the `be.error.*` key, because the UI shows that error on the Tauri path. Getting this wrong in the cautious direction is not free — a key no player will ever see still costs twelve translations and a row in every locale file, forever. Root `CLAUDE.md` rule 2 has the table; if a surface is genuinely ambiguous, ask. ## The 12 locales Source of truth: `SUPPORTED_LANGUAGES` in `src/i18n/index.ts`. | Code | Language | | Code | Language | |---|---|---|---|---| | `en` | English (source) | | `ru` | Russian | | `es` | Spanish | | `pt-BR` | Brazilian Portuguese | | `pt` | Portuguese | | `zh-CN` | Simplified Chinese | | `fr` | French | | `cs` | Czech | | `de` | German | | `tr` | Turkish | | `it` | Italian | | `id` | Indonesian | Files: `src/i18n/locales/<code>.json`. If `SUPPORTED_LANGUAGES` and this table ever disagree, `src/i18n/index.ts` wins — read it. It has grown before and will again: `id` was the twelfth, added in August 2026. This file and `src/CLAUDE.md` are the only two that state a count, because they are the only two that carry the list. The rest of the repository's docs say "every locale" on purpose: when `id` was added, a dozen files were left claiming eleven. Keep it that way. --- ## Procedure ### 1. Find the right key, don't invent a new one ```bash # Is this string, or something close, already translated? grep -rn "the exact english text" src/i18n/locales/en.json ``` Keys are nested and namespaced by feature (`squad.*`, `tactics.*`, `transfers.*`, `news.*`, `settings.*`). Put the new key where its siblings live. Reusing an existing key beats adding a near-duplicate — but do **not** reuse a key across contexts where a translator would need different wording (a noun label and a button verb are different keys even when English collapses them). ### 2. Add it to `en.json` first English is the source. Every other locale is validated against its key set. Use interpolation for anything dynamic — never build a sentence by concatenating translated fragments, because word order differs by language: ```jsonc // wrong — unassemblable in German or Turkish "signedFor": "signed for", // right "signedFor": "{{player}} signed for {{team}} for {{fee}}" ``` Pluralisation uses i18next suffixes (`_one`, `_other`, and the extra forms `ru` and `cs` need). If a count is involved, check how an existing pluralised key in `en.json` is written and match it. ### 3. Translate into the other 11 — properly Add the same key path to `cs`, `de`, `es`, `fr`, `id`, `it`, `pt`, `pt-BR`, `ru`, `tr`, `zh-CN`. - **Keep every interpolation placeholder identical.** `{{player}}` stays `{{player}}`; only the surrounding text and the word order change. - `pt` and `pt-BR` are genuinely different — European vs Brazilian vocabulary (*relvado* vs *gramado*, *equipa* vs *time*). Don't copy one into the other. - Football has established vocabulary in each language. Use the term a fan of that language would use, not a literal translation of the English. - `zh-CN` is Simplified Chinese. The font stack in `src/App.css` has CJK fallbacks — don't remove them. - If you genuinely cannot produce a confident translation for a locale, say so in your summary rather than shipping English text under a non-English key. The test will catch it anyway. ### 4. Match how the file already addresses the manager Most of these languages choose between a familiar and a polite second person — `du`/`Sie`, `tu`/`vous`, `ty`/`vy`, `kamu`/`Anda`, 你/您 — and that choice is not yours to make one string at a time. Each file has already made it. A string in the wrong register reads to a native speaker the way a stranger using your first name does: not wrong, exactly, but written by someone who wasn't paying attention. **Read four or five neighbouring values in the namespace you are editing, and copy their form.** Do not instead count pronouns across the whole file and follow the majority. Those counts are dominated by third-person text *about* players rather than text *to* the manager, and the markers are ambiguous in both directions. Spanish `su` is "his" far more often than polite "your". And a capital `Sie` only means formal "you" mid-sentence — at the start of one, where German capitalises regardless, `de.json` also uses it for "they" and for "it". One string carries the whole problem: ```text transfers.transferFeedbackCounterHeadline "Sie wollen mehr, bevor sie einschlagen." // They want more before shaking hands. ``` Both pronouns are the same word meaning the same thing — the club on the other side of the deal. Only sentence position capitalises the first. A grep for `Sie` counts one of them as formal address and misses the other entirely. Register follows **who is speaking**, which is why one file can hold both forms correctly. In `de.json`, a journalist's question under `match.press.*` is formal (`Sie`, throughout); a menu label, and the dialogue options the manager picks under `be.msg.playerEvent.options.*`, are familiar (`du`). Neither of those is a bug. Elsewhere in `de.json` the two forms are genuinely tangled — board correspondence under `be.msg.*` mixes them from one letter to the next. That is unresolved, not a pattern to copy: if the keys around yours disagree with each other, say so rather than picking one silently. ⚠️ **A polite form can force you to guess the manager's gender.** Formal Czech takes a plural auxiliary but keeps the participle singular and gendered: `uspořádal jste` says the manager is a man, `uspořádala jste` says she is a woman, and the game does not know. Rephrase so that nobody is the subject — `Tisková konference dnes už proběhla`, *a press conference has already taken place today*. Any locale that agrees a verb or adjective with the person being addressed can spring this. ### 5. `INTENTIONAL_SAME.json` — only for terms that truly don't translate `src/i18n/INTENTIONAL_SAME.json` allowlists keys whose value is legitimately identical to English: proper nouns, competition names, position abbreviations like `GK`. Entries are keyed by locale code, or `global` for all of them. This is an escape hatch for linguistics, **not** for unfinished work. If you find yourself adding several keys at once, you are using it wrong. ### 6. Backend strings are keys, not prose Rust never emits English text for the player. It emits a **translation key**, and the frontend resolves it: - `src/utils/backendI18n.ts` — the main mapping - `src/utils/backendI18nPlayerEvents.ts` — player event messages - `src/utils/backendI18n.legacy.ts` — keys kept for old saves So a new inbox message or news headline generated in `ofm_core` means: emit the key on the Rust side, map it in `backendI18n.ts` if the mapping isn't automatic, and add the key to every locale file. `src/utils/backendI18n.localeCoverage.test.ts` covers this half. ### 7. Run the gates ```bash npm exec --no -- vitest run src/i18n # localeCoverage + frontendKeyCoverage + index npm exec --no -- vitest run src/utils # backendI18n coverage, if you touched backend keys ``` - `localeCoverage.test.ts` — every locale has every `en.json` key, and no locale silently copies the English string (outside `INTENTIONAL_SAME.json`). - `frontendKeyCoverage.test.ts` — every literal `t("…")` key in `src/` exists in `en.json`. It parses the TypeScript AST, so typo'd keys fail too. Then the advisory sweep: ```bash npm run audit:i18n ``` **This command always exits 0.** It is a heuristic reporter over both `src/` and `src-tauri/`; read its output and check whether any candidate it lists is a string you just added. A clean run is not a pass — the vitest gates are. --- ## Checklist - [ ] Key added to `src/i18n/locales/en.json`, in the right namespace - [ ] Real translations added to all 11 other locales - [ ] Interpolation placeholders identical across every locale - [ ] `pt` and `pt-BR` translated separately - [ ] Form of address matches the neighbouring keys, and no wording assumes the manager’s gender - [ ] `INTENTIONAL_SAME.json` touched only for genuinely untranslatable terms - [ ] Backend keys mapped in `src/utils/backendI18n*.ts` if applicable - [ ] `npm exec --no -- vitest run src/i18n` green - [ ] `npm run audit:i18n` output read, not just run - [ ] Any new `aria-label` uses a translated string, not a hardcoded one
عرض على GitHub