Skip to content

Latest commit

 

History

History
222 lines (169 loc) · 9.11 KB

File metadata and controls

222 lines (169 loc) · 9.11 KB

Internationalisation (i18n)

Every user-facing string in CTLD is translatable. Source code never emits raw text to players — it calls ctld.tr("English text"), and the i18n layer resolves that to the active language at runtime. CTLD ships four dictionaries: English (reference), French, Spanish and Korean.

The i18n subsystem is deliberately split into logic and data. src/CTLD_i18n.lua holds the CTLDi18n singleton, the ctld.tr() function and the audit helpers — no strings. Each language lives in its own CTLD_i18n_XX.lua dictionary file, maintained independently (often by different translators). A PowerShell tool keeps those dictionaries in sync with the keys actually used in source.

The four dictionaries

File Language Role
CTLD_i18n.lua Class logic only: CTLDi18n singleton, ctld.tr(), audit helpers. No dictionary data.
CTLD_i18n_en.lua English Reference dictionary. Every value equals its key. Never empty.
CTLD_i18n_fr.lua French Empty value "" = untranslated (falls back to EN).
CTLD_i18n_es.lua Spanish Same rule.
CTLD_i18n_ko.lua Korean Same rule.

All five files are listed — in this order — in tools/build/listToMerge.txt, so CTLD_i18n.lua loads before any dictionary and the reference en dictionary loads first.

The active language is selected at the top of src/CTLD_i18n.lua:

ctld.i18n_lang = "en"
--ctld.i18n_lang = "fr"
--ctld.i18n_lang = "es"
--ctld.i18n_lang = "ko"

The ctld.tr() contract

ctld.tr() translates a key to the active language and substitutes any positional parameters.

ctld.tr("Troops loaded")
ctld.tr("Sorry you must wait %1 seconds before you can get another crate", waitTime)
ctld.tr("%1 loaded %2 vehicles into %3", pilotName, count, zoneName)

Rule: the key passed to ctld.tr() is ALWAYS a complete string literal. Variable parts are expressed with %1, %2, … placeholders inside the literal; dynamic values are passed as separate arguments. Never build a key by concatenation:

-- FORBIDDEN — breaks the static scan, keys go silently missing from dicts
ctld.tr("prefix_" .. suffix)
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.

Fallback chain

ctld.tr() never returns nil or an empty string. It resolves in this order:

  1. The active-language dictionary (ctld.i18n[ctld.i18n_lang]).
  2. The English dictionary.
  3. The key itself (which is the English text).

An unknown active language logs a WARN and falls back to en.

Dictionary versioning

Each dictionary carries its own translation_version, a "MAJOR.MINOR" string:

ctld.i18n["en"].translation_version = "1.8"

Versions are independent per dictionary — dicts are maintained separately, so each one is bumped only when it changes, not when a sibling changes. ctld.i18n_check() and ctld.i18n_audit() flag a mismatch between EN and another language as a signal that the other dictionary may be lagging behind EN and needs review. A mismatch is a warning, not an error: the fallback chain still resolves every key.

Keeping dictionaries in sync — generate_i18n_dicts.ps1

tools/build/generate_i18n_dicts.ps1 scans every src/*.lua file (excluding the dictionaries themselves) for ctld.tr() keys, then reconciles each dictionary against that set. Run it after any ctld.tr() addition or removal in source.

# Audit only — reports what would change, writes nothing (default)
powershell -ExecutionPolicy Bypass -File tools\build\generate_i18n_dicts.ps1

# Apply — add missing keys, mark stale ones, bump versions
powershell -ExecutionPolicy Bypass -File tools\build\generate_i18n_dicts.ps1 -Apply

Per dictionary, -Apply performs:

Situation Action
Key used in source, absent from dict Appended at end of file. EN gets value = key; other langs get value = "" (untranslated).
Key in dict, no longer used in source Its line is prefixed with -- STALE: — never deleted, so a human confirms before removing.
Any change made to the dict That dict's own translation_version is bumped (patch component +1).

The tool only rewrites the dictionary files — it does not regenerate any loader and does not touch listToMerge.txt. The merge order that assembles CTLD.lua is driven entirely by listToMerge.txt (see Architecture).

Adding a translation key

  1. Call ctld.tr("My new text") in the source module (literal key, %1… placeholders for variables).
  2. Run generate_i18n_dicts.ps1 -Apply. The new key is appended to all four dictionaries — with the English text as the EN value and "" in the others — and each modified dict's version is bumped.
  3. Translators fill in the empty values in CTLD_i18n_fr/es/ko.lua.
  4. Rebuild CTLD.lua (tools\build\merge_CTLD.ps1) and add busted coverage for the new logic.

Adding a language

To add a fifth language XX:

  1. Copy src/CTLD_i18n_en.lua to src/CTLD_i18n_XX.lua, replace every ctld.i18n["en"] with ctld.i18n["XX"], and translate the values (keep the keys identical to EN).

  2. Register the file in tools/build/listToMerge.txt, after the existing dictionaries.

  3. Register the language in the $DictFiles ordered map in tools/build/generate_i18n_dicts.ps1 so the sync tool tracks it:

    $DictFiles = [ordered]@{
        "en" = (Join-Path $DictDir "CTLD_i18n_en.lua")
        "fr" = (Join-Path $DictDir "CTLD_i18n_fr.lua")
        "es" = (Join-Path $DictDir "CTLD_i18n_es.lua")
        "ko" = (Join-Path $DictDir "CTLD_i18n_ko.lua")
        "XX" = (Join-Path $DictDir "CTLD_i18n_XX.lua")
    }
  4. Add the language as an option in src/CTLD_i18n.lua (--ctld.i18n_lang = "XX"), and select it there when you want it active.

  5. Load the dictionary in the tests that exercise all languages — tests/ci/unit/i18n_spec.lua dofiles the non-EN dictionaries explicitly (the shared tests/ci/helpers/loader.lua loads only CTLD_i18n_en.lua).

  6. Run generate_i18n_dicts.ps1 to confirm the new dictionary is complete, then rebuild.

Mission-maker overrides (ctld.i18n_overrides)

Mission makers can override any string without touching a dictionary file, from CTLD_userConfig.lua (the .miz-side config — ctld.i18n_overrides is never set inside src/):

-- In CTLD_userConfig.lua, not in src/
ctld.i18n_overrides = {
    fr = { ["Troops loaded"] = "Soldats embarqués" },
    en = { ["CTLD Commands"] = "Helicopter Commands" },
}

Overrides are applied once at startup, in memory, by CTLDi18n:_init() — the first call to CTLDi18n.getInstance() iterates ctld.i18n_overrides and overwrites the matching dictionary entries. The runtime sequence:

1. CTLD.lua loads      → dictionaries populated in ctld.i18n[lang]
2. CTLD_userConfig.lua → sets ctld.i18n_overrides
3. getInstance()       → _init() copies overrides into the in-memory dicts
4. ctld.tr(key)        → returns the overridden value

This is a runtime mechanism and is orthogonal to generate_i18n_dicts.ps1, which is a build-time tool operating on the dictionary source files. Overrides never touch src/.

Translator audit API

Three functions detect gaps between EN and a target language. The audit pair returns structured data (suitable for busted assertions and DO SCRIPT triggers); i18n_check is the legacy variant that logs straight to env.*.

ctld.i18n_audit(language)

Compares one language against EN and returns a result table (plus an error string when the language is unknown):

local result, err = ctld.i18n_audit("fr")
if err then
    -- language not loaded
else
    if not result.version_match then
        -- EN was bumped; FR may need review
    end
    -- result.en_version    : EN's translation_version
    -- result.lang_version  : target's translation_version
    -- result.missing       : keys present in EN but absent in FR
    -- result.untranslated  : keys where the FR value equals the EN value
end

ctld.i18n_auditAll()

Runs ctld.i18n_audit() on every loaded non-EN language and returns a table keyed by language code:

local results = ctld.i18n_auditAll()  -- results["fr"], results["es"], results["ko"]
for lang, r in pairs(results) do
    print(lang, "#missing=" .. #r.missing, "#untranslated=" .. #r.untranslated)
end

ctld.i18n_check(language, verbose) (legacy — DCS only)

Logs missing keys as env.error and untranslated keys as env.warning, writing directly to the DCS log. Because it does not return data it is unsuitable for assertions — use ctld.i18n_audit() in tests and scripts. A ready-made per-language gap report you can paste into a DO SCRIPT trigger sits in the comment block at the bottom of src/CTLD_i18n.lua.

See Building & testing for how the i18n specs run under busted, and Architecture for the merge order that assembles the dictionaries into CTLD.lua.