Source code for univention.lib.i18n

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Internationalization (i18n) utilities.
"""
# Copyright 2006-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.

import gettext
from locale import getlocale, Error, LC_MESSAGES
import re
from typing import Optional, Text  # noqa: F401

import six


[docs]class I18N_Error(Exception): """ Error in Internationalization. """ pass
[docs]class Locale(object): """ Represents a locale specification and provides simple access to language, territory, codeset and modifier. :param locale: The locale string `language[_territory][.codeset][@modifier]`. :type locale: str or None >>> Locale("deu_GER") >>> str(Locale("ca_ES@valencia")) >>> str(Locale("")) """ REGEX = re.compile( r'^' r'(?P<language>([a-z]{2}|C|POSIX))' r'(?:_(?P<territory>[A-Z]{2}))?' r'(?:\.(?P<codeset>[a-zA-Z-0-9]+))?' r'(?:@(?P<modifier>.+))?' r'$') def __init__(self, locale=None): # type: (Optional[str]) -> None self.__reset() if locale is not None: self.parse(locale) def __reset(self): # type: () -> None self.language = "" self.territory = "" self.codeset = "" self.modifier = ""
[docs] def parse(self, locale): # type: (str) -> None """ Parse locale string. :param str locale: The locale string `language[_territory][.codeset][@modifier]`. :raises TypeError: if `locale` is not a string. :raises I18N_Error: if `locale` does not match the format. """ if not isinstance(locale, six.string_types): raise TypeError('locale must be of type string') self.__reset() regex = Locale.REGEX.match(locale) if not regex: raise I18N_Error('attribute does not match locale specification language[_territory][.codeset][@modifier]') self.codeset = 'UTF-8' # default encoding for key, value in regex.groupdict().items(): if value is None: continue setattr(self, key, value)
def __bool__(self): # type: () -> bool return bool(self.language) __nonzero__ = __bool__ def __str__(self): # type: () -> str text = self.language or '' if self.language not in ('C', 'POSIX'): if self.territory: text += '_%s' % self.territory if self.codeset: text += '.%s' % self.codeset if self.modifier: text += '@%s' % self.modifier return text
[docs]class NullTranslation(object): """ Dummy translation. :param str namespace: The name of the translation domain. :param str locale_spec: The selected locale. :param str localedir: The name of the directory containing the translation files. """ def __init__(self, namespace, locale_spec=None, localedir=None): # type: (str, Optional[str], Optional[str]) -> None self._set_domain(namespace) # type: Optional[str] self._translation = None # type: Optional[gettext.NullTranslations] self._localedir = localedir # type: Optional[str] self._localespec = None # type: Optional[Locale] self._locale = locale_spec # type: Optional[str] if not self._locale: self.set_language() def _set_domain(self, namespace): # type: (str) -> None """ Select translation domain. :param str namespace: The name of the translation domain. """ if namespace is not None: self._domain = namespace.replace('/', '-').replace('.', '-') else: self._domain = None domain = property(fset=_set_domain)
[docs] def set_language(self, language=""): # type: (str) -> None """ Select language. :param str language: The language code. """ pass
def _get_locale(self): # type: () -> Optional[Locale] """ Return currently selected locale. :returns: The currently selected locale. :rtype: Locale """ return self._localespec def _set_locale(self, locale_spec=None): # type: (Optional[str]) -> None """ Select new locale. :param str locale_spec: The new locale specification. """ if locale_spec is None: return self._localespec = Locale(locale_spec) locale = property(fget=_get_locale, fset=_set_locale)
[docs] def translate(self, message): # type: (str) -> Text """ Translate message. :param str message: The message to translate. :returns: The localized message. :rtype: str """ if self._translation is None: return message if six.PY2: return self._translation.ugettext(message) return self._translation.gettext(message)
_ = translate
[docs]class Translation(NullTranslation): """ Translation. """ locale = Locale() # type: Locale # type: ignore
[docs] def set_language(self, language=""): # type: (str) -> None """ Select language. :param str language: The language code. :raises I18N_Error: if the given locale is not valid. """ if language: Translation.locale.parse(language) if not Translation.locale: try: lang = getlocale(LC_MESSAGES) language = lang[0] or "C" Translation.locale.parse(language) except Error as exc: raise I18N_Error('The given locale is not valid: %s' % (exc,)) if not self._domain: return try: self._translation = gettext.translation(self._domain, languages=(Translation.locale.language, ), localedir=self._localedir) except IOError: try: self._translation = gettext.translation(self._domain, languages=('%s_%s' % (Translation.locale.language, Translation.locale.territory), ), localedir=self._localedir) except IOError: self._translation = None