import gettext import os import re from openpilot.system.ui.lib.multilang import ( multilang as base_multilang, TRANSLATIONS_DIR, tr_noop, ) 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 | PoTranslations = gettext.NullTranslations() self._loaded_language: str = "" @property def languages(self): """Delegate to base multilang.""" return base_multilang.languages @property def language(self): """Delegate to base multilang.""" return base_multilang.language def _ensure_loaded(self): """Reload dragon translations if base language changed.""" current_lang = base_multilang.language if current_lang != self._loaded_language: self._loaded_language = current_lang po_path = TRANSLATIONS_DIR.joinpath(f'dragonpilot_{current_lang}.po') 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: self._ensure_loaded() result = self._dragon_translation.gettext(text) return result if result != text else base_multilang.tr(text) def trn(self, singular: str, plural: str, n: int) -> str: self._ensure_loaded() result = self._dragon_translation.ngettext(singular, plural, n) return result if result not in (singular, plural) else base_multilang.trn(singular, plural, n) multilang = DpMultilang() tr, trn = multilang.tr, multilang.trn __all__ = ['multilang', 'tr', 'trn', 'tr_noop', 'TRANSLATIONS_DIR']