Skip to content

Commit

Permalink
wip: i18n
Browse files Browse the repository at this point in the history
  • Loading branch information
MHajoha committed Jan 16, 2025
1 parent 2f51d6a commit 28c8321
Showing 1 changed file with 92 additions and 0 deletions.
92 changes: 92 additions & 0 deletions questionpy/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from gettext import GNUTranslations, NullTranslations
from importlib.resources.abc import Traversable

from questionpy_common.environment import Environment, Package, RequestUser

_DEFAULT_CATEGORY = "LC_MESSAGES"
_DEFAULT_DOMAIN = "package"
_NULL_TRANSLATIONS = NullTranslations()

_STATE: tuple[str, NullTranslations] | None = None


def _guess_untranslated_language(package: Package) -> str:
# We'll assume that the untranslated messages are in the first supported language according to the manifest.
if package.manifest.languages:
return next(iter(package.manifest.languages))
# If the package lists no supported languages in its manifest, we'll assume it's english.
# TODO: An alternative might be "C" or "unknown"?
return "en"


def _build_translations(
mos: dict[str, Traversable], package: Package, request_user: RequestUser
) -> tuple[str, NullTranslations]:
primary_lang: str | None = None
translations: NullTranslations | None = None

for preferred_lang in request_user.preferred_languages:
if preferred_lang not in mos:
continue

with mos[preferred_lang].open("rb") as mo_file:
# GNUTranslations reads the file immediately, so we can safely close it afterward.
lang_translations = GNUTranslations(mo_file)

if translations:
translations.add_fallback(lang_translations)
else:
translations = lang_translations
primary_lang = preferred_lang

if primary_lang and translations:
return primary_lang, translations

# There are no .mo files for any of the users preferred languages. We'll use NullTranslations, i.e. not translate
# anything, and guess at which language the untranslated messages are in.
return _guess_untranslated_language(package), _NULL_TRANSLATIONS


def _get_available_mos(package: Package) -> dict[str, Traversable]:
result = {}
locale_dir = package.get_path("locale")
if not locale_dir.is_dir():
return {}

for lang_dir in locale_dir.iterdir():
if not lang_dir.is_dir():
continue

mo_file = lang_dir / _DEFAULT_CATEGORY / f"{_DEFAULT_DOMAIN}.mo"
if mo_file.is_file():
result[lang_dir.name] = mo_file

return result


def get_state() -> tuple[str, NullTranslations]:
if not _STATE:
msg = f"i18n was not initialized. Call {__name__}.{initialize.__name__} first."
raise RuntimeError(msg)
return _STATE


def initialize(package: Package, env: Environment) -> None:
# PLW0603 discourages the global statement, but the alternative would be less readable.
# ruff: noqa: PLW0603

global _STATE
if _STATE:
# Prevent multiple initializations, which would add multiple on_request_callbacks overwriting each other.
return

available_mos = _get_available_mos(package)

def prepare_i18n(request_user: RequestUser) -> None:
global _STATE
_, translations = _STATE = _build_translations(available_mos, package, request_user)
translations.install()

_STATE = (_guess_untranslated_language(package), _NULL_TRANSLATIONS)
_NULL_TRANSLATIONS.install()
env.register_on_request_callback(prepare_i18n)

0 comments on commit 28c8321

Please sign in to comment.