feat(ui): add YAML-based UI string loading system (#409)
Adds UIStrings autoload that loads display text from a YAML file, replacing hardcoded strings in HUD and interaction prompt. Copy team can now author UI microcopy in client/data/ui-strings.yaml without touching GDScript. Includes 38 strings across 5 categories and 16 gdUnit4 tests for the parser and lookup API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
extends Node
|
||||
|
||||
# UIStrings — loads UI display text from data/ui-strings.yaml.
|
||||
# Access: UIStrings.get_text("hud.mode_label") -> "Mode"
|
||||
# Falls back to the key itself if not found, with a push_warning.
|
||||
|
||||
const STRINGS_PATH: String = "res://data/ui-strings.yaml"
|
||||
|
||||
var _strings: Dictionary = {}
|
||||
var _loaded: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_load_strings()
|
||||
|
||||
func _load_strings() -> void:
|
||||
if not FileAccess.file_exists(STRINGS_PATH):
|
||||
push_warning("UIStrings: file not found: %s" % STRINGS_PATH)
|
||||
return
|
||||
var file := FileAccess.open(STRINGS_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
push_warning("UIStrings: failed to open: %s" % STRINGS_PATH)
|
||||
return
|
||||
var text := file.get_as_text()
|
||||
file.close()
|
||||
_strings = _parse_yaml(text)
|
||||
_loaded = true
|
||||
print("UIStrings: loaded %d strings from %s" % [_strings.size(), STRINGS_PATH])
|
||||
|
||||
## Get a UI string by dotted key. Returns the key itself if not found.
|
||||
func get_text(key: String) -> String:
|
||||
if _strings.has(key):
|
||||
return _strings[key]
|
||||
push_warning("UIStrings: missing key '%s'" % key)
|
||||
return key
|
||||
|
||||
## Check if a key exists.
|
||||
func has_key(key: String) -> bool:
|
||||
return _strings.has(key)
|
||||
|
||||
## Get all keys.
|
||||
func get_all_keys() -> PackedStringArray:
|
||||
return PackedStringArray(_strings.keys())
|
||||
|
||||
## Reload from disk (useful for hot-reload during development).
|
||||
func reload() -> void:
|
||||
_strings.clear()
|
||||
_loaded = false
|
||||
_load_strings()
|
||||
|
||||
## Parse subset of YAML: one-level-nested key-value pairs.
|
||||
## Returns flat Dictionary with dotted keys: { "section.key": "value" }.
|
||||
static func _parse_yaml(text: String) -> Dictionary:
|
||||
var strings := {}
|
||||
var current_section := ""
|
||||
for line in text.split("\n"):
|
||||
var stripped := line.strip_edges(false, true)
|
||||
if stripped.is_empty() or stripped.begins_with("#"):
|
||||
continue
|
||||
var indent := line.length() - line.lstrip(" ").length()
|
||||
var content := stripped.strip_edges()
|
||||
var colon_pos := content.find(":")
|
||||
if colon_pos < 0:
|
||||
continue
|
||||
var key := content.substr(0, colon_pos).strip_edges()
|
||||
var val := content.substr(colon_pos + 1).strip_edges()
|
||||
if val.length() >= 2 and val.begins_with("\"") and val.ends_with("\""):
|
||||
val = val.substr(1, val.length() - 2)
|
||||
if indent == 0:
|
||||
current_section = key
|
||||
elif indent >= 2 and not current_section.is_empty():
|
||||
strings[current_section + "." + key] = val
|
||||
return strings
|
||||
Reference in New Issue
Block a user