Detect GUI language from browser settings
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user