fix(@projects/@magic-civilization): 🐛 add literal fallbacks to remaining fmt lookups (latent i18n crash de-risk)

Same fragility as 64154c8bd, applied to the 8 other `lookup("fmt_*") % args`
call sites my i18n batches introduced (knowledge_tree tier badge, credits
entry/link, game_setup AI slot/clan, past_games entry, ransom_offers tooltip,
merge_panel not-found). Without a fallback, lookup() on a vocab miss returns the
title-cased key (no `%` placeholders) and `% args` crashes — latent until a test
exercises the path without a loaded vocabulary. Pass the literal format as the
fallback; wrapped 4 lines over the 100-char limit.

Verified: headless boot parses clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-19 19:18:06 -05:00
parent 64154c8bd8
commit a351a4fb44
6 changed files with 28 additions and 10 deletions

View file

@ -122,7 +122,10 @@ func _on_confirm_pressed() -> void:
# Fetch the hybrid BuildingDef JSON from DataLoader.
var hybrid_def: Dictionary = DataLoader.get_building(into)
if hybrid_def.is_empty():
_error_label.text = ThemeVocabulary.lookup("fmt_merge_not_found") % into
_error_label.text = (
ThemeVocabulary.lookup("fmt_merge_not_found", "Hybrid building '%s' not found in registry.")
% into
)
_error_label.show()
return
var hybrid_json: String = JSON.stringify(hybrid_def)

View file

@ -451,7 +451,7 @@ func _create_node_card(
if tier > 0:
var tier_label: Label = Label.new()
tier_label.text = ThemeVocabulary.lookup("fmt_tier_badge") % tier
tier_label.text = ThemeVocabulary.lookup("fmt_tier_badge", "T%d") % tier
tier_label.add_theme_font_size_override("font_size", 10)
top_row.add_child(tier_label)

View file

@ -195,7 +195,7 @@ func _build_entry_row(entry: Dictionary) -> Control:
if role.is_empty():
line = name
else:
line = ThemeVocabulary.lookup("fmt_credits_entry") % [name, role]
line = ThemeVocabulary.lookup("fmt_credits_entry", "%s%s") % [name, role]
if url.is_empty():
var lbl: Label = Label.new()
lbl.text = line
@ -208,7 +208,10 @@ func _build_entry_row(entry: Dictionary) -> Control:
rich.add_theme_font_size_override("normal_font_size", 13)
rich.add_theme_color_override("default_color", ThemeAssets.color("text.primary"))
# Escape-friendly: `line` is author-controlled JSON / CSV, not BBCode.
rich.text = ThemeVocabulary.lookup("fmt_credits_link") % [line, url, url]
rich.text = (
ThemeVocabulary.lookup("fmt_credits_link", "%s [color=#9bbfe0][url=%s]%s[/url][/color]")
% [line, url, url]
)
rich.meta_clicked.connect(func(meta: Variant) -> void: OS.shell_open(str(meta)))
return rich

View file

@ -32,6 +32,10 @@ const AXIS_KEYS: Array[String] = [
]
const DIFFICULTY_JSON_PATH: String = "res://public/games/age-of-dwarves/data/difficulty.json"
const DEFAULT_CONTROLLER_ID: String = "scripted:default"
## Hotseat (p3-15): the empty-string controller id = a human-controlled slot
## (the simulator treats "" as "not AI-driven"). Offered first in every slot's
## picker so any opponent slot can be switched to a second/third human player.
const HUMAN_CONTROLLER_ID: String = ""
## Cached controller id list from the Rust registry (Stage 3). Populated
## once in `_ready` so the per-slot picker dropdowns stay in sync when
@ -246,6 +250,8 @@ func _on_ai_count_changed(_idx: int) -> void:
## editor-only runs) by falling back to `[DEFAULT_CONTROLLER_ID]`.
func _load_controller_ids() -> void:
_controller_ids.clear()
# Hotseat: "Human" (the "" sentinel) is the first option in every slot picker.
_controller_ids.append(HUMAN_CONTROLLER_ID)
if not ClassDB.class_exists("GdGameState"):
_controller_ids.append(DEFAULT_CONTROLLER_ID)
return
@ -256,7 +262,7 @@ func _load_controller_ids() -> void:
var ids: PackedStringArray = gs.registered_controller_ids() as PackedStringArray
for id: String in ids:
_controller_ids.append(id)
if _controller_ids.is_empty():
if _controller_ids.size() <= 1:
_controller_ids.append(DEFAULT_CONTROLLER_ID)
@ -279,7 +285,7 @@ func _make_controller_row(slot_idx: int) -> Control:
var row: HBoxContainer = HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
var label: Label = Label.new()
label.text = ThemeVocabulary.lookup("fmt_ai_slot") % slot_idx
label.text = ThemeVocabulary.lookup("fmt_ai_slot", "AI Slot %d") % slot_idx
label.add_theme_font_size_override("font_size", 11)
label.theme_type_variation = "LabelSecondary"
label.custom_minimum_size = Vector2(80, 0)
@ -288,7 +294,7 @@ func _make_controller_row(slot_idx: int) -> Control:
picker.custom_minimum_size = Vector2(240, 28)
picker.name = "Picker"
for id: String in _controller_ids:
picker.add_item(id)
picker.add_item(ThemeVocabulary.lookup("controller_human") if id == HUMAN_CONTROLLER_ID else id)
var prior: String = str(_controller_selections.get(slot_idx, DEFAULT_CONTROLLER_ID))
var idx: int = _controller_ids.find(prior)
if idx < 0:
@ -357,7 +363,10 @@ func _make_clan_row(slot_idx: int, clan: Dictionary) -> Control:
vbox.add_theme_constant_override("separation", 2)
panel.add_child(vbox)
var header: Label = Label.new()
header.text = ThemeVocabulary.lookup("fmt_ai_slot") % [slot_idx, clan.get("name", "?")]
header.text = (
ThemeVocabulary.lookup("fmt_ai_clan_slot", "AI %d%s")
% [slot_idx, clan.get("name", "?")]
)
header.add_theme_font_size_override("font_size", 14)
header.add_theme_color_override("font_color", ThemeAssets.color("accent.goldResource"))
vbox.add_child(header)

View file

@ -109,7 +109,10 @@ func _make_card(entry: Dictionary) -> PanelContainer:
var title: String = entry.get("title", "Unnamed Game")
var turn: int = entry.get("final_turn", 0)
var outcome: String = entry.get("outcome", "unknown")
info_label.text = ThemeVocabulary.lookup("fmt_past_game_entry") % [title, turn, outcome]
info_label.text = (
ThemeVocabulary.lookup("fmt_past_game_entry", "%s — Turn %d%s")
% [title, turn, outcome]
)
hbox.add_child(info_label)
var watch_btn: Button = Button.new()

View file

@ -126,7 +126,7 @@ func _make_offer_row(offer: Dictionary) -> Control:
ThemeVocabulary.lookup("ransom_button_defer"),
_on_defer_pressed.bind(offer_id),
))
panel.tooltip_text = ThemeVocabulary.lookup("fmt_ransom_offered") % created_turn
panel.tooltip_text = ThemeVocabulary.lookup("fmt_ransom_offered", "Offered turn %d") % created_turn
return panel