Switched to new i18nice package

See https://github.com/Krutyi-4el/i18nice/issues/3
This commit is contained in:
Orsiris de Jong 2023-03-27 16:25:53 +02:00
parent bb266eb158
commit 202edb4def
12 changed files with 2 additions and 481 deletions

View file

@ -1,11 +0,0 @@
from . import resource_loader
from .resource_loader import Loader, I18nFileLoadError, register_loader, load_config, reload_everything
from .translator import t
from .translations import add as add_translation
from .custom_functions import add_function
from . import config
from .config import set, get
resource_loader.init_loaders()
load_path = config.get('load_path')

View file

@ -1,57 +0,0 @@
try:
__import__("yaml")
yaml_available = True
except ImportError:
yaml_available = False
try:
__import__("json")
json_available = True
except ImportError:
json_available = False
# try to get existing path object
# in case if config is being reloaded
try:
from . import load_path
load_path.clear()
except ImportError:
load_path = []
settings = {
'filename_format': '{namespace}.{locale}.{format}',
'file_format': 'yml' if yaml_available else 'json' if json_available else 'py',
'available_locales': ['en'],
'load_path': load_path,
'locale': 'en',
'fallback': 'en',
'placeholder_delimiter': '%',
'on_missing_translation': None,
'on_missing_placeholder': None,
'on_missing_plural': None,
'encoding': 'utf-8',
'namespace_delimiter': '.',
'plural_few': 5,
'skip_locale_root_data': False,
'enable_memoization': False,
'argument_delimiter': '|'
}
def set(key, value):
if key not in settings:
raise KeyError("Invalid setting: {0}".format(key))
if key == 'placeholder_delimiter':
# hacky trick to reload formatter's configuration
from .translator import TranslationFormatter
TranslationFormatter.delimiter = value
del TranslationFormatter.pattern
TranslationFormatter.__init_subclass__()
elif key == 'load_path':
load_path.clear()
load_path.extend(value)
return
settings[key] = value
def get(key):
return settings[key]

View file

@ -1,17 +0,0 @@
from collections import defaultdict
global_functions = {}
locales_functions = defaultdict(dict)
def add_function(name, func, locale=None):
if locale:
locales_functions[locale][name] = func
else:
global_functions[name] = func
def get_function(name, locale=None):
if locale and name in locales_functions[locale]:
return locales_functions[locale][name]
return global_functions.get(name)

View file

@ -1,9 +0,0 @@
from .loader import Loader, I18nFileLoadError
from .python_loader import PythonLoader
from .. import config
if config.json_available:
from .json_loader import JsonLoader
if config.yaml_available:
from .yaml_loader import YamlLoader
del config

View file

@ -1,14 +0,0 @@
import json
from . import Loader, I18nFileLoadError
class JsonLoader(Loader):
"""class to load json files"""
def __init__(self):
super(JsonLoader, self).__init__()
def parse_file(self, file_content):
try:
return json.loads(file_content)
except json.JSONDecodeError as e:
raise I18nFileLoadError("invalid JSON: {0}".format(e.args[0])) from e

View file

@ -1,57 +0,0 @@
from .. import config
import io
import os.path
class I18nFileLoadError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return str(self.value)
class Loader(object):
"""Base class to load resources"""
loaded_files = {}
def __init__(self):
super(Loader, self).__init__()
def load_file(self, filename):
try:
with io.open(filename, 'r', encoding=config.get('encoding')) as f:
return f.read()
except IOError as e:
raise I18nFileLoadError("error loading file {0}: {1}".format(filename, e.strerror)) from e
def parse_file(self, file_content):
raise NotImplementedError("the method parse_file has not been implemented for class {0}".format(self.__class__.__name__))
def check_data(self, data, root_data):
return True if root_data is None else root_data in data
def get_data(self, data, root_data):
# use .pop to remove used data from cache
return data if root_data is None else data.pop(root_data)
def load_resource(self, filename, root_data, remember_content):
filename = os.path.abspath(filename)
if filename in self.loaded_files:
data = self.loaded_files[filename]
if not data:
# cache is missing or exhausted
return {}
else:
file_content = self.load_file(filename)
data = self.parse_file(file_content)
if not self.check_data(data, root_data):
raise I18nFileLoadError("error getting data from {0}: {1} not defined".format(filename, root_data))
enable_memoization = config.get('enable_memoization')
if enable_memoization:
if remember_content:
self.loaded_files[filename] = data
else:
self.loaded_files[filename] = None
return self.get_data(data, root_data)

View file

@ -1,27 +0,0 @@
import os.path
import sys
from importlib import util
from .. import config
from . import Loader, I18nFileLoadError
class PythonLoader(Loader):
"""class to load python files"""
def __init__(self):
super(PythonLoader, self).__init__()
def load_file(self, filename):
_, name = os.path.split(filename)
module_name, _ = os.path.splitext(name)
try:
spec = util.spec_from_file_location(module_name, filename)
module = util.module_from_spec(spec)
spec.loader.exec_module(module)
return vars(module)
except Exception as e:
raise I18nFileLoadError("error loading file {0}".format(filename)) from e
def parse_file(self, file_content):
return file_content

View file

@ -1,18 +0,0 @@
import yaml
from . import Loader, I18nFileLoadError
class YamlLoader(Loader):
"""class to load yaml files"""
loader = yaml.BaseLoader
def __init__(self):
super(YamlLoader, self).__init__()
def parse_file(self, file_content):
try:
return yaml.load(file_content, Loader=self.loader)
except yaml.YAMLError as e:
raise I18nFileLoadError("invalid YAML: {0}".format(str(e))) from e

View file

@ -1,127 +0,0 @@
import os.path
from . import config
from .loaders import Loader, I18nFileLoadError
from . import translations
loaders = {}
PLURALS = {"zero", "one", "few", "many"}
def register_loader(loader_class, supported_extensions):
if not issubclass(loader_class, Loader):
raise ValueError("loader class should be subclass of i18n.Loader")
loader = loader_class()
for extension in supported_extensions:
loaders[extension] = loader
def load_resource(filename, root_data, remember_content=False):
extension = os.path.splitext(filename)[1][1:]
if extension not in loaders:
raise I18nFileLoadError("no loader available for extension {0}".format(extension))
return loaders[extension].load_resource(filename, root_data, remember_content)
def init_loaders():
init_python_loader()
if config.yaml_available:
init_yaml_loader()
if config.json_available:
init_json_loader()
def init_python_loader():
from .loaders import PythonLoader
register_loader(PythonLoader, ["py"])
def init_yaml_loader():
from .loaders import YamlLoader
register_loader(YamlLoader, ["yml", "yaml"])
def init_json_loader():
from .loaders import JsonLoader
register_loader(JsonLoader, ["json"])
def load_config(filename):
settings_data = load_resource(filename, "settings")
for key, value in settings_data.items():
config.set(key, value)
def get_namespace_from_filepath(filename):
namespace = os.path.dirname(filename).strip(os.sep).replace(os.sep, config.get('namespace_delimiter'))
format = config.get('filename_format')
if '{namespace}' in format:
try:
splitted_filename = os.path.basename(filename).split('.')
if namespace:
namespace += config.get('namespace_delimiter')
namespace += splitted_filename[format.split(".").index('{namespace}')]
except ValueError as e:
raise I18nFileLoadError("incorrect file format.") from e
return namespace
def load_translation_file(filename, base_directory, locale=None):
if locale is None:
locale = config.get('locale')
skip_locale_root_data = config.get('skip_locale_root_data')
root_data = None if skip_locale_root_data else locale
# if the file isn't dedicated to one locale and may contain other `root_data`s
remember_content = "{locale}" not in config.get("filename_format") and root_data
translations_dic = load_resource(os.path.join(base_directory, filename), root_data, remember_content)
namespace = get_namespace_from_filepath(filename)
load_translation_dic(translations_dic, namespace, locale)
def reload_everything():
translations.clear()
Loader.loaded_files.clear()
def load_translation_dic(dic, namespace, locale):
if namespace:
namespace += config.get('namespace_delimiter')
for key, value in dic.items():
if type(value) == dict and len(PLURALS.intersection(value)) < 2:
load_translation_dic(value, namespace + key, locale)
else:
translations.add(namespace + key, value, locale)
def load_directory(directory, locale):
for f in os.listdir(directory):
path = os.path.join(directory, f)
if os.path.isfile(path) and path.endswith(config.get('file_format')):
if '{locale}' in config.get('filename_format') and not locale in f:
continue
load_translation_file(f, directory, locale)
def search_translation(key, locale=None):
if locale is None:
locale = config.get('locale')
splitted_key = key.split(config.get('namespace_delimiter'))
namespace = splitted_key[:-1]
if not namespace and '{namespace}' not in config.get('filename_format'):
for directory in config.get('load_path'):
load_directory(directory, locale)
else:
for directory in config.get('load_path'):
recursive_search_dir(namespace, '', directory, locale)
def recursive_search_dir(splitted_namespace, directory, root_dir, locale):
namespace = splitted_namespace[0] if splitted_namespace else ""
seeked_file = config.get('filename_format').format(namespace=namespace, format=config.get('file_format'), locale=locale)
dir_content = os.listdir(os.path.join(root_dir, directory))
if seeked_file in dir_content:
load_translation_file(os.path.join(directory, seeked_file), root_dir, locale)
elif namespace in dir_content:
recursive_search_dir(splitted_namespace[1:], os.path.join(directory, namespace), root_dir, locale)

View file

@ -1,28 +0,0 @@
from . import config
container = {}
def add(key, value, locale=None):
if locale is None:
locale = config.get('locale')
container.setdefault(locale, {})[key] = value
def has(key, locale=None):
if locale is None:
locale = config.get('locale')
return key in container.get(locale, {})
def get(key, locale=None):
if locale is None:
locale = config.get('locale')
return container[locale][key]
def clear(locale=None):
if locale is None:
container.clear()
elif locale in container:
container[locale].clear()

View file

@ -1,115 +0,0 @@
from string import Template
from . import config
from . import resource_loader
from . import translations
from .custom_functions import get_function
class TranslationFormatter(Template, dict):
delimiter = config.get('placeholder_delimiter')
idpattern = r"""
\w+ # name
(
\(
[^\(\){}]* # arguments
\)
)?
"""
def __init__(self, translation_key, template):
super(TranslationFormatter, self).__init__(template)
self.translation_key = translation_key
def format(self, locale, **kwargs):
self.clear()
self.update(kwargs)
self.locale = locale
if config.get('on_missing_placeholder'):
return self.substitute(self)
else:
return self.safe_substitute(self)
def __getitem__(self, key: str):
try:
name, _, args = key.partition("(")
if args:
f = get_function(name, self.locale)
if f:
i = f(**self)
args = args.strip(')').split(config.get('argument_delimiter'))
try:
return args[i]
except (IndexError, TypeError) as e:
raise ValueError(
"No argument {0!r} for function {1!r} (in {2!r})"
.format(i, name, self.template)
) from e
raise KeyError(
"No function {0!r} found for locale {1!r} (in {2!r})"
.format(name, self.locale, self.template)
)
return super().__getitem__(key)
except KeyError:
on_missing = config.get('on_missing_placeholder')
if not on_missing or on_missing == "error":
raise
return on_missing(self.translation_key, self.locale, self.template, key)
def t(key, **kwargs):
locale = kwargs.pop('locale', config.get('locale'))
if translations.has(key, locale):
return translate(key, locale=locale, **kwargs)
else:
resource_loader.search_translation(key, locale)
if translations.has(key, locale):
return translate(key, locale=locale, **kwargs)
elif locale != config.get('fallback'):
return t(key, locale=config.get('fallback'), **kwargs)
if 'default' in kwargs:
return kwargs['default']
on_missing = config.get('on_missing_translation')
if on_missing == "error":
raise KeyError('key {0} not found'.format(key))
elif on_missing:
return on_missing(key, locale, **kwargs)
else:
return key
def translate(key, **kwargs):
locale = kwargs.pop('locale', config.get('locale'))
translation = translations.get(key, locale=locale)
if 'count' in kwargs:
translation = pluralize(key, locale, translation, kwargs['count'])
return TranslationFormatter(key, translation).format(locale, **kwargs)
def pluralize(key, locale, translation, count):
return_value = key
try:
if type(translation) != dict:
return_value = translation
raise KeyError('use of count witouth dict for key {0}'.format(key))
if count == 0:
if 'zero' in translation:
return translation['zero']
elif count == 1:
if 'one' in translation:
return translation['one']
elif count <= config.get('plural_few'):
if 'few' in translation:
return translation['few']
if 'many' in translation:
return translation['many']
else:
raise KeyError('"many" not defined for key {0}'.format(key))
except KeyError:
on_missing = config.get('on_missing_plural')
if on_missing == "error":
raise
elif on_missing:
return on_missing(key, locale, translation, count)
else:
return return_value

View file

@ -12,7 +12,7 @@ pysimplegui>=4.6.0
requests
ruamel.yaml
psutil
pyyaml # Required for python-i18n which does not work with ruamel.yaml
pyyaml # Required for python-i18n / i18nice which does not work with ruamel.yaml
# python-i18n
# Replaced python-i18n with a fork that prevents boolean keys from being interpreted
# Also fixes some portability issues (still boolean key name issues) encountered when compiling on Centos 7 and executing on Almalinux 9
@ -20,5 +20,6 @@ pyyaml # Required for python-i18n which does not work with ruamel.yaml
# python-i18n @ git+https://github.com/Krutyi-4el/python-i18n.git@0.6.0#8999a0d380be8a08beed785e46fbb31dfc03c605
# Since PyPI and twine don't allow usage of direct references (git addresses)
# we'll use an inline version for now
i18nice>=0.6.2
packaging
pywin32; platform_system == "Windows"