Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .backlog/FIX-I18N-DEBT-REPAYMENT-2/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Status: ready

# FIX-I18N-DEBT-REPAYMENT-2 — Repay the i18n debt revived by FIX-I18N-STALE-COMMENT-PARSING

Follow-up to `FIX-I18N-STALE-COMMENT-PARSING` (PR #119, merged), which revived 56 keys wrongly
marked `-- STALE:` in all four dictionaries. Reuses the decisions grilled for
`FIX-I18N-DEBT-REPAYMENT` (PR #116) without re-grilling — same task shape, same constraints.

## Problem Statement

Reviving those 56 keys restored English text everywhere and recovered every FR translation that
had been sitting inert in dead comments, but left genuine translation gaps behind: entries that
were never translated even before the STALE-marking bug existed. A Mission Maker playing in
Korean or Spanish still hits English fallback text for these.

## Solution

Fill the remaining empty stubs directly (translations produced and written into the dictionary
files without an external API/CLI call — no `ANTHROPIC_API_KEY` available, and the Claude Code CLI
fallback added by `TOOLING-I18N-CLAUDE-CODE-TRANSLATE` can't be invoked from inside the Claude Code
session doing this work, same nested-session constraint hit during `FIX-I18N-DEBT-REPAYMENT`).

## User Stories

1. As a Mission Maker playing CTLD in Korean, I want the remaining F10/vehicle-category menu
entries translated, so I don't hit English fallback text for labels that have been live (just
never translated) since before the STALE-marking bug.
2. As a Mission Maker playing CTLD in Spanish, I want the same for the 7 vehicle-category labels
still empty there.
3. As a maintainer, I want the repayment scoped to whatever is empty at execution time (22 KO + 7
ES counted right after `FIX-I18N-STALE-COMMENT-PARSING` merged), not a number frozen at lot
creation — consistent with `FIX-I18N-DEBT-REPAYMENT`'s dynamic-scope decision.
4. As a maintainer, I want no dedicated KO/ES linguistic review, consistent with the accepted
mechanism from `FIX-I18N-DEBT-REPAYMENT` and `BUILD-DICT-AI-TRANSLATE` — verification is
mechanical (zero remaining empty stubs) plus normal PR diff review.

## Implementation Decisions

- Same as `FIX-I18N-DEBT-REPAYMENT`: translations produced directly (no `translate_i18n.py`
invocation), written into `src/CTLD_i18n_ko.lua` / `src/CTLD_i18n_es.lua` only — `fr` and `en`
are already complete after `FIX-I18N-STALE-COMMENT-PARSING`.
- Dynamic scope: every entry still empty at execution time, not a frozen list.
- No new tooling, no change to `translate_i18n.py`, `i18n_dict_utils.py`,
`generate_i18n_dicts.ps1`, `check_i18n_diff.py`, or the `i18n-guard` CI job.

## Testing Decisions

- No new or changed logic — existing `tools/build/` test suite (24 tests) must stay green,
untouched by this lot.
- Completeness verified mechanically: zero remaining empty entries in `CTLD_i18n_ko.lua` /
`CTLD_i18n_es.lua` (outside `__keep_en`), and a clean `generate_i18n_dicts.ps1` dry-run (no
`MISSING`, no unexpected `STALE`).

## Out of Scope

- Any change to i18n tooling code (translation content only).
- `CTLD_i18n_en.lua` / `CTLD_i18n_fr.lua` (already complete).
- The companion asset-check deprecation (separate roadmap item, David's call to defer).

## Further Notes

- Prior art: `FIX-I18N-DEBT-REPAYMENT` (PR #116) — identical task shape and decisions.
- Source of the debt: `FIX-I18N-STALE-COMMENT-PARSING` (PR #119) revived 56 keys; 29 of their
KO/ES translations (22 KO, 7 ES) were never produced in the first place.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Status: ready

# 01 — Repay KO + ES i18n stub debt (round 2)

## Parent

`.backlog/FIX-I18N-DEBT-REPAYMENT-2/PRD.md`

## What to build

Translate every remaining empty stub in `src/CTLD_i18n_ko.lua` and `src/CTLD_i18n_es.lua` directly
(no `ANTHROPIC_API_KEY`, Claude Code CLI unusable from a nested session), matching the exact method
used in `FIX-I18N-DEBT-REPAYMENT` ticket 01. Scope is dynamic — whatever is empty at execution time.

## Acceptance criteria

- [x] `src/CTLD_i18n_ko.lua` has zero entries equal to `""` outside its `__keep_en` block (22
translated; `FARP / FOB` added to `__keep_en` instead of translated — acronym, no
translatable content).
- [x] `src/CTLD_i18n_es.lua` has zero entries equal to `""` outside its `__keep_en` block (7
translated; `FARP / FOB` added to `__keep_en` same as KO).
- [x] `src/CTLD_i18n_en.lua` / `src/CTLD_i18n_fr.lua` unchanged.
- [x] No key added, removed, or renamed — only existing empty values filled in, plus one
`__keep_en` addition per language.
- [x] `pytest tools/build/` still passes (24/24, unchanged).
- [x] `generate_i18n_dicts.ps1` dry-run reports `OK` on all four dictionaries.
- [x] `CHANGELOG.md` `[Unreleased]` updated.
- [ ] PR opened against `develop`.

## Blocked by

None - can start immediately
1 change: 1 addition & 0 deletions .backlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ authored **per lot, when the lot is started** (not in batch).
| `CHORE-UNTRACK-BUILT-ENGINE` | merged (PR #110) | `CTLD.lua` is generated and committed anyway. `.gitignore` line 5 calls it deliberate — *"available at repo root for DCS missions"* — a bootstrap-era reason that no longer holds: nothing points at it, and **VMCT, the assumed consumer, does not** (its `vendored.yaml` pins `2.0.0-rc3` with *"re-download the CTLD.lua asset from the matching release"* and watches `github-release`). The cost is paid every time: **26 of the 28 merges touching `src/` over 30 days carried the regenerated file** — a one-megabyte generated diff nobody reviews, and a guaranteed conflict between parallel PRs. The trap, measured rather than assumed: `python-quality` runs on ubuntu and never builds the engine, so deleting the file alone would drop the suite from **262 passed** to **234 passed / 27 skipped / 1 failed** while CI stayed green. So: build the engine in that job first (`merge_CTLD.ps1` made portable — two `\` paths), fix `test_inject_into_miz` (it crashes instead of skipping), then untrack. Depends on `FEAT-DEV-BUILD-CHANNEL`, which is what keeps the engine downloadable. No history rewriting (471 blobs = 2.8 MiB packed). | `chore/untrack-built-engine` |
| `FEAT-CUSTOM-BEACON-SOUNDS` | merged (PR #112) | A beacon sound the Mission Maker chooses, instead of a text box naming a file the tool never installs. Grilled with Zip on 2026-08-08: custom is **derived** from `radioSound` (no second key that could disagree with the engine); a chosen file enters the mission under a **reserved name** (**ADR 0012**) because a Mission Maker whose own file is called `beacon.ogg` would otherwise see it silently overwritten; the original name survives as a schema-only label (`FIX-TOOL-I18N-LANG`'s lesson — a catalogue key would make every pre-lot configuration report a missing setting at mission start); the bytes are read at selection and live in the session, so reopening a `.miz` reinstalls them **on another machine with the original file gone**. `OggS` checked, no size cap, nothing deleted from the archive. | `feature/custom-beacon-sounds` |
| `FEAT-DEV-BUILD-CHANNEL` | merged (PR #109) | An exe to hand a tester between two releases. Zip's first idea — the exe grafting an arbitrary `CTLD.lua` into a copy of itself — **works** (verified: rc6 + 1.17 MB appended still runs) and was dropped anyway: it pairs a new engine with the exe's older schema and interface, an unsigned exe altered after the build reads as tampered, and `--version` would keep lying. The `build-exe` job already produces a complete exe from a commit in **2 min 06 s** on free public-repo runners; it only lacked a trigger. Built on every merge into `develop`, published as an artifact **and** a floating `dev` pre-release (an artifact answers `401` to an anonymous download), versioned `<ctld version>-<commit hash>`. | `feature/dev-build-channel` |
| [`FIX-I18N-DEBT-REPAYMENT-2`](FIX-I18N-DEBT-REPAYMENT-2/PRD.md) | merged (PR #120) | Follow-up to `FIX-I18N-STALE-COMMENT-PARSING` (PR #119), which revived 56 keys wrongly marked `-- STALE:` in all four dictionaries — 29 of their KO/ES translations (22 KO, 7 ES) were never produced in the first place (genuine debt, not part of the bug). Same method as `FIX-I18N-DEBT-REPAYMENT`: direct translation (no `ANTHROPIC_API_KEY`, Claude Code CLI unusable from a nested session), dynamic scope, no dedicated linguistic review. | `fix/i18n-debt-repayment-2` |
| [`FIX-I18N-STALE-COMMENT-PARSING`](FIX-I18N-STALE-COMMENT-PARSING/PRD.md) | merged (PR #119) | The shared i18n dict parser (`i18n_dict_utils.py`), `generate_i18n_dicts.ps1`'s `Get-DictKeys`, and `translate_i18n.py`'s `_apply_translations` all matched a `ctld.i18n[...] = "..."` line regardless of a `-- STALE:` comment prefix. Fixing it and re-running `-Apply` for real surfaced a production bug much bigger than the KO/ES cosmetic drift this lot was scoped for: **56 keys — every AA system component label plus several F10 menu labels — were wrongly marked `-- STALE:` in all four dictionaries including English**, breaking F10/AA-system text in every language. `-Apply` revived all 56; the FR/ES/KO translations sitting in the old dead comments were hand-recovered into the fresh entries rather than lost. No ADR — a bug fix, not a design trade-off. | `fix/i18n-stale-comment-parsing` |
| [`TOOLING-I18N-CLAUDE-CODE-TRANSLATE`](TOOLING-I18N-CLAUDE-CODE-TRANSLATE/PRD.md) | merged (PR #118) | `tools/build/translate_i18n.py` only auto-translates i18n stubs when `ANTHROPIC_API_KEY` is set (separate Anthropic Console billing) — a contributor with a Claude Code subscription but no such key gets nothing, lived concretely on `FIX-I18N-DEBT-REPAYMENT` (140 entries translated by hand for want of a key). Adds a fallback: when the API key is absent, shell out to the Claude Code CLI (`claude -p`, `--model claude-haiku-4-5-20251001` pinned) instead — dual mode, API path unchanged/first-priority, no pre-flight availability check (same non-blocking `try/except` as today), combined warning when neither is available. CI (`i18n-guard`) untouched — inspects dictionary content only, never the translation mechanism. See **ADR 0014**. | `tooling/i18n-claude-code-translate` |
| [`FIX-I18N-DEBT-REPAYMENT`](FIX-I18N-DEBT-REPAYMENT/PRD.md) | merged (PR #116) | Follow-up to `FIX-I18N-DICT-GUARD` (PR #115): repays the pre-existing i18n translation debt the guard deliberately left alone (93 empty `CTLD_i18n_ko.lua` entries + 78 empty `CTLD_i18n_es.lua` entries, counted at merge `cfb7cd6`, above the 91/76 ADR 0013 baseline — a few more landed before the guard shipped). No `ANTHROPIC_API_KEY` available, so the 70 live entries per language (the rest were `-- STALE:` dead keys, excluded) were translated directly rather than via `translate_i18n.py`; `JTAC` and `%1 [%2] %3.` added to `__keep_en`. No code changes — translation content only, no new tooling. | `fix/i18n-debt-repayment` |
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ Versioning follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed — remaining KO/ES i18n debt from the revived stale keys repaid (FIX-I18N-DEBT-REPAYMENT-2)

- **22 KO + 7 ES entries translated**: genuine debt among the 56 keys `FIX-I18N-STALE-COMMENT-PARSING`
revived — never translated even before that bug, not part of it. `CTLD_i18n_en.lua` and
`CTLD_i18n_fr.lua` were already complete after that fix.
- `FARP / FOB` added to KO's and ES's `__keep_en` block (acronym, no translatable content) —
the same mechanism already used for `JTAC`, `CTLD`, etc.
- No tooling changed, translation content only. `pytest tools/build/` 24/24 green (unchanged);
`generate_i18n_dicts.ps1` dry-run reports `OK` on all four dictionaries.

### Fixed — 56 i18n keys wrongly marked stale, breaking AA system / crate labels in every language (FIX-I18N-STALE-COMMENT-PARSING)

- **The i18n tooling's dictionary parser matched a `-- STALE:`-commented line the same as a live
Expand Down
15 changes: 8 additions & 7 deletions src/CTLD_i18n_es.lua
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ ctld.i18n["es"].__keep_en = {
["%1 [%2] %3."] = true,
["%1\nFOB @ %2"] = true,
["CTLD"] = true,
["FARP / FOB"] = true,
["JTAC"] = true,
}

Expand All @@ -553,13 +554,13 @@ ctld.i18n["es"]["Cannot deploy %1: AA system limit reached (%2/%3)"] = "No se pu
ctld.i18n["es"]["Cannot repair %1. No damaged %1 within %2m"] = "No se puede reparar %1. No hay %1 dañado en %2m"

--- Keys added by generate_i18n_dicts.ps1 on 2026-07-22
ctld.i18n["es"]["Air Defense (AA)"] = ""
ctld.i18n["es"]["Aircraft"] = ""
ctld.i18n["es"]["FARP / FOB"] = ""
ctld.i18n["es"]["Ground Vehicles"] = ""
ctld.i18n["es"]["Helicopters"] = ""
ctld.i18n["es"]["Infantry"] = ""
ctld.i18n["es"]["Ships"] = ""
ctld.i18n["es"]["Air Defense (AA)"] = "Defensa aérea (AA)"
ctld.i18n["es"]["Aircraft"] = "Aeronaves"
ctld.i18n["es"]["FARP / FOB"] = "FARP / FOB"
ctld.i18n["es"]["Ground Vehicles"] = "Vehículos terrestres"
ctld.i18n["es"]["Helicopters"] = "Helicópteros"
ctld.i18n["es"]["Infantry"] = "Infantería"
ctld.i18n["es"]["Ships"] = "Barcos"

--- Keys added by generate_i18n_dicts.ps1 on 2026-07-30
ctld.i18n["es"]["%1 setting(s) absent from the mission config — CTLD default used: %2"] = "%1 ajuste(s) ausente(s) de la configuración de la misión — se usó el valor por defecto de CTLD: %2"
Expand Down
45 changes: 23 additions & 22 deletions src/CTLD_i18n_ko.lua
Original file line number Diff line number Diff line change
Expand Up @@ -335,16 +335,16 @@ ctld.i18n["ko"]["%1 crate safely released."] = "%1 화물이 안전하게 분리
ctld.i18n["ko"]["%1 packed into %2 crate(s)."] = "%1이(가) %2개의 화물로 포장되었습니다."
ctld.i18n["ko"]["[activate]"] = "[활성화]"
ctld.i18n["ko"]["[deactivate]"] = "[비활성화]"
ctld.i18n["ko"]["Cannot build %1\n%2\n\nOr the crates are not close enough together"] = ""
ctld.i18n["ko"]["Cannot build %1\n%2\n\nOr the crates are not close enough together"] = "%1을(를) 건설할 수 없습니다\n%2\n\n또는 화물들이 충분히 가깝게 모여 있지 않습니다"
ctld.i18n["ko"]["Cargo: [%1] — %2 troops, %3 kg"] = "화물: [%1] — 병력 %2명, %3 kg"
ctld.i18n["ko"]["Clear Mine Field (%1 mines, ~%2 m)"] = "지뢰밭 제거 (지뢰 %1개, ~%2 m)"
ctld.i18n["ko"]["Crate Commands"] = "화물 명령"
ctld.i18n["ko"]["Cut Slingload"] = "슬링로드 절단"
ctld.i18n["ko"]["Drop Blue Smoke"] = ""
ctld.i18n["ko"]["Drop Crate(s)"] = ""
ctld.i18n["ko"]["Drop Green Smoke"] = ""
ctld.i18n["ko"]["Drop Orange Smoke"] = ""
ctld.i18n["ko"]["Drop Red Smoke"] = ""
ctld.i18n["ko"]["Drop Blue Smoke"] = "청색 연막 투하"
ctld.i18n["ko"]["Drop Crate(s)"] = "화물 투하"
ctld.i18n["ko"]["Drop Green Smoke"] = "녹색 연막 투하"
ctld.i18n["ko"]["Drop Orange Smoke"] = "주황색 연막 투하"
ctld.i18n["ko"]["Drop Red Smoke"] = "적색 연막 투하"
ctld.i18n["ko"]["Extracted [%1] (%2 troops)."] = "추출됨 [%1] (%2명)."
ctld.i18n["ko"]["FOB 90% complete - final installation in progress..."] = "FOB 90% 완료 - 최종 설치 진행 중..."
ctld.i18n["ko"]["FOB construction started by %1."] = "%1이(가) FOB 건설을 시작했습니다."
Expand All @@ -356,22 +356,22 @@ ctld.i18n["ko"]["into %1"] = "%1(으)로"
ctld.i18n["ko"]["into combat"] = "전투로"
ctld.i18n["ko"]["JTAC"] = "JTAC"
ctld.i18n["ko"]["JTAC Group 2"] = "JTAC 그룹 2"
ctld.i18n["ko"]["JTAC Status"] = ""
ctld.i18n["ko"]["JTAC Status"] = "JTAC 상태"
ctld.i18n["ko"]["Land near troops to extract them (%1m away)."] = "병력 근처에 착륙하여 추출하세요 (%1m 거리)."
ctld.i18n["ko"]["Load "] = ""
ctld.i18n["ko"]["Load / Extract Vehicles"] = ""
ctld.i18n["ko"]["Load "] = "적재 "
ctld.i18n["ko"]["Load / Extract Vehicles"] = "차량 적재 / 추출"
ctld.i18n["ko"]["Load from %1"] = "%1에서 적재"
ctld.i18n["ko"]["Loaded: %1 (%2 troops)."] = "적재됨: %1 (%2명)."
ctld.i18n["ko"]["Mine Field"] = "지뢰밭"
ctld.i18n["ko"]["Mine Field cleared by %1."] = "%1이(가) 지뢰밭을 제거했습니다."
ctld.i18n["ko"]["Missing %1\n"] = ""
ctld.i18n["ko"]["Missing %1\n"] = "누락: %1\n"
ctld.i18n["ko"]["No active JTACs."] = "활성화된 JTAC 없음."
ctld.i18n["ko"]["No crates loaded."] = "적재된 화물 없음."
ctld.i18n["ko"]["No extractable troops nearby!"] = ""
ctld.i18n["ko"]["No extractable troops nearby!"] = "근처에 추출 가능한 병력이 없습니다!"
ctld.i18n["ko"]["No slingloaded crate on board."] = "탑재된 슬링로드 화물 없음."
ctld.i18n["ko"]["no target"] = "목표 없음"
ctld.i18n["ko"]["No troops onboard and no extractable troops nearby."] = "탑승 병력 없음, 근처에 추출 가능한 병력도 없음."
ctld.i18n["ko"]["Out of parts for AA Systems. Current limit is %1\n"] = ""
ctld.i18n["ko"]["Out of parts for AA Systems. Current limit is %1\n"] = "AA 시스템 부품 부족. 현재 한도는 %1\n"
ctld.i18n["ko"]["Parachute Crates"] = "화물 낙하산 투하"
ctld.i18n["ko"]["Parachute Troops"] = "병력 낙하산 투하"
ctld.i18n["ko"]["Parachute Vehicle"] = "차량 낙하산 투하"
Expand All @@ -395,7 +395,7 @@ ctld.i18n["ko"]["Too high! %1 crate destroyed on impact."] = "너무 높습니
ctld.i18n["ko"]["Too low to hook crate.\n\nHold hover for %1 seconds"] = "너무 낮아서 화물을 걸 수 없습니다.\n\n%1초 동안 정지비행을 유지하세요"
ctld.i18n["ko"]["Troop Commands"] = "병력 명령"
ctld.i18n["ko"]["Troops returned to base."] = "병력이 기지로 복귀했습니다."
ctld.i18n["ko"]["Unload Vehicles"] = ""
ctld.i18n["ko"]["Unload Vehicles"] = "차량 하역"
ctld.i18n["ko"]["Vehicle Commands"] = "차량 명령"
ctld.i18n["ko"]["You must land inside the pickup zone to load troops."] = "병력을 태우려면 픽업 구역 안에 착륙해야 합니다."
ctld.i18n["ko"]["You must land to extract troops."] = "병력을 추출하려면 착륙해야 합니다."
Expand All @@ -407,6 +407,7 @@ ctld.i18n["ko"].__keep_en = {
["BRDM-2"] = true,
["BTR-D"] = true,
["CTLD"] = true,
["FARP / FOB"] = true,
["JTAC"] = true,
["LPWS C-RAM"] = true,
["MLRS"] = true,
Expand All @@ -415,20 +416,20 @@ ctld.i18n["ko"].__keep_en = {

--- Keys added by generate_i18n_dicts.ps1 on 2026-07-22
ctld.i18n["ko"]["%1 successfully deployed a full %2 in the field.\n\nAA Active System limit: %3\nActive: %4"] = "%1이(가) %2을(를) 현장에 성공적으로 배치했습니다.\n\nAA 활성 시스템 한도: %3\n활성: %4"
ctld.i18n["ko"]["%1 successfully rearmed a full %2 in the field"] = ""
ctld.i18n["ko"]["%1 successfully repaired a full %2 in the field."] = ""
ctld.i18n["ko"]["%1 successfully rearmed a full %2 in the field"] = "%1이(가) 현장에서 %2을(를) 완전히 재무장했습니다"
ctld.i18n["ko"]["%1 successfully repaired a full %2 in the field."] = "%1이(가) 현장에서 %2을(를) 완전히 수리했습니다."
ctld.i18n["ko"]["AI deployed a full %1.\n\nAA Active System limit: %2\nActive: %3"] = "AI가 %1을(를) 완전히 배치했습니다.\n\nAA 활성 시스템 한도: %2\n활성: %3"
ctld.i18n["ko"]["Cannot deploy %1: AA system limit reached (%2/%3)"] = "%1을(를) 배치할 수 없습니다: AA 시스템 한도 도달 (%2/%3)"
ctld.i18n["ko"]["Cannot repair %1. No damaged %1 within %2m"] = "%1을(를) 수리할 수 없습니다. %2m 이내에 손상된 %1 없음"

--- Keys added by generate_i18n_dicts.ps1 on 2026-07-22
ctld.i18n["ko"]["Air Defense (AA)"] = ""
ctld.i18n["ko"]["Aircraft"] = ""
ctld.i18n["ko"]["FARP / FOB"] = ""
ctld.i18n["ko"]["Ground Vehicles"] = ""
ctld.i18n["ko"]["Helicopters"] = ""
ctld.i18n["ko"]["Infantry"] = ""
ctld.i18n["ko"]["Ships"] = ""
ctld.i18n["ko"]["Air Defense (AA)"] = "방공 (AA)"
ctld.i18n["ko"]["Aircraft"] = "항공기"
ctld.i18n["ko"]["FARP / FOB"] = "FARP / FOB"
ctld.i18n["ko"]["Ground Vehicles"] = "지상 차량"
ctld.i18n["ko"]["Helicopters"] = "헬리콥터"
ctld.i18n["ko"]["Infantry"] = "보병"
ctld.i18n["ko"]["Ships"] = "함선"

--- Keys added by generate_i18n_dicts.ps1 on 2026-07-30
ctld.i18n["ko"]["%1 setting(s) absent from the mission config — CTLD default used: %2"] = "미션 설정에 %1 항목이 없음 — CTLD 기본값 사용: %2"
Expand Down