Source code for ascii_designer.i18n

"""I18n support for ASCII Designer or other purposes.

.. default-role:: py:obj

Provides the working-data structure `.Translations`, and functions to load and
save translation files.
"""
__all__ = [
    "Translations",
    "load_translations_json",
    "save_translations_json",
]

from pathlib import Path, PurePath
try:
    import importlib.resources as resources
except ImportError:
    resources = None
import sys
import os
import json
import ctypes
import locale
import logging


def L():
    return logging.getLogger(__name__)


[docs]class Translations(dict): """Mostly off-the shelf python dict, except for two facilities to aid translation. Translations should be retrieved via ``.get(key, default)`` method. The class has the two additional properties `.recording` and `.mark_missing`. * If `recording` is set to True, calls of `~.Translations.get` will add missing entries (i.e. `~.Translations.get` does the same as `.setdefault`). By setting it and opening all forms once, you can collect all translation keys and default strings. * If `mark_missing` is set and `~.Translations.get` finds a missing key, the given default value is prefixed with a ``$`` sign. """ recording: bool = False mark_missing: bool = False
[docs] def get(self, key, default=None): if self.recording: return self.setdefault(key, default) else: if self.mark_missing: default = "$" + default return super().get(key, default)
[docs]def load_translations_json(package_or_dir="locale", prefix="", language=None): """Locate and load translations from JSON file. JSON file format is a simple key value store. If given a package name, use the resource loading system. If given a dir, use file access. The argument is interpreted as dir if: * the string contains ``/`` or ``\\`` * the argument is a ``pathlib.PurePath`` instance. Resource name is formed by the rule "<prefix>.<language>.json" (first dot is ommited if one of both is empty). If both prefix and language are empty, we look for ``default.json``. If the language is not given, the OS's UI language is used. With the given or guessed language we look for an existing file: * First we look for the exact language string (e.g. "de_DE.json") * then we look for the first two letters of the language string ("de.json") * then we look for emtpy language (i.e. default set). If none of these exists, empty ``Translations`` object is returned. """ if ( isinstance(package_or_dir, PurePath) or "/" in package_or_dir or "\\" in package_or_dir ): # filesystem path path = find_json_path(package_or_dir, prefix, language) openfunc = lambda: path.open("r") type = "file" else: # resource dir if resources is None: L().error("importlib.resource is not available, translations must be loaded from file instead.") return Translations() path = find_resource(package_or_dir, prefix, language) openfunc = lambda: resources.open_text(package_or_dir, path) type = "resource" # Not found if path is None: L().debug("No translations found") return Translations() L().debug("Load translations from %s %s", type, path) with openfunc() as fp: d = json.load(fp) return Translations(d)
[docs]def save_translations_json(translations, path): """Save translations to JSON file. OVERWRITES existing file! In contrast to ``load_translations_json``, we only accept a path here. """ path = Path(path) with path.open("w") as fp: json.dump(translations, fp, indent=2) L().info("Saved translations to %s", path) return path
def _join_ne(*strings): return ".".join(part for part in strings if part) def _os_locale(): if sys.platform.startswith("linux"): lang = os.getenv("LANG") # split off charset part return lang.partition(".")[0] elif sys.platform.startswith("win32"): windll = ctypes.windll.kernel32 lang_id = windll.GetUserDefaultUILanguage() lang = locale.windows_locale[lang_id] return lang.partition(".")[0] else: raise RuntimeError("Cannot guess language on %s platform" % sys.platform) def find_json_path(dir, prefix="", language=None) -> Path: """Find location of translations file. ``dir`` gives the directory to search in, absolute or relative. Filename is formed by the rule "<prefix>.<language>.json" (first dot is ommited if one of both is empty). If both prefix and language are empty, we look for ``default.json``. If the language is not given, we try to get UI language of the OS. With the given or guessed language we look for an existing file: * First we look for the exact language string * then we look for the first two letters of the language string * then we look for emtpy language (i.e. default set). If none of these exists, None is returned. """ dir = Path(dir) if language is None: language = _os_locale() L().debug("OS language: %s", language) for name in [ _join_ne(prefix, language, "json"), _join_ne(prefix, language[:2], "json"), _join_ne(prefix or "default", "json"), ]: if (dir / name).exists(): return dir / name return None def find_resource(package, prefix="", language=None): if language is None: language = _os_locale() L().debug("OS language: %s", language) for name in [ _join_ne(prefix, language, "json"), _join_ne(prefix, language[:2], "json"), _join_ne(prefix or "default", "json"), ]: if resources.is_resource(package, name): return name return None