124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
"""Подсказки HTML/HTTP для киоск-Chromium: не предлагать перевод страницы."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Tuple
|
||
|
||
from flask import Response
|
||
|
||
# Префиксы страниц киоска (localhost / весы / загрузка).
|
||
# Киоск-страницы, где курсор мыши остаётся видимым (остальные киоск-страницы — cursor: none).
|
||
_KIOSK_CURSOR_VISIBLE_PATHS: Tuple[str, ...] = (
|
||
"/scales",
|
||
)
|
||
|
||
_KIOSK_HTML_PATH_PREFIXES: Tuple[str, ...] = (
|
||
"/scales",
|
||
"/starting",
|
||
"/setup",
|
||
"/calibration",
|
||
"/calibrate",
|
||
"/kiosk/",
|
||
"/static/startup-loading.html",
|
||
)
|
||
|
||
_HEAD_INJECT = (
|
||
'<meta name="google" content="notranslate">\n'
|
||
' <meta name="googlebot" content="notranslate">\n'
|
||
' <meta http-equiv="Content-Language" content="ru">\n'
|
||
)
|
||
|
||
_HTML_TAG_RE = re.compile(r"<html(\s[^>]*)?>", re.IGNORECASE)
|
||
_HAS_NOTRANSLATE_RE = re.compile(
|
||
r'<meta[^>]+name=["\']google["\'][^>]+content=["\']notranslate["\']',
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
|
||
def is_kiosk_html_path(path: str) -> bool:
|
||
if not path:
|
||
return False
|
||
for prefix in _KIOSK_HTML_PATH_PREFIXES:
|
||
if path == prefix or path.startswith(prefix):
|
||
return True
|
||
return False
|
||
|
||
|
||
def is_kiosk_cursor_hidden_path(path: str) -> bool:
|
||
"""Скрывать курсор на киоск-страницах, кроме явных исключений (например /scales)."""
|
||
if not is_kiosk_html_path(path):
|
||
return False
|
||
normalized = path.rstrip("/") or "/"
|
||
for visible in _KIOSK_CURSOR_VISIBLE_PATHS:
|
||
if normalized == visible.rstrip("/") or normalized.startswith(visible.rstrip("/") + "/"):
|
||
return False
|
||
return True
|
||
|
||
|
||
def kiosk_translate_response_headers() -> dict[str, str]:
|
||
return {
|
||
"Content-Language": "ru",
|
||
"Google-Translate": "no",
|
||
}
|
||
|
||
|
||
def inject_no_translate_html(html: str) -> str:
|
||
"""Вставить meta notranslate и translate=no на <html> (обман встроенного переводчика)."""
|
||
if _HAS_NOTRANSLATE_RE.search(html):
|
||
patched = html
|
||
elif "<head>" in html.lower():
|
||
patched = re.sub(
|
||
r"<head>",
|
||
"<head>\n " + _HEAD_INJECT.strip().replace("\n", "\n "),
|
||
html,
|
||
count=1,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
else:
|
||
patched = _HEAD_INJECT + html
|
||
|
||
def _html_repl(match: re.Match[str]) -> str:
|
||
attrs = match.group(1) or ""
|
||
if re.search(r'\btranslate\s*=', attrs, re.IGNORECASE):
|
||
return match.group(0)
|
||
if re.search(r'\bclass\s*=', attrs, re.IGNORECASE):
|
||
attrs = re.sub(
|
||
r'class\s*=\s*["\']([^"\']*)["\']',
|
||
r'class="\1 notranslate"',
|
||
attrs,
|
||
count=1,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
else:
|
||
attrs = f'{attrs} class="notranslate"'
|
||
if not re.search(r'\blang\s*=', attrs, re.IGNORECASE):
|
||
attrs = f'{attrs} lang="ru"'
|
||
return f"<html{attrs} translate=\"no\">"
|
||
|
||
return _HTML_TAG_RE.sub(_html_repl, patched, count=1)
|
||
|
||
|
||
def apply_kiosk_translate_hints(response: Response, path: str) -> Response:
|
||
if not is_kiosk_html_path(path):
|
||
return response
|
||
for key, value in kiosk_translate_response_headers().items():
|
||
response.headers[key] = value
|
||
content_type = (response.content_type or "").lower()
|
||
if "text/html" not in content_type:
|
||
return response
|
||
# send_from_directory и др. — direct passthrough; get_data() падает с RuntimeError.
|
||
if getattr(response, "direct_passthrough", False):
|
||
return response
|
||
try:
|
||
html = response.get_data(as_text=True)
|
||
except (TypeError, UnicodeDecodeError, RuntimeError):
|
||
return response
|
||
if not html or "<html" not in html.lower():
|
||
return response
|
||
patched = inject_no_translate_html(html)
|
||
response.set_data(patched)
|
||
if response.content_length is not None:
|
||
response.headers.pop("Content-Length", None)
|
||
return response
|