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")
|
LOG = logging.getLogger("frigate-camera-control-bridge")
|
||||||
STREAM_URL_RE = re.compile(r"(?:rtsp|rtsps|http|https)://[^\s'\"<>]+", re.IGNORECASE)
|
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
|
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
|
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():
|
def config_from_env():
|
||||||
# Env mode is the preferred sidecar mode. It needs only a Frigate API URL,
|
# 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
|
# MQTT connection parameters, and camera credentials. The old JSON config is
|
||||||
@@ -1582,7 +1615,7 @@ class CameraControlBridge:
|
|||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
parsed = urllib.parse.urlsplit(self.path)
|
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"):
|
if parsed.path in ("/healthz", "/readyz"):
|
||||||
self.send_json({"ok": True, "actions": bridge.action_count()})
|
self.send_json({"ok": True, "actions": bridge.action_count()})
|
||||||
return
|
return
|
||||||
@@ -1622,7 +1655,7 @@ class CameraControlBridge:
|
|||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
parsed = urllib.parse.urlsplit(self.path)
|
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):
|
if not bridge.http_authorized(self.headers, parsed.query):
|
||||||
self.send_response(401)
|
self.send_response(401)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -1697,12 +1730,15 @@ class CameraControlBridge:
|
|||||||
def html_page(self, lang=None, token_param=None):
|
def html_page(self, lang=None, token_param=None):
|
||||||
with self.action_lock:
|
with self.action_lock:
|
||||||
actions = list(self.actions.values())
|
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)
|
values = urllib.parse.parse_qs(query)
|
||||||
requested = values.get("lang", [self.default_language])[0]
|
requested = values.get("lang", [None])[0]
|
||||||
return normalize_language(requested)
|
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):
|
def _handle_signal(self, signum, _frame):
|
||||||
LOG.info("received signal %s, stopping", signum)
|
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