diff options
-rw-r--r-- | bin/web_kbn.py | 57 | ||||
-rw-r--r-- | include/py/homekit/config/__init__.py | 1 | ||||
-rw-r--r-- | include/py/homekit/config/config.py | 39 | ||||
-rw-r--r-- | web/kbn_assets/app.js | 9 | ||||
-rw-r--r-- | web/kbn_templates/base.j2 | 2 | ||||
-rw-r--r-- | web/kbn_templates/index.j2 | 7 | ||||
-rw-r--r-- | web/kbn_templates/modem_data.j2 | 10 | ||||
-rw-r--r-- | web/kbn_templates/modem_verbose.j2 | 2 | ||||
-rw-r--r-- | web/kbn_templates/modems.j2 | 2 | ||||
-rw-r--r-- | web/kbn_templates/sms.j2 | 8 |
10 files changed, 102 insertions, 35 deletions
diff --git a/bin/web_kbn.py b/bin/web_kbn.py index 18b50ad..21ae2ef 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -14,7 +14,8 @@ from io import StringIO from aiohttp import web from typing import Optional, Union from urllib.parse import quote_plus -from homekit.config import config, AppConfigUnit, is_development_mode, Translation +from contextvars import ContextVar +from homekit.config import config, AppConfigUnit, is_development_mode, Translation, Language from homekit.camera import IpcamConfig from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string, json_serial, validate_ipv4 from homekit.modem import E3372, ModemsConfig, MacroNetWorkType @@ -45,10 +46,10 @@ common_static_files = [ 'app.js', 'app.css' ] -static_version = 3 +static_version = 4 routes = web.RouteTableDef() -webkbn_strings = Translation('web_kbn') logger = logging.getLogger(__name__) +lang_context_var = ContextVar('lang', default=Translation.DEFAULT_LANGUAGE) def get_js_link(file, version=static_version) -> str: @@ -201,8 +202,29 @@ def get_current_upstream() -> str: return upstream -def lang(key: str): - return webkbn_strings.get()[key] +def get_preferred_lang(req: web.Request) -> Language: + lang_cookie = req.cookies.get('lang', None) + if lang_cookie is None: + return Translation.DEFAULT_LANGUAGE + try: + return Language(lang_cookie) + except ValueError: + logger.debug(f"unsupported lang_cookie value: {lang_cookie}") + return Translation.DEFAULT_LANGUAGE + + +@web.middleware +async def language_middleware(request, handler): + lang_context_var.set(get_preferred_lang(request)) + return await handler(request) + + +def lang(key, unit='web_kbn'): + strings = Translation(unit) + if isinstance(key, str) and '.' in key: + return strings.get(lang_context_var.get()).get(key) + else: + return strings.get(lang_context_var.get())[key] async def render(req: web.Request, @@ -214,7 +236,8 @@ async def render(req: web.Request, context = {} context = { **context, - 'head_static': get_head_static(assets) + 'head_static': get_head_static(assets), + 'user_lang': lang_context_var.get().value } if title is not None: context['title'] = title @@ -231,6 +254,8 @@ async def index(req: web.Request): cc = IpcamConfig() ctx['camzones'] = cc['zones'].keys() ctx['allcams'] = cc.get_all_cam_names() + ctx['lang_enum'] = Language + ctx['lang_selected'] = lang_context_var.get() return await render(req, 'index', title=lang('sitename'), @@ -240,7 +265,7 @@ async def index(req: web.Request): @routes.get('/modems.cgi') async def modems(req: web.Request): return await render(req, 'modems', - title='Состояние модемов', + title=lang('modem_statuses'), context=dict(modems=ModemsConfig())) @@ -280,9 +305,9 @@ async def modems_verbose(req: web.Request): ['Dialup connection', dialup_conn] ] - modem_name = ModemsConfig().getfullname(modem) + modem_name = Translation('modems').get(lang_context_var.get())[modem]['full'] return await render(req, 'modem_verbose', - title=f'Подробная информация о модеме "{modem_name}"', + title=lang('modem_verbose_info_about_modem') % (modem_name,), context=dict(data=data, modem_name=modem_name)) @@ -296,7 +321,7 @@ async def sms(req: web.Request): cl = get_modem_client(ModemsConfig()[modem]) messages = cl.sms_list(1, 20, is_outbox) return await render(req, 'sms', - title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})", + title=lang('sms_page_title') % (lang('sms_outbox') if is_outbox else lang('sms_inbox'), modem), context=dict( modems=ModemsConfig(), selected_modem=modem, @@ -512,6 +537,7 @@ async def routing_dhcp(req: web.Request): def init_web_app(app: web.Application): + app.middlewares.append(language_middleware) aiohttp_jinja2.setup( app, loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')), @@ -519,12 +545,11 @@ def init_web_app(app: web.Application): ) env = aiohttp_jinja2.get_env(app) - def filter_lang(key, unit='web_kbn'): - strings = Translation(unit) - if isinstance(key, str) and '.' in key: - return strings.get().get(key) - else: - return strings.get()[key] + # @pass_context is used only to prevent jinja2 from caching the result of lang filter results of constant values. + # as of now i don't know a better way of doing it + @jinja2.pass_context + def filter_lang(ctx, key, unit='web_kbn'): + return lang(key, unit) env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'), default=json_serial) env.filters['lang'] = filter_lang diff --git a/include/py/homekit/config/__init__.py b/include/py/homekit/config/__init__.py index 8fedfa6..6323697 100644 --- a/include/py/homekit/config/__init__.py +++ b/include/py/homekit/config/__init__.py @@ -3,6 +3,7 @@ from .config import ( ConfigUnit, AppConfigUnit, Translation, + Language, config, is_development_mode, setup_logging, diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 1eec97d..c5cc415 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -11,7 +11,6 @@ from argparse import ArgumentParser from enum import Enum, auto from os.path import join, isdir, isfile from ..util import Addr -from pprint import pprint class MyValidator(cerberus.Validator): @@ -55,6 +54,7 @@ class BaseConfigUnit(ABC): return key in self._data def load_from(self, path: str): + print(f'loading config from {path}') with open(path, 'r') as fd: self._data = yaml.safe_load(fd) if self._data is None: @@ -93,6 +93,7 @@ class ConfigUnit(BaseConfigUnit): NAME = 'dumb' _instance = None + __initialized: bool def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -101,9 +102,14 @@ class ConfigUnit(BaseConfigUnit): def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super(ConfigUnit, cls).__new__(cls, *args, **kwargs) + if cls._instance is not None: + cls._instance.__initialized = False return cls._instance def __init__(self, name=None, load=True): + if self.__initialized: + return + super().__init__() self._data = {} @@ -116,6 +122,8 @@ class ConfigUnit(BaseConfigUnit): elif name is not None: self.NAME = name + self.__initialized = True + @classmethod def get_config_path(cls, name=None) -> str: if name is None: @@ -262,6 +270,17 @@ class AppConfigUnit(ConfigUnit): return self._logging_verbose +class Language(Enum): + RU = 'ru' + EN = 'en' + + def name(self): + if self == Language.RU: + return 'Русский' + elif self == Language.EN: + return 'English' + + class TranslationUnit(BaseConfigUnit): pass @@ -270,10 +289,10 @@ TranslationInstances = {} class Translation: - LANGUAGES = ('en', 'ru') - DEFAULT_LANGUAGE = 'ru' + DEFAULT_LANGUAGE = Language.RU - _langs: dict[str, TranslationUnit] + _langs: dict[Language, TranslationUnit] + __initialized: bool # def __init_subclass__(cls, **kwargs): # super().__init_subclass__(**kwargs) @@ -283,14 +302,18 @@ class Translation: unit = args[0] if unit not in TranslationInstances: TranslationInstances[unit] = super(Translation, cls).__new__(cls) + TranslationInstances[unit].__initialized = False return TranslationInstances[unit] def __init__(self, name: str): + if self.__initialized: + return + self._langs = {} - for lang in self.LANGUAGES: + for lang in Language: for dirname in CONFIG_DIRECTORIES: if isdir(dirname): - filename = join(dirname, f'i18n-{lang}', f'{name}.yaml') + filename = join(dirname, f'i18n-{lang.value}', f'{name}.yaml') if lang in self._langs: raise RuntimeError(f'{name}: translation unit for lang \'{lang}\' already loaded') self._langs[lang] = TranslationUnit() @@ -301,7 +324,9 @@ class Translation: if len(diff) > 0: raise RuntimeError(f'{name}: translation units have difference in keys: ' + ', '.join(diff)) - def get(self, lang: str = DEFAULT_LANGUAGE) -> TranslationUnit: + self.__initialized = True + + def get(self, lang: Language = DEFAULT_LANGUAGE) -> TranslationUnit: return self._langs[lang] diff --git a/web/kbn_assets/app.js b/web/kbn_assets/app.js index 86504cd..0be801d 100644 --- a/web/kbn_assets/app.js +++ b/web/kbn_assets/app.js @@ -102,6 +102,15 @@ function removeClass(el, name) { } function indexInit() { + // language selector + var langSelect = document.getElementById('lang'); + langSelect.addEventListener('change', function() { + var selectedLang = this.value; + document.cookie = "lang=" + selectedLang + ";path=/"; + window.location.reload(); + }); + + // camera blocks var blocks = ['zones', 'list']; for (var i = 0; i < blocks.length; i++) { var button = ge('cam_'+blocks[i]+'_btn'); diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2 index 709d838..2ecb199 100644 --- a/web/kbn_templates/base.j2 +++ b/web/kbn_templates/base.j2 @@ -18,7 +18,7 @@ {% endmacro %} <!doctype html> -<html data-bs-theme="auto"> +<html lang="{{ user_lang }}" data-bs-theme="auto"> <head> <title>{{ title }}</title> <meta http-equiv="content-type" content="text/html; charset=utf-8"> diff --git a/web/kbn_templates/index.j2 b/web/kbn_templates/index.j2 index 0a7803b..7f69823 100644 --- a/web/kbn_templates/index.j2 +++ b/web/kbn_templates/index.j2 @@ -2,6 +2,13 @@ {% block content %} <div class="container py-4"> + <div style="float: right"> + <select name="lang" id="lang"> + {% for lang in lang_enum %} + <option value="{{ lang.value }}"{% if lang_selected == lang %} selected="selected"{% endif %}>{{ lang.name() }}</option> + {% endfor %} + </select> + </div> <nav aria-label="breadcrumb"> <ol class="breadcrumb"> <li class="breadcrumb-item active" aria-current="page">{{ "main"|lang }}</li> diff --git a/web/kbn_templates/modem_data.j2 b/web/kbn_templates/modem_data.j2 index 7f97b77..f96a197 100644 --- a/web/kbn_templates/modem_data.j2 +++ b/web/kbn_templates/modem_data.j2 @@ -1,13 +1,13 @@ {% with level=modem_data.level %} - <span class="text-secondary">Сигнал:</span> {% include 'signal_level.j2' %}<br> + <span class="text-secondary">{{ "modem_signal"|lang }}:</span> {% include 'signal_level.j2' %}<br> {% endwith %} -<span class="text-secondary">Тип сети:</span> <b>{{ modem_data.type }}</b><br> +<span class="text-secondary">{{ "modem_network_type"|lang }}:</span> <b>{{ modem_data.type }}</b><br> <span class="text-secondary">RSSI:</span> {{ modem_data.rssi }}<br/> {% if modem_data.sinr %} <span class="text-secondary">SINR:</span> {{ modem_data.sinr }}<br/> {% endif %} -<span class="text-secondary">Время соединения:</span> {{ modem_data.connected_time }}<br> -<span class="text-secondary">Принято/передано:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }} +<span class="text-secondary">{{ "modem_connection_time"|lang }}:</span> {{ modem_data.connected_time }}<br> +<span class="text-secondary">{{ "modem_tx_rx"|lang }}:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }} <br> -<a href="/modems/verbose.cgi?id={{ modem }}">Подробная информация</a> +<a href="/modems/verbose.cgi?id={{ modem }}">{{ "modem_verbose_info"|lang }}</a> diff --git a/web/kbn_templates/modem_verbose.j2 b/web/kbn_templates/modem_verbose.j2 index 7c6c930..4514451 100644 --- a/web/kbn_templates/modem_verbose.j2 +++ b/web/kbn_templates/modem_verbose.j2 @@ -2,7 +2,7 @@ {% block content %} {{ breadcrumbs([ - {'link': '/modems.cgi', 'text': "Модемы"}, + {'link': '/modems.cgi', 'text': 'modems'|lang}, {'text': modem_name} ]) }} diff --git a/web/kbn_templates/modems.j2 b/web/kbn_templates/modems.j2 index a8a420d..fee1dd3 100644 --- a/web/kbn_templates/modems.j2 +++ b/web/kbn_templates/modems.j2 @@ -4,7 +4,7 @@ {{ breadcrumbs([{'text': 'modems'|lang}]) }} {% for modem in modems %} -<h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modems.getfullname(modem) }}</h6> +<h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ (modem|lang('modems'))['full'] }}</h6> <div id="modem_data_{{ modem }}"> {% include "loading.j2" %} </div> diff --git a/web/kbn_templates/sms.j2 b/web/kbn_templates/sms.j2 index cd91610..41be6fc 100644 --- a/web/kbn_templates/sms.j2 +++ b/web/kbn_templates/sms.j2 @@ -7,7 +7,7 @@ <div class="nav nav-tabs" id="nav-tab"> {% for modem in modems.keys() %} {% if selected_modem != modem %}<a href="/sms.cgi?id={{ modem }}" class="text-decoration-none">{% endif %} - <button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ modems.getshortname(modem) }}</button> + <button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ (modem|lang('modems'))['short'] }}</button> {% if selected_modem != modem %}</a>{% endif %} {% endfor %} </div> @@ -43,11 +43,11 @@ </div> <h6 class="text-primary mt-4"> - Последние + {{ "sms_latest"|lang }} {% if not is_outbox %} - <b>входящие</b> <span class="text-black-50">|</span> <a href="/sms.cgi?id={{ selected_modem }}&outbox=1">исходящие</a> + <b>{{ "sms_inbox"|lang }}</b> <span class="text-black-50">|</span> <a href="/sms.cgi?id={{ selected_modem }}&outbox=1">{{ "sms_outbox"|lang }}</a> {% else %} - <a href="/sms.cgi?id={{ selected_modem }}">входящие</a> <span class="text-black-50">|</span> <b>исходящие</b> + <a href="/sms.cgi?id={{ selected_modem }}">{{ "sms_inbox"|lang }}</a> <span class="text-black-50">|</span> <b>{{ "sms_outbox"|lang }}</b> {% endif %} </h6> |