97 lines
4.1 KiB
GDScript
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)
|
|
|