125 lines
5.0 KiB
Python
125 lines
5.0 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 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.__environment.policies["ext.i18n.trimmed"] = True
|
|
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[bytes]:
|
|
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("rb") 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[bytes]]:
|
|
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[bytes]) -> 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("wb") 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=self, **context)
|
|
|
|
with open(output_path, "w", encoding="utf-8") as output_file:
|
|
output_file.write(content)
|