File size: 3,520 Bytes
bb59211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
"""服务端 i18n —— 让首屏 HTML 直接是正确语言,消掉"先英文后中文"的闪烁。

**问题**:语言原本只存在浏览器的 ``localStorage`` 里,服务端不知道。所以渲染出来
的 ``data-i18n`` 元素只能填**默认语言(英文)**,再由 ``DOMContentLoaded`` 里的 JS
改成中文 —— 改的时机在首屏之后,于是每次整页导航都闪一下。

**做法**:语言同时写进 cookie,服务端渲染时就用它取词。首屏直接是对的语言,
不需要任何"先渲染再纠正"。

**词典的真源**是 ``web/static/js/i18n-data.js`` —— 一个合法 JSON 载荷外面包一行
``window.__I18N__ = …``:

    * 浏览器把它当**普通脚本**同步加载(带 ``?v=`` 缓存,不会每页重下)
    * 这里剥掉包装后 ``json.loads``,拿到的就是同一份数据

两边读同一份文件,所以**不会分叉**。改词条只改那一个文件。
"""

from __future__ import annotations

import json
from contextvars import ContextVar
from pathlib import Path

from starlette.middleware.base import BaseHTTPMiddleware

# cookie 名与前端 localStorage 的键保持一致,便于两边对照排查。
LANG_COOKIE = "lang"
LANGUAGES = ("zh", "en")
# 没选过语言时用哪个。刻意保持 **en** —— 与前端原来的行为一致(切语言前的默认),
# 不借这次改动偷偷改变默认观感。要改成 zh 的话两边一起改(i18n.js 的 currentLang)。
DEFAULT_LANG = "en"

_DICT_PATH = (Path(__file__).resolve().parent.parent
              / "web" / "static" / "js" / "i18n-data.js")


def _load() -> dict[str, dict[str, str]]:
    """读 ``i18n-data.js`` 并把 JSON 载荷解析出来。

    解析失败**不抛异常**:页面照常起得来,只是文案退回 key 本身 —— 比整个服务
    因为一个词典文件挂掉好。真要查的话看 ``data`` 是不是空的。
    """
    try:
        raw = _DICT_PATH.read_text(encoding="utf-8")
        payload = raw[raw.index("{"):raw.rindex("}") + 1]
        data = json.loads(payload)
    except (OSError, ValueError, IndexError):
        return {}
    return data if isinstance(data, dict) else {}


DATA: dict[str, dict[str, str]] = _load()


def normalize(lang: str | None) -> str:
    """把任意输入收敛成一个可用语言码。认不出来就回默认。"""
    value = str(lang or "").strip().lower()
    return value if value in DATA else DEFAULT_LANG


_current: ContextVar[str] = ContextVar("egodata_lang", default=DEFAULT_LANG)


def get_current_lang() -> str:
    return _current.get()


def t(key: str, lang: str | None = None) -> str:
    """取词。**取不到就返回 key 本身** —— 页面上会显示出一个明显的
    ``nav_foo`` 而不是空白,一眼看得出是漏翻,比静默空着好排查。"""
    use = normalize(lang) if lang is not None else _current.get()
    table = DATA.get(use) or DATA.get(DEFAULT_LANG) or {}
    return table.get(key) or key


class LanguageMiddleware(BaseHTTPMiddleware):
    """从 cookie 读语言,放进 ContextVar —— Jinja 里注册的 ``t`` 直接用。

    用 ContextVar 而不是往每个 ``TemplateResponse`` 的 context 里塞:后者要改
    20 多处调用点,而 ``t`` 是**每个模板都要用**的东西,漏一个就是一片英文。
    """

    async def dispatch(self, request, call_next):
        token = _current.set(normalize(request.cookies.get(LANG_COOKIE)))
        try:
            return await call_next(request)
        finally:
            _current.reset(token)