Source code for univention.lib.i18n

#!/usr/bin/python3
# SPDX-FileCopyrightText: 2006-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

"""Internationalization (i18n) utilities."""

import gettext
import re
import weakref
from locale import LC_MESSAGES, Error, getlocale


[docs] class I18N_Error(Exception): """Error in Internationalization."""
[docs] class Locale: """ 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: str | None = None) -> None: self.__reset() if locale is not None: self.parse(locale) def __reset(self) -> None: self.language = "" self.territory = "" self.codeset = "" self.modifier = ""
[docs] def parse(self, locale: 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, str): 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) -> bool: return bool(self.language) __nonzero__ = __bool__ def __str__(self) -> str: text = self.language or '' if self.language not in ('C', 'POSIX') and 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: """ 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: str, locale_spec: str | None = None, localedir: str | None = None) -> None: self._set_domain(namespace) self._translation: gettext.NullTranslations | None = None self._localedir: str | None = localedir self._localespec: Locale | None = None self._locale: str | None = locale_spec if not self._locale: self.set_language() def _set_domain(self, namespace: 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: str = "") -> None: """ Select language. :param str language: The language code. """
def _get_locale(self) -> Locale | None: """ Return currently selected locale. :returns: The currently selected locale. :rtype: Locale """ return self._localespec def _set_locale(self, locale_spec: str | None = None) -> 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: str) -> str: """ Translate message. :param str message: The message to translate. :returns: The localized message. :rtype: str """ if self._translation is None: return message return self._translation.gettext(message)
_ = translate
[docs] class Translation(NullTranslation): """Translation.""" _instances: list[weakref.ReferenceType['Translation']] = [] locale: Locale = Locale() # type: ignore
[docs] def set_language(self, language: 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 OSError: try: self._translation = gettext.translation(self._domain, languages=('%s_%s' % (Translation.locale.language, Translation.locale.territory), ), localedir=self._localedir) except OSError: self._translation = None
def __new__(cls, *args, **kwargs): self = object.__new__(cls) cls._instances.append(weakref.ref(self)) cls._instances = [ref for ref in cls._instances if ref() is not None] return self
[docs] @classmethod def set_all_languages(cls, language: str) -> None: """ Set the language of all existing :class:`Translation` instances. This is required when instances are created during import time but later on the language should be changed. """ for ref in cls._instances: instance = ref() if instance is not None: instance.set_language(language)