diff --git a/.backlog/FIX-I18N-STALE-COMMENT-PARSING/PRD.md b/.backlog/FIX-I18N-STALE-COMMENT-PARSING/PRD.md new file mode 100644 index 0000000..0bff39e --- /dev/null +++ b/.backlog/FIX-I18N-STALE-COMMENT-PARSING/PRD.md @@ -0,0 +1,150 @@ +Status: ready + +# FIX-I18N-STALE-COMMENT-PARSING — i18n tooling stops treating `-- STALE:` lines as live + +Grilled with docs on 2026-08-10. Idea originated in `dev/roadmap.md` during the roadmap cleanup +following `FIX-I18N-DEBT-REPAYMENT`. + +## Problem Statement + +CTLD's i18n dictionary tooling (`tools/build/i18n_dict_utils.py`, `generate_i18n_dicts.ps1`, +`translate_i18n.py`) is meant to leave a `-- STALE:`-commented entry alone — a dead key no longer +referenced by any `ctld.tr()` call in `src/`, deliberately kept as a disabled line rather than +deleted ("not deleted — confirm manually"). In practice, every regex-based scan in this tooling +matches a `ctld.i18n[...] = "..."` pattern regardless of a `-- ` comment prefix in front of it, so +a STALE line is indistinguishable from a live one to the code that's supposed to skip it. + +Consequence, found concretely during `FIX-I18N-DEBT-REPAYMENT` (2026-08-10): 23 of the 93 KO stubs +and 8 of the 78 ES stubs counted at `FIX-I18N-DICT-GUARD`'s merge were actually `-- STALE:` keys in +`CTLD_i18n_en.lua` — dead, unused entries that `translate_i18n.py` would have spent API/CLI calls +translating and written into lines that are already disabled comments, had they not been manually +excluded from that lot's scope. + +**What this turned out to actually be**, discovered while fixing the parser and re-running the +corrected `generate_i18n_dicts.ps1` for real: those 23/8 keys were the tip of a much bigger +iceberg. 56 keys — every AA system component label (HAWK/BUK/KUB/NASAMS/Patriot/S-300), several +crate/smoke/vehicle F10 menu labels, and 7 vehicle-category labels — were marked `-- STALE:` in +**all four dictionaries, including English**, almost certainly because they're referenced only via +`CTLD_config.yaml`'s `desc:`/`name:` fields, not a `ctld.tr()` call, and were marked stale by a +version of `generate_i18n_dicts.ps1` that predates the config-YAML scan that would have kept them +recognized as in use. At runtime this meant `ctld.i18n["en"]["HAWK Launcher"]` (and 55 others) were +`nil` in the shipped `CTLD.lua` — broken or missing F10/AA-system text in **every** language, not a +KO/ES-only cosmetic issue. This is a live production bug this lot's fix surfaces and repairs, not +just a parsing correctness improvement. + +## Solution + +Every place in the i18n tooling that reads a `ctld.i18n[lang][key] = "value"` line — the shared +Python parser, the PowerShell key-existence scanner, and the Python dictionary-writer — is +corrected to skip any line whose stripped text starts with `-- ` (a comment, `STALE`-marked or +otherwise). This is a parsing-correctness fix, not a behavior change to the STALE/MISSING +classification rules themselves: a dead key was always supposed to be invisible to translation and +diff tooling once marked; now it actually is. As a one-time cleanup in the same lot, the corrected +PowerShell scanner is run with `-Apply` to properly mark the currently-unmarked-but-dead KO/ES +entries, closing the drift this lot was written to investigate. + +## User Stories + +1. As a CTLD contributor running `merge_CTLD.ps1` locally with `ANTHROPIC_API_KEY` (or the Claude + Code CLI fallback) available, I don't want translation calls spent on dead keys that no F10 menu + will ever display, so my local build doesn't waste time or usage quota on nothing. +2. As a maintainer reading `i18n-guard` CI output on a PR, I want the `MISSING`/new-empty-stub + checks to reason about genuinely live keys only, so a dead key commented `-- STALE:` in the PR's + diff never confuses the diff-scoped comparison in `check_i18n_diff.py`. +3. As a future contributor running `generate_i18n_dicts.ps1` (dry-run or `-Apply`), I want its + `STALE`/`MISSING` classification to be accurate for every one of the four dictionaries, not just + whichever one happens to have been scanned most recently by a human. +4. As a maintainer inspecting `CTLD_i18n_ko.lua`/`_es.lua`, I want a dead key to be visibly marked + `-- STALE:` there too, exactly as it already is in `CTLD_i18n_en.lua`, so the four dictionary + files agree on which keys are alive without needing to cross-reference `CTLD_i18n_en.lua` by + hand to find out. +5. As a developer touching this parsing logic later, I want it covered by unit tests exercising a + `-- STALE:`-commented line explicitly, so a future regression here is caught before it silently + wastes translation calls again. +6. As a maintainer, I want `translate_i18n.py`'s dictionary-writer to never touch a commented line + even if invoked differently in the future, so this class of bug can't resurface through a + different call path than the one that exposed it this time. +7. As a reviewer of this PR, I want the fix to be provably behavior-preserving for every currently + live, non-commented entry, so the correction is trusted as a bug fix rather than a risk to + already-working dictionaries. + +## Implementation Decisions + +- **Three call sites corrected in the same pass**, all sharing the identical defect (a regex or + line scan that matches `ctld.i18n[...] = "..."` without checking for a leading `-- ` comment + prefix): + - `tools/build/i18n_dict_utils.py`: `parse_dict`, `parse_keep_en`, and the underlying + `_ENTRY_RE`-based matching now skip any line whose stripped text starts with `--`. + - `tools/build/generate_i18n_dicts.ps1`: `Get-DictKeys` (the function that determines whether a + dictionary file currently contains a given key, driving both the `MISSING` and `STALE` + classification) gets the same line-level filter. + - `tools/build/translate_i18n.py`: `_apply_translations`'s write path is hardened the same way, + even though it can no longer receive a STALE key in practice once the shared parser is fixed + (`_collect_stubs` iterates `en_dict`, which will no longer include EN's own STALE keys) — kept + consistent so the same defect can't resurface via a different caller later. +- **`check_i18n_diff.py` needs no code change.** `find_new_empty_keys` already calls the shared + `parse_dict`, so it inherits the fix automatically. +- **No change to the `MISSING`/`STALE` classification rules themselves** — a key is still `STALE` + when unused in `src/` and `MISSING` when absent from a dictionary. This lot only fixes how + "present in a dictionary" is determined (a commented line no longer counts as present). +- **One-time real-file cleanup, same lot — actual outcome larger than planned.** Running + `generate_i18n_dicts.ps1 -Apply` with the corrected `Get-DictKeys` did not just re-mark the + 23/8 KO/ES entries expected from the `FIX-I18N-DEBT-REPAYMENT` count. It revealed that **56 keys + were wrongly `-- STALE:`-marked in all four dictionaries, including English** — a live production + bug (see Problem Statement), not the cosmetic KO/ES drift originally scoped. `-Apply` revived all + 56 (EN's own text restored automatically, since EN's value always equals its key). The FR/ES/KO + translations that existed in the old commented lines were recovered by hand into the newly-revived + entries — read from the dead comment, written into the fresh live stub — rather than lost or + left for a full re-translation pass. Genuinely-still-untranslated stubs (7 category labels in + KO/ES that were never translated even before this bug, plus 15 KO-only labels from the same + cause) are left empty. +- **Root cause of the historical drift, resolved for the 56-key case**: these keys are referenced + only via `CTLD_config.yaml`'s `desc:`/`name:` fields (confirmed for the AA system labels, e.g. + `desc: HAWK Launcher`), not a `ctld.tr()` call — invisible to the scan `generate_i18n_dicts.ps1` + used before its config-YAML scan (documented in the script as a later addition, "invisible to the + ctld.tr(\"...\") scan above"). A run predating that scan would have seen them as unused and + correctly-at-the-time marked them stale; once the YAML scan was added, nothing ever un-stales a + key automatically (by design — "not deleted, confirm manually"), so they stayed wrongly commented + until this lot's `-Apply` run. The narrower KO/ES-only drift for the original 23/8 count remains + unconfirmed in its exact mechanism, but is moot now that the underlying keys are live again. + +## Testing Decisions + +- Tests target external behavior of pure functions, consistent with the rest of `tools/build/`'s + test suite (`test_i18n_dict_utils.py`, `test_translate_i18n.py`, `test_check_i18n_diff.py`). +- **`test_i18n_dict_utils.py`**: new cases — a `-- STALE:`-commented entry is absent from + `parse_dict`'s result; a commented `__keep_en` block entry (if such a thing is constructed) is + absent from `parse_keep_en`'s result; a mix of live and commented lines for different keys in the + same text parses only the live ones. +- **`test_translate_i18n.py`**: new case — `_apply_translations` given a key whose only occurrence + in the text is a `-- STALE:`-commented line performs no write and reports zero keys written + (rather than silently uncommenting or corrupting the line). +- **No new test for `check_i18n_diff.py`** beyond what the shared parser's own tests already cover + — it has no logic of its own affected by this fix. +- **No test against the real `src/CTLD_i18n_*.lua` files.** Fixtures stay synthetic and in-memory, + matching the existing suite; coupling a unit test to live dictionary content would make it + fragile to unrelated future dictionary edits. +- **Manual verification**: run the corrected `generate_i18n_dicts.ps1` dry-run against the repo, + confirm it reports the expected `STALE` set without any `MISSING` regressions; run `-Apply` once + and diff the result to confirm only line-prefix changes on the expected dead keys, no value + changes; run `pytest tools/build/` to confirm the full suite (existing + new tests) stays green. + +## Out of Scope + +- **Deprecating the companion asset-check** (`tools/companion/asset_check.lua`) — a different + roadmap item, explicitly deferred by David (2026-07-20) to its own lot. +- **Any change to `i18n-guard` CI job behavior.** It continues to check the same `MISSING`/new-empty + signals; this lot only makes those signals computed correctly, not different. +- **Any new dictionary *content*/translation.** The one-time cleanup (Implementation Decisions) + changes line prefixes on already-dead entries only, never a translated value. +- **Root-causing the historical drift with git archaeology.** Judged disproportionate for what is, + going forward, a closed issue once this lot ships. + +## Further Notes + +- Idea source: `dev/roadmap.md`, "TOOLING — `i18n_dict_utils.py` ne distingue pas une entrée + `-- STALE:` d'une entrée live" (replaced by a pointer to this lot once formalized). +- Prior art: `FIX-I18N-DICT-GUARD` (ADR 0013) established the `MISSING`/`STALE` vocabulary and the + diff-scoped CI guard this lot's fix feeds into unchanged; `FIX-I18N-DEBT-REPAYMENT` is where the + 23/8 dead-key miscount was first observed; `TOOLING-I18N-CLAUDE-CODE-TRANSLATE` (ADR 0014) added + the CLI fallback backend that would otherwise also waste calls translating these same dead keys. diff --git a/.backlog/FIX-I18N-STALE-COMMENT-PARSING/tickets/01-skip-commented-lines-and-cleanup-drift.md b/.backlog/FIX-I18N-STALE-COMMENT-PARSING/tickets/01-skip-commented-lines-and-cleanup-drift.md new file mode 100644 index 0000000..119e817 --- /dev/null +++ b/.backlog/FIX-I18N-STALE-COMMENT-PARSING/tickets/01-skip-commented-lines-and-cleanup-drift.md @@ -0,0 +1,66 @@ +Status: ready + +# 01 — Skip `-- STALE:`-commented lines in i18n parsing + clean up the real drift + +## Parent + +`.backlog/FIX-I18N-STALE-COMMENT-PARSING/PRD.md` + +## What to build + +Fix the identical defect in three places: a regex or line scan that matches +`ctld.i18n[lang][key] = "value"` regardless of a `-- ` comment prefix in front of it, so a +`-- STALE:`-commented dead key is currently indistinguishable from a live one. + +- `tools/build/i18n_dict_utils.py`: `parse_dict`, `parse_keep_en`, and the underlying entry-matching + logic now skip any line whose stripped text starts with `--`. +- `tools/build/generate_i18n_dicts.ps1`: `Get-DictKeys` (drives both `MISSING` and `STALE` + classification) gets the same line-level filter. +- `tools/build/translate_i18n.py`: `_apply_translations`'s write path is hardened the same way, for + defense-in-depth against a future caller passing it a stale key — even though it can no longer + receive one in practice once the shared parser excludes STALE keys from `en_dict`, and therefore + from `_collect_stubs`'s output. + +No change to the `MISSING`/`STALE` classification rules — only to how "present in a dictionary" is +determined. `check_i18n_diff.py` needs no code change: it calls the shared `parse_dict` and inherits +the fix automatically. + +As a one-time cleanup in this same ticket, once `generate_i18n_dicts.ps1` is fixed, run it with +`-Apply` against the repo's real dictionaries. + +**Actual outcome, larger than scoped**: this didn't just re-mark 23 KO + 8 ES entries as expected. +It surfaced that 56 keys were wrongly `-- STALE:`-marked in **all four dictionaries including +English** — every AA system component label (HAWK/BUK/KUB/NASAMS/Patriot/S-300), several F10 menu +labels, and 7 vehicle-category labels, all referenced only via `CTLD_config.yaml`'s `desc:`/`name:` +fields (not `ctld.tr()`), almost certainly marked stale by a version of the script predating that +scan. This is a live production bug (`ctld.i18n["en"]["HAWK Launcher"]` etc. were `nil` at +runtime), not a cosmetic drift. `-Apply` revived all 56 (EN's text restored automatically); the +FR/ES/KO translations sitting in the old commented lines were recovered by hand into the freshly +revived stubs rather than lost. + +## Acceptance criteria + +- [x] `parse_dict` excludes a `-- STALE:`-commented entry from its returned dict. +- [x] `parse_keep_en` excludes a commented `__keep_en` entry the same way. +- [x] A mix of live and commented lines for different keys in the same text parses only the live + ones (no false exclusion of genuinely live neighbors). +- [x] `Get-DictKeys` in `generate_i18n_dicts.ps1` no longer counts a commented line as a key the + dictionary "has" — verified via the dry-run report on the repo's real dictionaries (went from + wrongly reporting these 56 keys as present/stale to correctly reporting them `MISSING`). +- [x] `_apply_translations` given a key whose only occurrence is a `-- STALE:`-commented line + performs no write and reports zero keys written, rather than uncommenting or corrupting it. +- [x] Every currently live (non-commented) entry across all four dictionaries parses identically to + before the fix — no behavior change for live content (confirmed via `luac -p` on all 4 files + plus the full `pytest tools/build/` suite staying green). +- [x] Unit tests added to `test_i18n_dict_utils.py` and `test_translate_i18n.py` covering the cases + above; no test added against real `src/CTLD_i18n_*.lua` files (fixtures stay synthetic). +- [x] `pytest tools/build/` stays green (24/24, 5 new tests). +- [x] `generate_i18n_dicts.ps1 -Apply` run once against the repo post-fix. Diff is larger than + originally scoped (see above) but contains only line additions/prefix changes on the 56 + affected keys across all 4 dictionaries — no unrelated `MISSING` regressions; final dry-run + reports `OK` for all four dictionaries. +- [x] `CHANGELOG.md` `[Unreleased]` updated, documenting the discovered production bug and its fix. + +## Blocked by + +None - can start immediately diff --git a/.backlog/README.md b/.backlog/README.md index e3e7045..47eb001 100644 --- a/.backlog/README.md +++ b/.backlog/README.md @@ -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 `-`. | `feature/dev-build-channel` | +| [`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` | | [`FIX-I18N-DICT-GUARD`](FIX-I18N-DICT-GUARD/PRD.md) | merged (PR #115) | Reported by **FullGas**: some F10 menu entries stay untranslated in Korean. Root cause: `translate_i18n.py`'s stub detection never matches the `""` empty-stub convention `generate_i18n_dicts.ps1 -Apply` actually writes, so freshly-added keys are never picked up for translation even with `ANTHROPIC_API_KEY` set — and the only existing guard (`.githooks/pre-push`, opt-in, `MISSING`-only) doesn't check translation content. New CI job `i18n-guard`, diff-scoped like `changelog-guard`: blocks unconditionally on `MISSING`, blocks by default (bypassable via `skip-i18n` label) on a newly-introduced empty non-EN entry. Fixes `translate_i18n.py`'s stub detection in the same lot. Pre-existing debt (91 KO + 76 ES empty entries) explicitly out of scope — a follow-up repayment lot must follow immediately. See **ADR 0013**. | `fix/i18n-dict-guard` | diff --git a/CHANGELOG.md b/CHANGELOG.md index 5878577..972baf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,30 @@ Versioning follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +### 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 + one** (`tools/build/i18n_dict_utils.py`'s `parse_dict`/`parse_keep_en`, `generate_i18n_dicts.ps1`'s + `Get-DictKeys`, and `translate_i18n.py`'s `_apply_translations` all shared the same comment-blind + regex). Fixed: all three now skip any line starting with `--`. +- **Consequence discovered while fixing it**: 56 keys — every HAWK/BUK/KUB/NASAMS/Patriot/S-300 AA + system component label, several crate/smoke/vehicle F10 menu labels, and the `Infantry`/`Air + Defense (AA)`/`Ground Vehicles`/`Helicopters`/`Aircraft`/`Ships`/`FARP / FOB` category labels — + were incorrectly marked `-- STALE:` in **every dictionary including English**, most likely predating + `generate_i18n_dicts.ps1`'s config-YAML `desc:`/`name:` scan (these keys are referenced there, not + via a `ctld.tr()` call, so an older version of the script would never have seen them as in use). + At runtime this meant `ctld.i18n["en"]["HAWK Launcher"]` and 55 others were `nil` — broken/missing + text in F10 menus and the AA system UI, in every language, not just KO/ES. +- **Fixed for real**: `generate_i18n_dicts.ps1 -Apply` re-run with the corrected parser revives all + 56 keys (EN gets its own text back automatically). The FR/ES/KO translations that were sitting + inert in the old commented lines were manually recovered into the freshly-revived entries rather + than lost or re-translated from scratch. Genuinely new stubs (7 category labels in KO/ES, and 15 + labels in KO that were never translated even before this bug) are left empty, ready for a future + translation pass. +- No change to `MISSING`/`STALE` classification rules — only to how "present in a dictionary" is + determined. `check_i18n_diff.py` needed no code change (inherits the fix via the shared parser). +- See `.backlog/FIX-I18N-STALE-COMMENT-PARSING/`. No ADR — a bug fix, not a design trade-off. + ### Added — Claude Code CLI as a local i18n auto-translate fallback (TOOLING-I18N-CLAUDE-CODE-TRANSLATE) - **`translate_i18n.py` no longer requires `ANTHROPIC_API_KEY`** to auto-translate empty i18n diff --git a/dev/roadmap.md b/dev/roadmap.md index b0a7915..8c5c89a 100644 --- a/dev/roadmap.md +++ b/dev/roadmap.md @@ -15,18 +15,19 @@ caisses) : absorbable par les scènes, ou couche séparée inévitable ? -## ctld-tools — validation `modTypes` + dépréciation du companion asset-check - -Contexte (émergé du grill TUI) : deux points liés. -1. `ctld-tools validate` ne vérifie un `unit` que contre le **datamine stock** — il ignore les - `modTypes` déclarés par le MM, donc un type de **mod légitime** est signalé « unknown » à tort. - À corriger : valider `unit ∈ datamine ∪ (modTypes du user-config)`. -2. Le **companion asset-check** (`dist/CTLD_asset_check.lua`, `tools/companion/`) fait exactement la - même validation (même datamine stock + `modTypes`), mais **dans DCS après coup** → largement - **redondant** avec `ctld-tools`. Une fois (1) fait, le déprécier/retirer. Résidu couvert : les - types **injectés au runtime** (scènes de plugins) que le design-time ne voit pas — marginal, et - rattrapé par le test-en-DCS. `logDefaults` reste (debug power-user). Décision David (2026-07-20) : - pas maintenant, à faire en lot séparé. +## ctld-tools — dépréciation du companion asset-check + +Contexte (émergé du grill TUI). Deux points liés à l'origine ; le premier est réglé : +1. ~~`ctld-tools validate` ne vérifie un `unit` que contre le **datamine stock**, ignorant les + `modTypes` déclarés par le MM~~ — **fait**, livré par `FIX-VALIDATE-MODTYPES` (PR #87) : + `validate.py` résout désormais `datamine ∪ modTypes` (constaté le 2026-08-10, vérification + roadmap). +2. Le **companion asset-check** (`tools/companion/asset_check.lua`) fait exactement la même + validation (même datamine stock + `modTypes`), mais **dans DCS après coup** → largement + **redondant** avec `ctld-tools`, maintenant que (1) est fait. À déprécier/retirer. Résidu + couvert : les types **injectés au runtime** (scènes de plugins) que le design-time ne voit pas — + marginal, et rattrapé par le test-en-DCS. `logDefaults` reste (debug power-user). Décision David + (2026-07-20) : pas maintenant, à faire en lot séparé. @@ -46,21 +47,9 @@ un chemin machine. Détail dans le PRD.)_ --- -## FEAT-MOVING-ZONE — Zones logistiques ancrées sur une Moving Zone DCS - -Contexte : DCS permet d'attacher une trigger zone à une unité (Moving Zone, configurée dans le ME). -La zone suit l'unité en jeu ; `trigger.misc.getZone(name)` retourne sa position courante. - -CTLD ne tire pas parti de ce mécanisme : `_discoverLGZ()` lit la position depuis -`env.mission.triggers.zones` (snapshot statique du `.miz` au load) et ne la rafraîchit jamais. -Une LGZ_ attachée à un véhicule dans le ME se comporterait donc comme une zone fixe. - -Solution identifiée : stocker le nom DCS de la zone dans `CTLDLogisticZone` au lieu de -snapshoter `_center`, et interroger `trigger.misc.getZone(name)` dans `getCenter()` à chaque -appel. Le mécanisme `linkedUnit` (logisticUnits legacy) resterait inchangé et coexisterait. - -Bénéfice MM : une LGZ_ attachée à un camion logistique ou un navire dans le ME suffit — sans -passer par la config `logisticUnits`. La zone logistique suit le véhicule en mouvement. + --- @@ -71,20 +60,16 @@ passer par la config `logisticUnits`. La zone logistique suit le véhicule en mo --- -## ctld-tools — `unit:` et `group:` manquants dans le schéma - -Le lot CTLD-TOOLS-MM-UX a mis dans `src/CTLD_config_schema.yaml` les **familles** (section -`families:` : `label` + `description` bilingues + `order`) et un **`label:` bilingue par réglage** -(137 entrées). Restent deux métadonnées dérivées côté frontend : - -- l'**unité**, extraite du texte de la `description` (`(m)`, `(kg)`, `(seconds)`) — fiable mais - indirect, et muette pour les réglages sans description. Un champ `unit:` serait explicite ; -- la **famille** des ~44 réglages sans `group:`, dérivée du nom de la clé (`familyOf`). Ça réduit la - famille fourre-tout `Other` de ~44 à 7, donc le gain restant est surtout cosmétique. - -Converge avec « générer les tableaux de config de la doc depuis le schéma » : mêmes métadonnées, même -source. Le vrai reste-à-faire coûteux, ce sont les **descriptions** des ~44 réglages non documentés -(les inventer serait contraire à la règle zéro-supposition). + ## CHANGELOG — réorganiser `[Unreleased]` avant la 2.0.0 stable @@ -112,27 +97,6 @@ des lignes. -## TOOLING — `i18n_dict_utils.py` ne distingue pas une entrée `-- STALE:` d'une entrée live - -Contexte (trouvé pendant `FIX-I18N-DEBT-REPAYMENT`, 2026-08-10) : le regex de -`tools/build/i18n_dict_utils.py`'s `parse_dict`/`_ENTRY_RE` matche `ctld.i18n["lang"]["key"] = -"..."` peu importe si la ligne est commentée par un préfixe `-- STALE: ` (marqueur de -`generate_i18n_dicts.ps1 -Apply` pour une clé qui n'est plus référencée dans `src/`). Conséquence -vécue : sur les 93 stubs KO / 78 ES comptés au merge de `FIX-I18N-DICT-GUARD`, 23 / 8 étaient en -réalité des clés `-- STALE:` dans `CTLD_i18n_en.lua` — `translate_i18n.py` les aurait traduites -(appels API gaspillés) et écrites dans des lignes déjà mortes (`_apply_translations`'s regex ne -distingue pas non plus). `check_i18n_diff.py` hérite du même gap via le même parseur partagé. - -Piste : `parse_dict`/`parse_keep_en` (et tout appelant) devraient ignorer les lignes dont la version -strippée commence par `--`. Repéré aussi : `CTLD_i18n_ko.lua`/`_es.lua` ont des clés vides que -`CTLD_i18n_en.lua` marque déjà `-- STALE:` mais que `ko`/`es` eux-mêmes n'ont pas encore marquées -ainsi (dérive entre dictionnaires). - -**À traiter dans ce lot, pas seulement le symptôme parseur** : remonter à la cause de cette dérive -avant de corriger uniquement `parse_dict` — sans quoi le patch du parseur masque le problème plutôt -que de le résoudre. Hypothèse à vérifier en premier : `generate_i18n_dicts.ps1`'s marquage `STALE` -scanne-t-il et applique-t-il le préfixe aux 4 dictionnaires (en/fr/es/ko) dans la même passe, ou -traite-t-il chaque fichier indépendamment avec un risque de désynchronisation (ex. un seul fichier -mis à jour lors d'un run partiel, ou un ordre de traitement qui laisse certains fichiers en -retard) ? Si le marquage est censé être atomique/uniforme et ne l'est pas, c'est un bug dans le -script à corriger en même temps que le parseur — pas seulement documenter le symptôme. + diff --git a/src/CTLD_i18n_en.lua b/src/CTLD_i18n_en.lua index a79b642..3842472 100644 --- a/src/CTLD_i18n_en.lua +++ b/src/CTLD_i18n_en.lua @@ -15,7 +15,7 @@ if not ctld then ctld = {} end if not ctld.i18n then ctld.i18n = {} end ctld.i18n["en"] = {} -ctld.i18n["en"].translation_version = "1.17" +ctld.i18n["en"].translation_version = "1.18" --- groups names ctld.i18n["en"]["Standard Group"] = "Standard Group" @@ -572,3 +572,61 @@ ctld.i18n["en"][" AIZ[%1] ERROR '%2': name already taken by zone '%3' — entry --- Keys added by generate_i18n_dicts.ps1 on 2026-08-09 ctld.i18n["en"]["%1 (%2 troops, %3m)"] = "%1 (%2 troops, %3m)" ctld.i18n["en"]["Extract: %1 (%2 troops)"] = "Extract: %1 (%2 troops)" + +--- Keys added by generate_i18n_dicts.ps1 on 2026-08-10 +ctld.i18n["en"]["%1 successfully rearmed a full %2 in the field"] = "%1 successfully rearmed a full %2 in the field" +ctld.i18n["en"]["%1 successfully repaired a full %2 in the field."] = "%1 successfully repaired a full %2 in the field." +ctld.i18n["en"]["Air Defense (AA)"] = "Air Defense (AA)" +ctld.i18n["en"]["Aircraft"] = "Aircraft" +ctld.i18n["en"]["BUK - All crates"] = "BUK - All crates" +ctld.i18n["en"]["BUK CC Radar"] = "BUK CC Radar" +ctld.i18n["en"]["BUK Launcher"] = "BUK Launcher" +ctld.i18n["en"]["BUK Repair"] = "BUK Repair" +ctld.i18n["en"]["BUK Search Radar"] = "BUK Search Radar" +ctld.i18n["en"]["Cannot build %1\n%2\n\nOr the crates are not close enough together"] = "Cannot build %1\n%2\n\nOr the crates are not close enough together" +ctld.i18n["en"]["Drop Blue Smoke"] = "Drop Blue Smoke" +ctld.i18n["en"]["Drop Crate(s)"] = "Drop Crate(s)" +ctld.i18n["en"]["Drop Green Smoke"] = "Drop Green Smoke" +ctld.i18n["en"]["Drop Orange Smoke"] = "Drop Orange Smoke" +ctld.i18n["en"]["Drop Red Smoke"] = "Drop Red Smoke" +ctld.i18n["en"]["FARP / FOB"] = "FARP / FOB" +ctld.i18n["en"]["Ground Vehicles"] = "Ground Vehicles" +ctld.i18n["en"]["HAWK - All crates"] = "HAWK - All crates" +ctld.i18n["en"]["HAWK CWAR"] = "HAWK CWAR" +ctld.i18n["en"]["HAWK Launcher"] = "HAWK Launcher" +ctld.i18n["en"]["HAWK PCP"] = "HAWK PCP" +ctld.i18n["en"]["HAWK Repair"] = "HAWK Repair" +ctld.i18n["en"]["HAWK Search Radar"] = "HAWK Search Radar" +ctld.i18n["en"]["HAWK Track Radar"] = "HAWK Track Radar" +ctld.i18n["en"]["Helicopters"] = "Helicopters" +ctld.i18n["en"]["Infantry"] = "Infantry" +ctld.i18n["en"]["JTAC Status"] = "JTAC Status" +ctld.i18n["en"]["KUB - All crates"] = "KUB - All crates" +ctld.i18n["en"]["KUB Launcher"] = "KUB Launcher" +ctld.i18n["en"]["KUB Radar"] = "KUB Radar" +ctld.i18n["en"]["KUB Repair"] = "KUB Repair" +ctld.i18n["en"]["Load "] = "Load " +ctld.i18n["en"]["Load / Extract Vehicles"] = "Load / Extract Vehicles" +ctld.i18n["en"]["Missing %1\n"] = "Missing %1\n" +ctld.i18n["en"]["NASAMS - All crates"] = "NASAMS - All crates" +ctld.i18n["en"]["NASAMS Command Post"] = "NASAMS Command Post" +ctld.i18n["en"]["NASAMS Launcher 120C"] = "NASAMS Launcher 120C" +ctld.i18n["en"]["NASAMS Repair"] = "NASAMS Repair" +ctld.i18n["en"]["NASAMS Search/Track Radar"] = "NASAMS Search/Track Radar" +ctld.i18n["en"]["No extractable troops nearby!"] = "No extractable troops nearby!" +ctld.i18n["en"]["Out of parts for AA Systems. Current limit is %1\n"] = "Out of parts for AA Systems. Current limit is %1\n" +ctld.i18n["en"]["Patriot - All crates"] = "Patriot - All crates" +ctld.i18n["en"]["Patriot AMG (optional)"] = "Patriot AMG (optional)" +ctld.i18n["en"]["Patriot ECS"] = "Patriot ECS" +ctld.i18n["en"]["Patriot Launcher"] = "Patriot Launcher" +ctld.i18n["en"]["Patriot Radar"] = "Patriot Radar" +ctld.i18n["en"]["Patriot Repair"] = "Patriot Repair" +ctld.i18n["en"]["S-300 - All crates"] = "S-300 - All crates" +ctld.i18n["en"]["S-300 Grumble Big Bird SR"] = "S-300 Grumble Big Bird SR" +ctld.i18n["en"]["S-300 Grumble C2"] = "S-300 Grumble C2" +ctld.i18n["en"]["S-300 Grumble Clam Shell SR"] = "S-300 Grumble Clam Shell SR" +ctld.i18n["en"]["S-300 Grumble Flap Lid-A TR"] = "S-300 Grumble Flap Lid-A TR" +ctld.i18n["en"]["S-300 Grumble TEL C"] = "S-300 Grumble TEL C" +ctld.i18n["en"]["S-300 Repair"] = "S-300 Repair" +ctld.i18n["en"]["Ships"] = "Ships" +ctld.i18n["en"]["Unload Vehicles"] = "Unload Vehicles" diff --git a/src/CTLD_i18n_es.lua b/src/CTLD_i18n_es.lua index d7191f2..d000745 100644 --- a/src/CTLD_i18n_es.lua +++ b/src/CTLD_i18n_es.lua @@ -10,7 +10,7 @@ if not ctld then ctld = {} end if not ctld.i18n then ctld.i18n = {} end ctld.i18n["es"] = {} -ctld.i18n["es"].translation_version = "1.17" +ctld.i18n["es"].translation_version = "1.18" --- groups names ctld.i18n["es"]["Standard Group"] = "Grupo estándar" @@ -578,3 +578,54 @@ ctld.i18n["es"][" AIZ[%1] ERROR '%2': name already taken by zone '%3' — entry --- Keys added by generate_i18n_dicts.ps1 on 2026-08-09 ctld.i18n["es"]["%1 (%2 troops, %3m)"] = "%1 (%2 tropas, %3 m)" ctld.i18n["es"]["Extract: %1 (%2 troops)"] = "Extraer: %1 (%2 tropas)" + +--- Keys added by generate_i18n_dicts.ps1 on 2026-08-10 +ctld.i18n["es"]["%1 successfully rearmed a full %2 in the field"] = "%1 rearmó con exito un %2 completo en el campo" +ctld.i18n["es"]["%1 successfully repaired a full %2 in the field."] = "%1 reparó con exito un %2 completo en el campo." +ctld.i18n["es"]["BUK - All crates"] = "BUK - Todas las cajas" +ctld.i18n["es"]["BUK CC Radar"] = "BUK - Radar de Control de Combate" +ctld.i18n["es"]["BUK Launcher"] = "BUK - Lanzador" +ctld.i18n["es"]["BUK Repair"] = "Reparar BUK" +ctld.i18n["es"]["BUK Search Radar"] = "BUK - Radar de Búsqueda" +ctld.i18n["es"]["Cannot build %1\n%2\n\nOr the crates are not close enough together"] = "Imposible construir %1\n%2\n\nO las cajas no están lo suficientemente cerca unas de otras." +ctld.i18n["es"]["Drop Blue Smoke"] = "Lanzar humo azul" +ctld.i18n["es"]["Drop Crate(s)"] = "Soltar caja(s)" +ctld.i18n["es"]["Drop Green Smoke"] = "Lanzar humo verde" +ctld.i18n["es"]["Drop Orange Smoke"] = "Lanzar humo naranja" +ctld.i18n["es"]["Drop Red Smoke"] = "Lanzar humo rojo" +ctld.i18n["es"]["HAWK - All crates"] = "HAWK - Todas las cajas" +ctld.i18n["es"]["HAWK CWAR"] = "HAWK - Sistema de Control de Guerra" +ctld.i18n["es"]["HAWK Launcher"] = "HAWK - Lanzador" +ctld.i18n["es"]["HAWK PCP"] = "HAWK - Puesto de Comando" +ctld.i18n["es"]["HAWK Repair"] = "Reparar HAWK" +ctld.i18n["es"]["HAWK Search Radar"] = "HAWK - Radar de Búsqueda" +ctld.i18n["es"]["HAWK Track Radar"] = "HAWK - Radar de Seguimiento" +ctld.i18n["es"]["JTAC Status"] = "Estado de JTAC" +ctld.i18n["es"]["KUB - All crates"] = "KUB - Todas las cajas" +ctld.i18n["es"]["KUB Launcher"] = "KUB - Lanzador" +ctld.i18n["es"]["KUB Radar"] = "KUB - Radar" +ctld.i18n["es"]["KUB Repair"] = "Reparar KUB" +ctld.i18n["es"]["Load "] = "Cargar " +ctld.i18n["es"]["Load / Extract Vehicles"] = "Cargar/Extraer vehículos" +ctld.i18n["es"]["Missing %1\n"] = "Faltan: %1\n" +ctld.i18n["es"]["NASAMS - All crates"] = "NASAMS - Todas las cajas" +ctld.i18n["es"]["NASAMS Command Post"] = "NASAMS - Puesto de Mando" +ctld.i18n["es"]["NASAMS Launcher 120C"] = "NASAMS - Lanzador 120C" +ctld.i18n["es"]["NASAMS Repair"] = "Reparar NASAMS" +ctld.i18n["es"]["NASAMS Search/Track Radar"] = "NASAMS - Radar de Búsqueda/Seguimiento" +ctld.i18n["es"]["No extractable troops nearby!"] = "¡No hay tropas extraíbles cerca!" +ctld.i18n["es"]["Out of parts for AA Systems. Current limit is %1\n"] = "Sin piezas para sistemas AA. El límite actual es %1\n" +ctld.i18n["es"]["Patriot - All crates"] = "Patriot - Todas las cajas" +ctld.i18n["es"]["Patriot AMG (optional)"] = "Patriot - AMG (opcional)" +ctld.i18n["es"]["Patriot ECS"] = "Patriot - Puesto de Mando" +ctld.i18n["es"]["Patriot Launcher"] = "Patriot - Lanzador" +ctld.i18n["es"]["Patriot Radar"] = "Patriot - Radar de Búsqueda" +ctld.i18n["es"]["Patriot Repair"] = "Reparar Patriot" +ctld.i18n["es"]["S-300 - All crates"] = "S-300 - Todas las cajas" +ctld.i18n["es"]["S-300 Grumble Big Bird SR"] = "S-300 Grumble Big Bird SR - Radar de Búsqueda" +ctld.i18n["es"]["S-300 Grumble C2"] = "S-300 Grumble C2 - Puesto de Mando" +ctld.i18n["es"]["S-300 Grumble Clam Shell SR"] = "S-300 Grumble Clam Shell SR - Radar de Búsqueda" +ctld.i18n["es"]["S-300 Grumble Flap Lid-A TR"] = "S-300 Grumble Flap Lid-A TR - Radar de Seguimiento" +ctld.i18n["es"]["S-300 Grumble TEL C"] = "S-300 Grumble TEL C - Lanzador" +ctld.i18n["es"]["S-300 Repair"] = "Reparar S-300" +ctld.i18n["es"]["Unload Vehicles"] = "Descargar vehículos" diff --git a/src/CTLD_i18n_fr.lua b/src/CTLD_i18n_fr.lua index 097bcd8..ef06695 100644 --- a/src/CTLD_i18n_fr.lua +++ b/src/CTLD_i18n_fr.lua @@ -9,7 +9,7 @@ if not ctld then ctld = {} end if not ctld.i18n then ctld.i18n = {} end ctld.i18n["fr"] = {} -ctld.i18n["fr"].translation_version = "1.17" +ctld.i18n["fr"].translation_version = "1.18" --- groups names ctld.i18n["fr"]["Standard Group"] = "Groupe standard" @@ -606,3 +606,59 @@ ctld.i18n["fr"][" AIZ[%1] ERROR '%2': name already taken by zone '%3' — entry --- Keys added by generate_i18n_dicts.ps1 on 2026-08-09 ctld.i18n["fr"]["%1 (%2 troops, %3m)"] = "%1 (%2 soldats, %3 m)" ctld.i18n["fr"]["Extract: %1 (%2 troops)"] = "Extraire : %1 (%2 soldats)" + +--- Keys added by generate_i18n_dicts.ps1 on 2026-08-10 +ctld.i18n["fr"]["Air Defense (AA)"] = "Défense aérienne (AA)" +ctld.i18n["fr"]["Aircraft"] = "Aéronefs" +ctld.i18n["fr"]["BUK - All crates"] = "BUK - Toutes les caisses" +ctld.i18n["fr"]["BUK CC Radar"] = "BUK - Radar de contrôle" +ctld.i18n["fr"]["BUK Launcher"] = "BUK - Lanceur" +ctld.i18n["fr"]["BUK Repair"] = "BUK - Réparation" +ctld.i18n["fr"]["BUK Search Radar"] = "BUK - Radar de recherche" +ctld.i18n["fr"]["Cannot build %1\n%2\n\nOr the crates are not close enough together"] = "Impossible de construire %1\n%2\n\nOu les caisses ne sont pas assez proches les unes des autres" +ctld.i18n["fr"]["Drop Blue Smoke"] = "Déposer Fumi Bleu" +ctld.i18n["fr"]["Drop Crate(s)"] = "Décharger caisse(s)" +ctld.i18n["fr"]["Drop Green Smoke"] = "Déposer Fumi Vert" +ctld.i18n["fr"]["Drop Orange Smoke"] = "Déposer Fumi Orange" +ctld.i18n["fr"]["Drop Red Smoke"] = "Déposer Fumi Rouge" +ctld.i18n["fr"]["FARP / FOB"] = "FARP / FOB" +ctld.i18n["fr"]["Ground Vehicles"] = "Véhicules terrestres" +ctld.i18n["fr"]["HAWK - All crates"] = "HAWK - Toutes les caisses" +ctld.i18n["fr"]["HAWK CWAR"] = "HAWK - CWAR" +ctld.i18n["fr"]["HAWK Launcher"] = "HAWK - Lanceur" +ctld.i18n["fr"]["HAWK PCP"] = "HAWK - PCP" +ctld.i18n["fr"]["HAWK Repair"] = "HAWK - Réparation" +ctld.i18n["fr"]["HAWK Search Radar"] = "HAWK - Radar de recherche" +ctld.i18n["fr"]["HAWK Track Radar"] = "HAWK - Radar de poursuite" +ctld.i18n["fr"]["Helicopters"] = "Hélicoptères" +ctld.i18n["fr"]["Infantry"] = "Infanterie" +ctld.i18n["fr"]["JTAC Status"] = "Statut JTAC" +ctld.i18n["fr"]["KUB - All crates"] = "KUB - Toutes les caisses" +ctld.i18n["fr"]["KUB Launcher"] = "KUB - Lanceur" +ctld.i18n["fr"]["KUB Radar"] = "KUB - Radar" +ctld.i18n["fr"]["KUB Repair"] = "KUB - Réparation" +ctld.i18n["fr"]["Load "] = "Charger " +ctld.i18n["fr"]["Load / Extract Vehicles"] = "Chargt / Déchargt Vehicules" +ctld.i18n["fr"]["Missing %1\n"] = "%1 manquant\n" +ctld.i18n["fr"]["NASAMS - All crates"] = "NASAMS - Toutes les caisses" +ctld.i18n["fr"]["NASAMS Command Post"] = "NASAMS - Poste de commandement" +ctld.i18n["fr"]["NASAMS Launcher 120C"] = "NASAMS - Lanceur 120C" +ctld.i18n["fr"]["NASAMS Repair"] = "NASAMS - Réparation" +ctld.i18n["fr"]["NASAMS Search/Track Radar"] = "NASAMS - Radar recherche/poursuite" +ctld.i18n["fr"]["No extractable troops nearby!"] = "Aucune troupe extractible à proximité !" +ctld.i18n["fr"]["Out of parts for AA Systems. Current limit is %1\n"] = "Plus de pièces pour les systèmes AA. La limite actuelle est de %1\n" +ctld.i18n["fr"]["Patriot - All crates"] = "Patriot - Toutes les caisses" +ctld.i18n["fr"]["Patriot AMG (optional)"] = "Patriot - AMG (optionnel)" +ctld.i18n["fr"]["Patriot ECS"] = "Patriot - ECS" +ctld.i18n["fr"]["Patriot Launcher"] = "Patriot - Lanceur" +ctld.i18n["fr"]["Patriot Radar"] = "Patriot - Radar" +ctld.i18n["fr"]["Patriot Repair"] = "Patriot - Réparation" +ctld.i18n["fr"]["S-300 - All crates"] = "S-300 - Toutes les caisses" +ctld.i18n["fr"]["S-300 Grumble Big Bird SR"] = "S-300 Grumble Big Bird SR" +ctld.i18n["fr"]["S-300 Grumble C2"] = "S-300 Grumble C2" +ctld.i18n["fr"]["S-300 Grumble Clam Shell SR"] = "S-300 Grumble Clam Shell SR" +ctld.i18n["fr"]["S-300 Grumble Flap Lid-A TR"] = "S-300 Grumble Flap Lid-A TR" +ctld.i18n["fr"]["S-300 Grumble TEL C"] = "S-300 Grumble TEL C" +ctld.i18n["fr"]["S-300 Repair"] = "S-300 - Réparation" +ctld.i18n["fr"]["Ships"] = "Navires" +ctld.i18n["fr"]["Unload Vehicles"] = "Décharger Vehicles" diff --git a/src/CTLD_i18n_ko.lua b/src/CTLD_i18n_ko.lua index 68462fe..90c08f1 100644 --- a/src/CTLD_i18n_ko.lua +++ b/src/CTLD_i18n_ko.lua @@ -10,7 +10,7 @@ if not ctld then ctld = {} end if not ctld.i18n then ctld.i18n = {} end ctld.i18n["ko"] = {} -ctld.i18n["ko"].translation_version = "1.17" +ctld.i18n["ko"].translation_version = "1.18" --- groups names ctld.i18n["ko"]["Standard Group"] = "표준 그룹" @@ -447,3 +447,39 @@ ctld.i18n["ko"][" AIZ[%1] ERROR '%2': name already taken by zone '%3' — entry --- Keys added by generate_i18n_dicts.ps1 on 2026-08-09 ctld.i18n["ko"]["%1 (%2 troops, %3m)"] = "%1 (%2명, %3 m)" ctld.i18n["ko"]["Extract: %1 (%2 troops)"] = "추출: %1 (%2명)" + +--- Keys added by generate_i18n_dicts.ps1 on 2026-08-10 +ctld.i18n["ko"]["BUK - All crates"] = "SA-11 - 전체 화물" +ctld.i18n["ko"]["BUK CC Radar"] = "SA-11 CC" +ctld.i18n["ko"]["BUK Launcher"] = "SA-11 포대" +ctld.i18n["ko"]["BUK Repair"] = "SA-11 수리킷" +ctld.i18n["ko"]["BUK Search Radar"] = "SA-11 탐지 레이더" +ctld.i18n["ko"]["HAWK - All crates"] = "호크 - 전체 화물" +ctld.i18n["ko"]["HAWK CWAR"] = "호크 CWAR" +ctld.i18n["ko"]["HAWK Launcher"] = "호크 포대" +ctld.i18n["ko"]["HAWK PCP"] = "호크 PCP" +ctld.i18n["ko"]["HAWK Repair"] = "호크 수리킷" +ctld.i18n["ko"]["HAWK Search Radar"] = "호크 탐지 레이더" +ctld.i18n["ko"]["HAWK Track Radar"] = "호크 추적 레이더" +ctld.i18n["ko"]["KUB - All crates"] = "SA-6 - 전체 화물" +ctld.i18n["ko"]["KUB Launcher"] = "SA-6 포대" +ctld.i18n["ko"]["KUB Radar"] = "SA-6 레이더" +ctld.i18n["ko"]["KUB Repair"] = "SA-6 수리킷" +ctld.i18n["ko"]["NASAMS - All crates"] = "NASAMS - 전체 화물" +ctld.i18n["ko"]["NASAMS Command Post"] = "NASAMS 관제소" +ctld.i18n["ko"]["NASAMS Launcher 120C"] = "NASAMS 포대 120C" +ctld.i18n["ko"]["NASAMS Repair"] = "NASAMS 수리킷" +ctld.i18n["ko"]["NASAMS Search/Track Radar"] = "NASAMS 레이더" +ctld.i18n["ko"]["Patriot - All crates"] = "패트리어트 - 전체 화물" +ctld.i18n["ko"]["Patriot AMG (optional)"] = "패트리어트 AMG (선택 사항)" +ctld.i18n["ko"]["Patriot ECS"] = "패트리어트 ECS" +ctld.i18n["ko"]["Patriot Launcher"] = "패트리어트 포대" +ctld.i18n["ko"]["Patriot Radar"] = "패트리어트 탐지 레이더" +ctld.i18n["ko"]["Patriot Repair"] = "패트리어트 수리킷" +ctld.i18n["ko"]["S-300 - All crates"] = "S-300 - 전체 화물" +ctld.i18n["ko"]["S-300 Grumble Big Bird SR"] = "S-300 Big Bird 탐지 레이더" +ctld.i18n["ko"]["S-300 Grumble C2"] = "S-300 관제소" +ctld.i18n["ko"]["S-300 Grumble Clam Shell SR"] = "S-300 Clam Shell 탐지 레이더" +ctld.i18n["ko"]["S-300 Grumble Flap Lid-A TR"] = "S-300 5N63 추적 레이더" +ctld.i18n["ko"]["S-300 Grumble TEL C"] = "S-300 C 포대" +ctld.i18n["ko"]["S-300 Repair"] = "S-300 수리킷" diff --git a/tools/build/generate_i18n_dicts.ps1 b/tools/build/generate_i18n_dicts.ps1 index 7d37b04..a1fb7c5 100644 --- a/tools/build/generate_i18n_dicts.ps1 +++ b/tools/build/generate_i18n_dicts.ps1 @@ -110,12 +110,17 @@ Write-Host "" # ============================================================================= function Get-DictKeys([string]$filePath) { + # A "-- STALE: " (or any "-- ") commented line is not a live entry - skipped, so a dead + # key already marked stale doesn't keep being reported as present in this dictionary. $keys = [System.Collections.Generic.HashSet[string]]::new() if (-not (Test-Path $filePath)) { return $keys } - $raw = Get-Content $filePath -Raw -Encoding UTF8 - $found = [regex]::Matches($raw, 'ctld\.i18n\["[^"]+"\]\["((?:[^"\\]|\\.)*)"\]') - foreach ($m in $found) { - [void]$keys.Add($m.Groups[1].Value) + $lines = Get-Content $filePath -Encoding UTF8 + foreach ($line in $lines) { + if ($line.TrimStart().StartsWith("--")) { continue } + $m = [regex]::Match($line, 'ctld\.i18n\["[^"]+"\]\["((?:[^"\\]|\\.)*)"\]') + if ($m.Success) { + [void]$keys.Add($m.Groups[1].Value) + } } return $keys } diff --git a/tools/build/i18n_dict_utils.py b/tools/build/i18n_dict_utils.py index a6fde25..9bfd07b 100644 --- a/tools/build/i18n_dict_utils.py +++ b/tools/build/i18n_dict_utils.py @@ -11,9 +11,18 @@ def parse_dict(text: str) -> dict[str, str]: - """Parse a CTLD_i18n_XX.lua dict file's content into {key: value} (excludes translation_version).""" + """Parse a CTLD_i18n_XX.lua dict file's content into {key: value} (excludes translation_version). + + A line commented out (e.g. generate_i18n_dicts.ps1's "-- STALE: " marker for a key no longer + referenced in src/) is not a live entry and is skipped. + """ result: dict[str, str] = {} - for m in _ENTRY_RE.finditer(text): + for line in text.splitlines(): + if line.strip().startswith("--"): + continue + m = _ENTRY_RE.search(line) + if not m: + continue key, val = m.group(1), m.group(2) if key != "translation_version": result[key] = val @@ -21,10 +30,15 @@ def parse_dict(text: str) -> dict[str, str]: def parse_keep_en(text: str) -> set[str]: - """Parse the keys listed in a dict file's `__keep_en = { ["key"] = true, ... }` block.""" + """Parse the keys listed in a dict file's `__keep_en = { ["key"] = true, ... }` block. + + A commented-out entry inside the block is not live and is skipped (see parse_dict). + """ keep_en: set[str] = set() in_block = False for line in text.splitlines(): + if line.strip().startswith("--"): + continue if "__keep_en" in line and "=" in line and "{" in line: in_block = True if in_block: diff --git a/tools/build/test_i18n_dict_utils.py b/tools/build/test_i18n_dict_utils.py index af787da..e0057c2 100644 --- a/tools/build/test_i18n_dict_utils.py +++ b/tools/build/test_i18n_dict_utils.py @@ -41,3 +41,27 @@ def test_parse_keep_en_collects_block_keys_only(): def test_parse_keep_en_empty_when_no_block(): text = 'ctld.i18n["fr"]["Actions"] = "Actions"\n' assert parse_keep_en(text) == set() + + +def test_parse_dict_skips_stale_commented_entry(): + text = ( + '-- STALE: ctld.i18n["ko"]["Dead Key"] = "죽은 키"\n' + 'ctld.i18n["ko"]["Live Key"] = "살아있는 키"\n' + ) + assert parse_dict(text) == {"Live Key": "살아있는 키"} + + +def test_parse_dict_skips_stale_entry_with_empty_value(): + text = '-- STALE: ctld.i18n["ko"]["Dead Key"] = ""\n' + assert parse_dict(text) == {} + + +def test_parse_keep_en_skips_commented_block_entry(): + text = ( + 'ctld.i18n["fr"].__keep_en = {\n' + ' ["BTR-D"] = true,\n' + ' -- ["Old Mod Name"] = true,\n' + "}\n" + 'ctld.i18n["fr"]["Actions"] = "Actions"\n' + ) + assert parse_keep_en(text) == {"BTR-D"} diff --git a/tools/build/test_translate_i18n.py b/tools/build/test_translate_i18n.py index c639e9a..7082818 100644 --- a/tools/build/test_translate_i18n.py +++ b/tools/build/test_translate_i18n.py @@ -1,6 +1,6 @@ -"""Stub detection and backend selection for translate_i18n.py.""" +"""Stub detection, backend selection, and dictionary writes for translate_i18n.py.""" -from translate_i18n import _collect_stubs, _is_stub, _select_backend +from translate_i18n import _apply_translations, _collect_stubs, _is_stub, _select_backend def test_empty_value_is_a_stub(): @@ -45,3 +45,24 @@ def test_select_backend_prefers_api_when_key_present(): def test_select_backend_falls_back_to_cli_when_key_absent(): assert _select_backend(has_api_key=False) == "cli" + + +def test_apply_translations_does_not_write_a_stale_commented_line(tmp_path): + original = '-- STALE: ctld.i18n["ko"]["Dead Key"] = ""\n' + path = tmp_path / "CTLD_i18n_ko.lua" + path.write_text(original, encoding="utf-8") + + written = _apply_translations(path, {"Dead Key": "죽은 번역"}, "ko") + + assert written == 0 + assert path.read_text(encoding="utf-8") == original + + +def test_apply_translations_writes_a_live_line(tmp_path): + path = tmp_path / "CTLD_i18n_ko.lua" + path.write_text('ctld.i18n["ko"]["Live Key"] = ""\n', encoding="utf-8") + + written = _apply_translations(path, {"Live Key": "살아있는 번역"}, "ko") + + assert written == 1 + assert path.read_text(encoding="utf-8") == 'ctld.i18n["ko"]["Live Key"] = "살아있는 번역"\n' diff --git a/tools/build/translate_i18n.py b/tools/build/translate_i18n.py index 8095e3b..61ee6f0 100644 --- a/tools/build/translate_i18n.py +++ b/tools/build/translate_i18n.py @@ -56,9 +56,14 @@ def _apply_translations(path: Path, translations: dict[str, str], lang: str) -> for key, new_val in translations.items(): # Escape backslashes and double-quotes in the new value escaped = new_val.replace("\\", "\\\\").replace('"', '\\"') - pattern = r'(ctld\.i18n\["' + re.escape(lang) + r'"\]\["' + re.escape(key) + r'"\]\s*=\s*)"(?:[^"\\]|\\.)*"' + # Anchored to line start (MULTILINE) so a "-- STALE: " commented line - which no + # longer starts with "ctld.i18n[...]" at column 0 - is never matched and rewritten. + pattern = re.compile( + r'^(ctld\.i18n\["' + re.escape(lang) + r'"\]\["' + re.escape(key) + r'"\]\s*=\s*)"(?:[^"\\]|\\.)*"', + re.MULTILINE, + ) replacement = r'\g<1>"' + escaped + '"' - new_text, n = re.subn(pattern, replacement, text) + new_text, n = pattern.subn(replacement, text) if n: text = new_text count += 1