188 lines
5.1 KiB
Python
188 lines
5.1 KiB
Python
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']
|