jean-web/jweb/context.py

121 lines
4.8 KiB
Python

import gettext
from contextlib import contextmanager
from gettext import GNUTranslations, NullTranslations
from importlib import import_module
from pathlib import Path
from typing import Any, Iterator
from jinja2 import pass_context
from jinja2.environment import Environment
from jinja2.loaders import FileSystemLoader
from jinja2.runtime import Context as JinjaContext
from jweb.content import Content
_DEFAULT_LANGUAGE = "en"
class Context:
def __init__(self, root_directory: Path) -> None:
self.__root_directory = root_directory
self.__output_directory = root_directory / "build"
self.__content_directory = root_directory / "content"
self.__environment = Environment(loader=FileSystemLoader(searchpath=root_directory / "src"))
self.__translations: dict[str, GNUTranslations | NullTranslations] = {}
self.__current_language: str | None = None
self.add_filters(load=self.__load, write=self.__write, glob=self.__glob)
self.add_globals(load=self.__load, write=self.__write, glob=self.__glob)
@property
def current_language(self) -> str | None:
return self.__current_language
def add_filters(self, **filters: Any) -> None:
self.__environment.filters.update(**filters)
def add_globals(self, **globals: Any) -> None:
self.__environment.globals.update(**globals)
def load_extensions(self, *extensions: str) -> None:
for extension in extensions:
module = import_module(extension)
module.load_extension(self)
def load_translations(self, domain: str, locale_dir: str | Path, *languages: str) -> None:
locale_dir = str(locale_dir)
self.__environment.add_extension("jinja2.ext.i18n")
self.__translations[_DEFAULT_LANGUAGE] = NullTranslations()
for language in languages:
self.__translations[language] = gettext.translation(
domain, localedir=str(locale_dir), languages=[language]
)
@contextmanager
def set_language(self, language: str) -> Iterator[None]:
translation = self.__translations[language]
old_language = self.__current_language
self.__environment.install_gettext_translations(translation, newstyle=True) # type: ignore
self.__current_language = language
yield
self.__current_language = old_language
self.__environment.uninstall_gettext_translations(translation) # type: ignore
def render(self, source: str, output: str | Path, **context: Any) -> None:
if self.__translations:
for language in self.__translations:
with self.set_language(language):
self.__render(source, self.__output_directory / language / output, **context)
else:
self.__render(source, self.__output_directory / output, **context)
@pass_context
def __load(self, context: JinjaContext, path: str | Path) -> Content[str]:
current_language = self.current_language
if current_language:
localized_path = self.__content_directory / current_language / path
if not localized_path.exists():
localized_path = self.__content_directory / _DEFAULT_LANGUAGE / path
else:
localized_path = self.__content_directory / path
with localized_path.open("r", encoding="utf-8") as content_file:
return Content(localized_path, content_file.read(), current_language)
@pass_context
def __glob(
self, context: JinjaContext, pattern: str, include_base_language: bool = False
) -> Iterator[Content[str]]:
roots: list[Path] = []
current_language = self.current_language
if current_language is None:
roots.append(self.__content_directory)
else:
if include_base_language:
roots.append(self.__content_directory / _DEFAULT_LANGUAGE)
roots.append(self.__content_directory / current_language)
paths: set[Path] = set()
for root in roots:
paths = paths | set(it.relative_to(root) for it in root.glob(pattern))
for path in paths:
yield self.__load(context, path)
def __write(self, content: Content[str]) -> Path:
relative_path = content.path.relative_to(self.__content_directory)
output_path = self.__output_directory / relative_path
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w") as output_file:
output_file.write(content.data)
return Path(f"/{relative_path}")
def __render(self, source: str, output_path: Path, **context: Any) -> None:
output_path.parent.mkdir(exist_ok=True, parents=True)
template = self.__environment.get_template(source)
content = template.render(**context)
with open(output_path, "w") as output_file:
output_file.write(content)