user language support, other important fixes
This commit is contained in:
parent
838b01c548
commit
840cbe4729
@ -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
|
||||
|
@ -3,6 +3,7 @@ from .config import (
|
||||
ConfigUnit,
|
||||
AppConfigUnit,
|
||||
Translation,
|
||||
Language,
|
||||
config,
|
||||
is_development_mode,
|
||||
setup_logging,
|
||||
|
@ -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]
|
||||
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
{{ breadcrumbs([
|
||||
{'link': '/modems.cgi', 'text': "Модемы"},
|
||||
{'link': '/modems.cgi', 'text': 'modems'|lang},
|
||||
{'text': modem_name}
|
||||
]) }}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user