初步的 kvmd 国际化(汉化)支持

1. 添加汉化文件
2. 添加 Lanuages().gettext 函数处理字符替换
3. 修改相关字符串调用
This commit is contained in:
mofeng-git 2024-08-06 21:45:16 +08:00
parent 6a966af5fb
commit 20927c7226
34 changed files with 999 additions and 113 deletions

1
babel.cfg Normal file
View File

@ -0,0 +1 @@
[python: kvmd/**.py]

View File

@ -31,6 +31,8 @@ import pygments
import pygments.lexers.data import pygments.lexers.data
import pygments.formatters import pygments.formatters
from gettext import translation
from .. import tools from .. import tools
from ..mouse import MouseRange from ..mouse import MouseRange
@ -103,6 +105,7 @@ from ..validators.hw import valid_otg_gadget
from ..validators.hw import valid_otg_id from ..validators.hw import valid_otg_id
from ..validators.hw import valid_otg_ethernet from ..validators.hw import valid_otg_ethernet
from ..lanuages import Lanuages
# ===== # =====
def init( def init(
@ -124,15 +127,16 @@ def init(
add_help=add_help, add_help=add_help,
formatter_class=argparse.ArgumentDefaultsHelpFormatter, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
) )
_ = translation(domain="message",localedir="/kvmd/i18n",languages=["zh"]).gettext
parser.add_argument("-c", "--config", default="/etc/kvmd/main.yaml", type=valid_abs_file, parser.add_argument("-c", "--config", default="/etc/kvmd/main.yaml", type=valid_abs_file,
help="Set config file path", metavar="<file>") help=_("Set config file path"), metavar="<file>")
parser.add_argument("-o", "--set-options", default=[], nargs="+", parser.add_argument("-o", "--set-options", default=[], nargs="+",
help="Override config options list (like sec/sub/opt=value)", metavar="<k=v>",) help=_("Override config options list (like sec/sub/opt=value)"), metavar="<k=v>",)
parser.add_argument("-m", "--dump-config", action="store_true", parser.add_argument("-m", "--dump-config", action="store_true",
help="View current configuration (include all overrides)") help=_("View current configuration (include all overrides)"))
if check_run: if check_run:
parser.add_argument("--run", dest="run", action="store_true", parser.add_argument("--run", dest="run", action="store_true",
help="Run the service") help=_("Run the service"))
(options, remaining) = parser.parse_known_args(argv) (options, remaining) = parser.parse_known_args(argv)
if options.dump_config: if options.dump_config:
@ -158,10 +162,7 @@ def init(
if check_run and not options.run: if check_run and not options.run:
raise SystemExit( raise SystemExit(
"To prevent accidental startup, you must specify the --run option to start.\n" _("To prevent accidental startup, you must specify the --run option to start.\n")+_("Try the --help option to find out what this service does.\n")+_("Make sure you understand exactly what you are doing!"))
"Try the --help option to find out what this service does.\n"
"Make sure you understand exactly what you are doing!"
)
return (parser, remaining, config) return (parser, remaining, config)

View File

@ -23,6 +23,8 @@
import secrets import secrets
import pyotp import pyotp
from gettext import translation
from ...logging import get_logger from ...logging import get_logger
from ... import aiotools from ... import aiotools
@ -49,26 +51,26 @@ class AuthManager:
totp_secret_path: str, totp_secret_path: str,
) -> None: ) -> None:
_ = translation(domain="message",localedir="/kvmd/i18n",languages=["zh"]).gettext
self.__enabled = enabled self.__enabled = enabled
if not enabled: if not enabled:
get_logger().warning("AUTHORIZATION IS DISABLED") get_logger().warning(_("AUTHORIZATION IS DISABLED"))
self.__unauth_paths = frozenset(unauth_paths) # To speed up self.__unauth_paths = frozenset(unauth_paths) # To speed up
for path in self.__unauth_paths: for path in self.__unauth_paths:
get_logger().warning("Authorization is disabled for API %r", path) get_logger().warning(_("Authorization is disabled for API %r"), path)
self.__internal_service: (BaseAuthService | None) = None self.__internal_service: (BaseAuthService | None) = None
if enabled: if enabled:
self.__internal_service = get_auth_service_class(internal_type)(**internal_kwargs) self.__internal_service = get_auth_service_class(internal_type)(**internal_kwargs)
get_logger().info("Using internal auth service %r", self.__internal_service.get_plugin_name()) get_logger().info(_("Using internal auth service %r"), self.__internal_service.get_plugin_name())
self.__force_internal_users = force_internal_users self.__force_internal_users = force_internal_users
self.__external_service: (BaseAuthService | None) = None self.__external_service: (BaseAuthService | None) = None
if enabled and external_type: if enabled and external_type:
self.__external_service = get_auth_service_class(external_type)(**external_kwargs) self.__external_service = get_auth_service_class(external_type)(**external_kwargs)
get_logger().info("Using external auth service %r", self.__external_service.get_plugin_name()) get_logger().info(_("Using external auth service %r"), self.__external_service.get_plugin_name())
self.__totp_secret_path = totp_secret_path self.__totp_secret_path = totp_secret_path
@ -96,7 +98,7 @@ class AuthManager:
if secret: if secret:
code = passwd[-6:] code = passwd[-6:]
if not pyotp.TOTP(secret).verify(code): if not pyotp.TOTP(secret).verify(code):
get_logger().error("Got access denied for user %r by TOTP", user) get_logger().error(_("Got access denied for user %r by TOTP"), user)
return False return False
passwd = passwd[:-6] passwd = passwd[:-6]
@ -107,9 +109,9 @@ class AuthManager:
ok = (await service.authorize(user, passwd)) ok = (await service.authorize(user, passwd))
if ok: if ok:
get_logger().info("Authorized user %r via auth service %r", user, service.get_plugin_name()) get_logger().info(_("Authorized user %r via auth service %r"), user, service.get_plugin_name())
else: else:
get_logger().error("Got access denied for user %r from auth service %r", user, service.get_plugin_name()) get_logger().error(_("Got access denied for user %r from auth service %r"), user, service.get_plugin_name())
return ok return ok
async def login(self, user: str, passwd: str) -> (str | None): async def login(self, user: str, passwd: str) -> (str | None):
@ -119,7 +121,7 @@ class AuthManager:
if (await self.authorize(user, passwd)): if (await self.authorize(user, passwd)):
token = self.__make_new_token() token = self.__make_new_token()
self.__tokens[token] = user self.__tokens[token] = user
get_logger().info("Logged in user %r", user) get_logger().info(_("Logged in user %r"), user)
return token return token
else: else:
return None return None
@ -129,7 +131,7 @@ class AuthManager:
token = secrets.token_hex(32) token = secrets.token_hex(32)
if token not in self.__tokens: if token not in self.__tokens:
return token return token
raise AssertionError("Can't generate new unique token") raise AssertionError(_("Can't generate new unique token"))
def logout(self, token: str) -> None: def logout(self, token: str) -> None:
assert self.__enabled assert self.__enabled
@ -140,7 +142,7 @@ class AuthManager:
if r_user == user: if r_user == user:
count += 1 count += 1
del self.__tokens[r_token] del self.__tokens[r_token]
get_logger().info("Logged out user %r (%d)", user, count) get_logger().info(_("Logged out user %r (%d)"), user, count)
def check(self, token: str) -> (str | None): def check(self, token: str) -> (str | None):
assert self.__enabled assert self.__enabled

View File

@ -28,6 +28,8 @@ from typing import AsyncGenerator
import aiohttp import aiohttp
import aiohttp.multipart import aiohttp.multipart
from .lanuages import Lanuages
from . import __version__ from . import __version__
@ -58,7 +60,7 @@ def get_filename(response: aiohttp.ClientResponse) -> str:
try: try:
return os.path.basename(response.url.path) return os.path.basename(response.url.path)
except Exception: except Exception:
raise aiohttp.ClientError("Can't determine filename") raise aiohttp.ClientError(Lanuages().gettext("Can't determine filename"))
@contextlib.asynccontextmanager @contextlib.asynccontextmanager

View File

@ -52,6 +52,8 @@ from .errors import IsBusyError
from .validators import ValidatorError from .validators import ValidatorError
from .lanuages import Lanuages
from . import aiotools from . import aiotools
@ -280,6 +282,7 @@ class HttpServer:
self.__ws_bin_handlers: dict[int, Callable] = {} self.__ws_bin_handlers: dict[int, Callable] = {}
self.__ws_sessions: list[WsSession] = [] self.__ws_sessions: list[WsSession] = []
self.__ws_sessions_lock = asyncio.Lock() self.__ws_sessions_lock = asyncio.Lock()
self.gettext=Lanuages().gettext
def run( def run(
self, self,
@ -350,7 +353,7 @@ class HttpServer:
async with self.__ws_sessions_lock: async with self.__ws_sessions_lock:
self.__ws_sessions.append(ws) self.__ws_sessions.append(ws)
get_logger(2).info("Registered new client session: %s; clients now: %d", ws, len(self.__ws_sessions)) get_logger(2).info(self.gettext("Registered new client session: %s; clients now: %d"), ws, len(self.__ws_sessions))
try: try:
await self._on_ws_opened() await self._on_ws_opened()
@ -365,20 +368,20 @@ class HttpServer:
try: try:
(event_type, event) = parse_ws_event(msg.data) (event_type, event) = parse_ws_event(msg.data)
except Exception as err: except Exception as err:
logger.error("Can't parse JSON event from websocket: %r", err) logger.error(self.gettext("Can't parse JSON event from websocket: %r"), err)
else: else:
handler = self.__ws_handlers.get(event_type) handler = self.__ws_handlers.get(event_type)
if handler: if handler:
await handler(ws, event) await handler(ws, event)
else: else:
logger.error("Unknown websocket event: %r", msg.data) logger.error(self.gettext("Unknown websocket event: %r"), msg.data)
elif msg.type == WSMsgType.BINARY and len(msg.data) >= 1: elif msg.type == WSMsgType.BINARY and len(msg.data) >= 1:
handler = self.__ws_bin_handlers.get(msg.data[0]) handler = self.__ws_bin_handlers.get(msg.data[0])
if handler: if handler:
await handler(ws, msg.data[1:]) await handler(ws, msg.data[1:])
else: else:
logger.error("Unknown websocket binary event: %r", msg.data) logger.error(self.gettext("Unknown websocket binary event: %r"), msg.data)
else: else:
break break
@ -409,7 +412,7 @@ class HttpServer:
async with self.__ws_sessions_lock: async with self.__ws_sessions_lock:
try: try:
self.__ws_sessions.remove(ws) self.__ws_sessions.remove(ws)
get_logger(3).info("Removed client socket: %s; clients now: %d", ws, len(self.__ws_sessions)) get_logger(3).info(self.gettext("Removed client socket: %s; clients now: %d"), ws, len(self.__ws_sessions))
await ws.wsr.close() await ws.wsr.close()
except Exception: except Exception:
pass pass

Binary file not shown.

View File

@ -0,0 +1,399 @@
# Chinese translations for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-08-06 21:17+0800\n"
"PO-Revision-Date: 2024-08-06 21:17+0800\n"
"Last-Translator: \n"
"Language: zh\n"
"Language-Team: zh <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.15.0\n"
#: kvmd/htclient.py:63
msgid "Can't determine filename"
msgstr "无法确定文件名"
#: kvmd/inotify.py:199
#, python-format
msgid "Watching for %s"
msgstr "监视 %s"
#: kvmd/inotify.py:258
#, python-format
msgid "Unwatching %s because IGNORED was received"
msgstr "因收到忽略标识而取消监视 %s"
#: kvmd/usb.py:36
msgid "Can't find any UDC"
msgstr "未找到任何 UDC"
#: kvmd/apps/__init__.py:132
msgid "Set config file path"
msgstr "设置配置文件路径"
#: kvmd/apps/__init__.py:134
msgid "Override config options list (like sec/sub/opt=value)"
msgstr "覆盖配置文件选项列表(如 sec/sub/opt=value"
#: kvmd/apps/__init__.py:136
msgid "View current configuration (include all overrides)"
msgstr "查看当前配置(包括所有覆盖配置文件)"
#: kvmd/apps/__init__.py:139
msgid "Run the service"
msgstr "启动此服务"
#: kvmd/apps/__init__.py:165
msgid ""
"To prevent accidental startup, you must specify the --run option to "
"start.\n"
msgstr "为了防止意外启动,必须在启动时指定 --run 选项。\n"
#: kvmd/apps/__init__.py:165
msgid "Try the --help option to find out what this service does.\n"
msgstr "尝试使用 --help 选项来了解某项服务的功能。\n"
#: kvmd/apps/__init__.py:165
msgid "Make sure you understand exactly what you are doing!"
msgstr "请确定你自己在做什么!"
#: kvmd/apps/kvmd/auth.py:57
msgid "AUTHORIZATION IS DISABLED"
msgstr "身份验证服务已被禁用"
#: kvmd/apps/kvmd/auth.py:61
#, python-format
msgid "Authorization is disabled for API %r"
msgstr "由于 API %r 身份验证服务已被禁用"
#: kvmd/apps/kvmd/auth.py:66
#, python-format
msgid "Using internal auth service %r"
msgstr "使用内部身份验证服务 %r"
#: kvmd/apps/kvmd/auth.py:73
#, python-format
msgid "Using external auth service %r"
msgstr "使用外部身份验证服务 %r"
#: kvmd/apps/kvmd/auth.py:101
#, python-format
msgid "Got access denied for user %r by TOTP"
msgstr "用户 %r 被 TOTP 拒绝访问"
#: kvmd/apps/kvmd/auth.py:112
#, python-format
msgid "Authorized user %r via auth service %r"
msgstr "用户 %r 已通过身份认证服务 %r 授权"
#: kvmd/apps/kvmd/auth.py:114
#, python-format
msgid "Got access denied for user %r from auth service %r"
msgstr "身份验证服务 %r 拒绝了用户 %r 的访问请求"
#: kvmd/apps/kvmd/auth.py:124
#, python-format
msgid "Logged in user %r"
msgstr "已登录用户 %r"
#: kvmd/apps/kvmd/auth.py:134
msgid "Can't generate new unique token"
msgstr "无法生成新的唯一令牌"
#: kvmd/apps/kvmd/auth.py:145
#, python-format
msgid "Logged out user %r (%d)"
msgstr "已注销用户 %r (%d)"
#: kvmd/plugins/atx/__init__.py:45
msgid "Performing another ATX operation, please try again later"
msgstr "正在处理另一个 ATX 动作,请稍等"
#: kvmd/plugins/atx/gpio.py:209
#, python-format
msgid "Clicked ATX button %r"
msgstr "ATX 按钮 %r 被点击"
#: kvmd/plugins/auth/http.py:94
#, python-format
msgid "Failed HTTP auth request for user %r"
msgstr "用户 %r 的 HTTP 验证请求失败"
#: kvmd/plugins/auth/ldap.py:106
#, python-format
msgid "LDAP server is down: %s"
msgstr "LDAP 服务已下线: %s"
#: kvmd/plugins/auth/ldap.py:108
#, python-format
msgid "Unexpected LDAP error: %s"
msgstr "LDAP 未知错误: %s"
#: kvmd/plugins/auth/pam.py:91
#, python-format
msgid "Unallowed UID of user %r: uid=%d < allow_uids_at=%d"
msgstr "未授权 UID user %r: uid=%d < allow_uids_at=%d"
#: kvmd/plugins/auth/pam.py:97
#, python-format
msgid "Can't authorize user %r using PAM: code=%d; reason=%s"
msgstr "无法使用 PAM 验证用户 %r code=%dreason=%s"
#: kvmd/plugins/auth/radius.py:445
#, python-format
msgid "Failed RADIUS auth request for user %r"
msgstr "用户 %r 的 RADIUS 验证请求失败"
#: kvmd/plugins/hid/bt/__init__.py:137 kvmd/plugins/hid/ch9329/__init__.py:99
msgid "Starting HID daemon ..."
msgstr "正在启动 HID 守护程序......"
#: kvmd/plugins/hid/bt/__init__.py:182 kvmd/plugins/hid/ch9329/__init__.py:141
msgid "Stopping HID daemon ..."
msgstr "正在启停止 HID 守护程序......"
#: kvmd/plugins/hid/bt/__init__.py:231
msgid "Unexpected HID error"
msgstr "未知 HID 错误"
#: kvmd/plugins/hid/bt/server.py:153
#, python-format
msgid "Listening [%s]:%d for %s ..."
msgstr "监听 [%s]:%d for %s ..."
#: kvmd/plugins/hid/bt/server.py:190
#, python-format
msgid "CTL socket error on %s: %s"
msgstr "CTL 套接字错误 %s: %s"
#: kvmd/plugins/hid/bt/server.py:204
#, python-format
msgid "INT socket error on %s: %s"
msgstr "INT 套接字错误 %s: %s"
#: kvmd/plugins/hid/bt/server.py:287
#, python-format
msgid "Can't send %s report to %s: %s"
msgstr "无法向 %s 发送 %s 报告: %s"
#: kvmd/plugins/hid/bt/server.py:314
#, python-format
msgid "Can't accept %s client"
msgstr "无法接受 %s 客户端"
#: kvmd/plugins/hid/bt/server.py:319
#, python-format
msgid "Refused %s client: %s: max clients reached"
msgstr "拒绝 %s 客户端:%s已到达最大客户端数量"
#: kvmd/plugins/hid/bt/server.py:328
#, python-format
msgid "Accepted %s client: %s"
msgstr "已接受 %s 客户端: %s"
#: kvmd/plugins/hid/bt/server.py:340
#, python-format
msgid "Closed %s client %s"
msgstr "关闭 %s 客户端 %s"
#: kvmd/plugins/hid/bt/server.py:375
msgid "Publishing ..."
msgstr "广播中......"
#: kvmd/plugins/hid/bt/server.py:375
msgid "Unpublishing ..."
msgstr "取消广播......"
#: kvmd/plugins/hid/bt/server.py:379
#, python-format
msgid "Can't change public mode: %s"
msgstr "无法更改公共模式:%s"
#: kvmd/plugins/hid/bt/server.py:383
#, python-format
msgid "Unpairing %s ..."
msgstr "正在取消配对 %s ......"
#: kvmd/plugins/hid/bt/server.py:387
#, python-format
msgid "Can't unpair %s: %s"
msgstr "无法取消配对 %s: %s"
#: kvmd/plugins/hid/ch9329/__init__.py:178
#, python-format
msgid "HID : mouse output = %s"
msgstr "HID鼠标输出 = %s"
#: kvmd/plugins/hid/ch9329/__init__.py:208
msgid "Unexpected error in the run loop"
msgstr "运行循环中出现意外错误"
#: kvmd/plugins/hid/ch9329/__init__.py:231
msgid "Unexpected error in the HID loop"
msgstr "HID 循环中出现意外错误"
#: kvmd/plugins/hid/ch9329/chip.py:58
msgid "Too short response, HID might be disconnected"
msgstr "响应时间太短HID 可能已断开"
#: kvmd/plugins/hid/ch9329/chip.py:64
msgid "Invalid response checksum"
msgstr "响应校验和无效"
#: kvmd/plugins/hid/otg/device.py:125
#, python-format
msgid "Unexpected HID-%s error"
msgstr "HID-%s 意外错误"
#: kvmd/plugins/hid/otg/device.py:152
#, python-format
msgid "Stopping HID-%s daemon ..."
msgstr "清除 HID-%s 鼠标事件 ......"
#: kvmd/plugins/hid/otg/device.py:197
#, python-format
msgid "HID-%s write() error: written (%s) != report length (%d)"
msgstr "HID-%s write() 错误:写入 (%s) != 报告长度 (%d)"
#: kvmd/plugins/hid/otg/device.py:205
#, python-format
msgid "HID-%s busy/unplugged (write): %s"
msgstr "HID-%s 忙碌/已拔(写入): %s"
#: kvmd/plugins/hid/otg/device.py:207
#, python-format
msgid "Can't write report to HID-%s"
msgstr "无法将内容写入 HID-%s"
#: kvmd/plugins/hid/otg/device.py:224
#, python-format
msgid "Can't select() for read HID-%s: %s"
msgstr "读取 HID-%s 时无法选择 %s"
#: kvmd/plugins/hid/otg/device.py:232
#, python-format
msgid "HID-%s busy/unplugged (read): %s"
msgstr "HID-%s 忙碌/已拔(读取): %s"
#: kvmd/plugins/hid/otg/device.py:234
#, python-format
msgid "Can't read report from HID-%s"
msgstr "无法读取 HID-%s 的回复"
#: kvmd/plugins/hid/otg/device.py:251
#, python-format
msgid "Missing HID-%s device: %s"
msgstr "丢失 HID-%s 设备:%s"
#: kvmd/plugins/hid/otg/device.py:263
#, python-format
msgid "Can't open HID-%s device %s: %s"
msgstr "无法打开 HID-%s 设备 %s: %s"
#: kvmd/plugins/hid/otg/device.py:274
#, python-format
msgid "HID-%s is busy/unplugged (write select)"
msgstr "HID-%s 忙碌/已拔(写入切换)"
#: kvmd/plugins/hid/otg/device.py:276
#, python-format
msgid "Can't select() for write HID-%s: %s"
msgstr "写 HID-%s 时无法选择 %s"
#: kvmd/plugins/hid/otg/keyboard.py:62
msgid "Clearing HID-keyboard events ..."
msgstr "清除 HID 键盘事件 ......"
#: kvmd/plugins/hid/otg/mouse.py:71
msgid "Clearing HID-mouse events ..."
msgstr "清除 HID 鼠标事件 ......"
#: kvmd/plugins/msd/__init__.py:57
msgid "Performing another MSD operation, please try again later"
msgstr "正在执行另一项虚拟存储驱动器操作,请稍后再试"
#: kvmd/plugins/msd/__init__.py:63
msgid "MSD is not found"
msgstr "虚拟存储驱动器不存在"
#: kvmd/plugins/msd/__init__.py:69
msgid "MSD is connected to Server, but shouldn't for this operation"
msgstr "虚拟存储驱动器已连接到服务器,但本操作不应连接到服务器"
#: kvmd/plugins/msd/__init__.py:75
msgid "MSD is disconnected from Server, but should be for this operation"
msgstr "虚拟存储驱动器与服务器断开连接,但进行此操作时应断开连接"
#: kvmd/plugins/msd/__init__.py:81
msgid "The image is not selected"
msgstr "没有选中任何镜像"
#: kvmd/plugins/msd/__init__.py:87
msgid "The image is not found in the storage"
msgstr "存储区中没有找到镜像"
#: kvmd/plugins/msd/__init__.py:93
msgid "This image is already exists"
msgstr "此镜像已存在"
#: kvmd/plugins/msd/disabled.py:40
msgid "MSD is disabled"
msgstr "虚拟存储驱动器已被禁用"
#: kvmd/plugins/msd/otg/__init__.py:148
#, python-format
msgid "Using OTG gadget %r as MSD"
msgstr "使用 OTG gadget %r 作为 MSD"
#: kvmd/plugins/msd/otg/__init__.py:223
msgid "Can't reset MSD properly"
msgstr "无法正确重置虚拟存储驱动器"
#: kvmd/plugins/msd/otg/__init__.py:442
msgid "Unexpected MSD watcher error"
msgstr "虚拟存储驱动器监视器意外出错"
#: kvmd/plugins/msd/otg/__init__.py:461
msgid "Probing to remount storage ..."
msgstr "探测以重新挂载存储 ......"
#: kvmd/plugins/msd/otg/__init__.py:467
msgid "Error while reloading MSD state; switching to offline"
msgstr "重新加载 MSD 状态时出错;切换到离线状态"
#: kvmd/plugins/msd/otg/__init__.py:495
#, python-format
msgid "Setting up initial image %r ..."
msgstr "设置初始镜像 %r ......"
#: kvmd/plugins/msd/otg/__init__.py:501
msgid "Can't setup initial image: ignored"
msgstr "无法设置初始镜像 %r: 已忽略"
#: kvmd/plugins/msd/otg/__init__.py:503
#, python-format
msgid "Can't find initial image %r: ignored"
msgstr "找不到初始镜像 %r: 已忽略"
#: kvmd/plugins/msd/otg/drive.py:36
msgid "MSD drive is locked on IO operation"
msgstr "虚拟存储驱动器在 IO 操作时被锁定"
#: kvmd/plugins/msd/otg/storage.py:297
msgid "Can't execute remount helper"
msgstr "无法执行重新挂载辅助程序"
#: kvmd/plugins/ugpio/anelpwr.py:152
#, python-format
msgid "Failed ANELPWR POST request to pin %s: %s"
msgstr "向引脚 %s 发送 ANELPWR POST 请求失败:%s"

View File

@ -34,6 +34,8 @@ from typing import Generator
from .logging import get_logger from .logging import get_logger
from .lanuages import Lanuages
from . import aiotools from . import aiotools
from . import libc from . import libc
@ -194,7 +196,7 @@ class Inotify:
for path in paths: for path in paths:
path = os.path.normpath(path) path = os.path.normpath(path)
assert path not in self.__wd_by_path, path assert path not in self.__wd_by_path, path
get_logger().info("Watching for %s", path) get_logger().info(Lanuages().gettext("Watching for %s"), path)
# Асинхронно, чтобы не висло на NFS # Асинхронно, чтобы не висло на NFS
wd = _inotify_check(await aiotools.run_async(libc.inotify_add_watch, self.__fd, _fs_encode(path), mask)) wd = _inotify_check(await aiotools.run_async(libc.inotify_add_watch, self.__fd, _fs_encode(path), mask))
self.__wd_by_path[path] = wd self.__wd_by_path[path] = wd
@ -253,7 +255,7 @@ class Inotify:
if event.mask & InotifyMask.IGNORED: if event.mask & InotifyMask.IGNORED:
ignored_path = self.__path_by_wd[event.wd] ignored_path = self.__path_by_wd[event.wd]
if self.__wd_by_path[ignored_path] == event.wd: if self.__wd_by_path[ignored_path] == event.wd:
logger.info("Unwatching %s because IGNORED was received", ignored_path) logger.info(Lanuages().gettext("Unwatching %s because IGNORED was received"), ignored_path)
del self.__wd_by_path[ignored_path] del self.__wd_by_path[ignored_path]
continue continue

7
kvmd/lanuages.py Normal file
View File

@ -0,0 +1,7 @@
from gettext import translation
class Lanuages:
t = translation(domain="message",localedir="/kvmd/i18n",languages=["zh"]).gettext
def gettext(self,string: str) -> str:
return self.t(string)

View File

@ -25,6 +25,8 @@ from typing import AsyncGenerator
from ...errors import OperationError from ...errors import OperationError
from ...errors import IsBusyError from ...errors import IsBusyError
from ...lanuages import Lanuages
from .. import BasePlugin from .. import BasePlugin
from .. import get_plugin_class from .. import get_plugin_class
@ -40,7 +42,7 @@ class AtxOperationError(OperationError, AtxError):
class AtxIsBusyError(IsBusyError, AtxError): class AtxIsBusyError(IsBusyError, AtxError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("Performing another ATX operation, please try again later") super().__init__(Lanuages().gettext("Performing another ATX operation, please try again later"))
# ===== # =====

View File

@ -39,6 +39,8 @@ from ...validators.basic import valid_float_f01
from ...validators.os import valid_abs_path from ...validators.os import valid_abs_path
from ...validators.hw import valid_gpio_pin from ...validators.hw import valid_gpio_pin
from ...lanuages import Lanuages
from . import AtxIsBusyError from . import AtxIsBusyError
from . import BaseAtx from . import BaseAtx
@ -88,6 +90,7 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
notifier=self.__notifier, notifier=self.__notifier,
) )
@classmethod @classmethod
def get_plugin_options(cls) -> dict: def get_plugin_options(cls) -> dict:
return { return {
@ -190,7 +193,7 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
await self.__inner_click(name, pin, delay) await self.__inner_click(name, pin, delay)
else: else:
await aiotools.run_region_task( await aiotools.run_region_task(
f"Can't perform ATX {name} click or operation was not completed", Lanuages().gettext(f"Can't perform ATX {name} click or operation was not completed"),
self.__region, self.__inner_click, name, pin, delay, self.__region, self.__inner_click, name, pin, delay,
) )
@ -203,4 +206,4 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
finally: finally:
self.__line_request.set_value(pin, gpiod.line.Value(False)) self.__line_request.set_value(pin, gpiod.line.Value(False))
await asyncio.sleep(1) await asyncio.sleep(1)
get_logger(0).info("Clicked ATX button %r", name) get_logger(0).info(Lanuages().gettext("Clicked ATX button %r"), name)

View File

@ -32,6 +32,8 @@ from ...logging import get_logger
from ... import htclient from ... import htclient
from ...lanuages import Lanuages
from . import BaseAuthService from . import BaseAuthService
@ -89,7 +91,7 @@ class Plugin(BaseAuthService):
htclient.raise_not_200(response) htclient.raise_not_200(response)
return True return True
except Exception: except Exception:
get_logger().exception("Failed HTTP auth request for user %r", user) get_logger().exception(Lanuages().gettext("Failed HTTP auth request for user %r"), user)
return False return False
async def cleanup(self) -> None: async def cleanup(self) -> None:

View File

@ -33,6 +33,8 @@ from ...logging import get_logger
from ... import tools from ... import tools
from ... import aiotools from ... import aiotools
from ...lanuages import Lanuages
from . import BaseAuthService from . import BaseAuthService
@ -101,9 +103,9 @@ class Plugin(BaseAuthService):
except ldap.INVALID_CREDENTIALS: except ldap.INVALID_CREDENTIALS:
pass pass
except ldap.SERVER_DOWN as err: except ldap.SERVER_DOWN as err:
get_logger().error("LDAP server is down: %s", tools.efmt(err)) get_logger().error(Lanuages().gettext("LDAP server is down: %s"), tools.efmt(err))
except Exception as err: except Exception as err:
get_logger().error("Unexpected LDAP error: %s", tools.efmt(err)) get_logger().error(Lanuages().gettext("Unexpected LDAP error: %s"), tools.efmt(err))
finally: finally:
if conn is not None: if conn is not None:
try: try:

View File

@ -34,6 +34,8 @@ from ...logging import get_logger
from ... import aiotools from ... import aiotools
from ...lanuages import Lanuages
from . import BaseAuthService from . import BaseAuthService
@ -86,13 +88,13 @@ class Plugin(BaseAuthService):
return False return False
else: else:
if uid < self.__allow_uids_at: if uid < self.__allow_uids_at:
get_logger().error("Unallowed UID of user %r: uid=%d < allow_uids_at=%d", get_logger().error(Lanuages().gettext("Unallowed UID of user %r: uid=%d < allow_uids_at=%d"),
user, uid, self.__allow_uids_at) user, uid, self.__allow_uids_at)
return False return False
pam_obj = pam.pam() pam_obj = pam.pam()
if not pam_obj.authenticate(user, passwd, service=self.__service): if not pam_obj.authenticate(user, passwd, service=self.__service):
get_logger().error("Can't authorize user %r using PAM: code=%d; reason=%s", get_logger().error(Lanuages().gettext("Can't authorize user %r using PAM: code=%d; reason=%s"),
user, pam_obj.code, pam_obj.reason) user, pam_obj.code, pam_obj.reason)
return False return False
return True return True

View File

@ -36,6 +36,8 @@ from ...logging import get_logger
from ... import aiotools from ... import aiotools
from ...lanuages import Lanuages
from . import BaseAuthService from . import BaseAuthService
@ -440,5 +442,5 @@ class Plugin(BaseAuthService):
response = client.SendPacket(request) response = client.SendPacket(request)
return (response.code == pyrad.packet.AccessAccept) return (response.code == pyrad.packet.AccessAccept)
except Exception: except Exception:
get_logger().exception("Failed RADIUS auth request for user %r", user) get_logger().exception(Lanuages().gettext("Failed RADIUS auth request for user %r"), user)
return False return False

View File

@ -46,6 +46,8 @@ from ....validators.basic import valid_float_f01
from ....validators.os import valid_abs_path from ....validators.os import valid_abs_path
from ....validators.hw import valid_gpio_pin_optional from ....validators.hw import valid_gpio_pin_optional
from ....lanuages import Lanuages
from .. import BaseHid from .. import BaseHid
from .gpio import Gpio from .gpio import Gpio
@ -144,6 +146,8 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
self.__stop_event = multiprocessing.Event() self.__stop_event = multiprocessing.Event()
self.gettext=Lanuages().gettext
@classmethod @classmethod
def get_plugin_options(cls) -> dict: def get_plugin_options(cls) -> dict:
return { return {
@ -247,7 +251,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
@aiotools.atomic_fg @aiotools.atomic_fg
async def cleanup(self) -> None: async def cleanup(self) -> None:
if self.is_alive(): if self.is_alive():
get_logger(0).info("Stopping HID daemon ...") get_logger(0).info(self.gettext("Stopping HID daemon ..."))
self.__stop_event.set() self.__stop_event.set()
if self.is_alive() or self.exitcode is not None: if self.is_alive() or self.exitcode is not None:
self.join() self.join()
@ -316,14 +320,14 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
with self.__gpio: with self.__gpio:
self.__hid_loop() self.__hid_loop()
if self.__phy.has_device(): if self.__phy.has_device():
logger.info("Clearing HID events ...") logger.info(self.gettext("Clearing HID events ..."))
try: try:
with self.__phy.connected() as conn: with self.__phy.connected() as conn:
self.__process_request(conn, ClearEvent().make_request()) self.__process_request(conn, ClearEvent().make_request())
except Exception: except Exception:
logger.exception("Can't clear HID events") logger.exception(self.gettext("Can't clear HID events"))
except Exception: except Exception:
logger.exception("Unexpected error in the GPIO loop") logger.exception(self.gettext("Unexpected error in the GPIO loop"))
time.sleep(1) time.sleep(1)
def __hid_loop(self) -> None: def __hid_loop(self) -> None:
@ -353,24 +357,24 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
reset = False reset = False
except Exception: except Exception:
self.clear_events() self.clear_events()
get_logger(0).exception("Unexpected error in the HID loop") get_logger(0).exception(self.gettext("Unexpected error in the HID loop"))
time.sleep(1) time.sleep(1)
def __hid_loop_wait_device(self, reset: bool) -> bool: def __hid_loop_wait_device(self, reset: bool) -> bool:
logger = get_logger(0) logger = get_logger(0)
if reset: if reset:
logger.info("Initial HID reset and wait for %s ...", self.__phy) logger.info(self.gettext("Initial HID reset and wait for %s ..."), self.__phy)
self.__gpio.reset() self.__gpio.reset()
# На самом деле SPI и Serial-девайсы не пропадают, # На самом деле SPI и Serial-девайсы не пропадают,
# а вот USB CDC (Pico HID Bridge) вполне себе пропадает # а вот USB CDC (Pico HID Bridge) вполне себе пропадает
for _ in range(10): for _ in range(10):
if self.__phy.has_device(): if self.__phy.has_device():
logger.info("Physical HID interface found: %s", self.__phy) logger.info(self.gettext("Physical HID interface found: %s"), self.__phy)
return True return True
if self.__stop_event.is_set(): if self.__stop_event.is_set():
break break
time.sleep(1) time.sleep(1)
logger.error("Missing physical HID interface: %s", self.__phy) logger.error(self.gettext("Missing physical HID interface: %s"), self.__phy)
self.__set_state_online(False) self.__set_state_online(False)
return False return False
@ -388,28 +392,28 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
try: try:
if len(response) < 4: if len(response) < 4:
read_retries -= 1 read_retries -= 1
raise _TempRequestError(f"No response from HID: request={request!r}") raise _TempRequestError(self.gettext(f"No response from HID: request={request!r}"))
if not check_response(response): if not check_response(response):
request = REQUEST_REPEAT request = REQUEST_REPEAT
raise _TempRequestError("Invalid response CRC; requesting response again ...") raise _TempRequestError(self.gettext("Invalid response CRC; requesting response again ..."))
code = response[1] code = response[1]
if code == 0x48: # Request timeout # pylint: disable=no-else-raise if code == 0x48: # Request timeout # pylint: disable=no-else-raise
raise _TempRequestError(f"Got request timeout from HID: request={request!r}") raise _TempRequestError(self.gettext(f"Got request timeout from HID: request={request!r}"))
elif code == 0x40: # CRC Error elif code == 0x40: # CRC Error
raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}") raise _TempRequestError(self.gettext(f"Got CRC error of request from HID: request={request!r}"))
elif code == 0x45: # Unknown command elif code == 0x45: # Unknown command
raise _PermRequestError(f"HID did not recognize the request={request!r}") raise _PermRequestError(self.gettext(f"HID did not recognize the request={request!r}"))
elif code == 0x24: # Rebooted? elif code == 0x24: # Rebooted?
raise _PermRequestError("No previous command state inside HID, seems it was rebooted") raise _PermRequestError(self.gettext("No previous command state inside HID, seems it was rebooted"))
elif code == 0x20: # Legacy done elif code == 0x20: # Legacy done
self.__set_state_online(True) self.__set_state_online(True)
return True return True
elif code & 0x80: # Pong/Done with state elif code & 0x80: # Pong/Done with state
self.__set_state_pong(response) self.__set_state_pong(response)
return True return True
raise _TempRequestError(f"Invalid response from HID: request={request!r}, response=0x{response!r}") raise _TempRequestError(self.gettext(f"Invalid response from HID: request={request!r}, response=0x{response!r}"))
except _RequestError as err: except _RequestError as err:
common_retries -= 1 common_retries -= 1
@ -440,7 +444,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
for msg in error_messages: for msg in error_messages:
logger.error(msg) logger.error(msg)
if not (common_retries and read_retries): if not (common_retries and read_retries):
logger.error("Can't process HID request due many errors: %r", request) logger.error(self.gettext("Can't process HID request due many errors: %r", request))
return error_retval return error_retval
def __set_state_online(self, online: bool) -> None: def __set_state_online(self, online: bool) -> None:

View File

@ -27,6 +27,7 @@ import gpiod
from ....logging import get_logger from ....logging import get_logger
from ....lanuages import Lanuages
# ===== # =====
class Gpio: # pylint: disable=too-many-instance-attributes class Gpio: # pylint: disable=too-many-instance-attributes
@ -50,6 +51,8 @@ class Gpio: # pylint: disable=too-many-instance-attributes
self.__line_request: (gpiod.LineRequest | None) = None self.__line_request: (gpiod.LineRequest | None) = None
self.__last_power: (bool | None) = None self.__last_power: (bool | None) = None
self.gettext=Lanuages().gettext
def __enter__(self) -> None: def __enter__(self) -> None:
if self.__power_detect_pin >= 0 or self.__reset_pin >= 0: if self.__power_detect_pin >= 0 or self.__reset_pin >= 0:
assert self.__line_request is None assert self.__line_request is None
@ -91,7 +94,7 @@ class Gpio: # pylint: disable=too-many-instance-attributes
assert self.__line_request assert self.__line_request
power = bool(self.__line_request.get_value(self.__power_detect_pin).value) power = bool(self.__line_request.get_value(self.__power_detect_pin).value)
if power != self.__last_power: if power != self.__last_power:
get_logger(0).info("HID power state changed: %s -> %s", self.__last_power, power) get_logger(0).info(self.gettext("HID power state changed: %s -> %s"), self.__last_power, power)
self.__last_power = power self.__last_power = power
return power return power
return True return True
@ -105,4 +108,4 @@ class Gpio: # pylint: disable=too-many-instance-attributes
finally: finally:
self.__line_request.set_value(self.__reset_pin, gpiod.line.Value(self.__reset_inverted)) self.__line_request.set_value(self.__reset_pin, gpiod.line.Value(self.__reset_inverted))
time.sleep(1) time.sleep(1)
get_logger(0).info("Reset HID performed") get_logger(0).info(self.gettext("Reset HID performed"))

View File

@ -40,6 +40,8 @@ from .... import aiotools
from .... import aiomulti from .... import aiomulti
from .... import aioproc from .... import aioproc
from ....lanuages import Lanuages
from .. import BaseHid from .. import BaseHid
from ..otg.events import ResetEvent from ..otg.events import ResetEvent
@ -107,6 +109,8 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
stop_event=self.__stop_event, stop_event=self.__stop_event,
) )
self.gettext=Lanuages().gettext
@classmethod @classmethod
def get_plugin_options(cls) -> dict: def get_plugin_options(cls) -> dict:
return { return {
@ -130,7 +134,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
} }
def sysprep(self) -> None: def sysprep(self) -> None:
get_logger(0).info("Starting HID daemon ...") get_logger(0).info(self.gettext("Starting HID daemon ..."))
self.__proc = multiprocessing.Process(target=self.__server_worker, daemon=True) self.__proc = multiprocessing.Process(target=self.__server_worker, daemon=True)
self.__proc.start() self.__proc.start()
@ -175,7 +179,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
async def cleanup(self) -> None: async def cleanup(self) -> None:
if self.__proc is not None: if self.__proc is not None:
if self.__proc.is_alive(): if self.__proc.is_alive():
get_logger(0).info("Stopping HID daemon ...") get_logger(0).info(self.gettext("Stopping HID daemon ..."))
self.__stop_event.set() self.__stop_event.set()
if self.__proc.is_alive() or self.__proc.exitcode is not None: if self.__proc.is_alive() or self.__proc.exitcode is not None:
self.__proc.join() self.__proc.join()
@ -224,5 +228,5 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
try: try:
self.__server.run() self.__server.run()
except Exception: except Exception:
logger.exception("Unexpected HID error") logger.exception(self.gettext("Unexpected HID error"))
time.sleep(5) time.sleep(5)

View File

@ -38,6 +38,8 @@ from .... import aiomulti
from ....keyboard.mappings import UsbKey from ....keyboard.mappings import UsbKey
from ....lanuages import Lanuages
from ..otg.events import BaseEvent from ..otg.events import BaseEvent
from ..otg.events import ClearEvent from ..otg.events import ClearEvent
from ..otg.events import ResetEvent from ..otg.events import ResetEvent
@ -115,6 +117,8 @@ class BtServer: # pylint: disable=too-many-instance-attributes
self.__keys: list[UsbKey | None] = [None] * 6 self.__keys: list[UsbKey | None] = [None] * 6
self.__mouse_buttons = 0 self.__mouse_buttons = 0
self.gettext=Lanuages().gettext
def run(self) -> None: def run(self) -> None:
with self.__iface: with self.__iface:
self.__iface.configure() self.__iface.configure()
@ -146,7 +150,7 @@ class BtServer: # pylint: disable=too-many-instance-attributes
@contextlib.contextmanager @contextlib.contextmanager
def __listen(self, role: _RoleT, addr: str, port: int) -> Generator[socket.socket, None, None]: def __listen(self, role: _RoleT, addr: str, port: int) -> Generator[socket.socket, None, None]:
get_logger(0).info("Listening [%s]:%d for %s ...", addr, port, role) get_logger(0).info(self.gettext("Listening [%s]:%d for %s ..."), addr, port, role)
with socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) as sock: # type: ignore with socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) as sock: # type: ignore
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(self.__socket_timeout) sock.settimeout(self.__socket_timeout)
@ -183,7 +187,7 @@ class BtServer: # pylint: disable=too-many-instance-attributes
elif data == b"\x71": elif data == b"\x71":
sock.send(b"\x00") sock.send(b"\x00")
except Exception as err: except Exception as err:
get_logger(0).exception("CTL socket error on %s: %s", client.addr, tools.efmt(err)) get_logger(0).exception(self.gettext("CTL socket error on %s: %s"), client.addr, tools.efmt(err))
self.__close_client("CTL", client, "ctl_sock") self.__close_client("CTL", client, "ctl_sock")
continue continue
@ -197,7 +201,7 @@ class BtServer: # pylint: disable=too-many-instance-attributes
elif data[:2] == b"\xA2\x01": elif data[:2] == b"\xA2\x01":
self.__process_leds(data[2]) self.__process_leds(data[2])
except Exception as err: except Exception as err:
get_logger(0).exception("INT socket error on %s: %s", client.addr, tools.efmt(err)) get_logger(0).exception(self.gettext("INT socket error on %s: %s"), client.addr, tools.efmt(err))
self.__close_client("INT", client, "ctl_sock") self.__close_client("INT", client, "ctl_sock")
if qr in ready_read: if qr in ready_read:
@ -280,7 +284,7 @@ class BtServer: # pylint: disable=too-many-instance-attributes
try: try:
client.int_sock.send(report) client.int_sock.send(report)
except Exception as err: except Exception as err:
get_logger(0).info("Can't send %s report to %s: %s", name, client.addr, tools.efmt(err)) get_logger(0).info(self.gettext("Can't send %s report to %s: %s"), name, client.addr, tools.efmt(err))
self.__close_client_pair(client) self.__close_client_pair(client)
def __clear_modifiers(self) -> None: def __clear_modifiers(self) -> None:
@ -307,12 +311,12 @@ class BtServer: # pylint: disable=too-many-instance-attributes
(sock, peer) = server_sock.accept() (sock, peer) = server_sock.accept()
sock.setblocking(True) sock.setblocking(True)
except Exception: except Exception:
get_logger(0).exception("Can't accept %s client", role) get_logger(0).exception(self.gettext("Can't accept %s client"), role)
else: else:
if peer[0] not in self.__clients: if peer[0] not in self.__clients:
if len(self.__clients) >= self.__max_clients: if len(self.__clients) >= self.__max_clients:
self.__close_sock(sock) self.__close_sock(sock)
get_logger(0).info("Refused %s client: %s: max clients reached", role, peer[0]) get_logger(0).info(self.gettext("Refused %s client: %s: max clients reached"), role, peer[0])
return return
self.__clients[peer[0]] = _BtClient(peer[0]) self.__clients[peer[0]] = _BtClient(peer[0])
client = self.__clients[peer[0]] client = self.__clients[peer[0]]
@ -321,7 +325,7 @@ class BtServer: # pylint: disable=too-many-instance-attributes
setattr(client, sock_attr, sock) setattr(client, sock_attr, sock)
self.__to_read.add(sock) self.__to_read.add(sock)
get_logger(0).info("Accepted %s client: %s", role, peer[0]) get_logger(0).info(self.gettext("Accepted %s client: %s"), role, peer[0])
self.__state_flags.update(online=True) self.__state_flags.update(online=True)
self.__set_public(len(self.__clients) < self.__max_clients) self.__set_public(len(self.__clients) < self.__max_clients)
@ -333,7 +337,7 @@ class BtServer: # pylint: disable=too-many-instance-attributes
setattr(client, sock_attr, None) setattr(client, sock_attr, None)
self.__to_read.remove(sock) self.__to_read.remove(sock)
get_logger(0).info("Closed %s client %s", role, client.addr) get_logger(0).info(self.gettext("Closed %s client %s"), role, client.addr)
if client.ctl_sock is None and client.int_sock is None: if client.ctl_sock is None and client.int_sock is None:
self.__clients.pop(client.addr) self.__clients.pop(client.addr)
@ -368,16 +372,16 @@ class BtServer: # pylint: disable=too-many-instance-attributes
def __set_public(self, public: bool) -> None: def __set_public(self, public: bool) -> None:
logger = get_logger(0) logger = get_logger(0)
if self.__control_public: if self.__control_public:
logger.info("Publishing ..." if public else "Unpublishing ...") logger.info(self.gettext("Publishing ...") if public else self.gettext("Unpublishing ..."))
try: try:
self.__iface.set_public(public) self.__iface.set_public(public)
except Exception as err: except Exception as err:
logger.error("Can't change public mode: %s", tools.efmt(err)) logger.error(self.gettext("Can't change public mode: %s"), tools.efmt(err))
def __unpair_client(self, client: _BtClient) -> None: def __unpair_client(self, client: _BtClient) -> None:
logger = get_logger(0) logger = get_logger(0)
logger.info("Unpairing %s ...", client.addr) logger.info(self.gettext("Unpairing %s ..."), client.addr)
try: try:
self.__iface.unpair(client.addr) self.__iface.unpair(client.addr)
except Exception as err: except Exception as err:
logger.error("Can't unpair %s: %s", client.addr, tools.efmt(err)) logger.error(self.gettext("Can't unpair %s: %s"), client.addr, tools.efmt(err))

View File

@ -41,6 +41,8 @@ from ....validators.basic import valid_float_f01
from ....validators.os import valid_abs_path from ....validators.os import valid_abs_path
from ....validators.hw import valid_tty_speed from ....validators.hw import valid_tty_speed
from ....lanuages import Lanuages
from .. import BaseHid from .. import BaseHid
from .chip import ChipResponseError from .chip import ChipResponseError
@ -82,6 +84,8 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
self.__keyboard = Keyboard() self.__keyboard = Keyboard()
self.__mouse = Mouse() self.__mouse = Mouse()
self.gettext=Lanuages().gettext
@classmethod @classmethod
def get_plugin_options(cls) -> dict: def get_plugin_options(cls) -> dict:
return { return {
@ -92,7 +96,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
} }
def sysprep(self) -> None: def sysprep(self) -> None:
get_logger(0).info("Starting HID daemon ...") get_logger(0).info(self.gettext("Starting HID daemon ..."))
self.start() self.start()
async def get_state(self) -> dict: async def get_state(self) -> dict:
@ -134,7 +138,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
@aiotools.atomic_fg @aiotools.atomic_fg
async def cleanup(self) -> None: async def cleanup(self) -> None:
if self.is_alive(): if self.is_alive():
get_logger(0).info("Stopping HID daemon ...") get_logger(0).info(self.gettext("Stopping HID daemon ..."))
self.__stop_event.set() self.__stop_event.set()
if self.is_alive() or self.exitcode is not None: if self.is_alive() or self.exitcode is not None:
self.join() self.join()
@ -171,7 +175,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
_ = keyboard_output _ = keyboard_output
if mouse_output is not None: if mouse_output is not None:
get_logger(0).info("HID : mouse output = %s", mouse_output) get_logger(0).info(self.gettext("HID : mouse output = %s"), mouse_output)
absolute = (mouse_output == "usb") absolute = (mouse_output == "usb")
self.__mouse.set_absolute(absolute) self.__mouse.set_absolute(absolute)
self._set_jiggler_absolute(absolute) self._set_jiggler_absolute(absolute)
@ -201,7 +205,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
try: try:
self.__hid_loop() self.__hid_loop()
except Exception: except Exception:
logger.exception("Unexpected error in the run loop") logger.exception(self.gettext("Unexpected error in the run loop"))
time.sleep(1) time.sleep(1)
def __hid_loop(self) -> None: def __hid_loop(self) -> None:
@ -224,7 +228,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
self.__process_cmd(conn, cmd) self.__process_cmd(conn, cmd)
except Exception: except Exception:
self.clear_events() self.clear_events()
get_logger(0).exception("Unexpected error in the HID loop") get_logger(0).exception(self.gettext("Unexpected error in the HID loop"))
time.sleep(2) time.sleep(2)
def __process_cmd(self, conn: ChipConnection, cmd: bytes) -> bool: # pylint: disable=too-many-branches def __process_cmd(self, conn: ChipConnection, cmd: bytes) -> bool: # pylint: disable=too-many-branches

View File

@ -25,6 +25,8 @@ import contextlib
from typing import Generator from typing import Generator
from ....lanuages import Lanuages
# ===== # =====
class ChipResponseError(Exception): class ChipResponseError(Exception):
@ -35,6 +37,7 @@ class ChipResponseError(Exception):
class ChipConnection: class ChipConnection:
def __init__(self, tty: serial.Serial) -> None: def __init__(self, tty: serial.Serial) -> None:
self.__tty = tty self.__tty = tty
self.gettext=Lanuages().gettext
def xfer(self, cmd: bytes) -> int: def xfer(self, cmd: bytes) -> int:
self.__send(cmd) self.__send(cmd)
@ -52,16 +55,16 @@ class ChipConnection:
def __recv(self) -> int: def __recv(self) -> int:
data = self.__tty.read(5) data = self.__tty.read(5)
if len(data) < 5: if len(data) < 5:
raise ChipResponseError("Too short response, HID might be disconnected") raise ChipResponseError(self.gettext("Too short response, HID might be disconnected"))
if data and data[4]: if data and data[4]:
data += self.__tty.read(data[4] + 1) data += self.__tty.read(data[4] + 1)
if self.__make_checksum(data[:-1]) != data[-1]: if self.__make_checksum(data[:-1]) != data[-1]:
raise ChipResponseError("Invalid response checksum") raise ChipResponseError(self.gettext("Invalid response checksum"))
if data[4] == 1 and data[5] != 0: if data[4] == 1 and data[5] != 0:
raise ChipResponseError(f"Response error code = {data[5]!r}") raise ChipResponseError(self.gettext(f"Response error code = {data[5]!r}"))
# led_byte (info) response # led_byte (info) response
return (data[7] if data[3] == 0x81 else -1) return (data[7] if data[3] == 0x81 else -1)

View File

@ -37,6 +37,8 @@ from .... import aiomulti
from .... import aioproc from .... import aioproc
from .... import usb from .... import usb
from ....lanuages import Lanuages
from .events import BaseEvent from .events import BaseEvent
@ -76,6 +78,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__logger: (logging.Logger | None) = None self.__logger: (logging.Logger | None) = None
self.gettext=Lanuages().gettext
def start(self, udc: str) -> None: # type: ignore # pylint: disable=arguments-differ def start(self, udc: str) -> None: # type: ignore # pylint: disable=arguments-differ
self.__udc_state_path = usb.get_udc_path(udc, usb.U_STATE) self.__udc_state_path = usb.get_udc_path(udc, usb.U_STATE)
super().start() super().start()
@ -118,7 +122,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
retries -= 1 retries -= 1
except Exception: except Exception:
self.__logger.exception("Unexpected HID-%s error", self.__name) self.__logger.exception(self.gettext("Unexpected HID-%s error"), self.__name)
time.sleep(1) time.sleep(1)
self.__close_device() self.__close_device()
@ -145,7 +149,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
def _stop(self) -> None: def _stop(self) -> None:
if self.is_alive(): if self.is_alive():
get_logger().info("Stopping HID-%s daemon ...", self.__name) get_logger().info(self.gettext("Stopping HID-%s daemon ..."), self.__name)
self.__stop_event.set() self.__stop_event.set()
if self.is_alive() or self.exitcode is not None: if self.is_alive() or self.exitcode is not None:
self.join() self.join()
@ -190,7 +194,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__state_flags.update(online=True) self.__state_flags.update(online=True)
return True return True
else: else:
logger.error("HID-%s write() error: written (%s) != report length (%d)", logger.error(self.gettext("HID-%s write() error: written (%s) != report length (%d)"),
self.__name, written, len(report)) self.__name, written, len(report))
except Exception as err: except Exception as err:
if isinstance(err, OSError) and ( if isinstance(err, OSError) and (
@ -198,9 +202,9 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
err.errno == errno.EAGAIN # pylint: disable=no-member err.errno == errno.EAGAIN # pylint: disable=no-member
or err.errno == errno.ESHUTDOWN # pylint: disable=no-member or err.errno == errno.ESHUTDOWN # pylint: disable=no-member
): ):
logger.debug("HID-%s busy/unplugged (write): %s", self.__name, tools.efmt(err)) logger.debug(self.gettext("HID-%s busy/unplugged (write): %s"), self.__name, tools.efmt(err))
else: else:
logger.exception("Can't write report to HID-%s", self.__name) logger.exception(self.gettext("Can't write report to HID-%s"), self.__name)
self.__state_flags.update(online=False) self.__state_flags.update(online=False)
return False return False
@ -217,7 +221,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
try: try:
read = bool(select.select([self.__fd], [], [], 0)[0]) read = bool(select.select([self.__fd], [], [], 0)[0])
except Exception as err: except Exception as err:
logger.error("Can't select() for read HID-%s: %s", self.__name, tools.efmt(err)) logger.error(self.gettext("Can't select() for read HID-%s: %s"), self.__name, tools.efmt(err))
break break
if read: if read:
@ -225,9 +229,9 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
report = os.read(self.__fd, self.__read_size) report = os.read(self.__fd, self.__read_size)
except Exception as err: except Exception as err:
if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member
logger.debug("HID-%s busy/unplugged (read): %s", self.__name, tools.efmt(err)) logger.debug(self.gettext("HID-%s busy/unplugged (read): %s"), self.__name, tools.efmt(err))
else: else:
logger.exception("Can't read report from HID-%s", self.__name) logger.exception(self.gettext("Can't read report from HID-%s"), self.__name)
else: else:
self._process_read_report(report) self._process_read_report(report)
@ -244,7 +248,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__close_device() self.__close_device()
self.__state_flags.update(online=False) self.__state_flags.update(online=False)
if not self.__no_device_reported: if not self.__no_device_reported:
logger.error("Missing HID-%s device: %s", self.__name, self.__device_path) logger.error(self.gettext("Missing HID-%s device: %s"), self.__name, self.__device_path)
self.__no_device_reported = True self.__no_device_reported = True
return False return False
self.__no_device_reported = False self.__no_device_reported = False
@ -256,7 +260,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY) flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY)
self.__fd = os.open(self.__device_path, flags) self.__fd = os.open(self.__device_path, flags)
except Exception as err: except Exception as err:
logger.error("Can't open HID-%s device %s: %s", logger.error(self.gettext("Can't open HID-%s device %s: %s"),
self.__name, self.__device_path, tools.efmt(err)) self.__name, self.__device_path, tools.efmt(err))
if self.__fd >= 0: if self.__fd >= 0:
@ -267,9 +271,9 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
return True return True
else: else:
# Если запись недоступна, то скорее всего устройство отключено # Если запись недоступна, то скорее всего устройство отключено
logger.debug("HID-%s is busy/unplugged (write select)", self.__name) logger.debug(self.gettext("HID-%s is busy/unplugged (write select)"), self.__name)
except Exception as err: except Exception as err:
logger.error("Can't select() for write HID-%s: %s", self.__name, tools.efmt(err)) logger.error(self.gettext("Can't select() for write HID-%s: %s"), self.__name, tools.efmt(err))
self.__state_flags.update(online=False) self.__state_flags.update(online=False)
return False return False

View File

@ -41,6 +41,7 @@ from .events import get_led_scroll
from .events import get_led_num from .events import get_led_num
from .events import make_keyboard_report from .events import make_keyboard_report
from ....lanuages import Lanuages
# ===== # =====
class KeyboardProcess(BaseDeviceProcess): class KeyboardProcess(BaseDeviceProcess):
@ -54,10 +55,11 @@ class KeyboardProcess(BaseDeviceProcess):
self.__pressed_modifiers: set[UsbKey] = set() self.__pressed_modifiers: set[UsbKey] = set()
self.__pressed_keys: list[UsbKey | None] = [None] * 6 self.__pressed_keys: list[UsbKey | None] = [None] * 6
self.gettext=Lanuages().gettext
def cleanup(self) -> None: def cleanup(self) -> None:
self._stop() self._stop()
get_logger().info("Clearing HID-keyboard events ...") get_logger().info(self.gettext("Clearing HID-keyboard events ..."))
self._cleanup_write(b"\x00" * 8) # Release all keys and modifiers self._cleanup_write(b"\x00" * 8) # Release all keys and modifiers
def send_clear_event(self) -> None: def send_clear_event(self) -> None:

View File

@ -36,6 +36,7 @@ from .events import MouseRelativeEvent
from .events import MouseWheelEvent from .events import MouseWheelEvent
from .events import make_mouse_report from .events import make_mouse_report
from ....lanuages import Lanuages
# ===== # =====
class MouseProcess(BaseDeviceProcess): class MouseProcess(BaseDeviceProcess):
@ -54,6 +55,7 @@ class MouseProcess(BaseDeviceProcess):
self.__x = 0 # For absolute self.__x = 0 # For absolute
self.__y = 0 self.__y = 0
self.__win98_fix = False self.__win98_fix = False
self.gettext=Lanuages().gettext
def is_absolute(self) -> bool: def is_absolute(self) -> bool:
return self.__absolute return self.__absolute
@ -66,7 +68,7 @@ class MouseProcess(BaseDeviceProcess):
def cleanup(self) -> None: def cleanup(self) -> None:
self._stop() self._stop()
get_logger().info("Clearing HID-mouse events ...") get_logger().info(self.gettext("Clearing HID-mouse events ..."))
report = make_mouse_report( report = make_mouse_report(
absolute=self.__absolute, absolute=self.__absolute,
buttons=0, buttons=0,

View File

@ -41,6 +41,8 @@ from ...validators.basic import valid_int_f1
from ...validators.basic import valid_float_f01 from ...validators.basic import valid_float_f01
from ...validators.hw import valid_gpio_pin_optional from ...validators.hw import valid_gpio_pin_optional
from ...lanuages import Lanuages
from ._mcu import BasePhyConnection from ._mcu import BasePhyConnection
from ._mcu import BasePhy from ._mcu import BasePhy
from ._mcu import BaseMcuHid from ._mcu import BaseMcuHid
@ -56,6 +58,7 @@ class _SpiPhyConnection(BasePhyConnection):
self.__xfer = xfer self.__xfer = xfer
self.__read_timeout = read_timeout self.__read_timeout = read_timeout
self.gettext=Lanuages().gettext
def send(self, request: bytes) -> bytes: def send(self, request: bytes) -> bytes:
assert len(request) == 8 assert len(request) == 8

View File

@ -40,6 +40,7 @@ from ... import aiotools
from .. import BasePlugin from .. import BasePlugin
from .. import get_plugin_class from .. import get_plugin_class
from ...lanuages import Lanuages
# ===== # =====
class MsdError(Exception): class MsdError(Exception):
@ -52,37 +53,44 @@ class MsdOperationError(OperationError, MsdError):
class MsdIsBusyError(IsBusyError, MsdError): class MsdIsBusyError(IsBusyError, MsdError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("Performing another MSD operation, please try again later") gettext=Lanuages().gettext
super().__init__(gettext("Performing another MSD operation, please try again later"))
class MsdOfflineError(MsdOperationError): class MsdOfflineError(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("MSD is not found") gettext=Lanuages().gettext
super().__init__(gettext("MSD is not found"))
class MsdConnectedError(MsdOperationError): class MsdConnectedError(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("MSD is connected to Server, but shouldn't for this operation") gettext=Lanuages().gettext
super().__init__(gettext("MSD is connected to Server, but shouldn't for this operation"))
class MsdDisconnectedError(MsdOperationError): class MsdDisconnectedError(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("MSD is disconnected from Server, but should be for this operation") gettext=Lanuages().gettext
super().__init__(gettext("MSD is disconnected from Server, but should be for this operation"))
class MsdImageNotSelected(MsdOperationError): class MsdImageNotSelected(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("The image is not selected") gettext=Lanuages().gettext
super().__init__(gettext("The image is not selected"))
class MsdUnknownImageError(MsdOperationError): class MsdUnknownImageError(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("The image is not found in the storage") gettext=Lanuages().gettext
super().__init__(gettext("The image is not found in the storage"))
class MsdImageExistsError(MsdOperationError): class MsdImageExistsError(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("This image is already exists") gettext=Lanuages().gettext
super().__init__(gettext("This image is already exists"))
# ===== # =====

View File

@ -31,11 +31,13 @@ from . import BaseMsdReader
from . import BaseMsdWriter from . import BaseMsdWriter
from . import BaseMsd from . import BaseMsd
from ...lanuages import Lanuages
# ===== # =====
class MsdDisabledError(MsdOperationError): class MsdDisabledError(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("MSD is disabled") gettext=Lanuages().gettext
super().__init__(gettext("MSD is disabled"))
# ===== # =====

View File

@ -28,6 +28,7 @@ import time
from typing import AsyncGenerator from typing import AsyncGenerator
from ....lanuages import Lanuages
from ....logging import get_logger from ....logging import get_logger
from ....inotify import InotifyMask from ....inotify import InotifyMask
@ -43,6 +44,8 @@ from ....validators.kvm import valid_msd_image_name
from .... import aiotools from .... import aiotools
from .... import fstab from .... import fstab
from ....lanuages import Lanuages
from .. import MsdIsBusyError from .. import MsdIsBusyError
from .. import MsdOfflineError from .. import MsdOfflineError
from .. import MsdConnectedError from .. import MsdConnectedError
@ -58,7 +61,6 @@ from .storage import Image
from .storage import Storage from .storage import Storage
from .drive import Drive from .drive import Drive
# ===== # =====
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class _DriveState: class _DriveState:
@ -140,9 +142,10 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
self.__notifier = aiotools.AioNotifier() self.__notifier = aiotools.AioNotifier()
self.__state = _State(self.__notifier) self.__state = _State(self.__notifier)
self.gettext=Lanuages().gettext
logger = get_logger(0) logger = get_logger(0)
logger.info("Using OTG gadget %r as MSD", gadget) logger.info(self.gettext("Using OTG gadget %r as MSD"), gadget)
aiotools.run_sync(self.__reload_state(notify=False)) aiotools.run_sync(self.__reload_state(notify=False))
@classmethod @classmethod
@ -217,7 +220,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
self.__drive.set_rw_flag(False) self.__drive.set_rw_flag(False)
await self.__storage.remount_rw(False) await self.__storage.remount_rw(False)
except Exception: except Exception:
get_logger(0).exception("Can't reset MSD properly") get_logger(0).exception(self.gettext("Can't reset MSD properly"))
@aiotools.atomic_fg @aiotools.atomic_fg
async def cleanup(self) -> None: async def cleanup(self) -> None:
@ -436,7 +439,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
if need_reload_state: if need_reload_state:
await self.__reload_state() await self.__reload_state()
except Exception: except Exception:
logger.exception("Unexpected MSD watcher error") logger.exception(self.gettext("Unexpected MSD watcher error"))
time.sleep(1) time.sleep(1)
async def __reload_state(self, notify: bool=True) -> None: async def __reload_state(self, notify: bool=True) -> None:
@ -455,13 +458,13 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
if self.__state.vd is None and drive_state.image is None: if self.__state.vd is None and drive_state.image is None:
# Если только что включились и образ не подключен - попробовать # Если только что включились и образ не подключен - попробовать
# перемонтировать хранилище (и создать images и meta). # перемонтировать хранилище (и создать images и meta).
logger.info("Probing to remount storage ...") logger.info(self.gettext("Probing to remount storage ..."))
await self.__storage.remount_rw(True) await self.__storage.remount_rw(True)
await self.__storage.remount_rw(False) await self.__storage.remount_rw(False)
await self.__setup_initial() await self.__setup_initial()
except Exception: except Exception:
logger.exception("Error while reloading MSD state; switching to offline") logger.exception(self.gettext("Error while reloading MSD state; switching to offline"))
self.__state.storage = None self.__state.storage = None
self.__state.vd = None self.__state.vd = None
@ -489,12 +492,12 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
logger = get_logger(0) logger = get_logger(0)
image = await self.__storage.make_image_by_name(self.__initial_image) image = await self.__storage.make_image_by_name(self.__initial_image)
if (await image.exists()): if (await image.exists()):
logger.info("Setting up initial image %r ...", self.__initial_image) logger.info(self.gettext("Setting up initial image %r ..."), self.__initial_image)
try: try:
self.__drive.set_rw_flag(False) self.__drive.set_rw_flag(False)
self.__drive.set_cdrom_flag(self.__initial_cdrom) self.__drive.set_cdrom_flag(self.__initial_cdrom)
self.__drive.set_image_path(image.path) self.__drive.set_image_path(image.path)
except Exception: except Exception:
logger.exception("Can't setup initial image: ignored") logger.exception(self.gettext("Can't setup initial image: ignored"))
else: else:
logger.error("Can't find initial image %r: ignored", self.__initial_image) logger.error(self.gettext("Can't find initial image %r: ignored"), self.__initial_image)

View File

@ -27,11 +27,13 @@ from .... import usb
from .. import MsdOperationError from .. import MsdOperationError
from ....lanuages import Lanuages
# ===== # =====
class MsdDriveLockedError(MsdOperationError): class MsdDriveLockedError(MsdOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("MSD drive is locked on IO operation") super().__init__(Lanuages().gettext("MSD drive is locked on IO operation"))
# ===== # =====

View File

@ -31,6 +31,8 @@ from typing import Optional
import aiofiles import aiofiles
import aiofiles.os import aiofiles.os
from ....lanuages import Lanuages
from .... import aiotools from .... import aiotools
from .... import aiohelpers from .... import aiohelpers
@ -292,4 +294,4 @@ class Storage(_StorageDc):
async def remount_rw(self, rw: bool, fatal: bool=True) -> None: async def remount_rw(self, rw: bool, fatal: bool=True) -> None:
if not (await aiohelpers.remount("MSD", self.__remount_cmd, rw)): if not (await aiohelpers.remount("MSD", self.__remount_cmd, rw)):
if fatal: if fatal:
raise MsdError("Can't execute remount helper") raise MsdError(Lanuages().gettext("Can't execute remount helper"))

View File

@ -41,6 +41,8 @@ from ...validators.basic import valid_bool
from ...validators.basic import valid_number from ...validators.basic import valid_number
from ...validators.basic import valid_float_f01 from ...validators.basic import valid_float_f01
from ...lanuages import Lanuages
from . import BaseUserGpioDriver from . import BaseUserGpioDriver
from . import GpioDriverOfflineError from . import GpioDriverOfflineError
@ -146,7 +148,8 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
) as response: ) as response:
htclient.raise_not_200(response) htclient.raise_not_200(response)
except Exception as err: except Exception as err:
get_logger().error("Failed ANELPWR POST request to pin %s: %s", pin, tools.efmt(err))
get_logger().error(Lanuages().gettext("Failed ANELPWR POST request to pin %s: %s"), pin, tools.efmt(err))
raise GpioDriverOfflineError(self) raise GpioDriverOfflineError(self)
self.__update_notifier.notify() self.__update_notifier.notify()

View File

@ -22,6 +22,8 @@
import os import os
from .lanuages import Lanuages
from . import env from . import env
@ -31,10 +33,10 @@ def find_udc(udc: str) -> str:
candidates = sorted(os.listdir(path)) candidates = sorted(os.listdir(path))
if not udc: if not udc:
if len(candidates) == 0: if len(candidates) == 0:
raise RuntimeError("Can't find any UDC") raise RuntimeError(Lanuages().gettext("Can't find any UDC"))
udc = candidates[0] udc = candidates[0]
elif udc not in candidates: elif udc not in candidates:
raise RuntimeError(f"Can't find selected UDC: {udc}") raise RuntimeError(Lanuages().gettext(f"Can't find selected UDC: {udc}"))
return udc # fe980000.usb return udc # fe980000.usb

3
make-zh_translation.sh Normal file
View File

@ -0,0 +1,3 @@
pybabel extract -F babel.cfg -o message.pot .
pybabel update -d kvmd/i18n -l zh -D message -i message.pot
pybabel compile -i kvmd/i18n/zh/LC_MESSAGES/message.po -o kvmd/i18n/zh/LC_MESSAGES/message.mo

398
message.pot Normal file
View File

@ -0,0 +1,398 @@
# Translations template for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-08-06 21:17+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.15.0\n"
#: kvmd/htclient.py:63
msgid "Can't determine filename"
msgstr ""
#: kvmd/inotify.py:199
#, python-format
msgid "Watching for %s"
msgstr ""
#: kvmd/inotify.py:258
#, python-format
msgid "Unwatching %s because IGNORED was received"
msgstr ""
#: kvmd/usb.py:36
msgid "Can't find any UDC"
msgstr ""
#: kvmd/apps/__init__.py:132
msgid "Set config file path"
msgstr ""
#: kvmd/apps/__init__.py:134
msgid "Override config options list (like sec/sub/opt=value)"
msgstr ""
#: kvmd/apps/__init__.py:136
msgid "View current configuration (include all overrides)"
msgstr ""
#: kvmd/apps/__init__.py:139
msgid "Run the service"
msgstr ""
#: kvmd/apps/__init__.py:165
msgid ""
"To prevent accidental startup, you must specify the --run option to "
"start.\n"
msgstr ""
#: kvmd/apps/__init__.py:165
msgid "Try the --help option to find out what this service does.\n"
msgstr ""
#: kvmd/apps/__init__.py:165
msgid "Make sure you understand exactly what you are doing!"
msgstr ""
#: kvmd/apps/kvmd/auth.py:57
msgid "AUTHORIZATION IS DISABLED"
msgstr ""
#: kvmd/apps/kvmd/auth.py:61
#, python-format
msgid "Authorization is disabled for API %r"
msgstr ""
#: kvmd/apps/kvmd/auth.py:66
#, python-format
msgid "Using internal auth service %r"
msgstr ""
#: kvmd/apps/kvmd/auth.py:73
#, python-format
msgid "Using external auth service %r"
msgstr ""
#: kvmd/apps/kvmd/auth.py:101
#, python-format
msgid "Got access denied for user %r by TOTP"
msgstr ""
#: kvmd/apps/kvmd/auth.py:112
#, python-format
msgid "Authorized user %r via auth service %r"
msgstr ""
#: kvmd/apps/kvmd/auth.py:114
#, python-format
msgid "Got access denied for user %r from auth service %r"
msgstr ""
#: kvmd/apps/kvmd/auth.py:124
#, python-format
msgid "Logged in user %r"
msgstr ""
#: kvmd/apps/kvmd/auth.py:134
msgid "Can't generate new unique token"
msgstr ""
#: kvmd/apps/kvmd/auth.py:145
#, python-format
msgid "Logged out user %r (%d)"
msgstr ""
#: kvmd/plugins/atx/__init__.py:45
msgid "Performing another ATX operation, please try again later"
msgstr ""
#: kvmd/plugins/atx/gpio.py:209
#, python-format
msgid "Clicked ATX button %r"
msgstr ""
#: kvmd/plugins/auth/http.py:94
#, python-format
msgid "Failed HTTP auth request for user %r"
msgstr ""
#: kvmd/plugins/auth/ldap.py:106
#, python-format
msgid "LDAP server is down: %s"
msgstr ""
#: kvmd/plugins/auth/ldap.py:108
#, python-format
msgid "Unexpected LDAP error: %s"
msgstr ""
#: kvmd/plugins/auth/pam.py:91
#, python-format
msgid "Unallowed UID of user %r: uid=%d < allow_uids_at=%d"
msgstr ""
#: kvmd/plugins/auth/pam.py:97
#, python-format
msgid "Can't authorize user %r using PAM: code=%d; reason=%s"
msgstr ""
#: kvmd/plugins/auth/radius.py:445
#, python-format
msgid "Failed RADIUS auth request for user %r"
msgstr ""
#: kvmd/plugins/hid/bt/__init__.py:137 kvmd/plugins/hid/ch9329/__init__.py:99
msgid "Starting HID daemon ..."
msgstr ""
#: kvmd/plugins/hid/bt/__init__.py:182 kvmd/plugins/hid/ch9329/__init__.py:141
msgid "Stopping HID daemon ..."
msgstr ""
#: kvmd/plugins/hid/bt/__init__.py:231
msgid "Unexpected HID error"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:153
#, python-format
msgid "Listening [%s]:%d for %s ..."
msgstr ""
#: kvmd/plugins/hid/bt/server.py:190
#, python-format
msgid "CTL socket error on %s: %s"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:204
#, python-format
msgid "INT socket error on %s: %s"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:287
#, python-format
msgid "Can't send %s report to %s: %s"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:314
#, python-format
msgid "Can't accept %s client"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:319
#, python-format
msgid "Refused %s client: %s: max clients reached"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:328
#, python-format
msgid "Accepted %s client: %s"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:340
#, python-format
msgid "Closed %s client %s"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:375
msgid "Publishing ..."
msgstr ""
#: kvmd/plugins/hid/bt/server.py:375
msgid "Unpublishing ..."
msgstr ""
#: kvmd/plugins/hid/bt/server.py:379
#, python-format
msgid "Can't change public mode: %s"
msgstr ""
#: kvmd/plugins/hid/bt/server.py:383
#, python-format
msgid "Unpairing %s ..."
msgstr ""
#: kvmd/plugins/hid/bt/server.py:387
#, python-format
msgid "Can't unpair %s: %s"
msgstr ""
#: kvmd/plugins/hid/ch9329/__init__.py:178
#, python-format
msgid "HID : mouse output = %s"
msgstr ""
#: kvmd/plugins/hid/ch9329/__init__.py:208
msgid "Unexpected error in the run loop"
msgstr ""
#: kvmd/plugins/hid/ch9329/__init__.py:231
msgid "Unexpected error in the HID loop"
msgstr ""
#: kvmd/plugins/hid/ch9329/chip.py:58
msgid "Too short response, HID might be disconnected"
msgstr ""
#: kvmd/plugins/hid/ch9329/chip.py:64
msgid "Invalid response checksum"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:125
#, python-format
msgid "Unexpected HID-%s error"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:152
#, python-format
msgid "Stopping HID-%s daemon ..."
msgstr ""
#: kvmd/plugins/hid/otg/device.py:197
#, python-format
msgid "HID-%s write() error: written (%s) != report length (%d)"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:205
#, python-format
msgid "HID-%s busy/unplugged (write): %s"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:207
#, python-format
msgid "Can't write report to HID-%s"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:224
#, python-format
msgid "Can't select() for read HID-%s: %s"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:232
#, python-format
msgid "HID-%s busy/unplugged (read): %s"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:234
#, python-format
msgid "Can't read report from HID-%s"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:251
#, python-format
msgid "Missing HID-%s device: %s"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:263
#, python-format
msgid "Can't open HID-%s device %s: %s"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:274
#, python-format
msgid "HID-%s is busy/unplugged (write select)"
msgstr ""
#: kvmd/plugins/hid/otg/device.py:276
#, python-format
msgid "Can't select() for write HID-%s: %s"
msgstr ""
#: kvmd/plugins/hid/otg/keyboard.py:62
msgid "Clearing HID-keyboard events ..."
msgstr ""
#: kvmd/plugins/hid/otg/mouse.py:71
msgid "Clearing HID-mouse events ..."
msgstr ""
#: kvmd/plugins/msd/__init__.py:57
msgid "Performing another MSD operation, please try again later"
msgstr ""
#: kvmd/plugins/msd/__init__.py:63
msgid "MSD is not found"
msgstr ""
#: kvmd/plugins/msd/__init__.py:69
msgid "MSD is connected to Server, but shouldn't for this operation"
msgstr ""
#: kvmd/plugins/msd/__init__.py:75
msgid "MSD is disconnected from Server, but should be for this operation"
msgstr ""
#: kvmd/plugins/msd/__init__.py:81
msgid "The image is not selected"
msgstr ""
#: kvmd/plugins/msd/__init__.py:87
msgid "The image is not found in the storage"
msgstr ""
#: kvmd/plugins/msd/__init__.py:93
msgid "This image is already exists"
msgstr ""
#: kvmd/plugins/msd/disabled.py:40
msgid "MSD is disabled"
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:148
#, python-format
msgid "Using OTG gadget %r as MSD"
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:223
msgid "Can't reset MSD properly"
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:442
msgid "Unexpected MSD watcher error"
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:461
msgid "Probing to remount storage ..."
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:467
msgid "Error while reloading MSD state; switching to offline"
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:495
#, python-format
msgid "Setting up initial image %r ..."
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:501
msgid "Can't setup initial image: ignored"
msgstr ""
#: kvmd/plugins/msd/otg/__init__.py:503
#, python-format
msgid "Can't find initial image %r: ignored"
msgstr ""
#: kvmd/plugins/msd/otg/drive.py:36
msgid "MSD drive is locked on IO operation"
msgstr ""
#: kvmd/plugins/msd/otg/storage.py:297
msgid "Can't execute remount helper"
msgstr ""
#: kvmd/plugins/ugpio/anelpwr.py:152
#, python-format
msgid "Failed ANELPWR POST request to pin %s: %s"
msgstr ""