Randoloot/scripts/game/interface_translation.gd
2026-10-27 22:20:01 +01:00

97 lines
4.1 KiB
GDScript

extends Translation
## Matches formatted legacy text as well as ordinary Godot translation keys.
var catalog_messages: Dictionary = {}
var _cache: Dictionary = {}
var _patterns: Array[Dictionary] = []
var _case_messages: Dictionary = {}
var _letters := RegEx.create_from_string("[\\p{L}]")
func configure(code: String, catalog: Dictionary) -> void:
locale = code
catalog_messages = catalog
for key: String in catalog:
_case_messages[key.to_upper()] = str(catalog[key]).to_upper()
_case_messages[key.to_lower()] = str(catalog[key]).to_lower()
var keys := catalog.keys()
keys.sort_custom(func(a: String, b: String) -> bool: return a.length() > b.length())
for key: String in keys:
if key.length() < 4: continue
var expression := ""
var tokens: Array[String] = []
var placeholder := RegEx.create_from_string("%(?:[0-9]*(?:\\.[0-9]+)?[sdif]|%)")
var offset := 0
for found in placeholder.search_all(key):
expression += _escape(key.substr(offset, found.get_start()-offset))
var token := found.get_string()
if token == "%%": expression += "%"
else:
tokens.append(token)
if token.ends_with("s"):
expression += "([^\\n]+)" if found.get_end() == key.length() else "([^\\n]+?)"
else: expression += "([-+]?[0-9]+(?:[.,][0-9]+)?)"
offset = found.get_end()
expression += _escape(key.substr(offset))
if expression.is_empty(): continue
# Word boundaries avoid replacing a noun inside a longer Polish word.
var start := "(?<![\\p{L}\\p{N}_])" if key.left(1).is_valid_identifier() else ""
var end := "(?![\\p{L}\\p{N}_])" if key.right(1).is_valid_identifier() else ""
var regex := RegEx.create_from_string(start+expression+end)
var hint := ""
for part in placeholder.sub(key, "\u0001", true).split("\u0001"):
if part.length() > hint.length(): hint = part
_patterns.append({"regex":regex,"target":str(catalog[key]),"tokens":tokens,"hint":hint})
func _escape(value: String) -> String:
var result := ""
for character in value:
if character in "\\.^$|?*+()[]{}": result += "\\"
result += character
return result
func _get_message(source: StringName, _context: StringName) -> StringName:
var text := str(source)
if catalog_messages.has(text): return StringName(catalog_messages[text])
if _case_messages.has(text): return StringName(_case_messages[text])
if _cache.has(text): return StringName(_cache[text])
if text.is_empty() or _letters.search(text) == null: return source
var candidates: Array[Dictionary] = []
for pattern in _patterns:
if not text.contains(pattern.hint): continue
for found in pattern.regex.search_all(text):
var start: int = found.get_start()
var end: int = found.get_end()
var target: String = pattern.target
# Replace format specifiers in order, retaining original numeric values.
var token_regex := RegEx.create_from_string("%(?:[0-9]*(?:\\.[0-9]+)?[sdif]|%)")
var matches := token_regex.search_all(target)
var capture := 1
var rebuilt := ""
var offset := 0
for token in matches:
rebuilt += target.substr(offset, token.get_start()-offset)
if token.get_string() == "%%": rebuilt += "%"
else:
var value: String = found.get_string(capture)
rebuilt += str(_get_message(value, &"")) if token.get_string().ends_with("s") else value
capture += 1
offset = token.get_end()
rebuilt += target.substr(offset)
candidates.append({"start":start,"end":end,"value":rebuilt})
# Prefer the longest rendered phrase, then translate its captured names too.
candidates.sort_custom(func(a: Dictionary,b: Dictionary) -> bool: return int(a.end)-int(a.start) > int(b.end)-int(b.start))
var replacements: Array[Dictionary] = []
for candidate in candidates:
var overlaps := false
for replacement in replacements:
if int(candidate.start) < int(replacement.end) and int(candidate.end) > int(replacement.start):
overlaps = true
break
if not overlaps: replacements.append(candidate)
replacements.sort_custom(func(a: Dictionary,b: Dictionary) -> bool: return a.start > b.start)
for replacement in replacements:
text = text.substr(0,replacement.start)+replacement.value+text.substr(replacement.end)
if _cache.size() > 4096: _cache.clear()
_cache[str(source)] = text
return StringName(text)