2023-08-11 15:57:32 +00:00
|
|
|
import json
|
2024-11-14 14:11:03 +00:00
|
|
|
import re
|
|
|
|
import threading
|
2024-11-19 10:27:48 +00:00
|
|
|
import warnings
|
2023-08-11 15:57:32 +00:00
|
|
|
import typing as t
|
|
|
|
from hashlib import sha256
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from trezorlib import cosi, device, models
|
|
|
|
from trezorlib._internal import translations
|
|
|
|
from trezorlib.debuglink import TrezorClientDebugLink as Client
|
|
|
|
|
|
|
|
from . import common
|
|
|
|
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
ROOT = HERE.parent
|
|
|
|
|
2024-11-14 14:11:03 +00:00
|
|
|
TRANSLATIONS_DIR = ROOT / "core" / "translations"
|
|
|
|
FONTS_DIR = TRANSLATIONS_DIR / "fonts"
|
|
|
|
ORDER_FILE = TRANSLATIONS_DIR / "order.json"
|
2023-08-11 15:57:32 +00:00
|
|
|
|
2024-11-14 14:11:03 +00:00
|
|
|
LANGUAGES = [file.stem for file in TRANSLATIONS_DIR.glob("??.json")]
|
|
|
|
|
|
|
|
_CURRENT_TRANSLATION = threading.local()
|
2023-08-11 15:57:32 +00:00
|
|
|
|
|
|
|
|
2024-02-28 10:01:32 +00:00
|
|
|
def prepare_blob(
|
2023-08-11 15:57:32 +00:00
|
|
|
lang_or_def: translations.JsonDef | Path | str,
|
|
|
|
model: models.TrezorModel,
|
2024-02-29 13:23:00 +00:00
|
|
|
version: translations.VersionTuple | tuple[int, int, int] | None = None,
|
2024-02-28 10:01:32 +00:00
|
|
|
) -> translations.TranslationsBlob:
|
2023-08-11 15:57:32 +00:00
|
|
|
order = translations.order_from_json(json.loads(ORDER_FILE.read_text()))
|
|
|
|
if isinstance(lang_or_def, str):
|
|
|
|
lang_or_def = get_lang_json(lang_or_def)
|
|
|
|
if isinstance(lang_or_def, Path):
|
|
|
|
lang_or_def = t.cast(translations.JsonDef, json.loads(lang_or_def.read_text()))
|
|
|
|
|
|
|
|
# generate raw blob
|
2024-02-29 13:23:00 +00:00
|
|
|
if version is None:
|
|
|
|
version = translations.version_from_json(lang_or_def["header"]["version"])
|
|
|
|
elif len(version) == 3:
|
|
|
|
# version coming from client object does not have build item
|
|
|
|
version = *version, 0
|
2024-02-28 10:01:32 +00:00
|
|
|
return translations.blob_from_defs(lang_or_def, order, model, version, FONTS_DIR)
|
|
|
|
|
2023-08-11 15:57:32 +00:00
|
|
|
|
2024-02-28 10:01:32 +00:00
|
|
|
def sign_blob(blob: translations.TranslationsBlob) -> bytes:
|
2023-08-11 15:57:32 +00:00
|
|
|
# build 0-item Merkle proof
|
|
|
|
digest = sha256(b"\x00" + blob.header_bytes).digest()
|
|
|
|
signature = cosi.sign_with_privkeys(digest, common.PRIVATE_KEYS_DEV)
|
|
|
|
blob.proof = translations.Proof(
|
|
|
|
merkle_proof=[],
|
|
|
|
sigmask=0b111,
|
|
|
|
signature=signature,
|
|
|
|
)
|
|
|
|
return blob.build()
|
|
|
|
|
|
|
|
|
2024-02-28 10:01:32 +00:00
|
|
|
def build_and_sign_blob(
|
|
|
|
lang_or_def: translations.JsonDef | Path | str,
|
2024-02-29 13:23:00 +00:00
|
|
|
client: Client,
|
2024-02-28 10:01:32 +00:00
|
|
|
) -> bytes:
|
2024-02-29 13:23:00 +00:00
|
|
|
blob = prepare_blob(lang_or_def, client.model, client.version)
|
2024-02-28 10:01:32 +00:00
|
|
|
return sign_blob(blob)
|
|
|
|
|
|
|
|
|
2023-08-11 15:57:32 +00:00
|
|
|
def set_language(client: Client, lang: str):
|
|
|
|
if lang.startswith("en"):
|
|
|
|
language_data = b""
|
|
|
|
else:
|
2024-02-29 13:23:00 +00:00
|
|
|
language_data = build_and_sign_blob(lang, client)
|
2023-08-11 15:57:32 +00:00
|
|
|
with client:
|
|
|
|
device.change_language(client, language_data) # type: ignore
|
2024-11-14 14:11:03 +00:00
|
|
|
_CURRENT_TRANSLATION.TR = TRANSLATIONS[lang]
|
2023-08-11 15:57:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_lang_json(lang: str) -> translations.JsonDef:
|
|
|
|
assert lang in LANGUAGES
|
2024-11-14 14:11:03 +00:00
|
|
|
lang_json = json.loads((TRANSLATIONS_DIR / f"{lang}.json").read_text())
|
2024-09-02 08:26:13 +00:00
|
|
|
if (fonts_safe3 := lang_json.get("fonts", {}).get("##Safe3")) is not None:
|
|
|
|
lang_json["fonts"]["T2B1"] = fonts_safe3
|
|
|
|
lang_json["fonts"]["T3B1"] = fonts_safe3
|
|
|
|
return lang_json
|
2023-08-11 15:57:32 +00:00
|
|
|
|
|
|
|
|
2024-11-14 14:11:03 +00:00
|
|
|
class Translation:
|
|
|
|
FORMAT_STR_RE = re.compile(r"\\{\d+\\}")
|
|
|
|
|
|
|
|
def __init__(self, lang: str) -> None:
|
|
|
|
self.lang = lang
|
|
|
|
self.lang_json = get_lang_json(lang)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def translations(self) -> dict[str, str]:
|
|
|
|
return self.lang_json["translations"]
|
|
|
|
|
2024-11-19 10:27:48 +00:00
|
|
|
def _translate_raw(self, key: str, _stacklevel: int = 0) -> str:
|
2024-11-14 14:11:03 +00:00
|
|
|
tr = self.translations.get(key)
|
|
|
|
if tr is not None:
|
|
|
|
return tr
|
|
|
|
if self.lang != "en":
|
2024-11-19 10:27:48 +00:00
|
|
|
warnings.warn(
|
|
|
|
f"Translation key '{key}' not found in '{self.lang}' translation file",
|
|
|
|
stacklevel=_stacklevel + 2,
|
|
|
|
)
|
2024-11-14 14:11:03 +00:00
|
|
|
return TRANSLATIONS["en"]._translate_raw(key)
|
|
|
|
raise KeyError(key)
|
|
|
|
|
2024-11-19 10:27:48 +00:00
|
|
|
def translate(self, key: str, _stacklevel: int = 0) -> str:
|
|
|
|
tr = self._translate_raw(key, _stacklevel=_stacklevel + 1)
|
2024-11-14 14:11:03 +00:00
|
|
|
return tr.replace("\xa0", " ").strip()
|
|
|
|
|
2024-11-19 10:27:48 +00:00
|
|
|
def as_regexp(self, key: str, _stacklevel: int = 0) -> re.Pattern:
|
|
|
|
tr = self.translate(key, _stacklevel=_stacklevel + 1)
|
2024-11-14 14:11:03 +00:00
|
|
|
re_safe = re.escape(tr)
|
|
|
|
return re.compile(self.FORMAT_STR_RE.sub(r".*?", re_safe))
|
|
|
|
|
|
|
|
|
|
|
|
TRANSLATIONS = {lang: Translation(lang) for lang in LANGUAGES}
|
|
|
|
|
|
|
|
|
2024-11-19 10:27:48 +00:00
|
|
|
def translate(key: str, _stacklevel: int = 0) -> str:
|
|
|
|
return _CURRENT_TRANSLATION.TR.translate(key, _stacklevel=_stacklevel + 1)
|
2024-11-14 14:11:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
def regexp(key: str) -> re.Pattern:
|
2024-11-19 10:27:48 +00:00
|
|
|
return _CURRENT_TRANSLATION.TR.as_regexp(key, _stacklevel=1)
|
2024-11-14 14:11:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
def __getattr__(key: str) -> str:
|
2024-11-19 10:27:48 +00:00
|
|
|
return translate(key, _stacklevel=1)
|