diff --git a/dragonpilot/system/ui/lib/multilang.py b/dragonpilot/system/ui/lib/multilang.py index 638ce09b..35d5378c 100644 --- a/dragonpilot/system/ui/lib/multilang.py +++ b/dragonpilot/system/ui/lib/multilang.py @@ -1,6 +1,6 @@ import gettext import os -import subprocess +import re from openpilot.system.ui.lib.multilang import ( multilang as base_multilang, TRANSLATIONS_DIR, @@ -8,11 +8,141 @@ from openpilot.system.ui.lib.multilang import ( ) +def _parse_po(po_path): + """Pure Python .po file parser - returns dict of msgid -> msgstr.""" + catalog = {} + + with open(po_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + i = 0 + n = len(lines) + + while i < n: + line = lines[i] + + # Skip comments (#, #., #:, #|, #~) + if line.startswith('#'): + i += 1 + continue + + # Blank line + if line.strip() == '': + i += 1 + continue + + # Try to match msgid + msgid_match = re.match(r'^msgid\s+"(.*)"\s*$', line) + if msgid_match: + msgid = msgid_match.group(1) + i += 1 + + # Collect continued msgid strings + while i < n: + cont = re.match(r'^"(.*)"\s*$', lines[i]) + if cont: + msgid += cont.group(1) + i += 1 + else: + break + + # Now expect msgid_plural or msgstr + if i < n and lines[i].startswith('msgid_plural'): + # Plural form + plur_match = re.match(r'^msgid_plural\s+"(.*)"\s*$', lines[i]) + msgid_plural = plur_match.group(1) if plur_match else '' + i += 1 + while i < n: + cont = re.match(r'^"(.*)"\s*$', lines[i]) + if cont: + msgid_plural += cont.group(1) + i += 1 + else: + break + + # Read msgstr[0], msgstr[1], ... + plural_strs = {} + while i < n: + p_match = re.match(r'^msgstr\[(\d+)\]\s+"(.*)"\s*$', lines[i]) + if p_match: + idx = int(p_match.group(1)) + val = p_match.group(2) + i += 1 + while i < n: + cont = re.match(r'^"(.*)"\s*$', lines[i]) + if cont: + val += cont.group(1) + i += 1 + else: + break + plural_strs[idx] = val + else: + break + + # Store: both singular and plural keys for N=1 + s_val = plural_strs.get(0, msgid) + catalog[msgid] = s_val + catalog[msgid + '\x00' + msgid_plural] = plural_strs.get(1, msgid_plural) + + else: + # Singular form + if i < n: + str_match = re.match(r'^msgstr\s+"(.*)"\s*$', lines[i]) + if str_match: + msgstr = str_match.group(1) + i += 1 + else: + # Maybe msgstr is empty on same line + empty_match = re.match(r'^msgstr\s+""\s*$', lines[i]) + if empty_match: + msgstr = '' + i += 1 + else: + msgstr = '' + # Don't advance - next loop handles it + + # Collect continued msgstr strings + while i < n: + cont = re.match(r'^"(.*)"\s*$', lines[i]) + if cont: + msgstr += cont.group(1) + i += 1 + else: + break + else: + msgstr = '' + + # Skip header entry (msgid ""), don't add to catalog + if msgid != '': + catalog[msgid] = msgstr + + else: + i += 1 + + return catalog + + +class PoTranslations(gettext.NullTranslations): + """Pure Python .po parser - no msgfmt/polib needed.""" + + def __init__(self, po_path): + super().__init__() + self._catalog = _parse_po(po_path) + + def gettext(self, msgid): + return self._catalog.get(msgid, msgid) + + def ngettext(self, singular, plural, n): + return self._catalog.get(singular + '\x00' + plural, + self._catalog.get(singular, + singular if n == 1 else plural)) + + class DpMultilang: """Wrapper that syncs with base multilang and adds dragonpilot translations.""" def __init__(self): - self._dragon_translation: gettext.NullTranslations | gettext.GNUTranslations = gettext.NullTranslations() + self._dragon_translation: gettext.NullTranslations | PoTranslations = gettext.NullTranslations() self._loaded_language: str = "" @property @@ -30,35 +160,13 @@ class DpMultilang: current_lang = base_multilang.language if current_lang != self._loaded_language: self._loaded_language = current_lang - mo_path = TRANSLATIONS_DIR.joinpath(f'dragonpilot_{current_lang}.mo') po_path = TRANSLATIONS_DIR.joinpath(f'dragonpilot_{current_lang}.po') - try: - with mo_path.open('rb') as fh: - self._dragon_translation = gettext.GNUTranslations(fh) - except FileNotFoundError: - # Auto-compile .po to .mo if source exists - if po_path.exists(): - try: - # Try msgfmt first (faster, available on device) - result = subprocess.run(['msgfmt', str(po_path), '-o', str(mo_path)], capture_output=True) - if result.returncode == 0 and mo_path.exists(): - with mo_path.open('rb') as fh: - self._dragon_translation = gettext.GNUTranslations(fh) - return - except FileNotFoundError: - pass - # Fallback: try polib - try: - import polib - po = polib.pofile(str(po_path)) - po.save_as_mofile(str(mo_path)) - with mo_path.open('rb') as fh: - self._dragon_translation = gettext.GNUTranslations(fh) - return - except ImportError: - pass - except Exception: - pass + if po_path.exists(): + try: + self._dragon_translation = PoTranslations(po_path) + except Exception: + self._dragon_translation = gettext.NullTranslations() + else: self._dragon_translation = gettext.NullTranslations() def tr(self, text: str) -> str: