From 2a87e9b6a564b89dc17574f69ecefa2dcd6714b4 Mon Sep 17 00:00:00 2001 From: Emerson Rocha Date: Fri, 2 Sep 2022 07:45:53 -0300 Subject: [PATCH] i18n (#3): TranslationLoader() created --- .github/requirements.txt | 2 +- .github/workflows/main.yml | 6 +- i18n/en/dir3/file3.json | 0 i18n/en/file2.yml | 1 + i18n/pt/file2.yml | 1 + scripts/readme-from-csv.py | 200 +++++++++++++++++++++++++++++-------- 6 files changed, 163 insertions(+), 47 deletions(-) create mode 100644 i18n/en/dir3/file3.json create mode 100644 i18n/en/file2.yml create mode 100644 i18n/pt/file2.yml diff --git a/.github/requirements.txt b/.github/requirements.txt index 5a2f010..0ec3741 100644 --- a/.github/requirements.txt +++ b/.github/requirements.txt @@ -1,6 +1,6 @@ frictionless chevron - +pyyaml # Template engine python-liquid # Used by i18n diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e57b741..a0037bd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Unlicense OR 0BSD -name: "aN1603_16" +name: "main" on: push: @@ -32,8 +32,8 @@ on: # working-directory: ./officina jobs: - "aN1603_16": - name: aN1603_16 + "main": + name: main runs-on: ubuntu-latest continue-on-error: true steps: diff --git a/i18n/en/dir3/file3.json b/i18n/en/dir3/file3.json new file mode 100644 index 0000000..e69de29 diff --git a/i18n/en/file2.yml b/i18n/en/file2.yml new file mode 100644 index 0000000..0cd5e32 --- /dev/null +++ b/i18n/en/file2.yml @@ -0,0 +1 @@ +file_2_var: en file 2 value \ No newline at end of file diff --git a/i18n/pt/file2.yml b/i18n/pt/file2.yml new file mode 100644 index 0000000..a1ff5a7 --- /dev/null +++ b/i18n/pt/file2.yml @@ -0,0 +1 @@ +file_2_var: pt file 2 value \ No newline at end of file diff --git a/scripts/readme-from-csv.py b/scripts/readme-from-csv.py index a5bd54b..e5c1a06 100755 --- a/scripts/readme-from-csv.py +++ b/scripts/readme-from-csv.py @@ -11,7 +11,9 @@ # OPTIONS: --- # # REQUIREMENTS: - python3 -# - chevron (pip install chevron) +# - pyyaml +# - python-liquid +# - python-liquid-extra # BUGS: --- # NOTES: --- # AUTHOR: Emerson Rocha @@ -24,6 +26,8 @@ # =============================================================================== +import yaml +import fnmatch import glob import os import argparse @@ -166,7 +170,8 @@ def make_args(self, hxl_output=True): dest='line_formatter', nargs='?', # required=True - default='{{.}}' + # default='{{.}}' + default=None ) # parser_table.add_argument( @@ -612,10 +617,10 @@ def print(self): self.line_formatter, line_variables).replace('\\n', "\n")) - # print(self.liquid.render( - # self.line_formatter, - # line_variables - # )) + print(self.liquid.render( + self.line_formatter, + line_variables + )) if self.group_suffix: print(self.group_suffix) @@ -726,55 +731,40 @@ class LiquidRenderer: @see https://jg-rp.github.io/liquid/extra/filters#t-translate """ - default_template: Type['Template'] = None env: Environment + i18n_data: dict = None + objective_bcp47: str = None + fallback_bcp47: str = None - def __init__(self) -> None: + def __init__( + self, + objective_bcp47: str = 'en', + fallback_bcp47: str = 'en', + ) -> None: - self.default_template = Template( - "Hello, {{ you }}!", - # tolerance=Mode.STRICT, - # undefined=StrictUndefined, - ) + self.objective_bcp47 = objective_bcp47 + self.fallback_bcp47 = fallback_bcp47 # pass # self.env = Environment() # self.env.add_filter("json", filters.JSON()) + globals = {'locale': self.objective_bcp47} self.env = Environment( tolerance=Mode.STRICT, - undefined=StrictUndefined, + # undefined=StrictUndefined, # loader=FileSystemLoader("./templates/"), + globals=globals ) self.env.add_filter("json", filters.JSON()) + tloader = TranslationLoader( + objective_bcp47=self.objective_bcp47, + fallback_bcp47=self.fallback_bcp47 + ) + self.i18n_data = tloader.get_data() + # print('self.i18n_data ', self.i18n_data) - some_locales = { - "default": { - "layout": { - "greeting": r"Hello {{ name }}", - }, - "cart": { - "general": { - "title": "Shopping Basket", - }, - }, - "pagination": { - "next": "Next Page", - }, - }, - "de": { - "layout": { - "greeting": r"Hallo {{ name }}", - }, - "cart": { - "general": { - "title": "Warenkorb", - }, - }, - "pagination": { - "next": "Nächste Seite", - }, - }, - } - self.env.add_filter(Translate.name, Translate(locales=some_locales)) + # raise NotImplementedError + + self.env.add_filter(Translate.name, Translate(locales=self.i18n_data)) def render(self, template: str = None, context: dict = None) -> str: @@ -799,10 +789,134 @@ def render(self, template: str = None, context: dict = None) -> str: compiled_template = self.env.from_string(template) extra_context = context + if 'locale' not in context: + context['locale'] = self.objective_bcp47 + result = compiled_template.render(extra_context) return result +class TranslationLoader: + """ TranslationLoader abstract load translation files from disk into memory + """ + + data: dict = { + 'default': { + 'error': 'translations not loaded' + } + } + + def __init__( + self, + objective_bcp47: str = 'pt', + fallback_bcp47: str = 'en', + base_namespace: str = 'translation', + locales_base: list = None, + file_extensions: list = None + ) -> None: + self.objective_bcp47 = objective_bcp47 + self.fallback_bcp47 = fallback_bcp47 + self.base_namespace = base_namespace + if not locales_base: + # public/locales/{lng}/translation.json + locales_base = [ + 'i18n', + 'locales', + ] + if not file_extensions: + # public/locales/{lng}/translation.json + file_extensions = [ + 'yml', + 'yaml', + 'json', + ] + + self.locales_base = locales_base + self.file_extensions = file_extensions + self.data[objective_bcp47] = { + 'error': 'no single translation file loaded' + } + self.data[fallback_bcp47] = { + 'error': 'no single translation file loaded' + } + self.load_all_from_disk() + + def load_all_from_disk(self): + """load_all_from_disk load only files targeted for use now + + """ + _base = None + for attempted_bases in self.locales_base: + if exists(attempted_bases): + _base = attempted_bases + break + if not _base: + # Stop if directory does not exist + return None + file_extensions = ','.join(self.file_extensions) + file_pattern = f'{_base}/*/*.{{{file_extensions}}}' + + matches = [] + current_lang = None + current_namespace = None + for root, _dirnames, filenames in os.walk(_base): + for filename in fnmatch.filter(filenames, '*.*'): + root_parts = root.split('/') + if len(root_parts) != 2: + # we expect something like 'i18n/en' not 'i18n/en/subdir' + # for now. + continue + current_lang = root_parts[1] + if current_lang not in [self.objective_bcp47, + self.fallback_bcp47, 'default']: + # Also skiping languages user do not specified + continue + filename_parts = filename.split('.') + current_namespace = filename_parts[0] + loaded_data = self.load_file(os.path.join(root, filename)) + # _data_now = { + # current_namespace: loaded_data + # } + if current_namespace == self.base_namespace: + _data_now = loaded_data + else: + _data_now = { + current_namespace: loaded_data + } + + self.data[current_lang] = { + **self.data[current_lang], + **_data_now + } + if 'error' in self.data[current_lang]: + del self.data[current_lang]['error'] + if current_lang == self.fallback_bcp47: + # self.data['default'] = { + # current_namespace: loaded_data + # } + self.data['default'] = { + **self.data['default'], + **_data_now + } + if 'error' in self.data['default']: + del self.data['default']['error'] + + def load_file(self, file: str) -> dict: + """load_file + + Args: + file (str): path to file + + Returns: + dict: return content of file as python dictionary + """ + with open(file) as _file: + data = yaml.load(_file, Loader=yaml.SafeLoader) + return data + + def get_data(self): + return self.data + # lrenderer = LiquidRenderer() # print(lrenderer.render("Hello, {{ you }}!", {'you': 'you value'}))