mirror of
https://github.com/netinvent/npbackup.git
synced 2025-10-29 23:06:17 +08:00
Switched to new i18nice package
See https://github.com/Krutyi-4el/i18nice/issues/3
This commit is contained in:
parent
bb266eb158
commit
202edb4def
12 changed files with 2 additions and 481 deletions
|
|
@ -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')
|
||||
|
|
@ -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]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
Loading…
Add table
Reference in a new issue