fix: 用纯Python解析.po文件,去掉msgfmt/polib依赖确保设备上翻译正常加载
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user