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
61 changes: 61 additions & 0 deletions .backlog/FIX-I18N-TRANSLATE-ALIAS/PRD.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions .backlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). | — |
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/developer/i18n.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
3 changes: 2 additions & 1 deletion docs/developer/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/CTLD_i18n.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

-- =====================================================================
Expand Down
23 changes: 19 additions & 4 deletions src/CTLD_i18n_en.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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 "
Expand Down Expand Up @@ -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"
23 changes: 19 additions & 4 deletions src/CTLD_i18n_es.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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 "
Expand Down Expand Up @@ -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"
23 changes: 19 additions & 4 deletions src/CTLD_i18n_fr.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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 "
Expand Down Expand Up @@ -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"
Loading
Loading