From 77120beecb6ad5d8b2756c6bdba69cff8c7cf92a Mon Sep 17 00:00:00 2001 From: 1okko <15377594951@189.cn> Date: Mon, 15 Jun 2026 21:50:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=A8=E7=BA=AFPython=E8=A7=A3?= =?UTF-8?q?=E6=9E=90.po=E6=96=87=E4=BB=B6=EF=BC=8C=E5=8E=BB=E6=8E=89msgfmt?= =?UTF-8?q?/polib=E4=BE=9D=E8=B5=96=E7=A1=AE=E4=BF=9D=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E4=B8=8A=E7=BF=BB=E8=AF=91=E6=AD=A3=E5=B8=B8=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dragonpilot/system/ui/lib/multilang.py | 168 ++++++++++++++++++++----- 1 file changed, 138 insertions(+), 30 deletions(-) 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: