#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
# Copyright 2019- Martin Sinn m.sinn@gmx.de
#########################################################################
# This file is part of SmartHomeNG
#
# SmartHomeNG is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SmartHomeNG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SmartHomeNG If not, see <http://www.gnu.org/licenses/>.
#########################################################################
"""
This library implements the multi language support of SmartHomeNG.
It is used in
- lib.module
- lib.shtime
- lib.model.module
- lib.model.smartplugin
The shortcut _(...) for translate(...) is defined in class SmartPluginWebIf()
SmartPluginWebIf() which is defined in lib.model.smartplugin.py
If is a replacement when rendering templates using Jinja2:
tplenv.globals['_'] = self.translate
"""
import logging
import os
import inspect
from lib.constants import (YAML_FILE)
import lib.shyaml as shyaml
logger = logging.getLogger(__name__)
_base_dir = ''
_default_language = ''
_fallback_language_order = ''
_global_translations = {}
_translations = {}
_translation_files = {}
[Doku]def initialize_translations(base_dir, default_language, fallback_language_order):
"""
Initialize the multi-language support
:param base_dir: Base directory of SmartHomeNG
:param default_language: language to be used for translations: 'de', 'en', 'fr', ...
:param fallback_language_order: string with the fallback langauges (komma seperated)
"""
global _base_dir
_base_dir = base_dir
set_default_language(default_language)
set_fallback_language_order(fallback_language_order)
load_translations('global', from_dir='bin', translation_id='global')
return
[Doku]def set_default_language(language):
"""
Set language to be used for translations
:param language: language to be used for translations: 'de', 'en', 'fr', ...
"""
global _default_language
_default_language = language.lower()
logger.debug("Default language set to '{}'".format(_default_language))
return
[Doku]def set_fallback_language_order(language_order):
"""
Set fallback languages and their order
Fallback languages are used, if a translation for the selected default_language is not available
:param language_order: string with the fallback langauges (komma seperated)
"""
global _fallback_language_order
_fallback_language_order = language_order.lower().split(',')
logger.debug("Fallback language order set to '{}'".format(_fallback_language_order))
return
[Doku]def load_translations(translation_type='global', from_dir='bin', translation_id='global'):
"""
Load global or plugin-specific translations from a locale.yaml file
:param translation_type: 'global' or 'plugin'
:param from_dir: 'bin' (for global) or 'plugins/<plugin name>'
:return: loaded translations as s dict
"""
global _translations
trans = {}
relative_filename = os.path.join(from_dir, 'locale' + YAML_FILE)
filename = os.path.join(_base_dir, relative_filename)
trans_dict = shyaml.yaml_load(filename, ordered=False, ignore_notfound=True)
if trans_dict != None:
logger.info(f"load_translations: translation_id={translation_id} from {relative_filename}")
if translation_type == 'global':
for translation_section in trans_dict.keys():
if translation_section.endswith('_translations'):
trans_id = translation_section.split('_')[0].replace('.', '/')
trans = trans_dict.get(translation_section, {})
_translations[trans_id] = trans
#if translation_id == 'global':
# _translations[trans_id] = trans
#else:
# _translations[trans_id].update(trans)
logger.info(f"Loading {translation_type} translations (id={trans_id}) from {relative_filename}")
logger.debug(" - translations = {}".format(trans))
else:
trans = trans_dict.get(translation_type+'_translations', {})
#logger.info(f"Loading {relative_filename} translations (id={translation_id}) from {relative_filename}")
if _translations.get(translation_id, None) is not None:
logger.dbghigh(f"Duplicate identifier '{translation_id}' used for translation_type '{translation_type}' to load from '{from_dir}' - translations not loaded")
return trans
_translations[translation_id] = trans
logger.debug(" - translations = {}".format(trans))
_translation_files[translation_id] = {}
_translation_files[translation_id]['type'] = translation_type
_translation_files[translation_id]['filename'] = filename
return trans
[Doku]def reload_translations():
"""
Reload translations for existing translation_ids - to test new translations without having to restart SmartHomeNG
"""
logger.notice("Reloading translations")
for id in _translation_files:
translation_type = _translation_files[id]['type']
filename = _translation_files[id]['filename']
trans_dict = shyaml.yaml_load(filename, ordered=False, ignore_notfound=True)
if trans_dict != None:
if translation_type == 'global':
for translation_section in trans_dict.keys():
if translation_section.endswith('_translations'):
id = translation_section.split('_')[0].replace('.', '/')
trans = trans_dict.get(translation_section, {})
logger.info(f"Reloading {translation_type} translations (id={id}) from {filename}")
_translations[id] = trans
else:
trans = trans_dict.get(translation_type+'_translations', {})
logger.info(f"Reloading {translation_type} translations (id={id}) from {filename}")
_translations[id] = trans
return True
def _get_translation(translation_lang, txt, plugin_translations=None, module_translations=None, additional_translations=None, log_missing=False):
"""
Returns translated text from for a specified language from plugin_translations or global_translations
:param translation_lang: Language to be used for translation
:param txt: Text to be translated
:param plugin_translations: Additional translation definitions
:return: translated text or '' if translation is not found
"""
translations = {}
translationtype_to_log = ''
translationinfo_to_log = ''
if plugin_translations is not None:
if plugin_translations in _translations.keys():
translations = _translations[plugin_translations].get(txt, {})
else:
logger.warning(f"Trying to use undefined plugin_translations '{plugin_translations}' (plugin has no locale.yaml)")
if translations == {} and additional_translations is not None:
if additional_translations in _translations.keys():
translations = _translations[additional_translations].get(txt, {})
else:
logger.warning(f"Trying to use undefined additional_translations '{additional_translations}'")
if translations == {} and module_translations is not None:
if module_translations in _translations.keys():
translations = _translations[module_translations].get(txt, {})
else:
logger.info(f"Trying to use undefined module_translations '{module_translations}' (module has no locale.yaml)")
if translations == {}:
if 'global' in _translations.keys():
translations = _translations['global'].get(txt, {})
if translations == {}:
if log_missing and txt != '':
logger.info(f"No translation for '{txt}' found in global (bin), plugin ({plugin_translations}), module ({module_translations}), additional ({additional_translations})")
else:
logger.error("Global translations not loaded")
else:
logger.debug(f"Using additional_translations for text '{txt}' = {translations}")
return translations.get(translation_lang, None)
[Doku]def translate(txt, vars=None, plugin_translations=None, module_translations=None, additional_translations=None):
"""
Returns translated text
:param txt: TEXT TO TRANSLATE
:param vars: dict with variables to replace in the translated text
:param plugin_translations: ID for additional translations (if None, only global translations are used)
:param module_translations: ID for additional translations (if None, only global and plugin translations are used)
:return: Translated text
"""
global _fallback_language_order
txt = str(txt)
translated_txt = _get_translation(_default_language, txt, plugin_translations=plugin_translations, module_translations=module_translations, additional_translations=additional_translations, log_missing=True)
if translated_txt is None:
logger.dbghigh(f"Translation of '{txt}' to language '{_default_language}' not found -> using fallback languages")
if len(_fallback_language_order) > 0:
for fallback_language in _fallback_language_order:
translated_txt = _get_translation(fallback_language, txt, plugin_translations=plugin_translations, module_translations=module_translations, additional_translations=additional_translations)
if translated_txt is None:
logger.debug(" - No translation found for fallback_language '{}'".format(fallback_language))
else:
break
if translated_txt is None:
translated_txt = txt
if txt != '':
logger.dbghigh(f" - No translation found for '{txt}' -> using original text")
if translated_txt == '=':
translated_txt = txt
logger.debug("Translation '{}' to '{}' -> '{}'".format(txt, _default_language, translated_txt))
# if variable parameters are given, replace them in the translated text
if vars is not None:
if isinstance(vars, dict):
logger.info(f"translate: Trying to use parameters {vars} for string '{translated_txt}'")
try:
translated_txt = translated_txt.format(**vars)
except Exception as e:
logger.error(f"translate: Could not fill in variables {vars}. Exception: {e}")
else:
logger.error(f"translate: Invalid vars for string {txt} -> vars must be a dict, not {type(vars)} '{vars}' (for text '{txt}')")
return translated_txt