diff --git a/AGENTS.md b/AGENTS.md
index eafbc646..ac145471 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -72,6 +72,11 @@ python scripts/sync_translation_keys.py
Locale files are in `locales/` (en, zh-CN, zh-TW, ja, ko, fr, de, es, ru, he).
+**Before translating anything, read `docs/i18n-translation-guidelines.md`** — it defines the
+term conventions (e.g. "Recipe" stays untranslated in French, 配方 in Chinese; model-type and
+brand names are never translated), per-locale preferred renderings, placeholder rules, and
+the known confusion hot-spots.
+
## Code Style
### Python
diff --git a/docs/i18n-translation-guidelines.md b/docs/i18n-translation-guidelines.md
new file mode 100644
index 00000000..5330aa75
--- /dev/null
+++ b/docs/i18n-translation-guidelines.md
@@ -0,0 +1,354 @@
+# i18n Translation Guidelines
+
+This document is the canonical set of conventions for translating LoRA Manager UI strings.
+It applies to **human translators and AI agents** alike. Read it before editing anything in
+`locales/`.
+
+Source of truth: `locales/en.json` (10 locales, 1810 leaf keys; all locales share the exact
+same key structure).
+
+Locales: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `he` (RTL).
+
+---
+
+## 1. Hard rules (do not violate)
+
+### R1 — Key structure is sacred
+- Only `locales/en.json` may add/remove/rename keys. All other locales must keep the exact
+ same nested key set. `tests/i18n/test_i18n.py` enforces this.
+- When a new UI string is added to `en.json`, run
+ `python scripts/sync_translation_keys.py` (adds the missing keys to all locales with
+ placeholder copies), then translate the newly added keys in every locale.
+- Never reorder, re-indent, or reformat a locale file "for tidiness". The sync script
+ preserves formatting; manual reformatting creates noisy diffs.
+
+### R2 — Placeholders and HTML must be preserved verbatim
+- `{name}`-style placeholders must appear in the translation exactly as in `en.json`.
+ Do not invent placeholders the source string does not have — the caller may not pass them
+ (example bug: `zh-CN recipes.controls.import.downloadLocationPreview` added `{path}`; the
+ template renders this key with no parameters, so the literal text `{path}` shows in the UI).
+- `{{...}}` in a locale value is an escaped literal brace — keep it identical.
+- Keep embedded HTML tags (e.g. `...`, `...`) intact.
+ You may move the tag around the sentence if the target language needs different word order.
+
+### R3 — Never translate or transliterate these
+- Model types: **LoRA, Checkpoint, Embedding, Diffusion Model**
+- Products/brands: **LoRA Manager, ComfyUI, CivitAI, CivArchive, HuggingFace, Ko-fi**
+- Ecosystem names: **LyCORIS, DoRA**, trigger-adjacent jargon **Prompt, Workflow**
+ (these are used as-is in the target-language SD community; see §2 per-language policy)
+- Theme names: **Nord, Midnight, Monokai, Dracula, Solarized**
+
+### R4 — The "Recipe" convention (the most important domain term)
+Product intent: a *Recipe* records a **LoRA combination + generation parameters**
+(prompt, seed, sampler, …) that reproduces an image style. The metaphor is a **cooking
+recipe** — "follow it and you get a similar dish". It is **not** a menu, not a dish list,
+not a prescription.
+
+Decision per language — translate only into a word whose everyday primary meaning is a
+cooking recipe; where that word would mislead users, **keep the English "Recipe(s)"**:
+
+| Locale | Use | Never use |
+|---|---|---|
+| fr | **Recipe / Recipes** (keep English) | recette(s) — cooking reading is secondary and it was explicitly judged misleading |
+| zh-CN / zh-TW | 配方 | 食谱 (reads as "food cookbook") |
+| ja | レシピ | — (leftover English "Recipe" in `initialization.recipes.title` / `toast.recipes.recipeSaved` → translate) |
+| ko | 레시피 | — |
+| de | Rezept / Rezepte | — (cooking meaning dominant; prescription reading acceptable) |
+| es | receta / recetas | — (cooking meaning dominant) |
+| ru | рецепт / рецепты | — (leftover English "Recipe" in `initialization.recipes.title` / `toast.recipes.recipeSaved` → translate) |
+| he | מתכון / מתכונים | — (cooking meaning dominant) |
+
+Whatever the choice, **one concept = one noun within a locale**. Currently violated in:
+- `fr` — "Recipe" (~97 keys, incl. nav) mixed with "recette" (~58 keys)
+- `zh-CN` / `zh-TW` — 配方 (126/122 keys) mixed with 食谱 / 食譜 (14/17 keys, all in the
+ *rematch* flow: `globalContextMenu.rematchRecipes.*`, `toast.recipes.rematch*`)
+- `de` — "Rezept" (136 keys) mixed with leftover English "Recipe" (5 keys)
+- `ja` / `ru` — leftover English "Recipe" in `initialization.recipes.title` ("Recipe Manager
+ zu initialisieren" / «Инициализация Recipe Manager») and `toast.recipes.recipeSaved`
+
+### R5 — One term, one rendering (within each locale)
+Same source word must not be translated several ways in one file. Known offender areas
+(see §5 for the full fix list): recipe, Checkpoint, Embedding, prompt, base model, preset,
+workflow, hash, metadata, tags, bulk. Every locale currently mixes variants of at least one
+of these — pick the preferred form in the §2 tables and normalize.
+
+### R6 — Register consistency
+- `zh-CN` / `zh-TW`: pick 你 or 您 once. Do not mix (zh-CN has 44×你 + 5×您; zh-TW has
+ 27×您 + 18×你).
+- `de`: pick "du" or "Sie" once (currently 143×Sie + ~7×du).
+- `es`: pick "tú" or "usted" once.
+
+### R7 — Punctuation per script
+- Full-width punctuation `:()` is correct **only in CJK locales** (zh-CN, zh-TW, ja, ko).
+- Latin/Cyrillic/Hebrew locales must use ASCII `: ()` — full-width colons leaked in there
+ are machine-translation artifacts. Known: `fr toast.recipes.createError/createFailed`,
+ `es toast.recipes.createError/createFailed` (e.g. "…de la receta:" should be "…de la receta:").
+- `fr` apostrophes must be U+2019 `'` / ASCII `'`, never a straight double quote:
+ `fr header.filter.allowSellingGeneratedContentTooltip` currently reads
+ `vendre d"images` → fix to `d'images`. Do not mix `'` and `'` in one file (fr has 299 vs 15).
+- Ellipsis: use ASCII `...` (project style). Don't introduce `…`.
+- Keep the sentence-ending period/omission consistent with the source string where the
+ language allows it.
+- `he` is RTL: mix of Hebrew and Latin scripts is normal; keep Latin term ordering natural.
+
+### R8 — No untranslated English leftovers
+Full sentences left byte-identical to `en.json` are bugs (brand names and URL placeholders
+are the exception). Every locale has them; see §6 for the per-locale checklist.
+
+### R9 — Mirror the source even when the source is wrong
+If `en.json` itself contains an inconsistency (e.g. the `Civitai` vs `CivitAI` casing split,
+or the `CivitArchive` typo in `modals.relinkCivitai.helpText.format4`), translate/transcribe
+it as-is in your locale and instead **fix the source** in `en.json` (then propagate by
+re-syncing and re-translating affected keys). Do not silently diverge in one locale only.
+
+---
+
+## 2. Per-language term maps
+
+Preferred rendering per term. "Fix" means the locale currently contains the wrong variant
+and must be normalized. `en` = keep the English word as-is.
+
+### fr
+
+| Term | Use | Fix |
+|---|---|---|
+| recipe | Recipe(s) | Replace all "recette(s)" (58 keys, e.g. `recipes.actions.deleteRecipeWithShortcut`, `toast.recipes.rematchComplete`) with "Recipe(s)" |
+| Checkpoint | Checkpoint | `statistics.modelTypes.checkpoint` = "Point de contrôle" → "Checkpoint" |
+| trigger words | mot(s)-clé(s) | unify: `modals.model.triggerWords.editWord` uses "mot déclencheur" — pick one |
+| prompt / negative prompt | Prompt / prompt négatif | — |
+| base model | modèle(s) de base | — |
+| preset | préréglage | unify: `modals.model.usageTips.addPresetParameter` "prédéfini", `toast.presets.restored` "par défaut" |
+| hash | hash | `conflictConfirm.message` "hachage" → "hash" |
+| tags | tags | `settings.sections.priorityTags` "Étiquettes" → "Tags" |
+| metadata | métadonnées | `loras.controls.refresh.fullTooltip` keeps English "metadata" |
+| duplicates | doublon(s) | unify with "dupliqué(e)s" |
+| bulk | groupé(e) | unify with "par lot / mode lot" variants |
+
+### de
+
+| Term | Use | Fix |
+|---|---|---|
+| recipe | Rezept/Rezepte | 5 leftover English "Recipe" keys → Rezept (e.g. `globalContextMenu.repairRecipes.label`, `toast.recipes.recipeSaved`) |
+| base model | pick Basis-Modell or Basismodell | currently 27× hyphenated vs 15× closed |
+| metadata | Metadaten | 4 keys use "Modelldaten" (`onboarding.steps.fetch.title/content`) → Metadaten |
+| bulk | pick Massen- or Sammelmodus | `loras.controls.bulk.action` = "Massen" reads as "crowds" — use "Massenbearbeitung"/"Mehrfachauswahl" |
+| register | Sie (formal) | 7 keys use "du/dein" (`settings.backup.managementHelp`, `modals.checkUpdates.message/tip`, `doctor.footer`, …) |
+
+### es
+
+| Term | Use | Fix |
+|---|---|---|
+| recipe | receta(s) | — |
+| Checkpoint | Checkpoint | 5 statistics keys "Punto(s) de control" → "Checkpoints" (`statistics.metrics.checkpoints`, `statistics.insights.unusedCheckpoints.*`, `statistics.modelTypes.checkpoint`) |
+| trigger words | palabra(s) de activación | 2 keys already use it; ~15 keys "palabra(s) clave" (reads as search keyword) → unify |
+| base model | modelo base | — |
+| preset | preajuste | 3 keys keep English "preset", 1 "preestablecido" → preajuste |
+| workflow | pick flujo de trabajo or workflow | currently 21× "flujo de trabajo" vs 10× "workflow" |
+| bulk | masivo / por lotes | unify; "Batch Import" → traducción |
+| tags | etiquetas | — |
+
+### ru
+
+| Term | Use | Fix |
+|---|---|---|
+| recipe | рецепт(ы) | English leftovers: `initialization.recipes.title`, `recipes.batchImport.*`, `toast.recipes.recipeSaved` → translate |
+| Checkpoint | Checkpoint (recommended) | 3 variants today: "Checkpoint" (17 keys), «Чекпойнт», «Контрольная точка» (statistics, 6 keys) — statistics MUST drop «Контрольная точка» |
+| Embedding | Embedding | «Эмбеддинг» variant exists in `settings.priorityTags.modelTypes.embedding` — unify |
+| prompt | промпт | 8 keys use «запрос» (reads as "database/HTTP request") → «промпт» |
+| base model | базовая модель | — |
+| preset | пресет | `header.theme.presets` "Предустановки" → пресеты |
+| workflow | Workflow (recommended) | «рабочий процесс» used in 4 keys — unify |
+| hash | pick хеш or хэш | both spellings co-occur |
+| tag(s) | тег(и) | — |
+| typos | — | `settings.misc.loraSyntaxFormatHelp`: «безпотерьного» → «беспотерьного» |
+
+### he
+
+| Term | Use | Fix |
+|---|---|---|
+| recipe | מתכון / מתכונים | — |
+| Checkpoint | Checkpoint | 5 statistics keys «נקודת/נקודות ביקורת» (road/security checkpoint) → "Checkpoint(s)" (`statistics.metrics.checkpoints`, `statistics.modelTypes.checkpoint`, `statistics.insights.unusedCheckpoints.*`) |
+| Embedding | Embedding | `statistics` keys use הטמעות → Embedding |
+| prompt | pick הנחיה or פרומפט | 9 keys הנחיה vs 3 פרומפט — unify (recommend פרומפט, SD-community loanword) |
+| preset | קביעה מראש | `header.filter.presetOverwriteConfirm` uses פריסט → unify |
+| hash | pick one of האש / גיבוב / hash | 3 variants co-occur — unify (recommend hash or גיבוב) |
+| metadata | pick מטא-דאטה or מטא-נתונים | 38 vs 17 keys — unify |
+| model | מודל | 13 keys use דגם/דגמים — unify |
+| bulk | pick one of 5 variants | 5 different renderings ("כמות גדולה", "המוני", "קבוצתי", "אצווה", …) — unify; `loras.controls.bulk.action` "כמות גדולה" reads as "large quantity" |
+
+### ja
+
+| Term | Use | Fix |
+|---|---|---|
+| recipe | レシピ | `initialization.recipes.title` keeps English "Recipe Manager" — translate to レシピマネージャー |
+| Checkpoint | Checkpoint or チェックポイント (pick one) | 3 variants: Checkpoint (~14), checkpoint lowercase (4), チェックポイント (4, e.g. `settings.priorityTags.modelTypes.checkpoint`) |
+| Embedding | Embedding | 4 keys lowercase "embedding" mid-sentence |
+| bulk | 一括 | `modals.checkUpdates.tip` "バルクモード" → 一括モード |
+| recipe counter | 件 or 個 | `repairRecipes.success` uses 件, `.cancelled` uses 個 — unify |
+
+### ko
+
+| Term | Use | Fix |
+|---|---|---|
+| recipe | 레시피 | — |
+| Checkpoint | Checkpoint (recommended) | 4 keys transliterate 체크포인트 (`settings.priorityTags.modelTypes.checkpoint`, `toast.recipes.missingCheckpointPath/missingCheckpointInfo/downloadCheckpointFailed`) |
+| Embedding | Embedding | 3 keys 임베딩 (`settings.priorityTags.modelTypes.embedding`, `uiHelpers.nodeSelector.embedding`) |
+| base model | 베이스 모델 | 6 keys «기본 모델» read as "default model" → 베이스 모델 (`settings.downloadSkipBaseModels.*`, `toast.loras.downloadSkippedByBaseModel`) |
+| workflow | pick 워크플로 or 워크플로우 | 26 vs 6 keys — unify |
+| bulk | 일괄 | `modals.checkUpdates.tip` "벌크 모드" → 일괄 모드 |
+| tag logic | — | `header.filter.tagLogicAny` = "모든 태그 일치 (OR)" is **inverted** (should be "하나 이상의 태그 일치") and identical to `tagLogicAll` |
+| particle | — | `modelCard.sendToWorkflow.checkpointNotImplemented`: "Checkpoint을" → "Checkpoint를" |
+
+### zh-CN / zh-TW
+
+| Term | zh-CN | zh-TW |
+|---|---|---|
+| recipe | 配方 (fix 食谱 → 配方, 14 keys in rematch flow) | 配方 (fix 食譜 → 配方, 17 keys in rematch flow) |
+| Checkpoint | Checkpoint (fix 检查点 → Checkpoint, 5 keys: `toast.recipes.missingCheckpointPath/missingCheckpointInfo/downloadCheckpointFailed`, `modelCard.actions.checkpointNameCopied`, `modelCard.sendToWorkflow.checkpointNotImplemented`) | Checkpoint (fix 檢查點 → Checkpoint, 4 keys: `modelCard.actions.copyCheckpointName`, `toast.recipes.missing*`×2, `toast.recipes.downloadCheckpointFailed`) |
+| base model | 基础模型 (fix 基模型 → 基础模型, 3 keys in `modals.model.versions.filters.*`) | 基礎模型 ✓ consistent |
+| prompt | 提示词 ✓ | 提示詞 ✓ |
+| preset | 预设 ✓ | 預設 ✓ |
+| workflow | 工作流 ✓ | 工作流 ✓ |
+| trigger words | 触发词 ✓ | 觸發詞 ✓ |
+| hash | 哈希 (哈希值 variant OK) | 雜湊 ✓ |
+| register | 你 (fix 5×您 → 你) | 您 (fix 18×你 → 您) |
+
+---
+
+## 3. Cross-cutting confusion hot-spots (must-fix list)
+
+Ordered by impact. Key paths refer to the current broken values documented in §2.
+
+1. **Checkpoint rendered as a literal security/road checkpoint** — fr, es, ru, he, zh-CN,
+ zh-TW all have 4–6 keys in the `statistics.*` domain that must revert to "Checkpoint".
+ This is the single most misleading pattern in the codebase.
+2. **"recipe" variants that break the one-noun rule** — fr "recette" (keep Recipe), zh
+ 食谱/食譜 (use 配方), de/ja/ru leftover English "Recipe".
+3. **ko `header.filter.tagLogicAny`** — inverted semantics + identical to `tagLogicAll`.
+4. **ja `modals.model.versions.actions.viewLocalTooltip`** = "近日対応予定" ("coming soon") —
+ stale string from an old version of the key; the button actually shows local versions.
+5. **Stale help texts that no longer describe the current `en.json` source** (the
+ en string changed, the translation is from an older wording):
+ - `settings.downloadSkipBaseModels.help` — es/ko/ja/ru describe "applies to all download
+ flows / only supported base models can be selected", source says "versions using the
+ selected base models will be skipped"
+ - `settings.aiProvider.providerHelp` / `apiBaseHelp` — ru/es/fr hard-code provider names
+ or invent "leave empty" instructions not in source
+ - `settings.hideEarlyAccessUpdates.help` — fr/ja are truncated fragments of the current
+ source sentence
+6. **en.json source bugs** (fix in `en.json` first, then re-sync + retranslate):
+ - "Civitai" (50×) vs "CivitAI" (11×) casing split — pick the official "CivitAI" and propagate
+ - `modals.relinkCivitai.helpText.format4` — "CivitArchive" typo → "CivArchive"
+ - `zh-CN recipes.controls.import.downloadLocationPreview` — remove the invented `{path}`
+
+---
+
+## 4. Placeholder contract deviations (current)
+
+`{...}` token sets differ from `en.json` in these keys (callers pass the full param set, so
+they render today, but they violate the contract and would break if the caller changes):
+
+| Locale | Key | Deviation |
+|---|---|---|
+| zh-CN | `modals.checkUpdates.title`, `.message` | `{typePlural}` → `{type}` — restore `{typePlural}` |
+| zh-TW | `modals.checkUpdates.title`, `.message` | same |
+| ja | `modals.checkUpdates.title`, `.message` | same |
+| ko | `modals.checkUpdates.title`, `.message` | same |
+| zh-CN | `recipes.controls.import.downloadLocationPreview` | **adds** `{path}` the source lacks (renders literally) — remove |
+| zh-TW | `toast.settings.mappingsUpdated` | drops `{plural}` (`({count} 個對應)`) — acceptable (no plural morphology) but keep source token if possible |
+| zh-TW | `toast.controls.refreshFailed` | drops `{action}` — restore |
+| ko | `toast.settings.mappingsUpdated` | drops `{plural}` — acceptable in Korean, keep if possible |
+
+---
+
+## 5. One term, one rendering — offender matrix
+
+Cross-locale summary of §2 inconsistencies. "✓" = already consistent.
+
+| Term | fr | de | es | ru | he | ja | ko | zh-CN | zh-TW |
+|---|---|---|---|---|---|---|---|---|---|
+| recipe | ✗ Recipe/recette | ✗ Rezept/Recipe | ✓ receta | ✗ рецепт/Recipe | ✓ מתכון | ✗ レシピ/Recipe | ✓ 레시피 | ✗ 配方/食谱 | ✗ 配方/食譜 |
+| Checkpoint | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
+| Embedding | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
+| prompt | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
+| base model | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
+| preset | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
+| workflow | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ |
+| hash | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
+| metadata | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ |
+| tags | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
+| duplicates | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
+| bulk | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ |
+
+---
+
+## 6. Untranslated English leftovers (per locale)
+
+Values byte-identical to `en.json` that are actual UI sentences (brand names and URL
+placeholders are excluded). Translate them.
+
+| Feature area | fr | de | es | ru | he | ja | ko | zh-CN | zh-TW |
+|---|---|---|---|---|---|---|---|---|---|
+| `recipes.batchImport.*` (whole modal, ~25–56 keys) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
+| `toast.recipes.batchImport*` (7 keys) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
+| `globalContextMenu.fetchMissingLicenses.*` (5 keys) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
+| `banners.communitySupport.*` (4 keys incl. long prose) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ |
+| `modals.model.license.noImageSell/noRentCivit/noRent/noSell` | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
+| `doctor.*` (subset: issues/actions/labels) | ✗ | ✗ | ✗ | ✗ | ✗ | — | — | — | — |
+| `toast.recipes.recipeSaved` | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | — | — |
+| `toast.settings.libraryLoadFailed / libraryActivateFailed` | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
+| `toast.api.moveFailed` | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
+| `settings.extraFolderPaths.restartRequired` | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
+| `checkpoints.modelTypes.diffusion_model` | — | ✗ | — | ✗ | — | ✗ | ✗ | — | — |
+| `sidebar.dragDrop.moveUnsupported` | — | — | — | — | — | — | — | ✗ | ✗ |
+| `initialization.recipes.title` (English "Recipe Manager") | — | — | — | ✗ | — | ✗ | — | — | — |
+| `uiHelpers.workflow.noPromptTargets` menu path line | — | — | ✗ | ✗ | — | — | — | ✗ | — |
+
+Legend: ✗ = untranslated (needs work), ✓ = translated and consistent, — = not flagged in
+the audit (assume translated; verify).
+
+Note on the license labels (`modals.model.license.*`): if they are intentionally kept in
+English (CivitAI license boilerplate), make that decision explicit for all locales instead of
+leaving it inconsistent — `zh-CN` already translates the sibling `creditRequired`.
+
+---
+
+## 7. Workflow for agents and translators
+
+### Adding a new UI string
+1. Add the key to `locales/en.json` only.
+2. Run `python scripts/sync_translation_keys.py` — it inserts the key into the other 9
+ locales (as an English placeholder) preserving formatting.
+3. Translate the new key in every locale, applying §1–§3 (placeholders verbatim, Recipe
+ rule, term maps, register).
+4. If the new string contains new terminology, extend §2 tables.
+
+### Fixing a translation bug
+1. Locate the key (dotted path) in the relevant locale file.
+2. Check the corresponding `en.json` value and the actual caller (grep `static/js` or
+ `web/comfyui` for the key) to learn which placeholders are passed.
+3. Fix trivially; for normalization sweeps (e.g. "recette" → "Recipe"), do it file-wide for
+ the offending keys only — do not touch unrelated lines.
+4. If the bug is in `en.json` itself (R9), fix the source first, then re-sync and update all
+ locales.
+
+### Verification
+```bash
+pytest tests/i18n/test_i18n.py # key parity + JSON validity + JS key references
+python scripts/sync_translation_keys.py --dry-run # shows which keys would change; add --verbose for per-key detail
+npm test # frontend tests incl. i18n helpers
+```
+
+`pytest tests/i18n` only checks structure. Quality conventions in this document are not
+machine-enforced — a human/agent review pass is required.
+
+### Anti-patterns checklist
+- [ ] Placeholders `{x}` / `{{x}}` differ from `en.json`
+- [ ] Same source term translated 2+ ways in the same file (see §5)
+- [ ] "Checkpoint" became a literal checkpoint; "recipe" became menu/prescription/food-cookbook
+- [ ] Brand names translated or transliterated (LoRA, CivitAI, ComfyUI, …)
+- [ ] Latin locale using full-width `:()`; fr using `"` as apostrophe
+- [ ] Mixed 你/您, du/Sie, tú/usted
+- [ ] Full English sentences left behind (see §6)
+- [ ] Register/typos/mojibake; source string is stale vs `en.json` (compare semantics, not
+ just words)
\ No newline at end of file