Source code for peng3d.i18n

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  i18n.py
#
#  Copyright 2018-2022 notna <notna@apparat.org>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#

__all__ = [
    "TranslationManager",
]

import glob
import re

from .util import ActionDispatcher

from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Any, Union

if TYPE_CHECKING:
    import peng3d


# TODO: add test cases for translations
[docs]class TranslationManager(ActionDispatcher): """ Manages sets of translation files in multiple languages. This Translation System uses language codes to identify languages, there is no requirement to follow a specific standard, but it is recommended to use simple 2-digit codes like ``en`` and ``de``\\ , adding an underscore to define sub-languages like ``en_gb`` and ``en_us``\\ . Whenever a new translation file is needed, it will be parsed and then cached. This speeds up access times and also practically eliminates load times when switching languages. Several events are sent by this class, see :ref:`events-i18n`\\ . Most of these events are also sent as actions, these actions are described in the methods that cause them. There are also severale config options that determine the behaviour of this class. See :ref:`cfg-i18n` for more information. This Manager requires the :py:class:`~peng3d.resource.ResourceManager()` to be already initialized. """ def __init__(self, peng: "peng3d.Peng"): if not peng.cfg["rsrc.enable"]: raise RuntimeError( "ResourceManager needs to be enabled to use Translations" ) elif peng.rsrcMgr is None: raise RuntimeError( "ResourceManager needs to be initialized before TranslationManager" ) self.peng: "peng3d.Peng" = peng self.lang: str = self.peng.cfg["i18n.lang"] self.cache: Dict[ str, Dict[str, Dict[str, str]] ] = {} # dict of dicts, first lang, then domain self.peng.sendEvent("peng3d:i18n.init", {"lang": self.lang, "i18n": self}) self.setLang(self.peng.cfg["i18n.lang"])
[docs] def setLang(self, lang: str) -> None: """ Sets the default language for all domains. For recommendations regarding the format of the language code, see :py:class:`TranslationManager`\\ . Note that the ``lang`` parameter of both :py:meth:`translate()` and :py:meth:`translate_lazy()` will override this setting. Also note that the code won't be checked for existence or plausibility. This may cause the fallback strings to be displayed instead if the language does not exist. Calling this method will cause the ``setlang`` action and the :peng3d:event`peng3d:i18n.set_lang` event to be triggered. Note that both action and event will be triggered even if the language did not actually change. This method also automatically updates the :confval:`i18n.lang` config value. """ self.lang = lang self.peng.cfg["i18n.lang"] = lang if lang not in self.cache: self.cache[lang] = {} self.doAction("setlang") self.peng.sendEvent("peng3d:i18n.set_lang", {"lang": self.lang, "i18n": self})
[docs] def discoverLangs(self, domain: str = "*") -> List[str]: """ Generates a list of languages based on files found on disk. The optional ``domain`` argument may specify a domain to use when checking for files. By default, all domains are checked. This internally uses the :py:mod:`glob` built-in module and the :confval:`i18n.lang.format` config option to find suitable filenames. It then applies the regex in :confval:`i18n.discover_regex` to extract the language code. """ rsrc = self.peng.cfg["i18n.lang.format"].format(domain=domain, lang="*") pattern = self.peng.rsrcMgr.resourceNameToPath( rsrc, self.peng.cfg["i18n.lang.ext"] ) files = glob.iglob(pattern) langs = set() r = re.compile(self.peng.cfg["i18n.discover_regex"]) for f in files: m = r.fullmatch(f.replace("\\", "/")) if m is not None: langs.add(m.group("lang")) return list(langs)
[docs] def translate( self, key: str, translate: bool = True, lang: Optional[str] = None ) -> str: """ Translates the given key. If no language was given, the language last passed to :py:meth:`setLang()` will be used. If the translation key could not be found (e.g. because the language code is invalid), the key itself will be returned. Note that this method returns a string and thus does not have any way to modify the returned value if the language is changed by the user. If dynamic translation is required, :py:meth:`translate_lazy()` should be used instead. """ if lang is None: lang = self.lang if not translate or key.count(":") != 1: return key domain, name = key.split(":") if domain not in self.cache[lang]: self.loadDomain(domain, lang) if ( lang not in self.cache or domain not in self.cache[lang] or name not in self.cache[lang][domain] ): self.peng.sendEvent("peng3d:i18n.miss", {"key": key, "lang": lang}) return key # would just display the key to the user, good enough to be understood return self.cache[lang][domain][name]
t = translate
[docs] def translate_lazy( self, key: str, data: Optional[Dict] = None, translate: bool = True, lang: Optional[str] = None, ) -> "_LazyTranslator": """ Lazily translates a given translation key. This method is similar to :py:meth:`translate()`\\ , but returns a special object rather than a string. This allows for on-the-fly changing of the language without having to re-set all the places where translated strings are used. Whenever the returned object is converted to a string by :py:func:`str()` or :py:func:`repr()` or is formatted using either the old ``%``\\ -notation or the newer :py:meth:`str.format()`\\ , the translation key will be looked up again, in case the language has changed. Note that this requires support from the widgets (or other consumers of the returned value), namely that they only convert to string just prior to rendering and re-render either regularly or whenever either the ``setlang`` action or the :peng3d:event:`peng3d:i18n.set_lang` event is called. Most built-in widgets support this, but some special cases are not supported yet. For example, setting the window title dynamically requires using the ``caption_t`` parameter instead of the raw ``caption`` parameter. """ return _LazyTranslator(self, key, data, translate, lang)
tl = translate_lazy
[docs] def loadDomain( self, domain: str, lang: Optional[str] = None, encoding: str = "utf-8" ) -> bool: """ Loads the translation data of a single domain for a specific language from disk into the cache. If no language was given, the current language is used. If the translation file could not be found or any errors occur while reading it, these errors will be silently discarded, only recognizable by a return value of ``False``\\ . If the load was successful, the action ``loaddomain`` will be executed and this method will return ``True``\\ . """ if lang is None: lang = self.lang if lang not in self.cache: self.cache[lang][ domain ] = {} # prevents errors if function aborts prematurely rsrc = self.peng.cfg["i18n.lang.format"].format(domain=domain, lang=lang) if not self.peng.rsrcMgr.resourceExists(rsrc, self.peng.cfg["i18n.lang.ext"]): return False # prevents errors fname = self.peng.rsrcMgr.resourceNameToPath( rsrc, self.peng.cfg["i18n.lang.ext"] ) try: with open(fname, "r", encoding=encoding, errors="surrogateescape") as f: data = f.readlines() except Exception: return False # prevents errors d = {} for line in data: line.strip() if line == "" or line.startswith("#"): continue ls = line.split("=") k = ls.pop(0) # first ever useful application of pop v = "=".join(ls).strip() d[k] = v.replace("\\n", "\n") self.cache[lang][domain] = d self.doAction("loaddomain") return True
def __getitem__(self, key: str) -> str: return self.translate(key)
class _LazyTranslator(object): _dynamic = True def __init__( self, i18n: TranslationManager, key: str, data: Optional[Dict] = None, translate: bool = True, lang: Optional[str] = None, ): self.i18n = i18n self.key = key self.data = data self.translate = translate self.lang = lang def __str__(self) -> str: t = self.i18n.translate(self.key, self.translate, self.lang) if self.data is not None: try: return t.format(**self.data) except Exception: return t else: return t def __repr__(self) -> str: return str(self) def __mod__(self, other: Union[Any, Tuple]) -> str: try: return str(self) % other except Exception: return str(self) def format(self, *args, **kwargs) -> str: try: return str(self).format(*args, **kwargs) except Exception: return str(self)