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 := "(? 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)