# This file is a part of the FeretUI project
#
# Copyright (C) 2024 Jean-Sebastien SUZANNE <js.suzanne@gmail.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file,You can
# obtain one at http://mozilla.org/MPL/2.0/.
"""Module feretui.translation.
The translation mechanism is used to translate the user interface.
The translation files is store in po file. To translate the pot in each
language with `PoEdit <https://poedit.net/>`_.
The translated object are:
* :class:`feretui.translation.TranslatedForm`
* :class:`feretui.translation.TranslatedMenu`
* :class:`feretui.translation.TranslatedTemplate`
* :class:`feretui.translation.TranslatedFileTemplate`
* :class:`feretui.translation.TranslatedStringTemplate`
* :class:`feretui.translation.TranslatedResource`
The Translation class have two methods to manipulate the catalogs:
* :meth:`.Translation.export_catalog` : Export the catalog at a path
for a specific addons
* :meth:`.Translation.load_catalog` : Load catalog for a specific lang
.. _POEntry: https://polib.readthedocs.io/en/latest/api.html#the-poentry-class
"""
from datetime import datetime
from logging import getLogger
from pathlib import Path
from typing import TYPE_CHECKING
from polib import POEntry, POFile, pofile
from feretui.exceptions import (
TranslationError,
TranslationFormError,
TranslationMenuError,
TranslationResourceError,
)
from feretui.form import FeretUIForm
from feretui.menus import Menu
from feretui.resources.resource import Resource
from feretui.session import Session
if TYPE_CHECKING:
from feretui.feretui import FeretUI
from feretui.template import Template
logger = getLogger(__name__)
[docs]
class TranslatedTemplate:
"""TranslatedTemplate class.
Declare a template. The instance is used to defined the template files
where take the entries to export in the catalog
::
mytranslation = TranslatedTemplate('my.addons')
Translation.add_translated_template(mytranslation)
Attributes
----------
* [addons:str] : the addons of the template file
:param addons: The addons where the message come from
:type addons: str
"""
def __init__(
self: "TranslatedTemplate",
addons: str = "feretui",
) -> "TranslatedTemplate":
"""TranslatedMessage class."""
self.addons: str = addons
def __str__(self: "TranslatedTemplate") -> str:
"""Return the instance as a string."""
return (
f'<{self.__class__.__name__} {getattr(self, "path", "")} '
f'addons={self.addons}>'
)
[docs]
def load(
self: "TranslatedFileTemplate",
template: "Template", # noqa: ARG002
) -> None: # pragma: no cover
"""Load the template in the template instance.
:param template: template instance
:type template: :class:`feretui.template.Template`
:exceptions: TranslationError
"""
raise TranslationError("This method must be overwriting")
[docs]
class TranslatedFileTemplate(TranslatedTemplate):
"""TranslatedFileTemplate class.
Declare a template file as translatable. The instance is used
to defined the template files where take the entries to export
in the catalog
::
mytranslation = TranslatedFileTemplate(
'path/of/the/teplate',
'my.addons'
)
translation.add_translated_template(mytranslation)
To declare a TranslatedFileTemplate more easily, a helper exist on
FeretUI :meth:`feretui.feretui.FeretUI.import_templates_file`.
Attributes
----------
* [path:str] : the template file path
* [addons:str] : the addons of the template file
:param template_path: the template file path
:type template_path: str
:param addons: The addons where the message come from
:type addons: str
"""
def __init__(
self: "TranslatedFileTemplate",
template_path: str,
addons: str = "feretui",
) -> "TranslatedFileTemplate":
"""TranslatedFileTemplate class."""
super().__init__(addons=addons)
self.path: str = template_path
[docs]
def load(self: "TranslatedFileTemplate", template: "Template") -> None:
"""Load the template in the template instance.
:param template: template instance
:type template: :class:`feretui.template.Template`
"""
with Path(self.path).open() as fp:
template.load_file(fp, ignore_missing_extend=True)
[docs]
class TranslatedStringTemplate(TranslatedTemplate):
"""TranslatedStringTemplate class.
Declare a template str as translatable. The instance is used
to defined the template str where take the entries to export
in the catalog
::
mytranslation = TranslatedStringTemplate(
'''
<template id="my-teplate">
...
</template>
''',
'my.addons'
)
translation.add_translated_template(mytranslation)
To declare a TranslatedStringTemplate more easily, a helper exist on
FeretUI :meth:`feretui.feretui.FeretUI.register_page`.
Attributes
----------
* [path:str] : the template file path
* [addons:str] : the addons of the template file
:param template_path: the template file path
:type template_path: str
:param addons: The addons where the message come from
:type addons: str
"""
def __init__(
self: "TranslatedStringTemplate",
template: str,
addons: str = "feretui",
) -> "TranslatedStringTemplate":
"""TranslatedStringTemplate class."""
super().__init__(addons=addons)
self.template: str = template
[docs]
def load(self: "TranslatedStringTemplate", template: "Template") -> None:
"""Load the template in the template instance.
:param template: template instance
:type template: :class:`feretui.template.Template`
"""
template.load_template_from_str(self.template)
[docs]
class TranslatedResource:
"""TranslatedForm class.
Declare a resource as translatable.
::
MyResource.build()
myresource = MyResource('Code', 'The label')
translation.add_translated_resource(myresource)
To declare a TranslatedForm more easily, The helpers exist on
FeretUI :
* :meth:`feretui.feretui.FeretUI.register_resource`.
Attributes
----------
* [resource:Resource] : the resource to translated
* [addons:str] : the addons of the template file
:param resource: the resource instance to translate
:type resource: :class:`feretui.resource.Resource`
:param addons: The addons where the message come from
:type addons: str
"""
def __init__(
self: "TranslatedResource",
resource: Resource,
addons: str = "feretui",
) -> None:
"""TranslatedForm class."""
if not isinstance(resource, Resource):
raise TranslationResourceError(
f"{resource} must be an instance of Resource",
)
self.resource: Resource = resource
self.addons: str = addons
def __str__(self: "TranslatedResource") -> str:
"""Return the instance as a string."""
return f"<TranslatedResource {self.resource} addons={self.addons}>"
[docs]
def export_catalog(
self: "TranslatedResource",
translation: "Translation",
po: POFile,
) -> None:
"""Export the resource translations in the catalog.
:param translation: The translation instance to add also inside it.
:type translation: :class:`.Translation`
:param po: The catalog instance
:type po: PoFile_
"""
self.resource.export_catalog(translation, po)
[docs]
class Translation:
"""Translation class.
This class is used to manipulate translation.
.. warning::
The behaviour work with thread local
"""
def __init__(self: "Translation", feretui: "FeretUI") -> "Translation":
"""Instance of the Translation class."""
self.feretui = feretui
self.langs: set = set()
self.translations: dict[tuple[str, str, str], str] = {}
self.templates: list[TranslatedTemplate] = []
self.menus: list[TranslatedMenu] = []
self.forms: list[TranslatedForm] = []
self.resources: list[TranslatedResource] = []
[docs]
def has_lang(self: "Translation", lang: str) -> bool:
"""Return True the lang is declared.
:param lang: The language tested
:type lang: str
:return: The verification
:rtype: bool
"""
return lang in self.langs
[docs]
def set(self: "Translation", lang: str, poentry: POEntry) -> None:
"""Add a new translation in translations.
:param lang: The language code
:type lang: str
:param poentry: The poentry defined
:type poentry: POEntry_
"""
self.langs.add(lang)
self.translations[(lang, poentry.msgctxt, poentry.msgid)] = (
poentry.msgstr if poentry.msgstr else poentry.msgid
)
[docs]
def get(
self: "Translation",
lang: str,
context: str,
message: str,
message_as_default: bool = True,
) -> str:
"""Get the translated message from translations.
:param lang: The language code
:type lang: str
:param context: The context in the catalog
:type context: str
:param message: The original message
:type message: str
:param message_as_default: If True then when no translation is found
it is return the message else return None
:type message_as_default: bool
:return: The translated message
:rtype: str
"""
return self.translations.get(
(lang, context, message),
message if message_as_default else None,
)
[docs]
def define(self: "Translation", context: str, message: str) -> POEntry:
"""Create a POEntry for a message.
:param context: The context in the catalog
:type context: str
:param message: The original message
:type message: str
:return: The poentry
:rtype: POEntry_
"""
logger.debug("msgctxt : %r, msgid: %r", context, message)
return POEntry(
msgctxt=context,
msgid=message,
msgstr="",
)
[docs]
def add_translated_template(
self: "Translation",
template: TranslatedTemplate,
) -> None:
"""Add in templates a TranslatedTemplate.
:param template: The template.
:type template: :class:`TranslatedTemplate`
"""
self.templates.append(template)
logger.debug("Translation : Added new template : %s", template)
[docs]
def add_translated_resource(
self: "Translation",
resource: TranslatedResource,
) -> None:
"""Add in forms a TranslatedResource.
:param resource: The resource instance.
:type resource: :class:`.TranslatedResource`
"""
self.resources.append(resource)
logger.debug("Translation : Added new resource : %s", resource)
[docs]
def export_catalog(
self: "Translation",
output_path: str,
version: str,
addons: str = None,
) -> None:
"""Export a catalog template.
:param output_path: The path where write the catalog
:type output_path: str
:param version: The version of the catalog
:type version: str
:param addons: The addons where the message come from
:type addons: str
"""
from feretui.template import Template
po = POFile()
po.metadata = {
"Project-Id-Version": version,
"POT-Creation-Date": datetime.now().isoformat(),
"MIME-Version": "1.0",
"Content-Type": "text/plain; charset=utf-8",
"Content-Transfer-Encoding": "8bit",
}
templates = self.templates
menus = self.menus
forms = self.forms
resources = self.resources
self.add_translated_form(
TranslatedForm(Session.LoginForm, addons="feretui"),
)
self.add_translated_form(
TranslatedForm(Session.SignUpForm, addons="feretui"),
)
if addons is not None:
templates = filter(lambda x: x.addons == addons, templates)
menus = filter(lambda x: x.addons == addons, menus)
forms = filter(lambda x: x.addons == addons, forms)
resources = filter(lambda x: x.addons == addons, resources)
for form_translated_message in FeretUIForm.TRANSLATED_MESSAGES:
po.append(
self.define(
FeretUIForm.get_context(),
form_translated_message,
),
)
tmpls = Template(Translation(self.feretui))
for template in templates:
template.load(tmpls)
tmpls.export_catalog(po)
for menu in menus:
menu.export_catalog(self, po)
for form in forms:
form.export_catalog(self, po)
for resource in resources:
resource.export_catalog(self, po)
po.save(Path(output_path).resolve())
[docs]
def load_catalog(
self: "Translation",
catalog_path: str,
lang: str,
) -> None:
"""Load a catalog in translations.
:param catalog_path: Path of the catalog
:type catalog_path: str
:param lang: Language code
:type lang: str
"""
po = pofile(catalog_path)
for entry in po:
self.set(lang, entry)