#!/usr/bin/env python3
"""Translation and localized feed runtime for the website intel server."""

from __future__ import annotations

import copy
import hashlib
import json
import os
import re
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock, Thread

from x_intel_core import load_environment, minimax_chat, resolve_minimax_key

ROOT = Path(__file__).resolve().parents[1]
DATA_ROOT = ROOT / "data"
FEED_PATH = DATA_ROOT / "x_intel_feed.json"
I18N_FEED_PATH = DATA_ROOT / "x_intel_feed_i18n.json"
TRANSLATION_CACHE_PATH = DATA_ROOT / "i18n_text_cache.json"

TRANSLATION_LOCK = Lock()
I18N_LOCK = Lock()
I18N_STATE_LOCK = Lock()
TRANSLATION_CACHE: dict[str, str] = {}
TRANSLATION_CACHE_DIRTY = False
# User decision: disable translation text cache to avoid stale/mixed-language reuse.
TRANSLATION_CACHE_ENABLED = False
I18N_BUILD_STATE: dict[str, object] = {
    "status": "idle",
    "started_at": "",
    "finished_at": "",
    "last_error": "",
    "source_generated_at": "",
    "target_langs": [],
    "langs": [],
    "lang_progress": {},
}

TRANSLATE_MAX_CHARS = 320
I18N_TARGET_LANGS = ["zh-Hant", "zh-Hans", "en", "ko"]
I18N_BUILD_VERSION = 8
I18N_FEED_TEXT_KEYS = {
    "headline",
    "conclusion",
    "title",
    "summary",
    "glance",
    "detail_summary",
    "point",
    "name",
    "for",
    "requirement",
    "label",
    "where",
    "who",
    "what",
    "why",
    "impact",
    "join",
    "schedule",
    "location",
    "audience",
    "reward",
    "message",
    "tags",
}
I18N_FEED_LIST_KEYS = {
    "takeaways",
    "bullets",
    "detail_lines",
}
I18N_MAX_TARGET_TEXTS = int(os.getenv("I18N_MAX_TARGET_TEXTS", "0") or "0")
I18N_MAX_LIST_ITEMS_PER_FIELD = int(os.getenv("I18N_MAX_LIST_ITEMS_PER_FIELD", "0") or "0")
I18N_MIN_ACCEPTABLE_COVERAGE = float(os.getenv("I18N_MIN_ACCEPTABLE_COVERAGE", "0.98") or "0.98")
I18N_FEED_CHUNK_SIZE = max(3, int(os.getenv("I18N_FEED_CHUNK_SIZE", "12") or "12"))
I18N_FEED_FALLBACK_MODE = str(os.getenv("I18N_FEED_FALLBACK_MODE", "base") or "base").strip().lower()
I18N_SKIP_KEYS = {
    "id",
    "url",
    "account",
    "provider",
    "template_id",
    "card_type",
    "layout",
    "type",
    "urgency",
    "published_at",
    "timeline_date",
    "generated_at",
    "latest_source_at",
}
I18N_CARD_TEXT_KEYS = (
    "title",
    "summary",
    "glance",
    "detail_summary",
    "headline",
)
I18N_CARD_FACT_TEXT_KEYS = (
    "participation",
    "schedule",
    "location",
    "audience",
    "reward",
    "message",
    "join",
    "where",
    "who",
    "what",
    "why",
    "impact",
)
I18N_VISIBLE_FEED_ROOT_KEYS = (
    "digest",
    "official_overview",
    "intel_sections",
    "intel_agenda",
    "format_templates",
    "key_terms",
)


def configure_i18n_runtime(data_root: Path, feed_path: Path | None = None) -> None:
    """Point runtime caches at the mounted/active website data directory."""
    global DATA_ROOT, FEED_PATH, I18N_FEED_PATH, TRANSLATION_CACHE_PATH
    DATA_ROOT = Path(data_root)
    FEED_PATH = Path(feed_path) if feed_path else DATA_ROOT / "x_intel_feed.json"
    I18N_FEED_PATH = DATA_ROOT / "x_intel_feed_i18n.json"
    TRANSLATION_CACHE_PATH = DATA_ROOT / "i18n_text_cache.json"


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _normalize_lang_tag(lang: str | None) -> str:
    tag = str(lang or "zh-Hant").strip()
    normalized = tag.lower().replace("_", "-")
    if normalized in {"zh", "zh-tw", "zh-hant", "tw", "traditional", "繁中"}:
        return "zh-Hant"
    if normalized in {"zh-cn", "zh-hans", "cn", "simplified", "简中", "簡中"}:
        return "zh-Hans"
    if normalized in {"en", "en-us", "english"}:
        return "en"
    if normalized in {"ko", "ko-kr", "kr", "korean"}:
        return "ko"
    return tag if tag in {"zh-Hant", "zh-Hans", "en", "ko"} else "zh-Hant"


def _safe_int(value: object, default: int = 0) -> int:
    try:
        return int(value)  # type: ignore[arg-type]
    except Exception:
        return default


def _read_feed_snapshot() -> dict:
    if not FEED_PATH.exists():
        return {}
    try:
        data = json.loads(FEED_PATH.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return data if isinstance(data, dict) else {}


def _translation_cache_key_legacy(lang: str, text: str) -> str:
    return f"{_normalize_lang_tag(lang)}\n{text}"


def _translation_cache_key(lang: str, text: str, entry_key: str = "") -> str:
    tag = _normalize_lang_tag(lang)
    source = str(text or "")
    scope = str(entry_key or "").strip()
    if not scope:
        return _translation_cache_key_legacy(tag, source)
    digest = hashlib.sha1(source.encode("utf-8")).hexdigest()[:16]
    return f"{tag}\n{scope}\n{digest}"


def _contains_cjk(text: str) -> bool:
    return bool(re.search(r"[\u3400-\u9fff]", str(text or "")))


def _script_counts(text: str) -> tuple[int, int, int]:
    src = str(text or "")
    latin = len(re.findall(r"[A-Za-z]", src))
    hangul = len(re.findall(r"[\uac00-\ud7a3]", src))
    cjk = len(re.findall(r"[\u3400-\u9fff]", src))
    return latin, hangul, cjk


def _looks_translated_for_lang(text: str, lang: str) -> bool:
    src = str(text or "").strip()
    if not src:
        return True
    tag = _normalize_lang_tag(lang)
    latin, hangul, cjk = _script_counts(src)
    total = max(1, latin + hangul + cjk)
    if tag == "en":
        latin_ratio = latin / total
        cjk_ratio = cjk / total
        if cjk_ratio >= 0.05:
            return False
        if latin >= 8:
            return True
        if latin_ratio >= 0.35:
            return True
        return False
    if tag == "ko":
        hangul_ratio = hangul / total
        cjk_ratio = cjk / total
        if cjk_ratio >= 0.06:
            return False
        if hangul >= 6:
            return True
        if hangul_ratio >= 0.30:
            return True
        return False
    return True


def _load_translation_cache_unlocked() -> None:
    global TRANSLATION_CACHE
    if not TRANSLATION_CACHE_ENABLED:
        TRANSLATION_CACHE = {}
        try:
            if TRANSLATION_CACHE_PATH.exists():
                TRANSLATION_CACHE_PATH.unlink()
        except Exception:
            pass
        return
    if TRANSLATION_CACHE:
        return
    if not TRANSLATION_CACHE_PATH.exists():
        TRANSLATION_CACHE = {}
        return
    try:
        raw = json.loads(TRANSLATION_CACHE_PATH.read_text(encoding="utf-8"))
    except Exception:
        TRANSLATION_CACHE = {}
        return
    if not isinstance(raw, dict):
        TRANSLATION_CACHE = {}
        return
    rows = raw.get("rows")
    if not isinstance(rows, dict):
        TRANSLATION_CACHE = {}
        return
    out: dict[str, str] = {}
    for key, value in rows.items():
        k = str(key or "").strip()
        v = str(value or "").strip()
        if k and v:
            out[k] = v
    TRANSLATION_CACHE = out


def _flush_translation_cache_unlocked(force: bool = False) -> None:
    global TRANSLATION_CACHE_DIRTY
    if not TRANSLATION_CACHE_ENABLED:
        TRANSLATION_CACHE_DIRTY = False
        return
    if not force and not TRANSLATION_CACHE_DIRTY:
        return
    TRANSLATION_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
    payload = {
        "updated_at": _now_iso(),
        "rows": TRANSLATION_CACHE,
    }
    TRANSLATION_CACHE_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    TRANSLATION_CACHE_DIRTY = False


def _lang_prompt_name(tag: str) -> str:
    mapping = {
        "zh-Hant": "繁體中文",
        "zh-Hans": "简体中文",
        "en": "English",
        "ko": "한국어",
    }
    return mapping.get(_normalize_lang_tag(tag), "繁體中文")


def _parse_json_array(raw: str) -> list[str]:
    text = str(raw or "").strip()
    if not text:
        return []
    candidates = [text]
    block = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", text, re.S)
    if block:
        candidates.insert(0, block.group(1))
    bare = re.search(r"(\[.*\])", text, re.S)
    if bare:
        candidates.insert(0, bare.group(1))
    for candidate in candidates:
        try:
            parsed = json.loads(candidate)
        except Exception:
            continue
        if isinstance(parsed, list):
            out = []
            for item in parsed:
                out.append(str(item if item is not None else "").strip())
            return out
    return []


ZH_HANS_TRANS = str.maketrans({
    "體": "体", "萬": "万", "與": "与", "專": "专", "業": "业", "叢": "丛", "東": "东",
    "絲": "丝", "丟": "丢", "兩": "两", "嚴": "严", "喪": "丧", "個": "个", "豐": "丰",
    "臨": "临", "為": "为", "麗": "丽", "舉": "举", "麼": "么", "義": "义", "烏": "乌",
    "樂": "乐", "喬": "乔", "習": "习", "鄉": "乡", "書": "书", "買": "买", "亂": "乱",
    "爭": "争", "於": "于", "虧": "亏", "雲": "云", "亞": "亚", "產": "产", "畝": "亩",
    "親": "亲", "褻": "亵", "億": "亿", "僅": "仅", "從": "从", "倉": "仓", "儀": "仪",
    "們": "们", "價": "价", "眾": "众", "優": "优", "會": "会", "傘": "伞", "偉": "伟",
    "傳": "传", "傷": "伤", "倫": "伦", "偽": "伪", "佇": "伫", "佔": "占", "餘": "余",
    "佛": "佛", "來": "来", "侖": "仑", "侶": "侣", "俠": "侠", "係": "系", "俁": "俣",
    "倆": "俩", "倖": "幸", "倣": "仿", "傑": "杰", "傖": "伧", "傘": "伞", "備": "备",
    "傢": "家", "傭": "佣", "傯": "偬", "債": "债", "傷": "伤", "傾": "倾", "僂": "偻",
    "僑": "侨", "僕": "仆", "僥": "侥", "僨": "偾", "價": "价", "儂": "侬", "億": "亿",
    "儈": "侩", "儉": "俭", "儐": "傧", "儔": "俦", "儕": "侪", "儘": "尽", "償": "偿",
    "儲": "储", "兒": "儿", "兌": "兑", "內": "内", "兩": "两", "冊": "册", "冪": "幂",
    "凈": "净", "凍": "冻", "凜": "凛", "幾": "几", "鳳": "凤", "凱": "凯", "別": "别",
    "刪": "删", "剛": "刚", "創": "创", "劃": "划", "劇": "剧", "劉": "刘", "劍": "剑",
    "劑": "剂", "勁": "劲", "動": "动", "務": "务", "勝": "胜", "勞": "劳", "勢": "势",
    "勛": "勋", "勵": "励", "勸": "劝", "區": "区", "醫": "医", "華": "华", "協": "协",
    "單": "单", "賣": "卖", "盧": "卢", "衛": "卫", "卻": "却", "廠": "厂", "廳": "厅",
    "歷": "历", "厲": "厉", "壓": "压", "參": "参", "雙": "双", "發": "发", "變": "变",
    "敘": "叙", "疊": "叠", "葉": "叶", "號": "号", "嘆": "叹", "後": "后", "嚇": "吓",
    "聽": "听", "啟": "启", "吳": "吴", "員": "员", "週": "周", "呼": "呼", "和": "和",
    "諮": "咨", "詠": "咏", "咼": "呙", "員": "员", "響": "响", "啞": "哑", "問": "问",
    "啟": "启", "啢": "唡", "喚": "唤", "喪": "丧", "喬": "乔", "單": "单", "喲": "哟",
    "嗆": "呛", "嗇": "啬", "嗎": "吗", "嗚": "呜", "嗩": "唢", "嗶": "哔", "嘆": "叹",
    "嘔": "呕", "嘖": "啧", "嘗": "尝", "嘜": "唛", "嘩": "哗", "嘮": "唠", "嘯": "啸",
    "嘰": "叽", "噓": "嘘", "噚": "寻", "噴": "喷", "噸": "吨", "噹": "当", "嚀": "咛",
    "嚇": "吓", "嚌": "哜", "嚕": "噜", "嚙": "啮", "嚥": "咽", "嚨": "咙", "嚮": "向",
    "嚳": "喾", "囑": "嘱", "囂": "嚣", "囅": "冁", "囈": "呓", "囉": "啰", "囌": "苏",
    "圍": "围", "園": "园", "國": "国", "圖": "图", "圓": "圆", "聖": "圣", "場": "场",
    "壞": "坏", "塊": "块", "堅": "坚", "壇": "坛", "壩": "坝", "墜": "坠", "墮": "堕",
    "墳": "坟", "墻": "墙", "壓": "压", "壘": "垒", "壙": "圹", "壚": "垆", "壟": "垄",
    "壢": "坜", "壩": "坝", "壯": "壮", "聲": "声", "壺": "壶", "壽": "寿", "夠": "够",
    "夢": "梦", "夥": "伙", "夾": "夹", "奐": "奂", "奧": "奥", "奩": "奁", "奪": "夺",
    "奮": "奋", "妝": "妆", "婦": "妇", "媽": "妈", "姍": "姗", "姦": "奸", "娛": "娱",
    "婁": "娄", "婦": "妇", "婭": "娅", "媧": "娲", "媯": "妫", "媼": "媪", "媽": "妈",
    "嫗": "妪", "嫵": "妩", "嫻": "娴", "嬋": "婵", "嬌": "娇", "嬙": "嫱", "嬡": "嫒",
    "嬤": "嬷", "孌": "娈", "孫": "孙", "學": "学", "孿": "孪", "宮": "宫", "寢": "寝",
    "實": "实", "寧": "宁", "審": "审", "寫": "写", "寬": "宽", "寵": "宠", "寶": "宝",
    "將": "将", "專": "专", "尋": "寻", "對": "对", "導": "导", "尷": "尴", "屆": "届",
    "屍": "尸", "屓": "屃", "屜": "屉", "屢": "屡", "層": "层", "屬": "属", "岡": "冈",
    "峴": "岘", "島": "岛", "峽": "峡", "崍": "崃", "崗": "岗", "嵐": "岚", "嶁": "嵝",
    "嶄": "崭", "嶇": "岖", "嶗": "崂", "嶠": "峤", "嶢": "峣", "嶧": "峄", "嶮": "崄",
    "嶴": "岙", "嶸": "嵘", "嶺": "岭", "嶼": "屿", "巋": "岿", "巒": "峦", "巔": "巅",
    "幀": "帧", "帥": "帅", "師": "师", "帳": "帐", "帶": "带", "幀": "帧", "幃": "帏",
    "幗": "帼", "幘": "帻", "幟": "帜", "幣": "币", "幫": "帮", "幬": "帱", "幹": "干",
    "庫": "库", "廁": "厕", "廂": "厢", "廈": "厦", "廚": "厨", "廟": "庙", "廠": "厂",
    "廣": "广", "廬": "庐", "廳": "厅", "弒": "弑", "弔": "吊", "張": "张", "強": "强",
    "彆": "别", "彈": "弹", "彌": "弥", "彎": "弯", "彙": "汇", "彞": "彝", "彥": "彦",
    "後": "后", "徑": "径", "從": "从", "徠": "徕", "復": "复", "徵": "征", "德": "德",
    "徹": "彻", "恆": "恒", "恥": "耻", "悅": "悦", "悞": "悮", "悵": "怅", "悶": "闷",
    "惡": "恶", "惱": "恼", "惲": "恽", "愛": "爱", "愜": "惬", "愴": "怆", "愷": "恺",
    "愾": "忾", "慄": "栗", "態": "态", "慍": "愠", "慘": "惨", "慚": "惭", "慟": "恸",
    "慣": "惯", "慤": "悫", "慪": "怄", "慫": "怂", "慮": "虑", "慳": "悭", "慶": "庆",
    "憂": "忧", "憊": "惫", "憐": "怜", "憑": "凭", "憒": "愦", "憚": "惮", "憤": "愤",
    "憫": "悯", "憮": "怃", "憲": "宪", "憶": "忆", "懇": "恳", "應": "应", "懌": "怿",
    "懍": "懔", "懞": "蒙", "懟": "怼", "懣": "懑", "懨": "恹", "懲": "惩", "懶": "懒",
    "懷": "怀", "懸": "悬", "懺": "忏", "懼": "惧", "懾": "慑", "戀": "恋", "戇": "戆",
    "戔": "戋", "戧": "戗", "戰": "战", "戲": "戏", "戶": "户", "拋": "抛", "挾": "挟",
    "捨": "舍", "捫": "扪", "掃": "扫", "掄": "抡", "掗": "挜", "掙": "挣", "掛": "挂",
    "採": "采", "揀": "拣", "揚": "扬", "換": "换", "揮": "挥", "損": "损", "搖": "摇",
    "搗": "捣", "搶": "抢", "摑": "掴", "摜": "掼", "摟": "搂", "摯": "挚", "摳": "抠",
    "摶": "抟", "摺": "折", "撈": "捞", "撐": "撑", "撓": "挠", "撥": "拨", "撫": "抚",
    "撲": "扑", "撳": "揿", "撻": "挞", "撾": "挝", "撿": "捡", "擁": "拥", "擄": "掳",
    "擇": "择", "擊": "击", "擋": "挡", "擔": "担", "據": "据", "擠": "挤", "擬": "拟",
    "擯": "摈", "擰": "拧", "擱": "搁", "擲": "掷", "擴": "扩", "擷": "撷", "擺": "摆",
    "擻": "擞", "擼": "撸", "擾": "扰", "攄": "摅", "攆": "撵", "攏": "拢", "攔": "拦",
    "攙": "搀", "攜": "携", "攝": "摄", "攢": "攒", "攣": "挛", "攤": "摊", "攪": "搅",
    "攬": "揽", "敗": "败", "敘": "叙", "敵": "敌", "數": "数", "斂": "敛", "斃": "毙",
    "斕": "斓", "斬": "斩", "斷": "断", "於": "于", "時": "时", "晉": "晋", "晝": "昼",
    "暈": "晕", "暉": "晖", "暢": "畅", "暫": "暂", "曄": "晔", "曆": "历", "曇": "昙",
    "曉": "晓", "曏": "向", "曖": "暧", "曠": "旷", "曬": "晒", "書": "书", "會": "会",
    "朧": "胧", "東": "东", "柵": "栅", "桿": "杆", "梔": "栀", "條": "条", "梟": "枭",
    "梲": "棁", "棄": "弃", "棖": "枨", "棗": "枣", "棟": "栋", "棧": "栈", "棲": "栖",
    "椏": "桠", "楊": "杨", "楓": "枫", "楨": "桢", "業": "业", "極": "极", "榪": "杩",
    "榮": "荣", "榲": "榅", "榿": "桤", "構": "构", "槍": "枪", "槓": "杠", "槤": "梿",
    "槧": "椠", "槨": "椁", "槳": "桨", "樁": "桩", "樂": "乐", "樅": "枞", "樓": "楼",
    "標": "标", "樞": "枢", "樣": "样", "樸": "朴", "樹": "树", "樺": "桦", "橈": "桡",
    "橋": "桥", "機": "机", "橢": "椭", "橫": "横", "檁": "檩", "檉": "柽", "檔": "档",
    "檜": "桧", "檟": "槚", "檢": "检", "檣": "樯", "檯": "台", "檳": "槟", "檸": "柠",
    "檻": "槛", "櫃": "柜", "櫓": "橹", "櫚": "榈", "櫛": "栉", "櫝": "椟", "櫞": "橼",
    "櫟": "栎", "櫥": "橱", "櫧": "槠", "櫨": "栌", "櫪": "枥", "櫫": "橥", "櫬": "榇",
    "櫳": "栊", "櫸": "榉", "櫻": "樱", "欄": "栏", "權": "权", "欏": "椤", "欒": "栾",
    "欖": "榄", "歡": "欢", "歲": "岁", "歷": "历", "歸": "归", "歿": "殁", "殘": "残",
    "殞": "殒", "殤": "殇", "殫": "殚", "殭": "僵", "殮": "殓", "殯": "殡", "殲": "歼",
    "殺": "杀", "殼": "壳", "毀": "毁", "毆": "殴", "毿": "毵", "氈": "毡", "氌": "氇",
    "氣": "气", "氫": "氢", "氬": "氩", "氳": "氲", "決": "决", "沒": "没", "沖": "冲",
    "況": "况", "洶": "汹", "浹": "浃", "涇": "泾", "涼": "凉", "淒": "凄", "淚": "泪",
    "淥": "渌", "淨": "净", "淪": "沦", "淵": "渊", "淶": "涞", "淺": "浅", "渙": "涣",
    "減": "减", "渢": "沨", "渦": "涡", "測": "测", "渾": "浑", "湊": "凑", "湞": "浈",
    "湯": "汤", "溈": "沩", "準": "准", "溝": "沟", "溫": "温", "滄": "沧", "滅": "灭",
    "滌": "涤", "滎": "荥", "滬": "沪", "滯": "滞", "滲": "渗", "滷": "卤", "滸": "浒",
    "滻": "浐", "滾": "滚", "滿": "满", "漁": "渔", "漚": "沤", "漢": "汉", "漣": "涟",
    "漬": "渍", "漲": "涨", "漵": "溆", "漸": "渐", "漿": "浆", "潁": "颍", "潑": "泼",
    "潔": "洁", "潙": "沩", "潛": "潜", "潤": "润", "潯": "浔", "潰": "溃", "潷": "滗",
    "潿": "涠", "澀": "涩", "澆": "浇", "澇": "涝", "澗": "涧", "澠": "渑", "澤": "泽",
    "澦": "滪", "澩": "泶", "澮": "浍", "澱": "淀", "濁": "浊", "濃": "浓", "濕": "湿",
    "濘": "泞", "濟": "济", "濤": "涛", "濫": "滥", "濰": "潍", "濱": "滨", "濺": "溅",
    "濼": "泺", "濾": "滤", "瀅": "滢", "瀆": "渎", "瀉": "泻", "瀋": "沈", "瀏": "浏",
    "瀕": "濒", "瀘": "泸", "瀝": "沥", "瀟": "潇", "瀠": "潆", "瀦": "潴", "瀧": "泷",
    "瀨": "濑", "瀰": "弥", "瀲": "潋", "瀾": "澜", "灃": "沣", "灄": "滠", "灑": "洒",
    "灘": "滩", "灣": "湾", "灤": "滦", "灧": "滟", "災": "灾", "為": "为", "烏": "乌",
    "無": "无", "煉": "炼", "煒": "炜", "煙": "烟", "煢": "茕", "煥": "焕", "煩": "烦",
    "煬": "炀", "熅": "煴", "熒": "荧", "熗": "炝", "熱": "热", "熲": "颎", "熾": "炽",
    "燁": "烨", "燈": "灯", "燉": "炖", "燒": "烧", "燙": "烫", "燜": "焖", "營": "营",
    "燦": "灿", "燭": "烛", "燴": "烩", "燼": "烬", "燾": "焘", "爍": "烁", "爐": "炉",
    "爛": "烂", "爭": "争", "爺": "爷", "爾": "尔", "牆": "墙", "牘": "牍", "牽": "牵",
    "犖": "荦", "犢": "犊", "犧": "牺", "狀": "状", "狹": "狭", "狼": "狼", "狽": "狈",
    "猙": "狰", "猶": "犹", "猻": "狲", "獁": "犸", "獄": "狱", "獅": "狮", "獎": "奖",
    "獨": "独", "獪": "狯", "獫": "猃", "獮": "狝", "獰": "狞", "獲": "获", "獵": "猎",
    "獸": "兽", "獺": "獭", "獻": "献", "獼": "猕", "玀": "猡", "現": "现", "琺": "珐",
    "琿": "珲", "瑋": "玮", "瑣": "琐", "瑤": "瑶", "瑩": "莹", "瑪": "玛", "瑲": "玱",
    "璉": "琏", "璣": "玑", "璦": "瑷", "環": "环", "璽": "玺", "瓊": "琼", "瓏": "珑",
    "瓔": "璎", "瓚": "瓒", "甕": "瓮", "產": "产", "畢": "毕", "異": "异", "畫": "画",
    "當": "当", "疇": "畴", "疊": "叠", "痙": "痉", "痠": "酸", "瘂": "痖", "瘋": "疯",
    "瘍": "疡", "瘓": "痪", "瘞": "瘗", "瘡": "疮", "瘧": "疟", "瘮": "瘆", "瘺": "瘘",
    "瘻": "瘘", "療": "疗", "癆": "痨", "癇": "痫", "癉": "瘅", "癒": "愈", "癘": "疠",
    "癟": "瘪", "癢": "痒", "癤": "疖", "癥": "症", "癧": "疬", "癩": "癞", "癬": "癣",
    "癭": "瘿", "癮": "瘾", "癰": "痈", "癱": "瘫", "癲": "癫", "發": "发", "皚": "皑",
    "皰": "疱", "皸": "皲", "皺": "皱", "盜": "盗", "盞": "盏", "盡": "尽", "監": "监",
    "盤": "盘", "盧": "卢", "眥": "眦", "眾": "众", "睏": "困", "睜": "睁", "瞞": "瞒",
    "瞭": "了", "矇": "蒙", "矓": "眬", "矚": "瞩", "矯": "矫", "硃": "朱", "硤": "硖",
    "硨": "砗", "硯": "砚", "碩": "硕", "碭": "砀", "確": "确", "碼": "码", "磑": "硙",
    "磚": "砖", "磣": "碜", "磧": "碛", "磯": "矶", "磽": "硗", "礎": "础", "礙": "碍",
    "礦": "矿", "礪": "砺", "礫": "砾", "礬": "矾", "祿": "禄", "禍": "祸", "禎": "祯",
    "禕": "祎", "禡": "祃", "禦": "御", "禪": "禅", "禮": "礼", "禰": "祢", "禱": "祷",
    "禿": "秃", "秈": "籼", "稅": "税", "稈": "秆", "稜": "棱", "種": "种", "稱": "称",
    "穀": "谷", "穌": "稣", "積": "积", "穎": "颖", "穠": "秾", "穡": "穑", "穢": "秽",
    "穩": "稳", "穫": "获", "穭": "稆", "窩": "窝", "窪": "洼", "窮": "穷", "窯": "窑",
    "窵": "窎", "窶": "窭", "窺": "窥", "竄": "窜", "竅": "窍", "竇": "窦", "竊": "窃",
    "競": "竞", "筆": "笔", "筍": "笋", "筧": "笕", "箋": "笺", "箏": "筝", "節": "节",
    "範": "范", "築": "筑", "篋": "箧", "篔": "筼", "篤": "笃", "篩": "筛", "篳": "筚",
    "簀": "箦", "簍": "篓", "簞": "箪", "簡": "简", "簣": "篑", "簫": "箫", "簹": "筜",
    "簽": "签", "簾": "帘", "籃": "篮", "籌": "筹", "籍": "籍", "籜": "箨", "籟": "籁",
    "籠": "笼", "籩": "笾", "籪": "簖", "籬": "篱", "籮": "箩", "籲": "吁", "粵": "粤",
    "糝": "糁", "糞": "粪", "糧": "粮", "糰": "团", "糲": "粝", "糴": "籴", "糶": "粜",
    "糾": "纠", "紀": "纪", "紂": "纣", "約": "约", "紅": "红", "紆": "纡", "紇": "纥",
    "紈": "纨", "紉": "纫", "紋": "纹", "納": "纳", "紐": "纽", "紓": "纾", "純": "纯",
    "紕": "纰", "紗": "纱", "紙": "纸", "級": "级", "紛": "纷", "紜": "纭", "紡": "纺",
    "索": "索", "緊": "紧", "紫": "紫", "累": "累", "細": "细", "紱": "绂", "紲": "绁",
    "紳": "绅", "紹": "绍", "紺": "绀", "紼": "绋", "絀": "绌", "終": "终", "組": "组",
    "絆": "绊", "結": "结", "絕": "绝", "絛": "绦", "絝": "绔", "絞": "绞", "絡": "络",
    "絢": "绚", "給": "给", "絨": "绒", "絰": "绖", "統": "统", "絲": "丝", "絳": "绛",
    "絹": "绢", "綁": "绑", "綃": "绡", "綆": "绠", "綈": "绨", "綉": "绣", "綏": "绥",
    "經": "经", "綜": "综", "綞": "缍", "綠": "绿", "綢": "绸", "綣": "绻", "綫": "线",
    "綬": "绶", "維": "维", "綰": "绾", "綱": "纲", "網": "网", "綳": "绷", "綴": "缀",
    "綵": "彩", "綸": "纶", "綹": "绺", "綺": "绮", "綻": "绽", "綽": "绰", "綾": "绫",
    "綿": "绵", "緄": "绲", "緇": "缁", "緊": "紧", "緋": "绯", "緒": "绪", "緓": "绬",
    "緔": "绱", "緗": "缃", "緘": "缄", "緙": "缂", "線": "线", "緝": "缉", "緞": "缎",
    "締": "缔", "緡": "缗", "緣": "缘", "緦": "缌", "編": "编", "緩": "缓", "緬": "缅",
    "緯": "纬", "緱": "缑", "緲": "缈", "練": "练", "緶": "缏", "緹": "缇", "緻": "致",
    "縈": "萦", "縉": "缙", "縊": "缢", "縋": "缒", "縐": "绉", "縑": "缣", "縕": "缊",
    "縗": "缞", "縛": "缚", "縝": "缜", "縞": "缟", "縟": "缛", "縣": "县", "縧": "绦",
    "縫": "缝", "縭": "缡", "縮": "缩", "縱": "纵", "縲": "缧", "縳": "缚", "縴": "纤",
    "縵": "缦", "縶": "絷", "縷": "缕", "縹": "缥", "總": "总", "績": "绩", "繃": "绷",
    "繅": "缫", "繆": "缪", "繈": "襁", "繒": "缯", "織": "织", "繕": "缮", "繚": "缭",
    "繞": "绕", "繡": "绣", "繢": "缋", "繩": "绳", "繪": "绘", "繫": "系", "繭": "茧",
    "繮": "缰", "繯": "缳", "繰": "缲", "繳": "缴", "繹": "绎", "繼": "继", "繽": "缤",
    "繾": "缱", "纈": "缬", "纊": "纩", "續": "续", "纍": "累", "纏": "缠", "纓": "缨",
    "纔": "才", "纖": "纤", "纘": "缵", "纜": "缆", "缽": "钵", "罈": "坛", "罌": "罂",
    "罰": "罚", "罵": "骂", "罷": "罢", "羅": "罗", "羆": "罴", "羈": "羁", "羋": "芈",
    "羥": "羟", "義": "义", "習": "习", "翬": "翚", "翹": "翘", "耬": "耧", "耮": "耢",
    "聖": "圣", "聞": "闻", "聯": "联", "聰": "聪", "聲": "声", "聳": "耸", "聵": "聩",
    "聶": "聂", "職": "职", "聹": "聍", "聽": "听", "聾": "聋", "肅": "肃", "脅": "胁",
    "脈": "脉", "脛": "胫", "脫": "脱", "脹": "胀", "腎": "肾", "腖": "胨", "腡": "脶",
    "腦": "脑", "腫": "肿", "腳": "脚", "腸": "肠", "膃": "腽", "膚": "肤", "膠": "胶",
    "膩": "腻", "膽": "胆", "膾": "脍", "膿": "脓", "臉": "脸", "臍": "脐", "臏": "膑",
    "臘": "腊", "臚": "胪", "臟": "脏", "臠": "脔", "臢": "臜", "臥": "卧", "臨": "临",
    "臺": "台", "與": "与", "興": "兴", "舉": "举", "舊": "旧", "艙": "舱", "艤": "舣",
    "艦": "舰", "艫": "舻", "艱": "艰", "艷": "艳", "藝": "艺", "節": "节", "芻": "刍",
    "莊": "庄", "莖": "茎", "莢": "荚", "莧": "苋", "華": "华", "萇": "苌", "萊": "莱",
    "萬": "万", "萵": "莴", "葉": "叶", "葒": "荭", "著": "著", "葤": "荮", "葦": "苇",
    "葷": "荤", "蒐": "搜", "蒔": "莳", "蒞": "莅", "蒼": "苍", "蓀": "荪", "蓋": "盖",
    "蓮": "莲", "蓯": "苁", "蓴": "莼", "蓽": "荜", "蔔": "卜", "蔞": "蒌", "蔣": "蒋",
    "蔥": "葱", "蔦": "茑", "蔭": "荫", "蕁": "荨", "蕆": "蒇", "蕎": "荞", "蕒": "荬",
    "蕓": "芸", "蕕": "莸", "蕘": "荛", "蕢": "蒉", "蕩": "荡", "蕪": "芜", "蕭": "萧",
    "蕷": "蓣", "薈": "荟", "薊": "蓟", "薌": "芗", "薔": "蔷", "薘": "荙", "薟": "莶",
    "薦": "荐", "薩": "萨", "薺": "荠", "藍": "蓝", "藎": "荩", "藝": "艺", "藥": "药",
    "藪": "薮", "藴": "蕴", "藶": "苈", "藹": "蔼", "藺": "蔺", "蘄": "蕲", "蘆": "芦",
    "蘇": "苏", "蘊": "蕴", "蘋": "苹", "蘚": "藓", "蘞": "蔹", "蘢": "茏", "蘭": "兰",
    "蘺": "蓠", "蘿": "萝", "處": "处", "虛": "虚", "虜": "虏", "號": "号", "虧": "亏",
    "蛺": "蛱", "蛻": "蜕", "蜆": "蚬", "蝕": "蚀", "蝟": "猬", "蝦": "虾", "蝸": "蜗",
    "螄": "蛳", "螞": "蚂", "螢": "萤", "螻": "蝼", "螿": "螀", "蟄": "蛰", "蟈": "蝈",
    "蟎": "螨", "蟣": "虮", "蟬": "蝉", "蟯": "蛲", "蟲": "虫", "蟶": "蛏", "蟻": "蚁",
    "蠅": "蝇", "蠆": "虿", "蠍": "蝎", "蠐": "蛴", "蠑": "蝾", "蠟": "蜡", "蠣": "蛎",
    "蠱": "蛊", "蠶": "蚕", "蠻": "蛮", "衆": "众", "衊": "蔑", "術": "术", "衕": "同",
    "衚": "胡", "衛": "卫", "衝": "冲", "袞": "衮", "裊": "袅", "裏": "里", "補": "补",
    "裝": "装", "裡": "里", "製": "制", "複": "复", "褲": "裤", "褳": "裢", "褸": "褛",
    "褻": "亵", "襇": "裥", "襏": "袯", "襖": "袄", "襝": "裣", "襠": "裆", "襤": "褴",
    "襪": "袜", "襬": "摆", "襯": "衬", "襲": "袭", "見": "见", "覎": "觃", "規": "规",
    "覓": "觅", "視": "视", "覘": "觇", "覡": "觋", "覥": "觍", "覦": "觎", "親": "亲",
    "覬": "觊", "覯": "觏", "覲": "觐", "覷": "觑", "覺": "觉", "覽": "览", "觀": "观",
    "觴": "觞", "觸": "触", "訂": "订", "訃": "讣", "計": "计", "訊": "讯", "訌": "讧",
    "討": "讨", "訐": "讦", "訒": "讱", "訓": "训", "訕": "讪", "訖": "讫", "託": "托",
    "記": "记", "訛": "讹", "訝": "讶", "訟": "讼", "訢": "欣", "訣": "诀", "訥": "讷",
    "訩": "讻", "訪": "访", "設": "设", "許": "许", "訴": "诉", "訶": "诃", "診": "诊",
    "註": "注", "詁": "诂", "詆": "诋", "詎": "讵", "詐": "诈", "詒": "诒", "詔": "诏",
    "評": "评", "詖": "诐", "詗": "诇", "詘": "诎", "詛": "诅", "詞": "词", "詠": "咏",
    "詡": "诩", "詢": "询", "詣": "诣", "試": "试", "詩": "诗", "詫": "诧", "詬": "诟",
    "詭": "诡", "詮": "诠", "詰": "诘", "話": "话", "該": "该", "詳": "详", "詵": "诜",
    "詼": "诙", "詿": "诖", "誄": "诔", "誅": "诛", "誆": "诓", "誇": "夸", "誌": "志",
    "認": "认", "誑": "诳", "誒": "诶", "誕": "诞", "誘": "诱", "誚": "诮", "語": "语",
    "誠": "诚", "誡": "诫", "誣": "诬", "誤": "误", "誥": "诰", "誦": "诵", "誨": "诲",
    "說": "说", "説": "说", "誰": "谁", "課": "课", "誶": "谇", "誹": "诽", "誼": "谊",
    "調": "调", "諂": "谄", "諄": "谆", "談": "谈", "諉": "诿", "請": "请", "諍": "诤",
    "諏": "诹", "諑": "诼", "諒": "谅", "論": "论", "諗": "谂", "諛": "谀", "諜": "谍",
    "諞": "谝", "諡": "谥", "諢": "诨", "諤": "谔", "諦": "谛", "諧": "谐", "諫": "谏",
    "諭": "谕", "諮": "咨", "諱": "讳", "諳": "谙", "諶": "谌", "諷": "讽", "諸": "诸",
    "諺": "谚", "諼": "谖", "諾": "诺", "謀": "谋", "謁": "谒", "謂": "谓", "謄": "誊",
    "謅": "诌", "謊": "谎", "謎": "谜", "謐": "谧", "謔": "谑", "謖": "谡", "謗": "谤",
    "謙": "谦", "謚": "谥", "講": "讲", "謝": "谢", "謠": "谣", "謡": "谣", "謨": "谟",
    "謫": "谪", "謬": "谬", "謭": "谫", "謳": "讴", "謹": "谨", "謾": "谩", "譁": "哗",
    "證": "证", "譎": "谲", "譏": "讥", "譖": "谮", "識": "识", "譙": "谯", "譚": "谭",
    "譜": "谱", "譫": "谵", "譯": "译", "議": "议", "譴": "谴", "護": "护", "譸": "诪",
    "譽": "誉", "讀": "读", "變": "变", "讎": "雠", "讒": "谗", "讓": "让", "讕": "谰",
    "讖": "谶", "讚": "赞", "讞": "谳", "豈": "岂", "豎": "竖", "豐": "丰", "豬": "猪",
    "貓": "猫", "貝": "贝", "貞": "贞", "負": "负", "財": "财", "貢": "贡", "貧": "贫",
    "貨": "货", "販": "贩", "貪": "贪", "貫": "贯", "責": "责", "貯": "贮", "貰": "贳",
    "貲": "赀", "貳": "贰", "貴": "贵", "貶": "贬", "買": "买", "貸": "贷", "貺": "贶",
    "費": "费", "貼": "贴", "貽": "贻", "貿": "贸", "賀": "贺", "賁": "贲", "賂": "赂",
    "賃": "赁", "賄": "贿", "資": "资", "賈": "贾", "賊": "贼", "賑": "赈", "賒": "赊",
    "賓": "宾", "賕": "赇", "賙": "赒", "賚": "赉", "賜": "赐", "賞": "赏", "賠": "赔",
    "賡": "赓", "賢": "贤", "賣": "卖", "賤": "贱", "賦": "赋", "質": "质", "賬": "账",
    "賭": "赌", "賴": "赖", "賵": "赗", "賺": "赚", "購": "购", "賽": "赛", "贄": "贽",
    "贅": "赘", "贈": "赠", "贊": "赞", "贍": "赡", "贏": "赢", "贐": "赆", "贓": "赃",
    "贔": "赑", "贖": "赎", "贗": "赝", "贛": "赣", "趙": "赵", "趕": "赶", "趨": "趋",
    "跡": "迹", "踐": "践", "踴": "踊", "蹌": "跄", "蹕": "跸", "蹟": "迹", "蹣": "蹒",
    "蹤": "踪", "蹺": "跷", "躂": "跶", "躉": "趸", "躊": "踌", "躋": "跻", "躍": "跃",
    "躑": "踯", "躒": "跞", "躓": "踬", "躕": "蹰", "躚": "跹", "躡": "蹑", "躥": "蹿",
    "躦": "躜", "躪": "躏", "軀": "躯", "車": "车", "軋": "轧", "軌": "轨", "軍": "军",
    "軒": "轩", "軔": "轫", "軛": "轭", "軟": "软", "軤": "轷", "軫": "轸", "軲": "轱",
    "軸": "轴", "軹": "轵", "軺": "轺", "軻": "轲", "軼": "轶", "軾": "轼", "較": "较",
    "輅": "辂", "輇": "辁", "載": "载", "輊": "轾", "輒": "辄", "輓": "挽", "輔": "辅",
    "輕": "轻", "輛": "辆", "輜": "辎", "輝": "辉", "輞": "辋", "輟": "辍", "輥": "辊",
    "輦": "辇", "輩": "辈", "輪": "轮", "輯": "辑", "輳": "辏", "輸": "输", "輻": "辐",
    "輾": "辗", "輿": "舆", "轀": "辒", "轂": "毂", "轄": "辖", "轅": "辕", "轆": "辘",
    "轉": "转", "轍": "辙", "轎": "轿", "轔": "辚", "轟": "轰", "轡": "辔", "轢": "轹",
    "轤": "轳", "辦": "办", "辭": "辞", "辮": "辫", "辯": "辩", "農": "农", "迴": "回",
    "逕": "迳", "這": "这", "連": "连", "週": "周", "進": "进", "遊": "游", "運": "运",
    "過": "过", "達": "达", "違": "违", "遙": "遥", "遜": "逊", "遞": "递", "遠": "远",
    "適": "适", "遲": "迟", "遷": "迁", "選": "选", "遺": "遗", "遼": "辽", "邁": "迈",
    "還": "还", "邇": "迩", "邊": "边", "邏": "逻", "邐": "逦", "郟": "郏", "郵": "邮",
    "鄆": "郓", "鄉": "乡", "鄒": "邹", "鄔": "邬", "鄖": "郧", "鄧": "邓", "鄭": "郑",
    "鄰": "邻", "鄲": "郸", "鄴": "邺", "鄶": "郐", "鄺": "邝", "酇": "酂", "酈": "郦",
    "醞": "酝", "醬": "酱", "醫": "医", "醱": "酦", "釀": "酿", "釁": "衅", "釃": "酾",
    "釅": "酽", "釋": "释", "釐": "厘", "釒": "钅", "釓": "钆", "釔": "钇", "釕": "钌",
    "釗": "钊", "釘": "钉", "釙": "钋", "針": "针", "釣": "钓", "釤": "钐", "釦": "扣",
    "釧": "钏", "釩": "钒", "釵": "钗", "釷": "钍", "釹": "钕", "釺": "钎", "鈀": "钯",
    "鈁": "钫", "鈃": "钘", "鈄": "钭", "鈈": "钚", "鈉": "钠", "鈍": "钝", "鈎": "钩",
    "鈐": "钤", "鈑": "钣", "鈒": "钑", "鈔": "钞", "鈕": "钮", "鈞": "钧", "鈣": "钙",
    "鈥": "钬", "鈦": "钛", "鈧": "钪", "鈮": "铌", "鈰": "铈", "鈳": "钶", "鈴": "铃",
    "鈷": "钴", "鈸": "钹", "鈹": "铍", "鈺": "钰", "鈽": "钸", "鈾": "铀", "鈿": "钿",
    "鉀": "钾", "鉅": "钜", "鉈": "铊", "鉉": "铉", "鉋": "刨", "鉍": "铋", "鉑": "铂",
    "鉕": "钷", "鉗": "钳", "鉚": "铆", "鉛": "铅", "鉞": "钺", "鉢": "钵", "鉤": "钩",
    "鉦": "钲", "鉬": "钼", "鉭": "钽", "鉶": "铏", "鉸": "铰", "鉺": "铒", "鉻": "铬",
    "鉿": "铪", "銀": "银", "銃": "铳", "銅": "铜", "銍": "铚", "銑": "铣", "銓": "铨",
    "銖": "铢", "銘": "铭", "銚": "铫", "銛": "铦", "銜": "衔", "銠": "铑", "銣": "铷",
    "銥": "铱", "銦": "铟", "銨": "铵", "銩": "铥", "銪": "铕", "銫": "铯", "銬": "铐",
    "銱": "铞", "銳": "锐", "銷": "销", "銹": "锈", "銻": "锑", "銼": "锉", "鋁": "铝",
    "鋃": "锒", "鋅": "锌", "鋇": "钡", "鋌": "铤", "鋏": "铗", "鋒": "锋", "鋝": "锊",
    "鋟": "锓", "鋣": "铘", "鋤": "锄", "鋥": "锃", "鋦": "锔", "鋨": "锇", "鋩": "铓",
    "鋪": "铺", "鋭": "锐", "鋮": "铖", "鋯": "锆", "鋰": "锂", "鋱": "铽", "鋶": "锍",
    "鋸": "锯", "鋼": "钢", "錄": "录", "錆": "锖", "錇": "锫", "錈": "锩", "錏": "铔",
    "錐": "锥", "錒": "锕", "錕": "锟", "錘": "锤", "錙": "锱", "錚": "铮", "錛": "锛",
    "錟": "锬", "錠": "锭", "錡": "锜", "錢": "钱", "錦": "锦", "錨": "锚", "錫": "锡",
    "錮": "锢", "錯": "错", "錳": "锰", "錶": "表", "鍀": "锝", "鍁": "锨", "鍃": "锪",
    "鍆": "钔", "鍇": "锴", "鍈": "锳", "鍋": "锅", "鍍": "镀", "鍔": "锷", "鍘": "铡",
    "鍚": "钖", "鍛": "锻", "鍤": "锸", "鍥": "锲", "鍩": "锘", "鍬": "锹", "鍰": "锾",
    "鍵": "键", "鍶": "锶", "鍺": "锗", "鍾": "钟", "鎂": "镁", "鎄": "锿", "鎇": "镅",
    "鎊": "镑", "鎌": "镰", "鎔": "镕", "鎖": "锁", "鎘": "镉", "鎚": "锤", "鎛": "镈",
    "鎝": "𨱏", "鎡": "镃", "鎢": "钨", "鎣": "蓥", "鎦": "镏", "鎧": "铠", "鎩": "铩",
    "鎪": "锼", "鎬": "镐", "鎮": "镇", "鎰": "镒", "鎵": "镓", "鎶": "𫓹", "鎸": "镌",
    "鎿": "镎", "鏃": "镞", "鏈": "链", "鏇": "镟", "鏌": "镆", "鏍": "镙", "鏐": "镠",
    "鏑": "镝", "鏗": "铿", "鏘": "锵", "鏜": "镗", "鏝": "镘", "鏞": "镛", "鏟": "铲",
    "鏡": "镜", "鏢": "镖", "鏤": "镂", "鏨": "錾", "鏰": "镚", "鏵": "铧", "鏷": "镤",
    "鏹": "镪", "鏽": "锈", "鐃": "铙", "鐋": "铴", "鐐": "镣", "鐒": "铹", "鐓": "镦",
    "鐔": "镡", "鐘": "钟", "鐙": "镫", "鐠": "镨", "鐦": "锎", "鐧": "锏", "鐨": "镄",
    "鐫": "镌", "鐮": "镰", "鐲": "镯", "鐳": "镭", "鐵": "铁", "鐶": "镮", "鐸": "铎",
    "鐺": "铛", "鐿": "镱", "鑄": "铸", "鑊": "镬", "鑌": "镔", "鑒": "鉴", "鑔": "镲",
    "鑕": "锧", "鑞": "镴", "鑠": "铄", "鑣": "镳", "鑥": "镥", "鑭": "镧", "鑰": "钥",
    "鑱": "镵", "鑲": "镶", "鑷": "镊", "鑹": "镩", "鑼": "锣", "鑽": "钻", "鑾": "銮",
    "鑿": "凿", "長": "长", "門": "门", "閂": "闩", "閃": "闪", "閆": "闫", "閉": "闭",
    "開": "开", "閌": "闶", "閎": "闳", "閏": "闰", "閑": "闲", "間": "间", "閔": "闵",
    "閘": "闸", "閡": "阂", "閣": "阁", "閤": "合", "閥": "阀", "閨": "闺", "閩": "闽",
    "閫": "阃", "閬": "阆", "閭": "闾", "閱": "阅", "閲": "阅", "閶": "阊", "閹": "阉",
    "閻": "阎", "閼": "阏", "閽": "阍", "閾": "阈", "闃": "阒", "闆": "板", "闈": "闱",
    "闊": "阔", "闋": "阕", "闌": "阑", "闍": "阇", "闐": "阗", "闔": "阖", "闕": "阙",
    "闖": "闯", "關": "关", "闞": "阚", "闡": "阐", "闢": "辟", "闤": "阛", "阨": "厄",
    "陘": "陉", "陝": "陕", "陣": "阵", "陰": "阴", "陳": "陈", "陸": "陆", "陽": "阳",
    "隉": "陧", "隊": "队", "階": "阶", "隕": "陨", "際": "际", "隨": "随", "險": "险",
    "隱": "隐", "隴": "陇", "隸": "隶", "隻": "只", "雋": "隽", "雖": "虽", "雙": "双",
    "雛": "雏", "雜": "杂", "雞": "鸡", "離": "离", "難": "难", "雲": "云", "電": "电",
    "霧": "雾", "霽": "霁", "靂": "雳", "靄": "霭", "靈": "灵", "靚": "靓", "靜": "静",
    "靦": "腼", "鞏": "巩", "鞝": "绱", "鞦": "秋", "韁": "缰", "韃": "鞑", "韆": "千",
    "韉": "鞯", "韋": "韦", "韌": "韧", "韓": "韩", "韙": "韪", "韜": "韬", "韞": "韫",
    "韻": "韵", "響": "响", "頁": "页", "頂": "顶", "頃": "顷", "項": "项", "順": "顺",
    "須": "须", "頊": "顼", "頌": "颂", "預": "预", "頑": "顽", "頒": "颁", "頓": "顿",
    "頗": "颇", "領": "领", "頜": "颌", "頡": "颉", "頤": "颐", "頦": "颏", "頭": "头",
    "頮": "颒", "頰": "颊", "頲": "颋", "頴": "颕", "頷": "颔", "頸": "颈", "頹": "颓",
    "頻": "频", "顆": "颗", "題": "题", "額": "额", "顎": "颚", "顏": "颜", "顒": "颙",
    "顓": "颛", "願": "愿", "顙": "颡", "顛": "颠", "類": "类", "顢": "颟", "顥": "颢",
    "顧": "顾", "顫": "颤", "顬": "颥", "顯": "显", "顰": "颦", "顱": "颅", "顳": "颞",
    "顴": "颧", "風": "风", "颮": "飑", "颯": "飒", "颱": "台", "颳": "刮", "颶": "飓",
    "颺": "飏", "颻": "飖", "飄": "飘", "飆": "飙", "飛": "飞", "飢": "饥", "飯": "饭",
    "飲": "饮", "飴": "饴", "飼": "饲", "飽": "饱", "飾": "饰", "餃": "饺", "餅": "饼",
    "餉": "饷", "養": "养", "餌": "饵", "餎": "饹", "餏": "饻", "餑": "饽", "餒": "馁",
    "餓": "饿", "餘": "余", "餚": "肴", "餛": "馄", "餜": "馃", "餞": "饯", "餡": "馅",
    "館": "馆", "餱": "糇", "餳": "饧", "餵": "喂", "餶": "馉", "餷": "馇", "餺": "馎",
    "餼": "饩", "餾": "馏", "餿": "馊", "饁": "馌", "饃": "馍", "饅": "馒", "饈": "馐",
    "饉": "馑", "饊": "馓", "饋": "馈", "饌": "馔", "饎": "饩", "饑": "饥", "饒": "饶",
    "饗": "飨", "饜": "餍", "饞": "馋", "饢": "馕", "馬": "马", "馭": "驭", "馮": "冯",
    "馱": "驮", "馳": "驰", "馴": "驯", "駁": "驳", "駐": "驻", "駑": "驽", "駒": "驹",
    "駔": "驵", "駕": "驾", "駘": "骀", "駙": "驸", "駛": "驶", "駝": "驼", "駟": "驷",
    "駡": "骂", "駢": "骈", "駭": "骇", "駰": "骃", "駱": "骆", "駸": "骎", "駿": "骏",
    "騁": "骋", "騂": "骍", "騅": "骓", "騌": "骔", "騍": "骒", "騎": "骑", "騏": "骐",
    "騖": "骛", "騙": "骗", "騫": "骞", "騭": "骘", "騮": "骝", "騰": "腾", "騶": "驺",
    "騷": "骚", "騸": "骟", "騾": "骡", "驀": "蓦", "驁": "骜", "驂": "骖", "驃": "骠",
    "驄": "骢", "驅": "驱", "驊": "骅", "驌": "骕", "驍": "骁", "驏": "骣", "驕": "骄",
    "驗": "验", "驚": "惊", "驛": "驿", "驟": "骤", "驢": "驴", "驤": "骧", "驥": "骥",
    "驦": "骦", "驪": "骊", "骯": "肮", "髏": "髅", "髒": "脏", "體": "体", "髕": "髌",
    "髖": "髋", "鬆": "松", "鬍": "胡", "鬚": "须", "鬥": "斗", "鬧": "闹", "鬩": "阋",
    "鬱": "郁", "魎": "魉", "魘": "魇", "魚": "鱼", "魛": "鱽", "魟": "𫚉", "魢": "鱾",
    "魨": "鲀", "魯": "鲁", "魴": "鲂", "鮁": "鲅", "鮃": "鲆", "鮊": "鲌", "鮋": "鲉",
    "鮍": "鲏", "鮎": "鲇", "鮐": "鲐", "鮑": "鲍", "鮒": "鲋", "鮓": "鲊", "鮚": "鲒",
    "鮜": "鲘", "鮝": "鲞", "鮞": "鲕", "鮦": "鲖", "鮪": "鲔", "鮫": "鲛", "鮭": "鲑",
    "鮮": "鲜", "鮳": "鲓", "鮶": "鲪", "鮺": "鲝", "鯀": "鲧", "鯁": "鲠", "鯇": "鲩",
    "鯉": "鲤", "鯊": "鲨", "鯒": "鲬", "鯔": "鲻", "鯕": "鲯", "鯖": "鲭", "鯗": "鲞",
    "鯛": "鲷", "鯝": "鲴", "鯡": "鲱", "鯢": "鲵", "鯤": "鲲", "鯧": "鲳", "鯨": "鲸",
    "鯪": "鲮", "鯫": "鲰", "鯰": "鲶", "鯴": "鲺", "鯷": "鳀", "鯽": "鲫", "鯿": "鳊",
    "鰁": "鳈", "鰂": "鲗", "鰃": "鳂", "鰈": "鲽", "鰉": "鳇", "鰍": "鳅", "鰏": "鲾",
    "鰐": "鳄", "鰒": "鳆", "鰓": "鳃", "鰜": "鳒", "鰟": "鳑", "鰠": "鳋", "鰣": "鲥",
    "鰥": "鳏", "鰨": "鳎", "鰩": "鳐", "鰭": "鳍", "鰱": "鲢", "鰲": "鳌", "鰳": "鳓",
    "鰵": "鳘", "鰷": "鲦", "鰹": "鲣", "鰻": "鳗", "鰼": "鳛", "鰾": "鳔", "鱂": "鳉",
    "鱈": "鳕", "鱉": "鳖", "鱒": "鳟", "鱔": "鳝", "鱖": "鳜", "鱗": "鳞", "鱘": "鲟",
    "鱝": "鲼", "鱟": "鲎", "鱠": "鲙", "鱣": "鳣", "鱧": "鳢", "鱨": "鲿", "鱭": "鲚",
    "鱷": "鳄", "鱸": "鲈", "鱺": "鲡", "鳥": "鸟", "鳧": "凫", "鳩": "鸠", "鳲": "鸤",
    "鳳": "凤", "鳴": "鸣", "鳶": "鸢", "鴆": "鸩", "鴇": "鸨", "鴉": "鸦", "鴒": "鸰",
    "鴕": "鸵", "鴛": "鸳", "鴝": "鸲", "鴞": "鸮", "鴟": "鸱", "鴣": "鸪", "鴦": "鸯",
    "鴨": "鸭", "鴯": "鸸", "鴰": "鸹", "鴴": "鸻", "鴻": "鸿", "鴿": "鸽", "鵂": "鸺",
    "鵃": "鸼", "鵑": "鹃", "鵒": "鹆", "鵓": "鹁", "鵜": "鹈", "鵝": "鹅", "鵠": "鹄",
    "鵡": "鹉", "鵪": "鹌", "鵬": "鹏", "鵮": "鹐", "鵯": "鹎", "鵲": "鹊", "鶇": "鸫",
    "鶉": "鹑", "鶊": "鹒", "鶓": "鹋", "鶖": "鹙", "鶘": "鹕", "鶚": "鹗", "鶡": "鹖",
    "鶥": "鹛", "鶩": "鹜", "鶪": "䴗", "鶯": "莺", "鶲": "鹟", "鶴": "鹤", "鶹": "鹠",
    "鶺": "鹡", "鶻": "鹘", "鶼": "鹣", "鶿": "鹚", "鷁": "鹢", "鷂": "鹞", "鷈": "䴘",
    "鷓": "鹧", "鷖": "鹥", "鷗": "鸥", "鷙": "鸷", "鷚": "鹨", "鷥": "鸶", "鷦": "鹪",
    "鷯": "鹩", "鷲": "鹫", "鷳": "鹇", "鷸": "鹬", "鷹": "鹰", "鷺": "鹭", "鷽": "鸴",
    "鸇": "鹯", "鸊": "䴙", "鸌": "鹱", "鸕": "鸬", "鸚": "鹦", "鸛": "鹳", "鸝": "鹂",
    "鹵": "卤", "鹹": "咸", "鹺": "鹾", "鹼": "碱", "鹽": "盐", "麗": "丽", "麥": "麦",
    "麩": "麸", "麼": "么", "黃": "黄", "黌": "黉", "點": "点", "黨": "党", "黲": "黪",
    "黴": "霉", "黷": "黩", "黽": "黾", "鼇": "鳌", "鼉": "鼍", "鼴": "鼹", "齊": "齐",
    "齋": "斋", "齎": "赍", "齏": "齑", "齒": "齿", "齔": "龀", "齕": "龁", "齟": "龃",
    "齡": "龄", "齣": "出", "齦": "龈", "齧": "啮", "齪": "龊", "齬": "龉", "齲": "龋",
    "齶": "腭", "齷": "龌", "龍": "龙", "龐": "庞", "龔": "龚", "龕": "龛", "龜": "龟",
})


def _to_zh_hans(text: str) -> str:
    return str(text or "").translate(ZH_HANS_TRANS)


def _translate_chunk_with_minimax(texts: list[str], lang: str, api_key: str) -> list[str]:
    if not texts:
        return []
    lang_name = _lang_prompt_name(lang)
    prompt = (
        f"你是專業翻譯。把輸入 JSON 陣列每一項翻成 {lang_name}。\n"
        "規則：\n"
        "1) 不可遺漏項目，順序必須完全一致。\n"
        "2) 保留 URL、@帳號、#標籤、數字、時間、貨幣單位（如 USD/USDT/NTD）、專有名詞（Renaiss/SBT/BNB/Discord）。\n"
        "3) 不要加註解，不要加前後文。\n"
        "4) 只輸出 JSON 字串陣列。\n\n"
        f"source={json.dumps(texts, ensure_ascii=False)}"
    )
    raw = minimax_chat(prompt, api_key)
    parsed = _parse_json_array(raw)
    if len(parsed) != len(texts):
        return list(texts)
    return [str(x or "").strip() or texts[idx] for idx, x in enumerate(parsed)]


def _translate_texts(texts: list[str], lang: str) -> tuple[list[str], str]:
    tag = _normalize_lang_tag(lang)
    rows = [str(x or "").replace("\n", " ").strip()[:TRANSLATE_MAX_CHARS] for x in (texts or [])]
    if tag == "zh-Hant":
        return rows, "base"
    if tag == "zh-Hans":
        return [_to_zh_hans(x) for x in rows], "local"

    load_environment()
    api_key = resolve_minimax_key()

    out = list(rows)
    need_texts: list[str] = []
    if TRANSLATION_CACHE_ENABLED:
        with TRANSLATION_LOCK:
            _load_translation_cache_unlocked()
            cached = TRANSLATION_CACHE
            need_indices: list[int] = []
            for idx, text in enumerate(rows):
                if not text:
                    continue
                key = _translation_cache_key(tag, text)
                got = cached.get(key)
                if got and not (tag != "zh-Hant" and _contains_cjk(text) and str(got) == text):
                    out[idx] = got
                else:
                    if got and tag != "zh-Hant" and _contains_cjk(text) and str(got) == text:
                        cached.pop(key, None)
                    need_indices.append(idx)
                    need_texts.append(text)
            if not need_texts:
                return out, "cache"
    else:
        need_texts = [text for text in rows if text]

    if not api_key:
        return rows, "no-key"

    unique_texts: list[str] = []
    seen: set[str] = set()
    for text in need_texts:
        if text in seen:
            continue
        seen.add(text)
        unique_texts.append(text)

    translated_map: dict[str, str] = {}
    chunk_size = 3
    for start in range(0, len(unique_texts), chunk_size):
        chunk = unique_texts[start:start + chunk_size]
        try:
            translated = _translate_chunk_with_minimax(chunk, tag, api_key)
        except Exception:
            translated = list(chunk)
        for idx, original in enumerate(chunk):
            candidate = str(translated[idx] if idx < len(translated) else original).strip()
            translated_map[original] = candidate or original

    if TRANSLATION_CACHE_ENABLED:
        with TRANSLATION_LOCK:
            _load_translation_cache_unlocked()
            for original, translated in translated_map.items():
                if not original:
                    continue
                if tag != "zh-Hant" and _contains_cjk(original) and str(translated) == original:
                    continue
                key = _translation_cache_key(tag, original)
                TRANSLATION_CACHE[key] = translated
            global TRANSLATION_CACHE_DIRTY
            TRANSLATION_CACHE_DIRTY = True
            _flush_translation_cache_unlocked()

    final_rows = []
    for text in rows:
        if not text:
            final_rows.append("")
            continue
        final_rows.append(translated_map.get(text, text))
    return final_rows, "live"


def _is_feed_translatable_text(key: str, text: str) -> bool:
    name = str(key or "").strip()
    if not name or name in I18N_SKIP_KEYS:
        return False
    value = str(text or "")
    stripped = value.strip()
    if not stripped or len(stripped) < 2:
        return False
    if re.match(r"^https?://", stripped, re.I):
        return False
    if re.fullmatch(r"[@#:/\-\d\s.,()%+]+", stripped):
        return False
    if name in I18N_FEED_TEXT_KEYS:
        return True
    return _contains_cjk(stripped)


def _join_entry_path(parent: str, key: str) -> str:
    k = str(key or "").strip()
    if not parent:
        return k
    if not k:
        return parent
    return f"{parent}.{k}"


def _collect_feed_i18n_entries(node: object) -> list[tuple[str, str]]:
    out: list[tuple[str, str]] = []
    seen_keys: set[str] = set()
    max_targets = max(0, I18N_MAX_TARGET_TEXTS)
    max_list_items = max(0, I18N_MAX_LIST_ITEMS_PER_FIELD)

    def _is_full() -> bool:
        return bool(max_targets and len(out) >= max_targets)

    def _iter_limited(rows: list[object]) -> list[object]:
        return rows[:max_list_items] if max_list_items else rows

    def _push(entry_key: str, value: str, field_key: str = "") -> None:
        if _is_full():
            return
        key = str(entry_key or "").strip()
        text = str(value or "")
        if not key or key in seen_keys:
            return
        if not _is_feed_translatable_text(field_key, text):
            return
        seen_keys.add(key)
        out.append((key, text))

    def _collect_section(value: object, path: str, field_key: str = "") -> None:
        if _is_full():
            return
        if isinstance(value, str):
            _push(path, value, field_key)
            return
        if isinstance(value, list):
            for idx, item in enumerate(_iter_limited(value)):
                if _is_full():
                    break
                item_path = f"{path}[{idx}]"
                if isinstance(item, str):
                    _push(item_path, item, field_key)
                elif isinstance(item, (dict, list)):
                    _collect_section(item, item_path, field_key)
            return
        if isinstance(value, dict):
            for child_key, child in value.items():
                if _is_full():
                    break
                child_name = str(child_key or "").strip()
                if not child_name:
                    continue
                child_path = _join_entry_path(path, child_name)
                _collect_section(child, child_path, child_name)

    def _collect_card(card: dict[str, object], index: int) -> None:
        if _is_full():
            return
        card_key = _card_lookup_key(card, index)
        card_path = f"cards[{card_key}]"
        for key in I18N_CARD_TEXT_KEYS:
            value = card.get(key)
            if isinstance(value, str):
                _push(f"{card_path}.{key}", value, key)
        for key in I18N_FEED_LIST_KEYS:
            value = card.get(key)
            if not isinstance(value, list):
                continue
            for idx, item in enumerate(_iter_limited(value)):
                if isinstance(item, str):
                    _push(f"{card_path}.{key}[{idx}]", item, key)
        facts = card.get("event_facts")
        if isinstance(facts, dict):
            for fact_key in I18N_CARD_FACT_TEXT_KEYS:
                raw = facts.get(fact_key)
                if isinstance(raw, str):
                    _push(f"{card_path}.event_facts.{fact_key}", raw, fact_key)

    if not isinstance(node, dict):
        return out
    for root_key in I18N_VISIBLE_FEED_ROOT_KEYS:
        if _is_full():
            break
        raw = node.get(root_key)
        if raw is None:
            continue
        _collect_section(raw, root_key, root_key)
    cards = node.get("cards")
    if not isinstance(cards, list):
        return out
    for idx, item in enumerate(_iter_limited(cards)):
        if not isinstance(item, dict):
            continue
        _collect_card(item, idx)
    return out


def _collect_feed_i18n_strings(node: object) -> list[str]:
    return [text for _, text in _collect_feed_i18n_entries(node)]


def _entry_map_from_node(node: object) -> dict[str, str]:
    out: dict[str, str] = {}
    for entry_key, value in _collect_feed_i18n_entries(node):
        key = str(entry_key or "").strip()
        if not key:
            continue
        out[key] = str(value or "")
    return out


def _apply_feed_translation(node: object, mapping: dict[str, str]) -> object:
    def _walk(value: object, parent_key: str = "", path: str = "") -> object:
        if isinstance(value, dict):
            out: dict[str, object] = {}
            for key, child in value.items():
                k = str(key or "")
                next_path = _join_entry_path(path, k)
                if isinstance(child, str):
                    if _is_feed_translatable_text(k, child):
                        out[key] = mapping.get(next_path, child)
                    else:
                        out[key] = child
                elif isinstance(child, list):
                    if k == "cards":
                        rows: list[object] = []
                        for idx, item in enumerate(child):
                            item_key = str(idx)
                            if isinstance(item, dict):
                                item_key = _card_lookup_key(item, idx)
                            card_path = f"{next_path}[{item_key}]"
                            if isinstance(item, str):
                                if _is_feed_translatable_text(k, item):
                                    rows.append(mapping.get(card_path, item))
                                else:
                                    rows.append(item)
                            elif isinstance(item, (dict, list)):
                                rows.append(_walk(item, k, card_path))
                            else:
                                rows.append(item)
                        out[key] = rows
                    elif k in I18N_FEED_LIST_KEYS:
                        rows: list[object] = []
                        for idx, item in enumerate(child):
                            item_path = f"{next_path}[{idx}]"
                            if isinstance(item, str):
                                if _is_feed_translatable_text(k, item):
                                    rows.append(mapping.get(item_path, item))
                                else:
                                    rows.append(item)
                            elif isinstance(item, (dict, list)):
                                rows.append(_walk(item, k, item_path))
                            else:
                                rows.append(item)
                        out[key] = rows
                    else:
                        out[key] = _walk(child, k, next_path)
                elif isinstance(child, dict):
                    out[key] = _walk(child, k, next_path)
                else:
                    out[key] = child
            return out
        if isinstance(value, list):
            rows: list[object] = []
            for idx, item in enumerate(value):
                item_path = f"{path}[{idx}]" if path else f"[{idx}]"
                if isinstance(item, str):
                    if _is_feed_translatable_text(parent_key, item):
                        rows.append(mapping.get(item_path, item))
                    else:
                        rows.append(item)
                elif isinstance(item, (dict, list)):
                    rows.append(_walk(item, parent_key, item_path))
                else:
                    rows.append(item)
            return rows
        return value

    return _walk(node)


def _build_best_effort_mapping(feed: dict[str, object], lang: str) -> tuple[dict[str, str], dict[str, object]]:
    tag = _normalize_lang_tag(lang)
    entries = _collect_feed_i18n_entries(feed)
    mapping: dict[str, str] = {}
    total = len(entries)
    translated = 0
    if tag == "zh-Hant":
        for entry_key, text in entries:
            mapping[entry_key] = text
        return mapping, {"lang": tag, "total": total, "translated": total, "coverage": 1.0, "mode": "base-cache"}
    if tag == "zh-Hans":
        for entry_key, text in entries:
            out = _to_zh_hans(text)
            mapping[entry_key] = out
        return mapping, {"lang": tag, "total": total, "translated": total, "coverage": 1.0, "mode": "local"}

    if not TRANSLATION_CACHE_ENABLED:
        for entry_key, text in entries:
            mapping[entry_key] = text
        return mapping, {
            "lang": tag,
            "total": total,
            "translated": 0,
            "coverage": 0.0 if total else 1.0,
            "mode": "no-cache-best-effort",
        }

    with TRANSLATION_LOCK:
        _load_translation_cache_unlocked()
        cache = TRANSLATION_CACHE
        dirty = False
        for entry_key, text in entries:
            key = _translation_cache_key(tag, text, entry_key=entry_key)
            out = str(cache.get(key) or "").strip()
            if not out:
                out = str(cache.get(_translation_cache_key_legacy(tag, text)) or "").strip()
            if out and _is_untranslated_for_lang(text, out, tag):
                cache.pop(key, None)
                cache.pop(_translation_cache_key_legacy(tag, text), None)
                out = ""
                dirty = True
            if out:
                mapping[entry_key] = out
                translated += 1
            else:
                mapping[entry_key] = text
        if dirty:
            global TRANSLATION_CACHE_DIRTY
            TRANSLATION_CACHE_DIRTY = True
            _flush_translation_cache_unlocked()
    coverage = round((translated / total), 4) if total else 1.0
    return mapping, {
        "lang": tag,
        "total": total,
        "translated": translated,
        "coverage": coverage,
        "mode": "cache-best-effort",
    }


def _best_effort_localized_feed(feed: dict[str, object], lang: str) -> tuple[dict[str, object], dict[str, object]]:
    tag = _normalize_lang_tag(lang)
    mapping, qa = _build_best_effort_mapping(feed, tag)
    localized = _apply_feed_translation(copy.deepcopy(feed), mapping)
    if isinstance(localized, dict):
        localized["lang"] = tag
        return localized, qa
    out = copy.deepcopy(feed)
    out["lang"] = tag
    return out, qa


def _card_lookup_key(card: dict[str, object], index: int) -> str:
    for key in ("id", "url", "_card_key"):
        value = str(card.get(key) or "").strip()
        if value:
            return value
    account = str(card.get("account") or "").strip().lower()
    published = str(card.get("published_at") or "").strip()
    title = str(card.get("title") or "").strip()
    return f"{account}|{published}|{title}|{index}"


def _iter_card_translatable_pairs(
    base_card: dict[str, object] | None,
    localized_card: dict[str, object],
) -> list[tuple[str, str]]:
    pairs: list[tuple[str, str]] = []
    src_card = base_card if isinstance(base_card, dict) else localized_card
    dst_card = localized_card if isinstance(localized_card, dict) else {}

    for key in I18N_CARD_TEXT_KEYS:
        source = str(src_card.get(key) or "").strip()
        translated = str(dst_card.get(key) or "").strip()
        if not source and not translated:
            continue
        pairs.append((source, translated))

    for key in I18N_FEED_LIST_KEYS:
        src_rows = src_card.get(key) if isinstance(src_card.get(key), list) else []
        dst_rows = dst_card.get(key) if isinstance(dst_card.get(key), list) else []
        total = max(len(src_rows), len(dst_rows))
        for idx in range(total):
            source = str(src_rows[idx] or "").strip() if idx < len(src_rows) else ""
            translated = str(dst_rows[idx] or "").strip() if idx < len(dst_rows) else ""
            if not source and not translated:
                continue
            pairs.append((source, translated))

    src_facts = src_card.get("event_facts") if isinstance(src_card.get("event_facts"), dict) else {}
    dst_facts = dst_card.get("event_facts") if isinstance(dst_card.get("event_facts"), dict) else {}
    for fact_key in I18N_CARD_FACT_TEXT_KEYS:
        source = str(src_facts.get(fact_key) or "").strip()
        translated = str(dst_facts.get(fact_key) or "").strip()
        if not source and not translated:
            continue
        pairs.append((source, translated))
    return pairs


def _card_has_target_text(
    card: dict[str, object],
    lang: str,
    base_card: dict[str, object] | None = None,
) -> bool:
    tag = _normalize_lang_tag(lang)
    if tag in {"en", "ko"}:
        src_card = base_card if isinstance(base_card, dict) else card
        # EN/KO publish gating is based on core card text only.
        # Secondary lists/facts may lag and should not hide the entire card.
        for key in I18N_CARD_TEXT_KEYS:
            source = str(src_card.get(key) or "").strip()
            translated = str(card.get(key) or "").strip()
            if not source and not translated:
                continue
            if _is_untranslated_for_lang(source, translated, tag):
                return False
        return True
    if tag in {"zh-Hant", "zh-Hans"}:
        if tag == "zh-Hans":
            sample = " ".join(str(card.get(k) or "") for k in I18N_CARD_TEXT_KEYS).strip()
            if not sample:
                return True
            return _to_zh_hans(sample) == sample
        return True
    sample = " ".join(str(card.get(k) or "") for k in I18N_CARD_TEXT_KEYS).strip()
    if not sample:
        return True
    return _looks_translated_for_lang(sample, tag)


def _apply_card_level_fallback(
    *,
    base_feed: dict[str, object],
    localized_feed: dict[str, object],
    lang: str,
    mode: str,
) -> tuple[dict[str, object], dict[str, int]]:
    base_cards = base_feed.get("cards")
    localized_cards = localized_feed.get("cards")
    if not isinstance(base_cards, list) or not isinstance(localized_cards, list):
        return localized_feed, {"total": 0, "ready": 0, "fallback": 0, "hidden": 0}

    normalized_mode = "hide" if str(mode or "").strip().lower() == "hide" else "base"
    base_index: dict[str, dict[str, object]] = {}
    localized_index: dict[str, dict[str, object]] = {}
    for idx, raw in enumerate(base_cards):
        if not isinstance(raw, dict):
            continue
        base_index[_card_lookup_key(raw, idx)] = raw

    for idx, raw in enumerate(localized_cards):
        if not isinstance(raw, dict):
            continue
        key = _card_lookup_key(raw, idx)
        if key not in localized_index:
            localized_index[key] = raw

    out_cards: list[dict[str, object]] = []
    total = 0
    ready = 0
    fallback = 0
    hidden = 0
    for idx, base_raw in enumerate(base_cards):
        if not isinstance(base_raw, dict):
            continue
        key = _card_lookup_key(base_raw, idx)
        localized_raw = localized_index.get(key)
        total += 1
        if isinstance(localized_raw, dict) and _card_has_target_text(localized_raw, lang, base_card=base_raw):
            ready += 1
            out_cards.append(localized_raw)
            continue
        if normalized_mode == "hide":
            hidden += 1
            continue
        base_card = base_index.get(key)
        if isinstance(base_card, dict):
            fallback += 1
            out_cards.append(copy.deepcopy(base_card))
        else:
            hidden += 1

    out = copy.deepcopy(localized_feed)
    out["cards"] = out_cards
    out["total_cards"] = len(out_cards)
    return out, {"total": total, "ready": ready, "fallback": fallback, "hidden": hidden}


def _resolve_card_fallback_mode(lang: str) -> str:
    # Card-level publishing mode:
    # - hide: only show cards already translated for target language
    # - base: show translated cards first, fallback to base-language card per item
    if _normalize_lang_tag(lang) in {"en", "ko"}:
        return "hide"
    raw_mode = str(I18N_FEED_FALLBACK_MODE or "base").strip().lower()
    if raw_mode in {"hide", "strict", "translated-only"}:
        return "hide"
    return "base"


def _fallback_feed_from_base(feed: dict[str, object], lang: str, reason: str) -> dict[str, object]:
    out = copy.deepcopy(feed)
    out["lang"] = _normalize_lang_tag(lang)
    out["_i18n"] = {
        "mode": "base-fallback",
        "source_generated_at": str(feed.get("generated_at") or ""),
        "qa": {"coverage": 0.0, "reason": reason},
        "state": _i18n_state_snapshot(),
    }
    return out


def _is_untranslated_for_lang(source: str, translated: str, lang: str) -> bool:
    src = str(source or "").strip()
    dst = str(translated or "").strip()
    if _normalize_lang_tag(lang) in {"en", "ko"}:
        if not dst:
            return True
        # For EN/KO, reject CJK echo outputs so they don't poison cache.
        if _contains_cjk(dst):
            return True
        # Source has CJK but output still doesn't look like target language.
        if _contains_cjk(src) and not _looks_translated_for_lang(dst, _normalize_lang_tag(lang)):
            return True
        return False
    return False


def _translate_chunk_agent(texts: list[str], lang: str, api_key: str, strict: bool = False) -> list[str]:
    if not texts:
        return []
    lang_name = _lang_prompt_name(lang)
    strict_rules = (
        "5) 若原文是中文，目標語言是 English/한국어 時，不可原樣保留中文，必須翻譯。\n"
        if strict
        else ""
    )
    prompt = (
        f"你是高精度翻譯代理。把輸入 JSON 陣列每一項翻成 {lang_name}。\n"
        "規則：\n"
        "1) 順序完全一致，不可遺漏。\n"
        "2) 保留 URL、@帳號、#標籤、數字、時間、貨幣與專有名詞（Renaiss/SBT/BNB/Discord）。\n"
        "3) 只輸出 JSON 字串陣列，不要任何說明。\n"
        "4) 語意必須完整，不可只翻一半。\n"
        f"{strict_rules}\n"
        f"source={json.dumps(texts, ensure_ascii=False)}"
    )
    max_attempts = int(str(os.getenv("I18N_TRANSLATE_RETRY_MAX_ATTEMPTS") or "0") or 0)
    retry_forever = str(os.getenv("I18N_TRANSLATE_RETRY_FOREVER", "1") or "1").strip().lower() in {"1", "true", "yes", "on"}
    attempt = 0
    while True:
        attempt += 1
        raw = minimax_chat(prompt, api_key)
        parsed = _parse_json_array(raw)
        if len(parsed) == len(texts):
            return [str(x or "").strip() or texts[idx] for idx, x in enumerate(parsed)]
        if not retry_forever and max_attempts > 0 and attempt >= max_attempts:
            raise RuntimeError(f"translate chunk parse mismatch: expected={len(texts)} got={len(parsed)}")
        time.sleep(min(12.0, 1.2 * (1.5 ** max(0, attempt - 1))))


def _translate_feed_text_map(
    entries: list[tuple[str, str]] | list[str],
    lang: str,
    previous_source_map: dict[str, str] | None = None,
    previous_translation_map: dict[str, str] | None = None,
) -> tuple[dict[str, str], dict[str, object]]:
    tag = _normalize_lang_tag(lang)
    normalized_entries: list[tuple[str, str]] = []
    seen_entry_keys: set[str] = set()
    if entries and isinstance(entries[0], str):  # backward-compatible input
        for idx, text in enumerate(entries):  # type: ignore[arg-type]
            source = str(text or "")
            if not source:
                continue
            entry_key = f"legacy[{idx}]"
            if entry_key in seen_entry_keys:
                continue
            seen_entry_keys.add(entry_key)
            normalized_entries.append((entry_key, source))
    else:
        for row in entries:  # type: ignore[assignment]
            if not isinstance(row, (list, tuple)) or len(row) < 2:
                continue
            entry_key = str(row[0] or "").strip()
            source = str(row[1] or "")
            if not entry_key or not source or entry_key in seen_entry_keys:
                continue
            seen_entry_keys.add(entry_key)
            normalized_entries.append((entry_key, source))

    mapping = {entry_key: source for entry_key, source in normalized_entries}
    qa: dict[str, object] = {
        "lang": tag,
        "total": len(normalized_entries),
        "cached_hits": 0,
        "reused_hits": 0,
        "pending_count": 0,
        "translated": 0,
        "coverage": 0.0,
        "mode": "base" if tag == "zh-Hant" else "pending",
        "sample_pending": [],
    }
    if tag == "zh-Hant" or not normalized_entries:
        qa["translated"] = len(normalized_entries)
        qa["coverage"] = 1.0
        _set_i18n_lang_progress(
            tag,
            total=len(normalized_entries),
            done=len(normalized_entries),
            status="ok",
            mode=str(qa["mode"]),
            sample_pending=[],
        )
        return mapping, qa
    if tag == "zh-Hans":
        mapping = {entry_key: _to_zh_hans(source) for entry_key, source in normalized_entries}
        qa["mode"] = "local"
        qa["translated"] = len(normalized_entries)
        qa["coverage"] = 1.0
        _set_i18n_lang_progress(
            tag,
            total=len(normalized_entries),
            done=len(normalized_entries),
            status="ok",
            mode="local",
            sample_pending=[],
        )
        return mapping, qa

    prev_sources = previous_source_map if isinstance(previous_source_map, dict) else {}
    prev_translations = previous_translation_map if isinstance(previous_translation_map, dict) else {}

    pending_entries: list[tuple[str, str]] = []
    reused_hits = 0
    for entry_key, source in normalized_entries:
        old_source = str(prev_sources.get(entry_key) or "")
        old_translated = str(prev_translations.get(entry_key) or "").strip()
        if old_translated and not _is_untranslated_for_lang(source, old_translated, tag):
            # Normal incremental reuse: source text unchanged.
            if old_source and old_source == source:
                mapping[entry_key] = old_translated
                reused_hits += 1
                continue
            # Legacy migration path: old bundles may not have `sources`.
            if not old_source:
                mapping[entry_key] = old_translated
                reused_hits += 1
                continue
        pending_entries.append((entry_key, source))
    qa["reused_hits"] = reused_hits
    qa["cached_hits"] = reused_hits

    if TRANSLATION_CACHE_ENABLED:
        with TRANSLATION_LOCK:
            _load_translation_cache_unlocked()
            migrated = False
            pending_next: list[tuple[str, str]] = []
            for entry_key, source in pending_entries:
                key = _translation_cache_key(tag, source, entry_key=entry_key)
                legacy_key = _translation_cache_key_legacy(tag, source)
                cached = str(TRANSLATION_CACHE.get(key) or "").strip()
                if not cached:
                    legacy = str(TRANSLATION_CACHE.get(legacy_key) or "").strip()
                    if legacy:
                        cached = legacy
                        TRANSLATION_CACHE[key] = legacy
                        migrated = True
                if cached and _is_untranslated_for_lang(source, cached, tag):
                    TRANSLATION_CACHE.pop(key, None)
                    TRANSLATION_CACHE.pop(legacy_key, None)
                    cached = ""
                    migrated = True
                if cached:
                    mapping[entry_key] = cached
                    qa["cached_hits"] = int(qa.get("cached_hits") or 0) + 1
                    continue
                pending_next.append((entry_key, source))
            if migrated:
                global TRANSLATION_CACHE_DIRTY
                TRANSLATION_CACHE_DIRTY = True
                _flush_translation_cache_unlocked()
        pending_entries = pending_next

    translated_count = len(normalized_entries) - len(pending_entries)
    qa["translated"] = translated_count
    qa["pending_count"] = len(pending_entries)
    pending_entry_keys = [entry_key for entry_key, _ in pending_entries]
    if not pending_entries:
        qa["mode"] = "reused" if int(qa.get("reused_hits") or 0) > 0 else "cache"
        qa["coverage"] = 1.0
        _set_i18n_lang_progress(
            tag,
            total=len(normalized_entries),
            done=len(normalized_entries),
            status="ok",
            mode=str(qa.get("mode") or "cache"),
            sample_pending=[],
        )
        return mapping, qa

    load_environment()
    api_key = resolve_minimax_key()
    if not api_key:
        still_pending: list[tuple[str, str]] = []
        reused_without_key = 0
        for entry_key, source in pending_entries:
            old_translated = str(prev_translations.get(entry_key) or "").strip()
            if old_translated and not _is_untranslated_for_lang(source, old_translated, tag):
                mapping[entry_key] = old_translated
                reused_without_key += 1
            else:
                still_pending.append((entry_key, source))
        if reused_without_key:
            qa["reused_hits"] = int(qa.get("reused_hits") or 0) + reused_without_key
            qa["cached_hits"] = int(qa.get("cached_hits") or 0) + reused_without_key
            translated_count += reused_without_key
            pending_entries = still_pending
            qa["translated"] = translated_count
            qa["pending_count"] = len(pending_entries)
        qa["mode"] = "no-key"
        qa["coverage"] = round((translated_count / len(normalized_entries)), 4) if normalized_entries else 1.0
        qa["sample_pending"] = [entry_key for entry_key, _ in pending_entries[:8]]
        _set_i18n_lang_progress(
            tag,
            total=len(normalized_entries),
            done=translated_count,
            status="pending",
            mode="no-key",
            error="missing MiniMax key",
            sample_pending=[str(x) for x in qa["sample_pending"] if str(x).strip()][:8],
        )
        return mapping, qa

    entry_keys_by_source: dict[str, list[str]] = {}
    for entry_key, source in pending_entries:
        entry_keys_by_source.setdefault(source, []).append(entry_key)
    pending_texts = list(entry_keys_by_source.keys())

    chunk_size = I18N_FEED_CHUNK_SIZE
    done_count = translated_count
    unresolved_keys: set[str] = set()
    _set_i18n_lang_progress(
        tag,
        total=len(normalized_entries),
        done=done_count,
        status="running",
        mode="live",
        sample_pending=pending_entry_keys[:8],
    )
    for start in range(0, len(pending_texts), chunk_size):
        chunk = pending_texts[start:start + chunk_size]
        translated = _translate_chunk_agent(chunk, tag, api_key, strict=False)
        for idx, source in enumerate(chunk):
            candidate = str(translated[idx] if idx < len(translated) else source).strip() or source
            source_entry_keys = entry_keys_by_source.get(source, [])
            if _is_untranslated_for_lang(source, candidate, tag):
                for entry_key in source_entry_keys:
                    old_translated = str(prev_translations.get(entry_key) or "").strip()
                    if old_translated and not _is_untranslated_for_lang(source, old_translated, tag):
                        mapping[entry_key] = old_translated
                        done_count += 1
                        qa["reused_hits"] = int(qa.get("reused_hits") or 0) + 1
                        qa["cached_hits"] = int(qa.get("cached_hits") or 0) + 1
                    else:
                        mapping[entry_key] = source
                        unresolved_keys.add(entry_key)
                continue
            for entry_key in source_entry_keys:
                mapping[entry_key] = candidate
                done_count += 1
        _set_i18n_lang_progress(
            tag,
            total=len(normalized_entries),
            done=done_count,
            status="running",
            mode="live",
            sample_pending=pending_entry_keys[:8],
        )

    if TRANSLATION_CACHE_ENABLED:
        with TRANSLATION_LOCK:
            _load_translation_cache_unlocked()
            cache_updated = False
            for entry_key, source in pending_entries:
                if entry_key in unresolved_keys:
                    continue
                translated = str(mapping.get(entry_key, source) or "").strip() or source
                if _is_untranslated_for_lang(source, translated, tag):
                    continue
                key = _translation_cache_key(tag, source, entry_key=entry_key)
                if TRANSLATION_CACHE.get(key) != translated:
                    TRANSLATION_CACHE[key] = translated
                    cache_updated = True
            if cache_updated:
                TRANSLATION_CACHE_DIRTY = True
                _flush_translation_cache_unlocked()

    if unresolved_keys:
        translated_final = max(0, len(normalized_entries) - len(unresolved_keys))
        pending_final = len(unresolved_keys)
        qa["mode"] = "live-partial"
        qa["translated"] = translated_final
        qa["pending_count"] = pending_final
        qa["coverage"] = round((translated_final / len(normalized_entries)), 4) if normalized_entries else 1.0
        qa["sample_pending"] = sorted(list(unresolved_keys))[:8]
        _set_i18n_lang_progress(
            tag,
            total=len(normalized_entries),
            done=translated_final,
            status="pending",
            mode="live-partial",
            sample_pending=sorted(list(unresolved_keys))[:8],
        )
        return mapping, qa

    qa["mode"] = "live"
    qa["translated"] = len(normalized_entries)
    qa["pending_count"] = 0
    qa["coverage"] = 1.0
    _set_i18n_lang_progress(
        tag,
        total=len(normalized_entries),
        done=len(normalized_entries),
        status="ok",
        mode="live",
        sample_pending=[],
    )
    return mapping, qa


def _write_i18n_feed_bundle(bundle: dict[str, object]) -> None:
    I18N_FEED_PATH.parent.mkdir(parents=True, exist_ok=True)
    I18N_FEED_PATH.write_text(json.dumps(bundle, ensure_ascii=False, indent=2), encoding="utf-8")


def _load_i18n_feed_bundle() -> dict[str, object]:
    if not I18N_FEED_PATH.exists():
        return {}
    try:
        raw = json.loads(I18N_FEED_PATH.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return raw if isinstance(raw, dict) else {}


def _i18n_state_snapshot() -> dict[str, object]:
    def _progress_from_qa(tag: str, qa: dict[str, object], total_hint: int) -> dict[str, object]:
        total = _safe_int(qa.get("total"), total_hint)
        if total <= 0:
            total = total_hint
        translated = _safe_int(qa.get("translated"), _safe_int(qa.get("done"), 0))
        if translated <= 0 and total > 0:
            coverage = float(qa.get("coverage") or 0.0) if qa else 0.0
            translated = int(round(total * max(0.0, min(1.0, coverage))))
        done = total if tag == "zh-Hant" else min(total, max(0, translated))
        status = "ok" if done >= total else "pending"
        mode = str(qa.get("mode") or ("base" if tag == "zh-Hant" else ""))
        percent = 100 if total == 0 and status == "ok" else round((done / total) * 100, 1) if total else 0
        safe_done = min(done, total)
        return {
            "lang": tag,
            "status": status,
            "mode": mode,
            "total": total,
            "done": safe_done,
            "remaining": max(0, total - safe_done),
            "cached_hits": _safe_int(qa.get("cached_hits"), 0),
            "pending_count": _safe_int(qa.get("pending_count"), max(0, total - safe_done)),
            "sample_pending": [str(x) for x in (qa.get("sample_pending") or []) if str(x).strip()][:8],
            "percent": percent,
            "error": "",
            "updated_at": str(qa.get("updated_at") or ""),
        }

    with I18N_STATE_LOCK:
        state = copy.deepcopy(I18N_BUILD_STATE)
    progress = state.get("lang_progress")
    if not isinstance(progress, dict):
        progress = {}
    else:
        progress = copy.deepcopy(progress)

    bundle = _load_i18n_feed_bundle()
    qa_rows = bundle.get("qa") if isinstance(bundle.get("qa"), dict) else {}
    total_hint = _safe_int(bundle.get("targets_count"), 0)
    if total_hint <= 0:
        try:
            feed = _read_feed_snapshot()
            total_hint = len(_collect_feed_i18n_strings(feed)) if isinstance(feed, dict) else 0
        except Exception:
            total_hint = 0

    for tag in I18N_TARGET_LANGS:
        if tag == "zh-Hant":
            progress[tag] = {
                "lang": tag,
                "status": "ok",
                "mode": "base",
                "total": total_hint,
                "done": total_hint,
                "remaining": 0,
                "cached_hits": total_hint,
                "pending_count": 0,
                "sample_pending": [],
                "percent": 100 if total_hint else 100,
                "error": "",
                "updated_at": "",
            }
            continue
        qa = qa_rows.get(tag) if isinstance(qa_rows.get(tag), dict) else {}
        if qa and tag not in progress:
            progress[tag] = _progress_from_qa(tag, qa, total_hint)
        elif qa and str(progress.get(tag, {}).get("status") or "").lower() not in {"running", "queued"}:
            progress[tag] = _progress_from_qa(tag, qa, total_hint)
        elif tag not in progress:
            progress[tag] = {
                "lang": tag,
                "status": "pending",
                "mode": "",
                "total": total_hint,
                "done": 0,
                "remaining": total_hint,
                "cached_hits": 0,
                "pending_count": total_hint,
                "sample_pending": [],
                "percent": 0,
                "error": "",
                "updated_at": "",
            }

    state["lang_progress"] = progress
    state["target_langs"] = list(I18N_TARGET_LANGS)
    if isinstance(bundle.get("langs"), dict):
        state["langs"] = sorted([str(x) for x in bundle["langs"].keys() if str(x).strip()])
    current_status = str(state.get("status") or "").strip().lower()
    if current_status not in {"running", "queued", "failed"}:
        non_base_rows = [
            progress.get(tag)
            for tag in I18N_TARGET_LANGS
            if tag != "zh-Hant" and isinstance(progress.get(tag), dict)
        ]
        non_base_statuses = [str(row.get("status") or "").strip().lower() for row in non_base_rows]
        if non_base_statuses:
            if any(st in {"running", "queued"} for st in non_base_statuses):
                state["status"] = "running"
            elif any(st == "failed" for st in non_base_statuses):
                state["status"] = "failed"
            elif any(st in {"pending"} for st in non_base_statuses):
                state["status"] = "pending"
            elif all(st == "ok" for st in non_base_statuses):
                state["status"] = "ok"
    return state


def _set_i18n_lang_progress(
    lang: str,
    *,
    total: int,
    done: int,
    status: str,
    mode: str = "",
    error: str = "",
    sample_pending: list[str] | None = None,
) -> None:
    tag = _normalize_lang_tag(lang)
    safe_total = max(0, int(total or 0))
    safe_done = min(max(0, int(done or 0)), safe_total)
    percent = 100 if safe_total == 0 and status == "ok" else round((safe_done / safe_total) * 100, 1) if safe_total else 0
    with I18N_STATE_LOCK:
        progress = I18N_BUILD_STATE.get("lang_progress")
        if not isinstance(progress, dict):
            progress = {}
            I18N_BUILD_STATE["lang_progress"] = progress
        current = progress.get(tag) if isinstance(progress.get(tag), dict) else {}
        existing_pending = [str(x) for x in (current.get("sample_pending") or []) if str(x).strip()] if isinstance(current, dict) else []
        sample_rows = [str(x) for x in (sample_pending or []) if str(x).strip()][:8] if sample_pending is not None else existing_pending[:8]
        progress[tag] = {
            "lang": tag,
            "status": status,
            "mode": mode,
            "total": safe_total,
            "done": safe_done,
            "remaining": max(0, safe_total - safe_done),
            "cached_hits": _safe_int(progress.get(tag, {}).get("cached_hits"), 0) if isinstance(progress.get(tag), dict) else 0,
            "pending_count": max(0, safe_total - safe_done),
            "sample_pending": sample_rows,
            "percent": percent,
            "error": str(error or ""),
            "updated_at": _now_iso(),
        }


def _is_active_i18n_source(source_generated_at: str) -> bool:
    expected = str(source_generated_at or "").strip()
    if not expected:
        return True
    with I18N_STATE_LOCK:
        status = str(I18N_BUILD_STATE.get("status") or "").strip().lower()
        current = str(I18N_BUILD_STATE.get("source_generated_at") or "").strip()
    if status != "running":
        return True
    return current == expected


def _build_i18n_feed_bundle(
    feed: dict[str, object],
    force: bool = False,
    target_langs: list[str] | tuple[str, ...] | None = None,
) -> dict[str, object]:
    src_generated = str(feed.get("generated_at") or "").strip()
    request_langs = [
        _normalize_lang_tag(x)
        for x in (target_langs or I18N_TARGET_LANGS)
        if str(x or "").strip()
    ]
    seen_langs: set[str] = set()
    normalized_targets: list[str] = []
    for tag in request_langs:
        if tag in seen_langs:
            continue
        seen_langs.add(tag)
        normalized_targets.append(tag)
    if not normalized_targets:
        normalized_targets = list(I18N_TARGET_LANGS)
    if "zh-Hant" not in normalized_targets:
        normalized_targets.insert(0, "zh-Hant")

    with I18N_LOCK:
        cached = _load_i18n_feed_bundle()
        cache_version_ok = int(cached.get("version") or 0) == I18N_BUILD_VERSION if isinstance(cached, dict) else False
        cached_source_generated = str(cached.get("source_generated_at") or "").strip() if isinstance(cached, dict) else ""
        cache_source_ok = bool(cache_version_ok and cached and cached_source_generated == src_generated)
        cached_langs = cached.get("langs") if isinstance(cached.get("langs"), dict) else {}
        cached_qa = cached.get("qa") if isinstance(cached.get("qa"), dict) else {}
        cached_sources = cached.get("sources") if isinstance(cached.get("sources"), dict) else {}
        if not isinstance(cached_langs, dict):
            cached_langs = {}
        if not isinstance(cached_qa, dict):
            cached_qa = {}
        if not isinstance(cached_sources, dict):
            cached_sources = {}
        has_all_targets = all(isinstance(cached_langs.get(tag), dict) for tag in normalized_targets)
        if not force and cache_source_ok and has_all_targets:
            return cached

        entries = _collect_feed_i18n_entries(feed)
        entry_source_map = {str(entry_key): str(source or "") for entry_key, source in entries if str(entry_key or "").strip()}
        langs_payload: dict[str, object] = dict(cached_langs)
        qa_payload: dict[str, object] = dict(cached_qa)
        sources_payload: dict[str, object] = dict(cached_sources)
        bundle: dict[str, object] = {
            "version": I18N_BUILD_VERSION,
            "generated_at": _now_iso(),
            "source_generated_at": src_generated,
            "langs": langs_payload,
            "qa": qa_payload,
            "sources": sources_payload,
            "targets_count": len(entries),
        }
        for tag in normalized_targets:
            if not _is_active_i18n_source(src_generated):
                return _load_i18n_feed_bundle()
            previous_source_map = sources_payload.get(tag) if isinstance(sources_payload.get(tag), dict) else {}
            if not previous_source_map and cached_source_generated == src_generated:
                previous_source_map = dict(entry_source_map)
            previous_translation_map = _entry_map_from_node(langs_payload.get(tag)) if isinstance(langs_payload.get(tag), dict) else {}
            mapping, qa = _translate_feed_text_map(
                entries,
                tag,
                previous_source_map=previous_source_map,
                previous_translation_map=previous_translation_map,
            )
            if not _is_active_i18n_source(src_generated):
                return _load_i18n_feed_bundle()
            localized = _apply_feed_translation(copy.deepcopy(feed), mapping)
            if isinstance(localized, dict):
                localized["lang"] = tag
            langs_payload[tag] = localized
            qa_payload[tag] = qa
            sources_payload[tag] = dict(entry_source_map)
            # Persist per-language progress so completed langs can be served
            # immediately while other langs are still translating.
            bundle = {
                "version": I18N_BUILD_VERSION,
                "generated_at": _now_iso(),
                "source_generated_at": src_generated,
                "langs": dict(langs_payload),
                "qa": dict(qa_payload),
                "sources": dict(sources_payload),
                "targets_count": len(entries),
            }
            if not _is_active_i18n_source(src_generated):
                return _load_i18n_feed_bundle()
            _write_i18n_feed_bundle(bundle)

        bundle = {
            "version": I18N_BUILD_VERSION,
            "generated_at": _now_iso(),
            "source_generated_at": src_generated,
            "langs": dict(langs_payload),
            "qa": dict(qa_payload),
            "sources": dict(sources_payload),
            "targets_count": len(entries),
        }
        if not _is_active_i18n_source(src_generated):
            return _load_i18n_feed_bundle()
        _write_i18n_feed_bundle(bundle)
        return bundle


def _build_i18n_feed_bundle_async(
    feed: dict[str, object],
    force: bool = False,
    target_langs: list[str] | tuple[str, ...] | None = None,
) -> None:
    if not isinstance(feed, dict):
        return
    src_generated = str(feed.get("generated_at") or "").strip()
    requested = [
        _normalize_lang_tag(x)
        for x in (target_langs or I18N_TARGET_LANGS)
        if str(x or "").strip()
    ]
    seen_tags: set[str] = set()
    target_tags: list[str] = []
    for tag in requested:
        if tag in seen_tags:
            continue
        seen_tags.add(tag)
        target_tags.append(tag)
    if not target_tags:
        target_tags = list(I18N_TARGET_LANGS)

    with I18N_STATE_LOCK:
        status = str(I18N_BUILD_STATE.get("status") or "idle").strip().lower()
        running_for = str(I18N_BUILD_STATE.get("source_generated_at") or "").strip()
        if status == "running" and running_for == src_generated:
            current_targets = I18N_BUILD_STATE.get("target_langs")
            if not isinstance(current_targets, list):
                current_targets = []
            merged_targets = list(current_targets)
            for tag in target_tags:
                if tag not in merged_targets:
                    merged_targets.append(tag)
                    progress = I18N_BUILD_STATE.get("lang_progress")
                    if not isinstance(progress, dict):
                        progress = {}
                        I18N_BUILD_STATE["lang_progress"] = progress
                    progress[tag] = {
                        "lang": tag,
                        "status": "queued",
                        "mode": "",
                        "total": 0,
                        "done": 0,
                        "remaining": 0,
                        "percent": 0,
                        "error": "",
                        "updated_at": _now_iso(),
                    }
            I18N_BUILD_STATE["target_langs"] = merged_targets
            return
        I18N_BUILD_STATE.update(
            {
                "status": "running",
                "started_at": _now_iso(),
                "finished_at": "",
                "last_error": "",
                "source_generated_at": src_generated,
                "target_langs": list(target_tags),
            }
        )

    def _worker() -> None:
        current_targets = list(target_tags)
        try:
            while current_targets:
                with I18N_STATE_LOCK:
                    if str(I18N_BUILD_STATE.get("source_generated_at") or "").strip() != src_generated:
                        return
                bundle = _build_i18n_feed_bundle(feed, force=force, target_langs=current_targets)
                langs = []
                raw_langs = bundle.get("langs")
                if isinstance(raw_langs, dict):
                    langs = sorted([str(x) for x in raw_langs.keys() if str(x).strip()])
                with I18N_STATE_LOCK:
                    if str(I18N_BUILD_STATE.get("source_generated_at") or "").strip() != src_generated:
                        return
                    requested = [
                        str(x)
                        for x in (I18N_BUILD_STATE.get("target_langs") or [])
                        if str(x).strip()
                    ]
                    pending = [x for x in requested if x not in langs]
                    if pending:
                        current_targets = pending
                        continue
                    I18N_BUILD_STATE.update(
                        {
                            "status": "ok",
                            "finished_at": _now_iso(),
                            "last_error": "",
                            "source_generated_at": src_generated,
                            "langs": langs,
                        }
                    )
                    current_targets = []
        except Exception as exc:
            with I18N_STATE_LOCK:
                I18N_BUILD_STATE.update(
                    {
                        "status": "failed",
                        "finished_at": _now_iso(),
                        "last_error": str(exc),
                        "source_generated_at": src_generated,
                    }
                )

    Thread(target=_worker, daemon=True).start()


def _localized_feed_from_bundle(feed: dict[str, object], lang: str) -> dict[str, object]:
    tag = _normalize_lang_tag(lang)
    if tag == "zh-Hant":
        out = copy.deepcopy(feed)
        if isinstance(out, dict):
            out["lang"] = "zh-Hant"
            out["_i18n"] = {"mode": "base", "qa": {"coverage": 1.0}}
        return out if isinstance(out, dict) else dict(feed)

    def _base_building_response(reason: str) -> dict[str, object]:
        out = copy.deepcopy(feed)
        if not isinstance(out, dict):
            out = dict(feed)
        cards = out.get("cards")
        total_cards = len(cards) if isinstance(cards, list) else 0
        if tag in {"en", "ko"}:
            out["cards"] = []
            out["total_cards"] = 0
            card_state = {"total": total_cards, "ready": 0, "fallback": 0, "hidden": total_cards}
        else:
            card_state = {"total": total_cards, "ready": total_cards, "fallback": 0, "hidden": 0}
        out["lang"] = tag
        out["_i18n"] = {
            "mode": "building",
            "source_generated_at": str(feed.get("generated_at") or ""),
            "qa": {
                "lang": tag,
                "coverage": 0.0,
                "reason": reason,
                "card_state": card_state,
            },
            "state": _i18n_state_snapshot(),
        }
        return out

    def _stale_building_response(bundle_obj: dict[str, object], reason: str) -> dict[str, object] | None:
        if not isinstance(bundle_obj, dict):
            return None
        langs = bundle_obj.get("langs") if isinstance(bundle_obj.get("langs"), dict) else {}
        stale = langs.get(tag) if isinstance(langs, dict) else None
        if not isinstance(stale, dict):
            return None
        out = copy.deepcopy(stale)
        if not isinstance(out, dict):
            return None
        fallback_mode = _resolve_card_fallback_mode(tag)
        out, card_state = _apply_card_level_fallback(
            base_feed=feed,
            localized_feed=out,
            lang=tag,
            mode=fallback_mode,
        )
        qa_rows = bundle_obj.get("qa") if isinstance(bundle_obj.get("qa"), dict) else {}
        qa_for_tag = qa_rows.get(tag) if isinstance(qa_rows.get(tag), dict) else {}
        qa_with_cards = dict(qa_for_tag or {})
        qa_with_cards["reason"] = reason
        qa_with_cards["card_state"] = card_state
        out["lang"] = tag
        out["_i18n"] = {
            "mode": "building-stale",
            "source_generated_at": str(feed.get("generated_at") or ""),
            "qa": qa_with_cards,
            "state": _i18n_state_snapshot(),
        }
        return out

    bundle = _load_i18n_feed_bundle()
    src_generated = str(feed.get("generated_at") or "").strip()
    bundle_generated = str(bundle.get("source_generated_at") or "").strip() if isinstance(bundle, dict) else ""
    bundle_version_ok = int(bundle.get("version") or 0) == I18N_BUILD_VERSION if isinstance(bundle, dict) else False
    if not bundle or bundle_generated != src_generated or not bundle_version_ok:
        _build_i18n_feed_bundle_async(feed, force=False, target_langs=[tag])
        stale = _stale_building_response(bundle if isinstance(bundle, dict) else {}, "bundle_missing_or_outdated")
        if isinstance(stale, dict):
            return stale
        return _base_building_response("bundle_missing_or_outdated")

    langs = bundle.get("langs") if isinstance(bundle.get("langs"), dict) else {}
    qa_rows = bundle.get("qa") if isinstance(bundle.get("qa"), dict) else {}
    localized = langs.get(tag)
    qa_for_tag = qa_rows.get(tag) if isinstance(qa_rows.get(tag), dict) else {}
    coverage = float(qa_for_tag.get("coverage") or 0.0) if isinstance(qa_for_tag, dict) else 0.0
    acceptable = tag in {"zh-Hant", "zh-Hans"} or coverage >= I18N_MIN_ACCEPTABLE_COVERAGE
    if not isinstance(localized, dict):
        _build_i18n_feed_bundle_async(feed, force=False, target_langs=[tag])
        stale = _stale_building_response(bundle, "lang_not_ready")
        if isinstance(stale, dict):
            return stale
        return _base_building_response("lang_not_ready")

    out = copy.deepcopy(localized)
    if not isinstance(out, dict):
        return _base_building_response("lang_payload_invalid")
    out["lang"] = tag
    base_cards = feed.get("cards")
    out_cards = out.get("cards")
    if not isinstance(out_cards, list) and isinstance(base_cards, list):
        out_cards = copy.deepcopy(base_cards)
        out["cards"] = out_cards
    fallback_mode = _resolve_card_fallback_mode(tag)
    out, card_state = _apply_card_level_fallback(
        base_feed=feed,
        localized_feed=out,
        lang=tag,
        mode=fallback_mode,
    )
    qa_with_cards = dict(qa_for_tag or {})
    qa_with_cards["card_state"] = card_state
    out["_i18n"] = {
        "mode": (
            "pretranslated"
            if acceptable and card_state.get("fallback", 0) == 0 and card_state.get("hidden", 0) == 0
            else "pretranslated-partial"
        ),
        "source_generated_at": str(bundle.get("source_generated_at") or ""),
        "qa": qa_with_cards,
        "state": _i18n_state_snapshot(),
    }
    return out




def _normalize_retranslate_langs(langs: list[str] | tuple[str, ...] | str | None) -> list[str]:
    raw_rows: list[str]
    if isinstance(langs, str):
        raw_rows = [x.strip() for x in langs.split(",") if x.strip()]
    elif isinstance(langs, (list, tuple)):
        raw_rows = [str(x).strip() for x in langs if str(x).strip()]
    else:
        raw_rows = []
    if not raw_rows:
        raw_rows = ["en", "ko", "zh-Hans"]
    out: list[str] = []
    seen: set[str] = set()
    for item in raw_rows:
        tag = _normalize_lang_tag(item)
        if tag == "zh-Hant":
            continue
        if tag in seen:
            continue
        seen.add(tag)
        out.append(tag)
    return out


def _clear_translation_cache_for_langs(target_langs: list[str]) -> int:
    tags = set(_normalize_retranslate_langs(target_langs))
    if not tags:
        return 0
    removed = 0
    with TRANSLATION_LOCK:
        _load_translation_cache_unlocked()
        keys = list(TRANSLATION_CACHE.keys())
        for key in keys:
            row = str(key or "")
            if any(row.startswith(f"{tag}\n") for tag in tags):
                TRANSLATION_CACHE.pop(key, None)
                removed += 1
        if removed:
            global TRANSLATION_CACHE_DIRTY
            TRANSLATION_CACHE_DIRTY = True
            _flush_translation_cache_unlocked(force=True)
    return removed


def _drop_bundle_langs(target_langs: list[str]) -> None:
    tags = set(_normalize_retranslate_langs(target_langs))
    if not tags:
        return
    with I18N_LOCK:
        bundle = _load_i18n_feed_bundle()
        if not bundle:
            return
        langs = bundle.get("langs")
        qa = bundle.get("qa")
        sources = bundle.get("sources")
        changed = False
        if isinstance(langs, dict):
            for tag in tags:
                if tag in langs:
                    langs.pop(tag, None)
                    changed = True
        if isinstance(qa, dict):
            for tag in tags:
                if tag in qa:
                    qa.pop(tag, None)
                    changed = True
        if isinstance(sources, dict):
            for tag in tags:
                if tag in sources:
                    sources.pop(tag, None)
                    changed = True
        if changed:
            bundle["generated_at"] = _now_iso()
            _write_i18n_feed_bundle(bundle)


def _queue_i18n_retranslate(
    feed: dict[str, object],
    target_langs: list[str] | tuple[str, ...] | str | None = None,
) -> dict[str, object]:
    tags = _normalize_retranslate_langs(target_langs)
    if not tags:
        return {"langs": [], "cache_removed": 0, "queued": False}
    removed = _clear_translation_cache_for_langs(tags)
    for tag in tags:
        _set_i18n_lang_progress(tag, total=0, done=0, status="pending", mode="manual", sample_pending=[])
    _build_i18n_feed_bundle_async(feed, force=True, target_langs=tags)
    return {
        "langs": tags,
        "cache_removed": removed,
        "queued": True,
        "source_generated_at": str(feed.get("generated_at") or ""),
    }


def _rebuild_i18n_bundle_sync(
    feed: dict[str, object],
    target_langs: list[str] | tuple[str, ...] | str | None = None,
    force: bool = True,
) -> dict[str, object]:
    requested = target_langs
    if isinstance(target_langs, str):
        requested = [x.strip() for x in target_langs.split(",") if x.strip()]
    rows = [str(x).strip() for x in (requested or I18N_TARGET_LANGS) if str(x).strip()]
    if "zh-Hant" not in rows:
        rows = ["zh-Hant", *rows]
    return _build_i18n_feed_bundle(feed, force=bool(force), target_langs=rows)


# Public aliases used by ai_intel_server.py. Keep the internal helper names
# stable so old cached jobs/status payloads continue to match existing keys.
translate_texts = _translate_texts
build_i18n_feed_bundle_async = _build_i18n_feed_bundle_async
localized_feed_from_bundle = _localized_feed_from_bundle
i18n_state_snapshot = _i18n_state_snapshot
queue_i18n_retranslate = _queue_i18n_retranslate
rebuild_i18n_bundle_sync = _rebuild_i18n_bundle_sync
