diff --git a/server/app/api/v1/legal.py b/server/app/api/v1/legal.py index 0a16e2c..d6cf918 100644 --- a/server/app/api/v1/legal.py +++ b/server/app/api/v1/legal.py @@ -7,7 +7,13 @@ from fastapi.responses import HTMLResponse from pydantic import BaseModel, HttpUrl from app.core.config import get_settings -from app.legal_docs import PRIVACY_POLICY_MD, TERMS_OF_USE_MD, choose_content_by_lang, render_as_simple_html +from app.legal_docs import ( + PRIVACY_POLICY_MD, + TERMS_OF_USE_MD, + choose_content_by_lang, + render_as_simple_html, + split_bilingual_markdown, +) router = APIRouter(prefix="/v1/legal", tags=["legal"]) @@ -64,6 +70,7 @@ Dear Mama|技術支援 # 当前多语言仅支持 EN / TC(繁体) ResolvedLang = Literal["en", "tc"] +BILINGUAL_CONTENT_LANGUAGE = "en, zh-Hant" class LegalLinksResponse(BaseModel): @@ -132,6 +139,18 @@ def _normalize_internal_url(request: Request, url: str, internal_path: str) -> s return _join_base_url(str(request.base_url), internal_path) +def _build_bilingual_content(text: str) -> str: + """ + 固定输出双语协议内容:EN 在前,TC 在后。 + 若缺少 TC,则仅返回 EN。 + """ + + en, tc = split_bilingual_markdown(text) + if not tc: + return en + return f"{en}\n\n---\n{tc}" + + @router.get("/links", response_model=LegalLinksResponse) async def get_legal_links(request: Request) -> LegalLinksResponse: """ @@ -156,11 +175,18 @@ async def get_privacy_policy(request: Request) -> HTMLResponse: """ accept_language = request.headers.get("accept-language") - lang = _resolve_lang(accept_language) - content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang) - title = "Dear Mama | Privacy Policy" if resolved == "en" else "Dear Mama|隱私權政策" - page = render_as_simple_html(title=title, content=content) - return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"}) + if accept_language: + lang = _resolve_lang(accept_language) + content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang) + title = "Dear Mama | Privacy Policy" if resolved == "en" else "Dear Mama|隱私權政策" + html_lang = "en" if resolved == "en" else "zh-Hant" + page = render_as_simple_html(title=title, content=content, html_lang=html_lang) + return HTMLResponse(content=page, headers={"Content-Language": html_lang}) + + content = _build_bilingual_content(PRIVACY_POLICY_MD) + title = "Dear Mama | Privacy Policy / 隱私權政策" + page = render_as_simple_html(title=title, content=content, html_lang="en") + return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE}) @router.get("/terms", response_class=HTMLResponse) @@ -170,11 +196,18 @@ async def get_terms_of_use(request: Request) -> HTMLResponse: """ accept_language = request.headers.get("accept-language") - lang = _resolve_lang(accept_language) - content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang) - title = "Dear Mama – Terms of Use" if resolved == "en" else "Dear Mama 使用條款" - page = render_as_simple_html(title=title, content=content) - return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"}) + if accept_language: + lang = _resolve_lang(accept_language) + content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang) + title = "Dear Mama – Terms of Use" if resolved == "en" else "Dear Mama 使用條款" + html_lang = "en" if resolved == "en" else "zh-Hant" + page = render_as_simple_html(title=title, content=content, html_lang=html_lang) + return HTMLResponse(content=page, headers={"Content-Language": html_lang}) + + content = _build_bilingual_content(TERMS_OF_USE_MD) + title = "Dear Mama – Terms of Use / 使用條款" + page = render_as_simple_html(title=title, content=content, html_lang="en") + return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE}) @router.get("/support", response_class=HTMLResponse) @@ -184,9 +217,16 @@ async def get_support_page(request: Request) -> HTMLResponse: """ accept_language = request.headers.get("accept-language") - lang = _resolve_lang(accept_language) - content, resolved = choose_content_by_lang(SUPPORT_MD, lang) - title = "Dear Mama | Technical Support" if resolved == "en" else "Dear Mama|技術支援" - page = render_as_simple_html(title=title, content=content) - return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"}) + if accept_language: + lang = _resolve_lang(accept_language) + content, resolved = choose_content_by_lang(SUPPORT_MD, lang) + title = "Dear Mama | Technical Support" if resolved == "en" else "Dear Mama|技術支援" + html_lang = "en" if resolved == "en" else "zh-Hant" + page = render_as_simple_html(title=title, content=content, html_lang=html_lang) + return HTMLResponse(content=page, headers={"Content-Language": html_lang}) + + content = _build_bilingual_content(SUPPORT_MD) + title = "Dear Mama | Technical Support / 技術支援" + page = render_as_simple_html(title=title, content=content, html_lang="en") + return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE}) diff --git a/server/app/legal_docs.py b/server/app/legal_docs.py index ebf9b1b..42ffddc 100644 --- a/server/app/legal_docs.py +++ b/server/app/legal_docs.py @@ -265,7 +265,7 @@ def choose_content_by_lang(text: str, lang: ResolvedLang) -> tuple[str, Resolved return en, "en" -def render_as_simple_html(title: str, content: str) -> str: +def render_as_simple_html(title: str, content: str, html_lang: str = "en") -> str: """ 将文本以简单 HTML 的方式展示(使用 pre 保留换行并自动换行)。 不做 Markdown 渲染,避免引入额外依赖,确保“最小可用、必有内容”。 @@ -273,8 +273,9 @@ def render_as_simple_html(title: str, content: str) -> str: safe_title = html.escape(title) safe_content = html.escape(content) + safe_lang = html.escape(html_lang or "en") return f""" - +
diff --git a/spec_kit/overview.md b/spec_kit/overview.md index f8e9649..57f4c59 100644 --- a/spec_kit/overview.md +++ b/spec_kit/overview.md @@ -203,6 +203,8 @@ - `GET /v1/legal/privacy` - `GET /v1/legal/terms` - `GET /v1/legal/support`(技术支持页面,提供审核可用的公开支持信息) + - 协议展示策略调整:`/v1/legal/privacy`、`/v1/legal/terms`、`/v1/legal/support` 在携带 `Accept-Language` 时按语言单语展示(EN/TC),缺省时展示 EN + TC(EN 在前、TC 在后) + - 协议页面语义修复:HTML 根节点 `lang` 属性不再写死,改为随页面实际语言输出(单语 en/zh-Hant;双语默认 en) - 客户端新增协议接口封装 `client/src/services/legalApi.ts` - 客户端工程化:新增统一 HTTP 封装 `client/src/utils/http.ts`(baseURL/超时/JSON/统一错误),并将 `legalApi.ts` / `recoApi.ts` 接入 - 客户端接入两处入口:`app/(splash)/splash.tsx`、`components/home/ProfileModal.tsx`