textual-paint/localization/i18n.py
Isaiah Odhner adcaed47cd Handle base language (English) better
- Don't log an error about a missing language file at startup.
- Don't append to untranslated.txt for strings that don't need
  translation because they're already in the target language.
  I could use an early return for this too, but it's not important.
2023-04-19 02:59:48 -04:00

118 lines
4.2 KiB
Python

from typing import Optional
import json
import re
translations = {}
base_language = "en"
current_language = base_language
def get_direction() -> str:
"""Get the text direction for the current language."""
if current_language in ["ar", "he"]:
return "rtl"
return "ltr"
def load_language(language_code: str):
"""Load a language from the translations directory."""
global translations
translations = {}
if language_code == base_language:
return
try:
with open(f"localization/{language_code}/localizations.js", "r") as f:
# find the JSON object
js = f.read()
start = js.find("{")
end = js.rfind("}")
# parse the JSON object
translations = json.loads(js[start:end + 1])
current_language = language_code
except FileNotFoundError:
print(f"Could not find language file for '{language_code}'.")
except json.decoder.JSONDecodeError as e:
print(f"Could not parse language file for '{language_code}': {e}")
except Exception as e:
print(f"Could not load language '{language_code}': {e}")
untranslated = set()
try:
with open("localization/untranslated.txt", "r") as f:
untranslated = set(f.read().splitlines())
except FileNotFoundError:
pass
def get(base_language_str: str, *interpolations: str) -> str:
"""Get a localized string."""
def find_localization(base_language_str: str):
amp_index = index_of_hotkey(base_language_str)
if amp_index > -1:
without_hotkey = remove_hotkey(base_language_str)
if without_hotkey in translations:
hotkey_def = base_language_str[amp_index:amp_index + 2]
if translations[without_hotkey].upper().find(hotkey_def.upper()) > -1:
return translations[without_hotkey]
else:
if has_hotkey(translations[without_hotkey]):
# window.console && console.warn(`Localization has differing accelerator (hotkey) hint: '${translations[without_hotkey]}' vs '${base_language_str}'`);
# @TODO: detect differing accelerator more generally
return f"{remove_hotkey(translations[without_hotkey])} ({hotkey_def})"
return f"{translations[without_hotkey]} ({hotkey_def})"
if base_language_str in translations:
return translations[base_language_str]
# special case for menu items, where we need to split the string into two parts to translate them separately
# (maybe only because of how I preprocessed the localization files)
parts = base_language_str.split("\t")
if len(parts) == 2:
parts[0] = find_localization(parts[0])
return "\t".join(parts)
# special handling for ellipsis
if base_language_str[-3:] == "...":
return find_localization(base_language_str[:-3]) + "..."
if base_language_str not in untranslated and current_language != base_language:
untranslated.add(base_language_str)
# append to untranslated strings file
with open("localization/untranslated.txt", "a") as f:
f.write(base_language_str + "\n")
return base_language_str
def interpolate(text: str, interpolations: list):
for i in range(len(interpolations)):
text = text.replace(f"%{i + 1}", interpolations[i])
return text
return interpolate(find_localization(base_language_str), interpolations)
# & defines accelerators (hotkeys) in menus and buttons and things, which get underlined in the UI.
# & can be escaped by doubling it, e.g. "&Taskbar && Start Menu"
def index_of_hotkey(text: str) -> int:
# Returns the index of the ampersand that defines a hotkey, or -1 if not present.
# The space here handles beginning-of-string matching and counteracts the offset for the [^&] so it acts like a negative lookbehind
m = re.search(r"[^&]&[^&\s]", f" {text}")
return m.start() if m else -1
def has_hotkey(text: str) -> bool:
return index_of_hotkey(text) != -1
def remove_hotkey(text: str) -> str:
text = re.sub(r"\s?\(&.\)", "", text)
text = re.sub(r"([^&]|^)&([^&\s])", r"\1\2", text)
return text
def markup_hotkey(text: str) -> str:
"""Returns Rich API-compatible markup underlining the hotkey if present."""
index = index_of_hotkey(text)
if index == -1:
return text
else:
return text[:index] + f"[u]{text[index + 1]}[/u]" + text[index + 2:]
def get_hotkey(text: str) -> Optional[str]:
"""Returns the hotkey if present."""
index = index_of_hotkey(text)
if index == -1:
return None
else:
return text[index + 1]