Skip to content

Localization

Every piece of text the player sees is looked up by a key, a short name like missions/coastal_patrol/mission_name. The game finds the text for that key in the player's language. This page shows how a mod gives its keys their text, in one language or many, without touching any of the game's own files.

The simplest way: text in the mission JSON

A mission can carry its text directly in its JSON file. No script, no keys:

{
    "_rawMissionName": "The Lost Bombers",
    "_rawMissionDescription": "A flight of our own bombers is limping home from France."
}

This is one language only, and it covers exactly nine slots per mission. See Text in the JSON instead of a term below. For anything else you register text from a script.

Defining text from a script

localization.SetTerm("en", "mymod/greeting", "Good morning, gentlemen.")

SetTerm(language_code, key, text) gives a key its text in one language. A key with its texts is called a term.

Call it at the top level of a script, not inside a handler. The top level runs once, when the game loads the script, so the text exists before anything needs it.

A script that only defines text is perfectly valid. It needs no handler, no JSON and no mission id. Put it in the mod's Missions/ folder like any other script; the game runs every script it finds there.

Why this is safe

Text you register does not go into the game's translation files. The game keeps a separate store for text from scripts, adds keys to it as scripts register them, and looks there as well. Nothing you do with SetTerm changes a file on disk, and there is no translation step to run.

If the key already exists in the game's own texts, SetTerm replaces that text for as long as your mod is loaded. That is how a mod can reword a built-in line. It is also why you should prefix every key of your own with your mod name: an accidental match silently overwrites the game's text.

Mission texts

A mission uses nine keys, all shaped missions/<missionId>/<slot>.

Three are read by the game itself and cannot be renamed:

Key Shown in
missions/<id>/mission_name the mission list and the result window
missions/<id>/mission_description the mission window
missions/<id>/intel_threat_report the threat line in the mission window

If one is missing, the raw key appears in the game.

Six are used by the mission script. The shared default mission script uses exactly these names, so a mission without a script of its own needs them too:

Key When
missions/<id>/ap_transmission in flight, going to plan
missions/<id>/gw_transmission in flight, gone wrong
missions/<id>/ap_success_transmission as planned, skill passed
missions/<id>/ap_failure_transmission as planned, skill failed
missions/<id>/gw_success_transmission gone wrong, skill passed
missions/<id>/gw_failure_transmission gone wrong, skill failed

ap means "as planned", gw means "gone wrong". The four *_success and *_failure keys match the four outcome slots of the mission JSON one to one.

A small helper keeps a mission script readable:

MISSION_ID = "mymod_coastal_patrol"


def _term(slot, text):
    localization.SetTerm("en", "missions/{}/{}".format(MISSION_ID, slot), text)


_term("mission_name", "Coastal Patrol")
_term("mission_description", "Fly the shoreline. Report anything that moves.")

A mission script may register as many extra keys as it likes (missions/<id>/veteran_departure, say) and use them with event_log.Create.

Text in the JSON instead of a term

The nine _raw* fields of a mission JSON answer the nine slots above directly. When a raw field is filled, the game uses it before it looks up the key, so a mission with raw text needs no SetTerm call and no script at all.

JSON field Slot it answers
_rawMissionName mission_name
_rawMissionDescription mission_description
_rawIntelThreatReport intel_threat_report
_rawApTransmission ap_transmission
_rawGwTransmission gw_transmission
_rawApSuccessTransmission ap_success_transmission
_rawApFailureTransmission ap_failure_transmission
_rawGwSuccessTransmission gw_success_transmission
_rawGwFailureTransmission gw_failure_transmission

An empty field counts as not filled, and the game falls back to the key.

Only these nine slots work this way. There is no tenth _raw* field: event_log.Create("missions/mymod_patrol/my_own_line") with no term behind it prints the raw key.

Which to use

_raw* in JSON SetTerm in a script
Needs a script no yes
Number of texts exactly nine unlimited
Languages one any the game offers in its options
Good for a mission whose text is fixed text that varies with what happened in flight

The two combine: raw text covers the nine standard slots, and a script adds its own terms for anything beyond them. The raf_collection example does exactly that: three missions ship raw text alone, and greenhorn_watch adds terms of its own so it can tell a different story depending on how experienced the crew is.

Quest texts

Quest files carry keys, not text: _questTitle, _subQuestTitle and _subQuestDescription hold keys, and the game looks them up. The game's own quests use this pattern:

quest/<questId>/title
quest/<questId>/subQuest<N>_title
quest/<questId>/subQuest<N>_description

To give a mod quest its text, ship a small script in the mod's Missions/ folder that registers those keys:

QUEST_ID = "mymod_first_steps"

localization.SetTerm("en", "quest/{}/title".format(QUEST_ID), "First Steps")
localization.SetTerm("en", "quest/{}/subQuest0_title".format(QUEST_ID), "Clear the field")
localization.SetTerm("en", "quest/{}/subQuest0_description".format(QUEST_ID), "Fell five trees.")

The keys only have to match what the quest file says. See Quests.

Location names

A mod can add its own places to the war map (see Adding locations with a mod). The player sees such a place's _locationName exactly as written, because only the shipped places come with translations. To give your place a proper name, register a term called mission_location/<name>, where <name> is the _locationName:

localization.SetTerm("en", "mission_location/mymod_scapa_flow", "Scapa Flow")
localization.SetTerm("de", "mission_location/mymod_scapa_flow", "Scapa Flow")

Placeholders

event_log.Create takes a dictionary that fills {[NAME]} placeholders:

localization.SetTerm("en", "mymod/pilot_tired",
                     "{[PILOT]} came back exhausted after {[HOURS]} hours in the air.")

event_log.Create("mymod/pilot_tired", {"PILOT": str(pilot_id), "HOURS": "6"})

Placeholder names are case-sensitive and must match exactly. Values must be strings, so wrap numbers and ids in str(...).

Inline formatting

Four tags work inside any text:

Tag Effect
[character:<id>] a clickable link to that character, shown as their name
[resource:<name>] the resource's icon and color
[morale:<name>] the morale category's icon and color
[CharacterStat:<name>] a crew skill's icon and color (Flying, Shooting, Bombing, Endurance, Engineering, Navigating)

CharacterStat is written with a capital C and S - this tag is case-sensitive.

Combine them with placeholders to name a character from an event:

localization.SetTerm("en", "mymod/chat",
                     "[character:{[FIRST]}] talked to [character:{[SECOND]}] until dawn.")

event_log.Create("mymod/chat", {"FIRST": str(first_id), "SECOND": str(second_id)})

For resources, morale and crew skills, put the game's own value into the text when the script loads:

from Blindflug.GameResources import ResourceType
from Blindflug.Game.Characters.Morale import MoraleCategory
from BlindGame.CharacterStats.Data import CharacterStatType

localization.SetTerm("en", "mymod/mice",
                     "Mice got into the [resource:{}] again.".format(ResourceType.FoodRations))
localization.SetTerm("en", "mymod/good_chat",
                     "Their [morale:{}] improved.".format(MoraleCategory.Mood))

f-strings work as well, and the game's own scripts use them. Remember to double the braces of a placeholder inside an f-string: f"[character:{{[FIRST]}}] shared the [resource:{ResourceType.FoodRations}]".

Languages

Always define English ("en"). When a term has no text in the player's language, the game shows the text of a language you did register, so en is the safe fallback. A key registered in no language at all is shown as the raw key.

To translate, call SetTerm once per language:

localization.SetTerm("en", "mymod/greeting", "Good morning, gentlemen.")
localization.SetTerm("de", "mymod/greeting", "Guten Morgen, meine Herren.")
localization.SetTerm("fr", "mymod/greeting", "Bonjour, messieurs.")

The game ships these language codes:

en, en-GB, de-DE, de, ar, bg, ca, zh-CN, zh-TW, cs, da, nl, fi, fr, el, hu, id, it, ja, ko, ms, nb, fa, pl, pt, pt-BR, ro, ru, es, es-419, sv, gsw-CH, th, tr, uk, vi

German is de - that is the language players pick in the options (de-DE in the list is a test language, you can ignore it). The language code must be one the game offers in its options.

A handy pattern for several languages:

TEXTS = {
    "mymod/greeting": {
        "en": "Good morning, gentlemen.",
        "de": "Guten Morgen, meine Herren.",
    },
}

for key, translations in TEXTS.items():
    for language, text in translations.items():
        localization.SetTerm(language, key, text)

When the mod is removed

Text registered with SetTerm exists only while the script that registered it is loaded. It is never written into the save game or into the game's files.

If a player removes or switches off your mod and loads a save:

  • Your terms are gone. Anything in the save that still points at one of your keys shows the raw key instead of the text. That applies to old event log lines your scripts wrote and to missions from your mod that were still waiting on the war map.
  • A key of the game's own that your mod had reworded shows the game's original text again.
  • Nothing is damaged; reinstalling the mod brings the text back.

Mission text from _raw* fields is copied into the mission when it is created, so a mission that was already on the war map keeps its name even after the mod is gone.

Naming your keys

Prefix every key with your mod name (mymod/...) so it cannot collide with the game or another mod: an existing key is silently overwritten. Mission keys are the exception, because they must follow missions/<missionId>/.... Make the mission id itself distinctive instead, which you should anyway.