diff --git a/.backlog/FIX-I18N-TRANSLATE-ALIAS/PRD.md b/.backlog/FIX-I18N-TRANSLATE-ALIAS/PRD.md new file mode 100644 index 00000000..1bf4307b --- /dev/null +++ b/.backlog/FIX-I18N-TRANSLATE-ALIAS/PRD.md @@ -0,0 +1,61 @@ +# FIX-I18N-TRANSLATE-ALIAS — 13 JTAC radio messages escape the i18n guard through the `ctld.i18n_translate` alias + +**Status:** ✅ done (PR #269) + +GitHub issue #265 (automated review of `develop` at `673b064`), decided with the maintainer on 2026-10-20: replace the alias in `src/`, check +that nothing in the code uses it any more, and keep it supported (defined and recognised by the guard) for the scripts outside the repository +that may still call it. + +## Problem Statement + +`ctld.i18n_translate` is a plain alias of `ctld.tr`. The dictionary generator (`generate_i18n_dicts.ps1`), the `i18n-guard` CI job and the +`pre-push` hook all rely on a scan for the literal name `ctld.tr(`, so everything written through the alias is invisible to the three of them. +`CTLD_jtac.lua` calls the alias 16 times (the JTAC radio messages): 13 of the 16 keys are in none of the four dictionaries. `ctld.tr` falls +back to the English key, so on a French, Spanish or Korean server every JTAC radio call comes out in English, with `lasing` / `standing by on` +inserted in the middle of a translated sentence. The gap widens on its own: the last batch of changes added two JTAC messages through the alias +that reached no dictionary, with no signal. + +## Solution + +The JTAC module calls `ctld.tr` like the rest of `src/`, so the guard sees its keys. The alias stays defined, for the scripts outside the +repository, and the generator also recognises it, so any key written through it still reaches the dictionaries. The 13 missing keys are added +to the four dictionaries and translated. A test locks the rule: every literal key passed to `ctld.tr` or to the alias in `src/` is a key of +the English dictionary. + +## User Stories + +1. As a player on a French, Spanish or Korean server, I want the JTAC radio messages in my language, so that I read one language in a sentence. +2. As a CTLD developer, I want one way of writing a translated text in `src/`, so that the guard sees every key. +3. As a mission maker with a script that calls `ctld.i18n_translate`, I want it to keep working, so that my mission does not break. +4. As a CTLD developer, I want the generator to also see keys written through the alias, so that a script or a plugin using it is not silently + left out of the dictionaries. +5. As a CTLD developer, I want a test that fails when a literal key used in `src/` is missing from the English dictionary, so that this cannot + happen again whatever the guard does. +6. As a maintainer, I want to know the alias has no remaining caller in `src/`. + +## Implementation Decisions + +- The 16 `ctld.i18n_translate(...)` calls of `CTLD_jtac.lua` become `ctld.tr(...)`; same arguments, same behaviour. +- The alias definition in `CTLD_i18n.lua` stays, with its comment now saying it is kept for external scripts (new code uses `ctld.tr`). +- The generator's key scan matches `ctld.tr` and `ctld.i18n_translate`. +- The dictionaries are regenerated by the build (13 new keys in `en`, stubs in `fr`/`es`/`ko`), then translated (proposal for the maintainer to + review). `translation_version` is bumped by the generator. +- Documentation: the i18n developer page (EN + FR) says the alias is kept for external scripts and that the guard recognises it. +- No catalogue or configuration change. + +## Testing Decisions + +- A good test observes the invariant (every literal key is translatable), not how the scan is written. +- A busted spec reads the source files of `src/`, extracts the literals passed to `ctld.tr` and to the alias, and checks each is a key of the + English dictionary. It fails before the change (the 13 JTAC keys) and passes after. +- The existing i18n specs and the `i18n-guard` job stay as they are. + +## Out of Scope + +- Removing the alias (it stays for external scripts). +- Translating keys not passed as a literal, or the config catalogue labels. +- Any change to the JTAC messages themselves. + +## Further Notes + +The legacy monolith (`migration/source/`, immutable) uses the alias heavily; it is not part of `src/` and not scanned. diff --git a/.backlog/FIX-I18N-TRANSLATE-ALIAS/tickets/01-jtac-through-tr-and-guard-sees-the-alias.md b/.backlog/FIX-I18N-TRANSLATE-ALIAS/tickets/01-jtac-through-tr-and-guard-sees-the-alias.md new file mode 100644 index 00000000..9c479bee --- /dev/null +++ b/.backlog/FIX-I18N-TRANSLATE-ALIAS/tickets/01-jtac-through-tr-and-guard-sees-the-alias.md @@ -0,0 +1,24 @@ +# 01 — JTAC messages through `ctld.tr`, and the guard sees the alias + +**Status:** ✅ done (PR #269) · **Type:** AFK + +## Parent + +[PRD — FIX-I18N-TRANSLATE-ALIAS](../PRD.md). Stories 1-6. GitHub issue #265. + +## What to build + +Replace the 16 alias calls of the JTAC module with `ctld.tr`; make the generator also match the alias; regenerate the dictionaries (13 new keys) +and translate them FR, ES, KO; add the invariant spec (every literal key used in `src/` is in the English dictionary); update the i18n +developer page (EN + FR). Test first. Then CHANGELOG, backlog index, PR. + +## Acceptance criteria + +- [ ] The new spec fails before the change (the 13 keys) and passes after it; the existing specs pass unchanged. +- [ ] `src/` has no caller of `ctld.i18n_translate` left; the alias is still defined. +- [ ] The generator finds a key written through the alias. +- [ ] Dictionaries regenerated by `merge_CTLD.ps1`, FR/ES/KO translated; luacheck clean; `busted` green; CHANGELOG entry; docs EN + FR. + +## Blocked by + +None - can start immediately. diff --git a/.backlog/README.md b/.backlog/README.md index d779dfbc..9df1252b 100644 --- a/.backlog/README.md +++ b/.backlog/README.md @@ -14,6 +14,7 @@ authored **per lot, when the lot is started** (not in batch). | Lot | Status | Description | Branch | |-----|--------|-------------|--------| +| [`FIX-I18N-TRANSLATE-ALIAS`](FIX-I18N-TRANSLATE-ALIAS/PRD.md) | merged (PR #269) — 1 ticket ([JTAC through tr, guard sees the alias](FIX-I18N-TRANSLATE-ALIAS/tickets/01-jtac-through-tr-and-guard-sees-the-alias.md)) | The 16 JTAC radio keys reach the four dictionaries; the JTAC module calls ctld.tr and the generator also recognises the alias. | | [`FIX-FARP-PACK-FALSE-SUCCESS`](FIX-FARP-PACK-FALSE-SUCCESS/PRD.md) | merged (PR #268) — 1 ticket ([report the crates created](FIX-FARP-PACK-FALSE-SUCCESS/tickets/01-report-the-crates-created.md)) | Packing a FARP now reports a pack that created no crate, or only some, instead of always announcing a success. | | [`FIX-UNPACK-FALSE-SUCCESS`](FIX-UNPACK-FALSE-SUCCESS/PRD.md) | merged (PR #267) — 1 ticket ([announce success only on true](FIX-UNPACK-FALSE-SUCCESS/tickets/01-announce-success-only-on-true.md)) | The Unpack Crate click no longer announces a success when a JTAC limit or a missing spawn position produced nothing. | | [`FEAT-JTAC-LOG-REAL-LASER-CODE`](FEAT-JTAC-LOG-REAL-LASER-CODE/PRD.md) | ready — 1 ticket ([01 log the code, then measure the LSS](FEAT-JTAC-LOG-REAL-LASER-CODE/tickets/01-log-the-code-then-measure-the-lss.md)) | Reported by VMCT from the Sinai Open Training v6 (2026-10-12): Reaper 1 (imposed 1511) claimed nine targets in an hour, yet an A-10C's TGP LSS on 1511 at 15 nm found no spot. The log holds only claim and release, never the code or the spot, so the report cannot be settled. Log both, then test the hypotheses (code, LSS range, line of sight, shared code). | — | diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e5f8cd..a616533d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ Versioning follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed — the JTAC radio messages are translated, and the guard sees the `ctld.i18n_translate` alias (FIX-I18N-TRANSLATE-ALIAS) + +- The JTAC radio messages were written through `ctld.i18n_translate`, an alias of `ctld.tr` that the dictionary generator, the `i18n-guard` + job and the `pre-push` hook do not scan for: 16 JTAC keys were in no English dictionary and 13 in none at all, so every JTAC radio call came + out in English on a French, Spanish or Korean server (with `lasing` / `standing by on` in the middle of a translated sentence). The JTAC + module now calls `ctld.tr`, the 16 keys are in the four dictionaries (FR/ES/KO texts are a proposal for review), and the generator also + recognises the alias, which stays defined for scripts outside the repository. A new spec checks that every literal key used in `src/` is a + key of the English dictionary (issue #265). + ### Fixed — Packing a FARP reports how many crates were created (FIX-FARP-PACK-FALSE-SUCCESS) - Packing a FARP destroys the scene first, then creates its crates, and always announced "FARP packed successfully!" whatever the creation diff --git a/docs/developer/i18n.fr.md b/docs/developer/i18n.fr.md index 31233582..9c2a1a70 100644 --- a/docs/developer/i18n.fr.md +++ b/docs/developer/i18n.fr.md @@ -59,8 +59,9 @@ Cela a son importance parce que `generate_i18n_dicts.ps1` extrait les clés avec régulière. Si chaque clé est un littéral, le scan est fiable à 100 % ; la concaténation crée des angles morts. -`ctld.i18n_translate` est un alias de compatibilité ascendante pour `ctld.tr` — le nouveau code -utilise `ctld.tr`. +`ctld.i18n_translate` est un alias de compatibilité ascendante pour `ctld.tr`, conservé pour les scripts +extérieurs au dépôt — le code de `src/` utilise `ctld.tr`. Le générateur reconnaît aussi l'alias : une clé +écrite par son intermédiaire arrive quand même dans les dictionnaires. ### Chaîne de repli { #fallback-chain } diff --git a/docs/developer/i18n.md b/docs/developer/i18n.md index 09bee750..6f3a806e 100644 --- a/docs/developer/i18n.md +++ b/docs/developer/i18n.md @@ -55,7 +55,8 @@ ctld.tr(someVariable) This matters because `generate_i18n_dicts.ps1` extracts keys with a regex. If every key is a literal the scan is 100% reliable; concatenation creates blind spots. -`ctld.i18n_translate` is a backward-compatibility alias for `ctld.tr` — new code uses `ctld.tr`. +`ctld.i18n_translate` is a backward-compatibility alias for `ctld.tr`, kept for scripts outside the repository — code in `src/` +uses `ctld.tr`. The generator also recognises the alias, so a key written through it still reaches the dictionaries. ### Fallback chain diff --git a/src/CTLD_i18n.lua b/src/CTLD_i18n.lua index 8157f5f4..18a7050a 100644 --- a/src/CTLD_i18n.lua +++ b/src/CTLD_i18n.lua @@ -105,7 +105,8 @@ function ctld.tr(text, ...) return _text end ---- Backward-compatibility alias. +--- Backward-compatibility alias, kept for scripts outside the repository. Code in src/ calls ctld.tr; the dictionary +--- generator also recognises this name. ctld.i18n_translate = ctld.tr -- ===================================================================== diff --git a/src/CTLD_i18n_en.lua b/src/CTLD_i18n_en.lua index 0e1484eb..a746843b 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.27" +ctld.i18n["en"].translation_version = "1.28" --- groups names ctld.i18n["en"]["Standard Group"] = "Standard Group" @@ -195,8 +195,6 @@ ctld.i18n["en"]["List Beacons"] = "List Beacons" --- JTAC messages -- STALE: ctld.i18n["en"]["JTAC Group %1 KIA!"] = "JTAC Group %1 KIA!" --- STALE: ctld.i18n["en"]["%1, selected target reacquired, %2"] = "%1, selected target reacquired, %2" --- STALE: ctld.i18n["en"][". CODE: %1. POSITION: %2"] = ". CODE: %1. POSITION: %2" -- STALE: ctld.i18n["en"]["new target, "] = "new target, " -- STALE: ctld.i18n["en"]["standing by on %1"] = "standing by on %1" -- STALE: ctld.i18n["en"]["lasing %1"] = "lasing %1" @@ -208,7 +206,6 @@ ctld.i18n["en"]["List Beacons"] = "List Beacons" -- STALE: ctld.i18n["en"]["%1 %2 target destroyed."] = "%1 %2 target destroyed." -- STALE: ctld.i18n["en"]["JTAC STATUS: \n\n"] = "JTAC STATUS: \n\n" -- STALE: ctld.i18n["en"][", available on %1 %2,"] = ", available on %1 %2," --- STALE: ctld.i18n["en"]["UNKNOWN"] = "UNKNOWN" -- STALE: ctld.i18n["en"][" targeting "] = " targeting " -- STALE: ctld.i18n["en"][" targeting selected unit "] = " targeting selected unit " -- STALE: ctld.i18n["en"][" attempting to find selected unit, temporarily targeting "] = " attempting to find selected unit, temporarily targeting " @@ -597,3 +594,21 @@ ctld.i18n["en"]["Unpack failed: the equipment could not be created."] = "Unpack --- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 ctld.i18n["en"]["FARP packed: %1 of %2 crates created."] = "FARP packed: %1 of %2 crates created." ctld.i18n["en"]["Pack failed: the crates could not be created."] = "Pack failed: the crates could not be created." + +--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 +ctld.i18n["en"]["%1, %2 new target, %3"] = "%1, %2 new target, %3" +ctld.i18n["en"]["%1, laser code changed to %2, now on %3 %4."] = "%1, laser code changed to %2, now on %3 %4." +ctld.i18n["en"]["%1, laser code changed to %2."] = "%1, laser code changed to %2." +ctld.i18n["en"]["%1, no targets in range."] = "%1, no targets in range." +ctld.i18n["en"]["%1, selected %2 %3"] = "%1, selected %2 %3" +ctld.i18n["en"]["%1, selected target destroyed."] = "%1, selected target destroyed." +ctld.i18n["en"]["%1, selected target lost, temporarily lasing %2"] = "%1, selected target lost, temporarily lasing %2" +ctld.i18n["en"]["%1, selected target reacquired, %2"] = "%1, selected target reacquired, %2" +ctld.i18n["en"]["%1, target destroyed."] = "%1, target destroyed." +ctld.i18n["en"]["%1, target lost."] = "%1, target lost." +ctld.i18n["en"][". CODE: %1. POSITION: %2"] = ". CODE: %1. POSITION: %2" +ctld.i18n["en"]["JTAC %1 KIA!"] = "JTAC %1 KIA!" +ctld.i18n["en"]["lasing"] = "lasing" +ctld.i18n["en"]["standing by on"] = "standing by on" +ctld.i18n["en"]["unknown"] = "unknown" +ctld.i18n["en"]["UNKNOWN"] = "UNKNOWN" diff --git a/src/CTLD_i18n_es.lua b/src/CTLD_i18n_es.lua index 98db9906..1a9ce457 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.27" +ctld.i18n["es"].translation_version = "1.28" --- groups names ctld.i18n["es"]["Standard Group"] = "Grupo estándar" @@ -190,8 +190,6 @@ ctld.i18n["es"]["List Beacons"] = "Listar balizas" --- JTAC messages -- STALE: ctld.i18n["es"]["JTAC Group %1 KIA!"] = "¡Grupo JTAC %1 KIA!" --- STALE: ctld.i18n["es"]["%1, selected target reacquired, %2"] = "%1, objetivo seleccionado readquirido, %2" --- STALE: ctld.i18n["es"][". CODE: %1. POSITION: %2"] = ". CÓDIGO: %1. POSICIÓN: %2" -- STALE: ctld.i18n["es"]["new target, "] = "nuevo objetivo, " -- STALE: ctld.i18n["es"]["standing by on %1"] = "en espera en %1" -- STALE: ctld.i18n["es"]["lasing %1"] = "láser %1" @@ -203,7 +201,6 @@ ctld.i18n["es"]["List Beacons"] = "Listar balizas" -- STALE: ctld.i18n["es"]["%1 %2 target destroyed."] = "%1 %2 objetivo destruido." -- STALE: ctld.i18n["es"]["JTAC STATUS: \n\n"] = "ESTADO JTAC: \n\n" -- STALE: ctld.i18n["es"][", available on %1 %2,"] = ", disponible en %1 %2," --- STALE: ctld.i18n["es"]["UNKNOWN"] = "DESCONOCIDO" -- STALE: ctld.i18n["es"][" targeting "] = " apuntando " -- STALE: ctld.i18n["es"][" targeting selected unit "] = " apuntando a la unidad indicada" -- STALE: ctld.i18n["es"][" attempting to find selected unit, temporarily targeting "] = " intentando encontrar la unidad indicada, laser activo " @@ -604,3 +601,21 @@ ctld.i18n["es"]["Unpack failed: the equipment could not be created."] = "Fallo a --- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 ctld.i18n["es"]["FARP packed: %1 of %2 crates created."] = "FARP reempacado: %1 de %2 cajas creadas." ctld.i18n["es"]["Pack failed: the crates could not be created."] = "Error al reempacar: no se pudieron crear las cajas." + +--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 +ctld.i18n["es"]["%1, %2 new target, %3"] = "%1, nuevo objetivo, %2 %3" +ctld.i18n["es"]["%1, laser code changed to %2, now on %3 %4."] = "%1, código láser cambiado a %2, ahora en %3 %4." +ctld.i18n["es"]["%1, laser code changed to %2."] = "%1, código láser cambiado a %2." +ctld.i18n["es"]["%1, no targets in range."] = "%1, no hay objetivos al alcance." +ctld.i18n["es"]["%1, selected %2 %3"] = "%1, %2 %3, objetivo seleccionado" +ctld.i18n["es"]["%1, selected target destroyed."] = "%1, objetivo seleccionado destruido." +ctld.i18n["es"]["%1, selected target lost, temporarily lasing %2"] = "%1, objetivo seleccionado perdido, designación temporal de %2" +ctld.i18n["es"]["%1, selected target reacquired, %2"] = "%1, objetivo seleccionado readquirido, %2" +ctld.i18n["es"]["%1, target destroyed."] = "%1, objetivo destruido." +ctld.i18n["es"]["%1, target lost."] = "%1, objetivo perdido." +ctld.i18n["es"][". CODE: %1. POSITION: %2"] = ". CÓDIGO: %1. POSICIÓN: %2" +ctld.i18n["es"]["JTAC %1 KIA!"] = "¡JTAC %1 KIA!" +ctld.i18n["es"]["lasing"] = "designando" +ctld.i18n["es"]["standing by on"] = "en espera en" +ctld.i18n["es"]["unknown"] = "desconocido" +ctld.i18n["es"]["UNKNOWN"] = "DESCONOCIDO" diff --git a/src/CTLD_i18n_fr.lua b/src/CTLD_i18n_fr.lua index c06103c3..bb8aaf6f 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.27" +ctld.i18n["fr"].translation_version = "1.28" --- groups names ctld.i18n["fr"]["Standard Group"] = "Groupe standard" @@ -189,8 +189,6 @@ ctld.i18n["fr"]["List Beacons"] = "Lister balises" --- JTAC messages -- STALE: ctld.i18n["fr"]["JTAC Group %1 KIA!"] = "Groupe JTAC %1 KIA !" --- STALE: ctld.i18n["fr"]["%1, selected target reacquired, %2"] = "%1, cible sélectionnée réacquise, %2" --- STALE: ctld.i18n["fr"][". CODE: %1. POSITION: %2"] = ". CODE : %1. POSITION : %2" -- STALE: ctld.i18n["fr"]["new target, "] = "nouvelle cible, " -- STALE: ctld.i18n["fr"]["standing by on %1"] = "en attente sur %1" -- STALE: ctld.i18n["fr"]["lasing %1"] = "laser %1" @@ -202,7 +200,6 @@ ctld.i18n["fr"]["List Beacons"] = "Lister balises" -- STALE: ctld.i18n["fr"]["%1 %2 target destroyed."] = "%1 %2 cible détruite." -- STALE: ctld.i18n["fr"]["JTAC STATUS: \n\n"] = "ÉTAT JTAC : \n\n" -- STALE: ctld.i18n["fr"][", available on %1 %2,"] = ", disponible sur %1 %2," --- STALE: ctld.i18n["fr"]["UNKNOWN"] = "INCONNU" -- STALE: ctld.i18n["fr"][" targeting "] = " ciblage " -- STALE: ctld.i18n["fr"][" targeting selected unit "] = " ciblage de l'unité sélectionnée " -- STALE: ctld.i18n["fr"][" attempting to find selected unit, temporarily targeting "] = " tentative de recherche de l'unité sélectionnée, ciblage temporaire " @@ -629,3 +626,21 @@ ctld.i18n["fr"]["Unpack failed: the equipment could not be created."] = "Échec --- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 ctld.i18n["fr"]["FARP packed: %1 of %2 crates created."] = "FARP remballé : %1 caisse(s) sur %2 créée(s)." ctld.i18n["fr"]["Pack failed: the crates could not be created."] = "Remballage impossible : les caisses n'ont pas pu être créées." + +--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 +ctld.i18n["fr"]["%1, %2 new target, %3"] = "%1, nouvelle cible, %2 %3" +ctld.i18n["fr"]["%1, laser code changed to %2, now on %3 %4."] = "%1, code laser changé en %2, maintenant sur %3 %4." +ctld.i18n["fr"]["%1, laser code changed to %2."] = "%1, code laser changé en %2." +ctld.i18n["fr"]["%1, no targets in range."] = "%1, aucune cible à portée." +ctld.i18n["fr"]["%1, selected %2 %3"] = "%1, %2 %3, cible sélectionnée" +ctld.i18n["fr"]["%1, selected target destroyed."] = "%1, cible sélectionnée détruite." +ctld.i18n["fr"]["%1, selected target lost, temporarily lasing %2"] = "%1, cible sélectionnée perdue, désignation temporaire de %2" +ctld.i18n["fr"]["%1, selected target reacquired, %2"] = "%1, cible sélectionnée réacquise, %2" +ctld.i18n["fr"]["%1, target destroyed."] = "%1, cible détruite." +ctld.i18n["fr"]["%1, target lost."] = "%1, cible perdue." +ctld.i18n["fr"][". CODE: %1. POSITION: %2"] = ". CODE : %1. POSITION : %2" +ctld.i18n["fr"]["JTAC %1 KIA!"] = "JTAC %1 KIA !" +ctld.i18n["fr"]["lasing"] = "désigne" +ctld.i18n["fr"]["standing by on"] = "en attente sur" +ctld.i18n["fr"]["unknown"] = "inconnu" +ctld.i18n["fr"]["UNKNOWN"] = "INCONNU" diff --git a/src/CTLD_i18n_ko.lua b/src/CTLD_i18n_ko.lua index 7c881a5b..2b7cfb6c 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.27" +ctld.i18n["ko"].translation_version = "1.28" --- groups names ctld.i18n["ko"]["Standard Group"] = "표준 그룹" @@ -483,3 +483,21 @@ ctld.i18n["ko"]["Unpack failed: the equipment could not be created."] = "풀기 --- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 ctld.i18n["ko"]["FARP packed: %1 of %2 crates created."] = "FARP 포장됨: 상자 %2개 중 %1개 생성됨." ctld.i18n["ko"]["Pack failed: the crates could not be created."] = "포장 실패: 상자를 생성할 수 없습니다." + +--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20 +ctld.i18n["ko"]["%1, %2 new target, %3"] = "%1, 새 표적, %2 %3" +ctld.i18n["ko"]["%1, laser code changed to %2, now on %3 %4."] = "%1, 레이저 코드가 %2(으)로 변경됨, 현재 %3 %4." +ctld.i18n["ko"]["%1, laser code changed to %2."] = "%1, 레이저 코드가 %2(으)로 변경됨." +ctld.i18n["ko"]["%1, no targets in range."] = "%1, 사거리 내 표적 없음." +ctld.i18n["ko"]["%1, selected %2 %3"] = "%1, %2 %3, 선택된 표적" +ctld.i18n["ko"]["%1, selected target destroyed."] = "%1, 선택된 표적 파괴." +ctld.i18n["ko"]["%1, selected target lost, temporarily lasing %2"] = "%1, 선택된 표적 소실, 임시 레이저 지시: %2" +ctld.i18n["ko"]["%1, selected target reacquired, %2"] = "%1, 선택된 표적 재획득, %2" +ctld.i18n["ko"]["%1, target destroyed."] = "%1, 표적 파괴." +ctld.i18n["ko"]["%1, target lost."] = "%1, 표적 소실." +ctld.i18n["ko"][". CODE: %1. POSITION: %2"] = ". 코드: %1. 위치: %2" +ctld.i18n["ko"]["JTAC %1 KIA!"] = "JTAC %1 전사!" +ctld.i18n["ko"]["lasing"] = "레이저 지시:" +ctld.i18n["ko"]["standing by on"] = "대기:" +ctld.i18n["ko"]["unknown"] = "알 수 없음" +ctld.i18n["ko"]["UNKNOWN"] = "알 수 없음" diff --git a/src/CTLD_jtac.lua b/src/CTLD_jtac.lua index 0e0295e8..2f5ddd9c 100644 --- a/src/CTLD_jtac.lua +++ b/src/CTLD_jtac.lua @@ -362,59 +362,59 @@ CTLDJTACMessage = {} -- full : detailed, displayed on screen (includes code + position) function CTLDJTACMessage.build(ctx) local name = ctx.jtacName or "JTAC" - local ttype = ctx.targetType or ctld.i18n_translate("unknown") - local code = ctx.laserCode and tostring(ctx.laserCode) or ctld.i18n_translate("UNKNOWN") + local ttype = ctx.targetType or ctld.tr("unknown") + local code = ctx.laserCode and tostring(ctx.laserCode) or ctld.tr("UNKNOWN") local pos = ctx.positionStr or "" - local codePosStr = ctld.i18n_translate(". CODE: %1. POSITION: %2", code, pos) + local codePosStr = ctld.tr(". CODE: %1. POSITION: %2", code, pos) local short, full if ctx.event == "new_target" then local verb = ctx.standby - and ctld.i18n_translate("standing by on") - or ctld.i18n_translate("lasing") + and ctld.tr("standing by on") + or ctld.tr("lasing") if ctx.wasSelected then - short = ctld.i18n_translate("%1, selected %2 %3", name, verb, ttype) + short = ctld.tr("%1, selected %2 %3", name, verb, ttype) else - short = ctld.i18n_translate("%1, %2 new target, %3", name, verb, ttype) + short = ctld.tr("%1, %2 new target, %3", name, verb, ttype) end full = short .. codePosStr elseif ctx.event == "target_reacquired" then - short = ctld.i18n_translate("%1, selected target reacquired, %2", name, ttype) + short = ctld.tr("%1, selected target reacquired, %2", name, ttype) full = short .. codePosStr elseif ctx.event == "target_lost" then if ctx.wasSelected then - short = ctld.i18n_translate("%1, selected target lost, temporarily lasing %2", name, ttype) + short = ctld.tr("%1, selected target lost, temporarily lasing %2", name, ttype) else - short = ctld.i18n_translate("%1, target lost.", name) + short = ctld.tr("%1, target lost.", name) end full = short elseif ctx.event == "target_destroyed" then if ctx.wasSelected then - short = ctld.i18n_translate("%1, selected target destroyed.", name) + short = ctld.tr("%1, selected target destroyed.", name) else - short = ctld.i18n_translate("%1, target destroyed.", name) + short = ctld.tr("%1, target destroyed.", name) end full = short elseif ctx.event == "kia" then - short = ctld.i18n_translate("JTAC %1 KIA!", name) + short = ctld.tr("JTAC %1 KIA!", name) full = short elseif ctx.event == "no_targets" then - short = ctld.i18n_translate("%1, no targets in range.", name) + short = ctld.tr("%1, no targets in range.", name) full = short elseif ctx.event == "code_changed" then if ctx.radio and ctx.radio.freq then - short = ctld.i18n_translate("%1, laser code changed to %2, now on %3 %4.", + short = ctld.tr("%1, laser code changed to %2, now on %3 %4.", name, code, ctx.radio.freq, string.upper(ctx.radio.mod or "FM")) else - short = ctld.i18n_translate("%1, laser code changed to %2.", name, code) + short = ctld.tr("%1, laser code changed to %2.", name, code) end full = short diff --git a/tests/ci/unit/i18n_source_keys_spec.lua b/tests/ci/unit/i18n_source_keys_spec.lua new file mode 100644 index 00000000..10254308 --- /dev/null +++ b/tests/ci/unit/i18n_source_keys_spec.lua @@ -0,0 +1,70 @@ +---@diagnostic disable +-- FIX-I18N-TRANSLATE-ALIAS: every text written as a literal in src/ through ctld.tr, or through its alias ctld.i18n_translate, is a key of +-- the English dictionary. The dictionary generator, the i18n-guard job and the pre-push hook all scan the source for these calls; this spec +-- holds the same invariant from the Lua side, so a key written through a name the scan does not know cannot go unnoticed. +-- ============================================================ + +local _root = debug.getinfo(1, "S").source:match("^@(.+)tests[\\/]unit[\\/]") +if not _root then _root = "" end -- relative path: cwd is repo root +local SRC = _root .. "src" + +local function listSources() + local windows = package.config:sub(1, 1) == "\\" + local cmd = windows and ('dir /s /b "' .. SRC:gsub("/", "\\") .. '\\*.lua" 2>nul') + or ('find "' .. SRC .. '" -name "*.lua" 2>/dev/null') + local files = {} + local p = io.popen(cmd) + for line in p:lines() do + local name = line:match("([^\\/]+)$") + -- the dictionaries and the translation module itself only hold or define keys, they do not use them + if name and not name:match("^CTLD_i18n") then files[#files + 1] = line end + end + p:close() + return files +end + +-- Literals passed as the first argument of ctld.tr( or ctld.i18n_translate(, as the generator reads them. +local function literalKeys(content) + local keys = {} + for _, name in ipairs({ "ctld%.tr", "ctld%.i18n_translate" }) do + local from = 1 + while true do + local _, e = content:find(name .. "%s*%(%s*\"", from) + if not e then break end + local i = e + 1 + while i <= #content do + local c = content:sub(i, i) + if c == "\\" then i = i + 2 + elseif c == '"' then break + else i = i + 1 end + end + keys[#keys + 1] = content:sub(e + 1, i - 1) + from = i + 1 + end + end + return keys +end + +describe("the literal keys used in src/", function() + + it("are all keys of the English dictionary", function() + local missing, seen, count = {}, {}, 0 + for _, path in ipairs(listSources()) do + local f = io.open(path, "rb") + local content = f:read("*a") + f:close() + for _, raw in ipairs(literalKeys(content)) do + local chunk = loadstring('return "' .. raw .. '"') + local key = chunk and chunk() or raw + count = count + 1 + if ctld.i18n["en"][key] == nil and not seen[key] then + seen[key] = true + missing[#missing + 1] = key .. " (" .. path:match("([^\\/]+)$") .. ")" + end + end + end + assert.is_true(count > 100, "the scan found only " .. count .. " literal keys: it is not looking at src/") + table.sort(missing) + assert.same({}, missing) + end) +end) diff --git a/tools/build/generate_i18n_dicts.ps1 b/tools/build/generate_i18n_dicts.ps1 index 70a190ba..3028d680 100644 --- a/tools/build/generate_i18n_dicts.ps1 +++ b/tools/build/generate_i18n_dicts.ps1 @@ -70,7 +70,8 @@ $usedKeys = [System.Collections.Generic.HashSet[string]]::new() foreach ($file in $sourceFiles) { $content = Get-Content $file.FullName -Raw -Encoding UTF8 # Keys are ALWAYS complete string literals — no concatenation allowed (see specs/i18n_rules.md) - $found = [regex]::Matches($content, 'ctld\.tr\s*\(\s*"((?:[^"\\]|\\.)*)"') + # ctld.i18n_translate is the backward-compatibility alias of ctld.tr: a key written through it is a key too. + $found = [regex]::Matches($content, '(?:ctld\.tr|ctld\.i18n_translate)\s*\(\s*"((?:[^"\\]|\\.)*)"') foreach ($m in $found) { [void]$usedKeys.Add($m.Groups[1].Value) }