diff --git a/api/config.py b/api/config.py index f9db9ca..86c9777 100644 --- a/api/config.py +++ b/api/config.py @@ -777,6 +777,7 @@ _SETTINGS_DEFAULTS = { 'sync_to_insights': False, # mirror WebUI token usage to state.db for /insights 'check_for_updates': True, # check if webui/agent repos are behind upstream 'theme': 'dark', # active UI theme name (no enum gate -- allows custom themes) + 'language': 'en', # UI locale code; must match a key in static/i18n.js LOCALES 'bot_name': os.getenv('HERMES_WEBUI_BOT_NAME', 'Hermes'), # display name for the assistant 'sound_enabled': False, # play notification sound when assistant finishes 'notifications_enabled': False, # browser notification when tab is in background @@ -800,6 +801,8 @@ _SETTINGS_ENUM_VALUES = { 'send_key': {'enter', 'ctrl+enter'}, } _SETTINGS_BOOL_KEYS = {'show_token_usage', 'show_cli_sessions', 'sync_to_insights', 'check_for_updates', 'sound_enabled', 'notifications_enabled'} +# Language codes are validated as short alphanumeric BCP-47-like tags (e.g. 'en', 'zh', 'fr') +_SETTINGS_LANG_RE = __import__('re').compile(r'^[a-zA-Z]{2,10}(-[a-zA-Z0-9]{2,8})?$') def save_settings(settings: dict) -> dict: """Save settings to disk. Returns the merged settings. Ignores unknown keys.""" @@ -818,6 +821,9 @@ def save_settings(settings: dict) -> dict: # Validate enum-constrained keys if k in _SETTINGS_ENUM_VALUES and v not in _SETTINGS_ENUM_VALUES[k]: continue + # Validate language codes (BCP-47-like: 'en', 'zh', 'fr', 'zh-CN') + if k == 'language' and (not isinstance(v, str) or not _SETTINGS_LANG_RE.match(v)): + continue # Coerce bool keys if k in _SETTINGS_BOOL_KEYS: v = bool(v) diff --git a/api/routes.py b/api/routes.py index ff2ed9e..6cc1cf5 100644 --- a/api/routes.py +++ b/api/routes.py @@ -72,10 +72,28 @@ except ImportError: _permanent_approved = set() +# ── Login page locale strings ───────────────────────────────────────────────── +# Add entries here to support more languages on the login page. +# The key must match the 'language' setting value (from static/i18n.js LOCALES). +_LOGIN_LOCALE = { + 'en': { + 'lang': 'en', 'title': 'Sign in', + 'subtitle': 'Enter your password to continue', + 'placeholder': 'Password', 'btn': 'Sign in', + 'invalid_pw': 'Invalid password', 'conn_failed': 'Connection failed', + }, + 'zh': { + 'lang': 'zh-CN', 'title': '\u767b\u5f55', + 'subtitle': '\u8f93\u5165\u5bc6\u7801\u7ee7\u7eed\u4f7f\u7528', + 'placeholder': '\u5bc6\u7801', 'btn': '\u767b\u5f55', + 'invalid_pw': '\u5bc6\u7801\u9519\u8bef', 'conn_failed': '\u8fde\u63a5\u5931\u8d25', + }, +} + # ── Login page (self-contained, no external deps) ──────────────────────────── _LOGIN_PAGE_HTML = ''' - -{{BOT_NAME}} — Sign in + +{{BOT_NAME}} — {{LOGIN_TITLE}}