Detect GUI language from browser settings

This commit is contained in:
2026-08-03 01:07:54 +02:00
parent 10d9936d22
commit dcd4ef58d0
2 changed files with 70 additions and 7 deletions
+43 -7
View File
@@ -20,7 +20,7 @@ DEFAULT_CONFIG = "/etc/frigate-camera-control-bridge.json"
LOG = logging.getLogger("frigate-camera-control-bridge")
STREAM_URL_RE = re.compile(r"(?:rtsp|rtsps|http|https)://[^\s'\"<>]+", re.IGNORECASE)
from .i18n import DEFAULT_LANGUAGE, normalize_language, text_for
from .i18n import DEFAULT_LANGUAGE, available_languages, normalize_language, text_for
from .ui import read_static_file, render_page
@@ -113,6 +113,39 @@ def env_secret(name):
raise RuntimeError(f"failed to read {name}_FILE={path}: {exc}") from exc
def language_from_accept_header(value):
# HTTP Accept-Language can contain regional tags and q weights. The GUI
# ships language catalogs by base code, so pl-PL and pl both resolve to pl.
if not value:
return DEFAULT_LANGUAGE
supported = available_languages()
matches = []
for index, raw_item in enumerate(value.split(",")):
parts = [item.strip() for item in raw_item.split(";") if item.strip()]
if not parts:
continue
tag = parts[0].lower().replace("_", "-")
if tag == "*":
continue
quality = 1.0
for param in parts[1:]:
if not param.lower().startswith("q="):
continue
try:
quality = float(param.split("=", 1)[1])
except ValueError:
quality = 0.0
if quality <= 0:
continue
base = tag.split("-", 1)[0]
if base in supported:
matches.append((-quality, index, base))
if not matches:
return DEFAULT_LANGUAGE
matches.sort()
return matches[0][2]
def config_from_env():
# Env mode is the preferred sidecar mode. It needs only a Frigate API URL,
# MQTT connection parameters, and camera credentials. The old JSON config is
@@ -1582,7 +1615,7 @@ class CameraControlBridge:
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlsplit(self.path)
lang = bridge.request_language(parsed.query)
lang = bridge.request_language(parsed.query, self.headers)
if parsed.path in ("/healthz", "/readyz"):
self.send_json({"ok": True, "actions": bridge.action_count()})
return
@@ -1622,7 +1655,7 @@ class CameraControlBridge:
def do_POST(self):
parsed = urllib.parse.urlsplit(self.path)
lang = bridge.request_language(parsed.query)
lang = bridge.request_language(parsed.query, self.headers)
if not bridge.http_authorized(self.headers, parsed.query):
self.send_response(401)
self.end_headers()
@@ -1697,12 +1730,15 @@ class CameraControlBridge:
def html_page(self, lang=None, token_param=None):
with self.action_lock:
actions = list(self.actions.values())
return render_page(actions, lang or self.default_language, token_param)
return render_page(actions, lang or DEFAULT_LANGUAGE, token_param)
def request_language(self, query):
def request_language(self, query, headers=None):
values = urllib.parse.parse_qs(query)
requested = values.get("lang", [self.default_language])[0]
return normalize_language(requested)
requested = values.get("lang", [None])[0]
if requested:
return normalize_language(requested)
accept_language = headers.get("Accept-Language") if headers else None
return language_from_accept_header(accept_language)
def _handle_signal(self, signum, _frame):
LOG.info("received signal %s, stopping", signum)
+27
View File
@@ -0,0 +1,27 @@
import unittest
from frigate_camera_control_bridge.app import language_from_accept_header
class LanguageDetectionTest(unittest.TestCase):
def test_browser_language_uses_supported_base_code(self):
self.assertEqual(
language_from_accept_header("pl-PL,pl;q=0.9,en-US;q=0.8"),
"pl",
)
def test_quality_weight_wins_over_order(self):
self.assertEqual(
language_from_accept_header("en-US;q=0.4,pl-PL;q=0.9"),
"pl",
)
def test_unsupported_languages_fall_back_to_english(self):
self.assertEqual(language_from_accept_header("fr-FR,de;q=0.8"), "en")
def test_empty_header_falls_back_to_english(self):
self.assertEqual(language_from_accept_header(None), "en")
if __name__ == "__main__":
unittest.main()