mirror of
https://github.com/1j01/textual-paint.git
synced 2024-12-22 14:21:33 +03:00
adcaed47cd
- 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.
118 lines
4.2 KiB
Python
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]
|