diff --git a/jupyterlab_server/tests/test_translation_api.py b/jupyterlab_server/tests/test_translation_api.py index 7b99d0bf..177458ea 100644 --- a/jupyterlab_server/tests/test_translation_api.py +++ b/jupyterlab_server/tests/test_translation_api.py @@ -14,7 +14,7 @@ get_installed_packages_locale, get_language_pack, get_language_packs, is_valid_locale, merge_locale_data, - run_process_and_parse) + run_process_and_parse, translator) from .utils import maybe_patch_ioloop @@ -101,6 +101,24 @@ async def test_get_locale_not_valid(jp_fetch): assert result["data"] == {} +# --- Backend locale +# ------------------------------------------------------------------------ +async def test_backend_locale(jp_fetch): + locale = "es_CO" + r = await jp_fetch("lab", "api", "translations", locale) + trans = translator.load("jupyterlab") + result = trans.__("MORE ABOUT PROJECT JUPYTER") + assert result == "Más sobre el proyecto jupyter" + + +async def test_backend_locale_extension(jp_fetch): + locale = "es_CO" + r = await jp_fetch("lab", "api", "translations", locale) + trans = translator.load("jupyterlab_some_package") + result = trans.__("BOOM") + assert result == "Foo bar 2" + + # --- Utils testing # ------------------------------------------------------------------------ def test_get_installed_language_pack_locales_fails(): diff --git a/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/extensions/__init__.py b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/extensions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/jupyterlab.json b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.json similarity index 100% rename from jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/jupyterlab.json rename to jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.json diff --git a/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.mo b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.mo new file mode 100644 index 00000000..fa46000f Binary files /dev/null and b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.mo differ diff --git a/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.po b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.po new file mode 100644 index 00000000..3b3e7fe3 --- /dev/null +++ b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab.po @@ -0,0 +1,17 @@ +msgid "" +msgstr "" +"Project-Id-Version: jupyterlab\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language-Team: Spanish\n" +"Language: es_CO\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.1\n" + +#: /example +msgid "MORE ABOUT PROJECT JUPYTER" +msgstr "Más sobre el proyecto jupyter" diff --git a/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/extensions/jupyterlab_some_package.json b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.json similarity index 100% rename from jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/extensions/jupyterlab_some_package.json rename to jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.json diff --git a/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.mo b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.mo new file mode 100644 index 00000000..cd4fad6f Binary files /dev/null and b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.mo differ diff --git a/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.po b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.po new file mode 100644 index 00000000..bba45244 --- /dev/null +++ b/jupyterlab_server/tests/translations/jupyterlab-language-pack-es_CO/jupyterlab_language_pack_es_CO/locale/es_CO/LC_MESSAGES/jupyterlab_some_package.po @@ -0,0 +1,17 @@ +msgid "" +msgstr "" +"Project-Id-Version: jupyterlab_some_package\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language-Team: Spanish\n" +"Language: es_CO\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.1\n" + +#: /example +msgid "BOOM" +msgstr "Foo bar 2" diff --git a/jupyterlab_server/translation_utils.py b/jupyterlab_server/translation_utils.py index b3d85b73..01b51d04 100644 --- a/jupyterlab_server/translation_utils.py +++ b/jupyterlab_server/translation_utils.py @@ -3,7 +3,9 @@ localization data. """ +import gettext import json +import importlib import os import subprocess import sys @@ -124,11 +126,8 @@ def run_process_and_parse(cmd: list): p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() result = json.loads(stdout.decode('utf-8')) - - # FIXME: Use case? - result["message"] = stderr.decode('utf-8') except Exception: - result["message"] = traceback.format_exc() + result["message"] = traceback.format_exc() + "\n" + repr(stderr.decode('utf-8')) return result["data"], result["message"] @@ -386,5 +385,256 @@ def get_language_pack(locale: str) -> tuple: return locale_data, "\n".join(messages) +# --- Translators +# ---------------------------------------------------------------------------- +class TranslationBundle: + """ + Translation bundle providing gettext translation functionality. + """ + + def __init__(self, domain: str, locale: str): + self._domain = domain + self._locale = locale + + self.update_locale(locale) + + def update_locale(self, locale: str): + """ + Update the locale environment variables. + + Parameters + ---------- + locale: str + The language name to use. + """ + # TODO: Need to handle packages that provide their own .mo files + self._locale = locale + localedir = None + if locale != DEFAULT_LOCALE: + language_pack_module = f"jupyterlab_language_pack_{locale}" + try: + mod = importlib.import_module(language_pack_module) + localedir = os.path.join(os.path.dirname(mod.__file__), LOCALE_DIR) + except Exception: + pass + + gettext.bindtextdomain(self._domain, localedir=localedir) + + def gettext(self, msgid: str) -> str: + """ + Translate a singular string. + + Parameters + ---------- + msgid: str + The singular string to translate. + + Returns + ------- + str + The translated string. + """ + return gettext.dgettext(self._domain, msgid) + + def ngettext(self, msgid: str, msgid_plural: str, n: int) -> str: + """ + Translate a singular string with pluralization. + + Parameters + ---------- + msgid: str + The singular string to translate. + msgid_plural: str + The plural string to translate. + n: int + The number for pluralization. + + Returns + ------- + str + The translated string. + """ + return gettext.dngettext(self._domain, msgid, msgid_plural, n) + + def pgettext(self, msgctxt: str, singular: str) -> str: + """ + Translate a singular string with context. + + Parameters + ---------- + msgctxt: str + The message context. + msgid: str + The singular string to translate. + + Returns + ------- + str + The translated string. + """ + return gettext.dpgettext(self._domain, msgctxt, msgid) + + def npgettext(self, msgctxt: str, msgid: str, msgid_plural: str, n: int) -> str: + """ + Translate a singular string with context and pluralization. + + Parameters + ---------- + msgctxt: str + The message context. + msgid: str + The singular string to translate. + msgid_plural: str + The plural string to translate. + n: int + The number for pluralization. + + Returns + ------- + str + The translated string. + """ + return gettext.dnpgettext(self._domain, msgctxt, msgid, msgid_plural, n) + + # Shorthands + def __(self, msgid: str) -> str: + """ + Shorthand for gettext. + + Parameters + ---------- + msgid: str + The singular string to translate. + + Returns + ------- + str + The translated string. + """ + return self.gettext(msgid) + + def _n(self, msgid: str, msgid_plural: str, n: int) -> str: + """ + Shorthand for ngettext. + + Parameters + ---------- + msgid: str + The singular string to translate. + msgid_plural: str + The plural string to translate. + n: int + The number for pluralization. + + Returns + ------- + str + The translated string. + """ + return self.ngettext(msgid, plural, n) + + def _p(self, msgctxt: str, msgid: str) -> str: + """ + Shorthand for pgettext. + + Parameters + ---------- + msgctxt: str + The message context. + msgid: str + The singular string to translate. + + Returns + ------- + str + The translated string. + """ + return self.pgettext(msgctxt, msgid) + + def _np(self, msgctxt: str, msgid: str, msgid_plular: str, n: str) -> str: + """ + Shorthand for npgettext. + + Parameters + ---------- + msgctxt: str + The message context. + msgid: str + The singular string to translate. + msgid_plural: str + The plural string to translate. + n: int + The number for pluralization. + + Returns + ------- + str + The translated string. + """ + return self.npgettext(msgctxt, msgid, msgid_plular, n) + + +class translator: + """ + Translations manager. + """ + _TRANSLATORS = {} + _LOCALE = DEFAULT_LOCALE + + @staticmethod + def _update_env(locale: str): + """ + Update the locale environment variables based on the settings. + + Parameters + ---------- + locale: str + The language name to use. + """ + for key in ["LANGUAGE", "LANG"]: + os.environ[key] = f"{locale}.UTF-8" + + @classmethod + def set_locale(cls, locale: str): + """ + Set locale for the translation bundles based on the settings. + + Parameters + ---------- + locale: str + The language name to use. + """ + if is_valid_locale(locale): + cls._LOCALE = locale + translator._update_env(locale) + for domain, bundle in cls._TRANSLATORS.items(): + bundle.update_locale(locale) + + @classmethod + def load(cls, domain: str) -> TranslationBundle: + """ + Load translation domain. + + The domain is usually the normalized ``package_name``. + + Parameters + ---------- + domain: str + The translations domain. The normalized python package name. + + Returns + ------- + Translator + A translator instance bound to the domain. + """ + if domain in cls._TRANSLATORS: + trans = cls._TRANSLATORS[domain] + else: + trans = TranslationBundle(domain, cls._LOCALE) + cls._TRANSLATORS[domain] = trans + + return trans + + if __name__ == "__main__": _main() diff --git a/jupyterlab_server/translations_handler.py b/jupyterlab_server/translations_handler.py index d81bf675..fa84fda0 100644 --- a/jupyterlab_server/translations_handler.py +++ b/jupyterlab_server/translations_handler.py @@ -13,7 +13,7 @@ from tornado import gen, web from .settings_handler import get_settings -from .translation_utils import get_language_pack, get_language_packs, is_valid_locale +from .translation_utils import get_language_pack, get_language_packs, is_valid_locale, translator from .server import APIHandler, url_path_join @@ -74,6 +74,7 @@ def get(self, locale=""): data, message = get_language_packs( display_locale=get_current_locale(self.lab_config)) else: + translator.set_locale(locale) data, message = get_language_pack(locale) if data == {} and message == "": if is_valid_locale(locale):