-------------------------------------------------------------------------------
--  EllesmereUI_koKR / Core.lua  (WoW 12.0.5)
--  한국어 번역 패치 엔진. Locale\*.lua 에 EUI_L["원문"] = "번역" 만 추가하면 적용.
--
--  파이프라인:
--    Phase 1  WidgetFactory 후킹 (사본 후 텍스트만 변환, 원본 보호)
--    Phase 2  사이드바 데이터 mutate (_addonInfoByFolder, ADDON_GROUPS, VIS_*)
--    Phase 3  mainFrame 재귀 스캔 (텍스트 번역만 — 폰트는 안 건드림)
--    Phase 4  RegisterModule / Popup / WidgetTooltip / GameTooltip 후크
--    Phase 4b AuraBuff Reminders 한글화 (WoW API spell/item 이름)
--  ※ 폰트 강제 일절 없음. 글자 치환(SetText)만 한다. 한글 폰트는 EllesmereUI 원본 로직
--    (koKR=LOCALE_FONT_FALLBACK=2002.TTF) 그대로 쓰거나, 사용자가 Expressway.TTF 파일을 교체.
--
--  성능:
--    - 옵션 패널 닫힘 → ticker 즉시 정지 (≈0ms)
--    - 옵션 패널 열림 → 0.3s tick: popup 스캔만 / 0.9s tick: mainFrame 전체
-------------------------------------------------------------------------------
---@diagnostic disable: undefined-global

EUI_L = EUI_L or {}

-- ============================================================
--  Upvalue 캐시 — 글로벌 lookup 회피
-- ============================================================
local type, pairs, ipairs, pcall, unpack = type, pairs, ipairs, pcall, unpack
local _G, C_Timer = _G, C_Timer
-- ★ 폰트 변수 제거 — 애드온은 폰트를 일절 지정하지 않는다(글자 치환만). 한글 폰트는 사용자가 수동 복사.

-- ★ [임시 진단] /krver 입력 시 빌드 출력. 타이밍 무관 — 직접 입력해 확인.
--   (SlashCmdList 재할당 금지 — 키만 add. 재할당하면 전체 taint.)
SLASH_EUIKRVER1 = "/krver"
SlashCmdList["EUIKRVER"] = function()
    print("|cff11ace9[EllesmereUI_koKR]|r 빌드: |cff00ff00TAINT-TEST-10|r  (★ CharSheet 화폐탭 가드 + 필드쓰기 전수 weak + 폰트 제거)")
end

-- ============================================================
--  번역 헬퍼
-- ============================================================
local function TR(s)
    if type(s) ~= "string" or s == "" then return s end
    return EUI_L[s] or s
end

local TRCfg, TRValues  -- 상호참조

TRValues = function(values)
    if type(values) ~= "table" then return values end
    local out = {}
    for k, v in pairs(values) do
        if k == "_menuOpts" then
            out[k] = v
        elseif type(v) == "string" then
            out[k] = EUI_L[v] or v
        elseif type(v) == "table" then
            local entry = {}
            for ek, ev in pairs(v) do entry[ek] = ev end
            if entry.text and EUI_L[entry.text] then entry.text = EUI_L[entry.text] end
            if entry.note and EUI_L[entry.note] then entry.note = EUI_L[entry.note] end
            if type(entry.subnav) == "table" then
                local sub = {}
                for sk, sv in pairs(entry.subnav) do sub[sk] = sv end
                if sub.values then sub.values = TRValues(sub.values) end
                if sub.text and EUI_L[sub.text] then sub.text = EUI_L[sub.text] end
                entry.subnav = sub
            end
            out[k] = entry
        else
            out[k] = v
        end
    end
    return out
end

TRCfg = function(cfg)
    if type(cfg) ~= "table" then return cfg end
    local out = {}
    for k, v in pairs(cfg) do out[k] = v end
    if out.text            then out.text            = TR(out.text)            end
    if out.tooltip         then out.tooltip         = TR(out.tooltip)         end
    if out.disabledTooltip then out.disabledTooltip = TR(out.disabledTooltip) end
    if type(out.values) == "table" then out.values = TRValues(out.values) end
    return out
end

-- secret string taint 방어 — closure 재생성 방지용 named functions
local function _safeLookup(txt)
    if txt == "" then return nil, true end
    return EUI_L[txt], false
end

-- 합성 문자열(숫자/색코드 섞임) 라벨 부분만 gsub 치환. 매칭 시 변환 문자열,
-- 아니면 nil. UpgradeCalc/Keys 등 동적 텍스트에 공용.
local _COMPOSITE_SUBS = {
    { "Current iLvl:",            "현재 아이템 레벨:" },
    { "Max Possible:",            "최대 가능:" },
    { "Total Missing Upgrades:",  "총 미강화:" },
    { "Total Crests Needed",      "필요한 총 문장" },
    { "Upgradable Items %(",      "강화 가능 아이템 (" },
    -- 정확도 태그 (강화 NPC 보정 여부)
    { "%(exact%)",                "(정확)" },
    { "%(est%)",                  "(예상)" },
    -- "숫자 트랙" → "트랙(한글) 숫자개". %f[%A] 로 Heroic/Mythic 오탐 방지.
    -- ★ 색코드로 감싼 경우(요약 "|cffff800080 Hero|r")는 색 hex(8000)와 수량(80)이
    --   붙어 %d+ 가 "800080"으로 잘못 잡힘 → 색코드(|c+8hex)를 먼저 소비하는
    --   패턴을 *평문 패턴보다 앞에* 둬서 수량만 정확히 캡처.
    { "(|c%x%x%x%x%x%x%x%x)(%d+) Adventurer%f[%A]", "%1모험가 %2개" },
    { "(|c%x%x%x%x%x%x%x%x)(%d+) Veteran%f[%A]",    "%1노련가 %2개" },
    { "(|c%x%x%x%x%x%x%x%x)(%d+) Champion%f[%A]",   "%1챔피언 %2개" },
    { "(|c%x%x%x%x%x%x%x%x)(%d+) Hero%f[%A]",       "%1영웅 %2개" },
    { "(|c%x%x%x%x%x%x%x%x)(%d+) Myth%f[%A]",       "%1신화 %2개" },
    -- 평문 (대기열 "120 Hero", 총합 "120 Hero  40 Champion")
    { "(%d+) Adventurer%f[%A]",   "모험가 %1개" },
    { "(%d+) Veteran%f[%A]",      "노련가 %1개" },
    { "(%d+) Champion%f[%A]",     "챔피언 %1개" },
    { "(%d+) Hero%f[%A]",         "영웅 %1개" },
    { "(%d+) Myth%f[%A]",         "신화 %1개" },
    { ": None",                   ": 없음" },
    { "Nothing needed",           "필요 없음" },
    { "At Max",                   "최대 도달" },
    -- 미니맵 친구 목록 툴팁 섹션 헤더 "Favorites (|cff..N|r)" — 개수·색코드가
    -- 붙어 정확키 매칭 불가 → 접두사만 치환(나머지 (N) 유지).
    { "^Favorites %(",            "즐겨찾기 (" },
    { "^Guild %(",                "길드 (" },
    { "^Friends %(",              "친구 (" },
    -- MythicTimer 포맷 결과 → 한글 어순으로 재배열.
    -- 원문: format("%d/%d%s") / "%d/%d - %.2f%% Enemy Forces" / "%.2f%% Enemy Forces"
    --       / "%d remaining Enemy Forces" / "%d left" / "%d Death(s)  -%s"
    { "^(%d+)/(%d+) Enemy Forces$",                "적 병력 %1/%2" },
    { "^(%d+)/(%d+) %- ([%d%.]+)%% Enemy Forces$", "적 병력 %1/%2 - %3%%" },
    { "^(%d+) remaining Enemy Forces$",            "적 병력 %1 남음" },
    { "^([%d%.]+)%% Enemy Forces$",                "적 병력 %1%%" },
    { "^(%d+) left$",                              "%1개 남음" },
    { "^(%d+) remaining$",                         "%1개 남음" },
    { " Enemy Forces$",                            " 적 병력" },          -- 위 5개 외 변형 대비 fallback
    { "^(%d+) Deaths?  %-(.+)$",                   "사망 %1회  -%2" },
}
local function _TranslateComposite(txt)
    if type(txt) ~= "string" then return nil end
    local s, changed = txt, false
    for i = 1, #_COMPOSITE_SUBS do
        local before = s
        s = s:gsub(_COMPOSITE_SUBS[i][1], _COMPOSITE_SUBS[i][2])
        if s ~= before then changed = true end
    end
    if changed then return s end
    return nil
end

-- ============================================================
--  컨텍스트 기반 번역 override
--  같은 영문이 frame 별로 다른 한글이 필요할 때 (예: "Armor" → 캐릭터
--  시트 = "방어도", 가방 카테고리 = "방어구").
--  Locale 파일에서 아래와 같이 추가:
--    EUI_L_CTX = EUI_L_CTX or {}
--    EUI_L_CTX["EllesmereUIBags"] = { ["Armor"] = "방어구" }
--  매칭 규칙: FontString 의 부모 체인을 따라 올라가며 GetName() prefix 가
--  CTX 키와 일치하면 그 매핑 우선 적용 (없으면 EUI_L 일반 lookup).
--  매칭 한 번에 깊이 8 까지만 (성능 가드).
-- ============================================================
EUI_L_CTX = EUI_L_CTX or {}

local function _findCtxOverride(fs, key)
    -- 빠른 종료 — CTX 자체가 비어있으면 walk 안 함
    if next(EUI_L_CTX) == nil then return nil end
    if not fs or not fs.GetParent then return nil end
    local p = fs:GetParent()
    local depth = 0
    while p and type(p) == "table" and depth < 8 do
        local okN, name = pcall(p.GetName, p)
        if okN and type(name) == "string" then
            for prefix, tbl in pairs(EUI_L_CTX) do
                if name:find(prefix, 1, true) then
                    local v = tbl[key]
                    if v ~= nil then return v end
                end
            end
        end
        local okP, np = pcall(p.GetParent, p)
        if not okP or np == p or np == UIParent or np == nil then break end
        p = np
        depth = depth + 1
    end
    return nil
end

-- ★ 전투 중 미터기 위젯(EllesmereUIDM*) 자식 FontString 은 처리 skip.
--   미터기는 매 프레임 SetText 갱신 → 우리 SetFont/SetAlpha/SetText 재적용이
--   누적되어 비용. 전투 시작 시 정지, PLAYER_REGEN_ENABLED 후 재개.
--   부모 체인 6단계까지 GetName 검사 (성능 가드).
local function _IsInMeterFrame(fs)
    if not fs or not fs.GetParent then return false end
    local p = fs:GetParent()
    local d = 0
    while p and type(p) == "table" and d < 6 do
        local okN, name = pcall(p.GetName, p)
        if okN and type(name) == "string" and name:find("EllesmereUIDM", 1, true) then
            return true
        end
        local okP, np = pcall(p.GetParent, p)
        if not okP or np == p or np == UIParent or np == nil then break end
        p = np
        d = d + 1
    end
    return false
end

-- ★ StaticPopup / GameDialog 트리는 어떤 작업도 *완전히* 회피.
--   심지어 우리가 fs 에 사용자 필드를 *쓰는 것만으로도* taint 됨(그래서 외부 weak 테이블 사용) →
--   Blizzard StaticPopup_OnClick 안에서 그 FontString 다루는 시점에 taint propagate →
--   OnAccept 의 보호 함수 호출(EndBoundTradeable, AcceptSpellConfirmationPrompt,
--   ConfirmBindOnUse, EndRefund 등) 차단(ADDON_ACTION_FORBIDDEN).
--
--   ★★★ 캐시는 외부 weak 테이블 — fs 에 어떤 필드도 쓰지 않음. fs 에 쓰는 순간 taint.
--   Blizzard StaticPopup 텍스트는 이미 클라이언트가 한글화하므로 손실 없음.
-- ★ 통합 가드 — Blizzard 보호 다이얼로그/팝업 트리는 *어떤 것도* 만지지 않음.
-- 매칭 패턴(이름):
--   StaticPopup%d       — 클래식 StaticPopup (StaticPopup1..StaticPopup4)
--   GameDialog          — Blizzard_StaticPopup_Game/GameDialog (12.x 신형)
--   GameDialogBox       — GameDialog 내부 박스
--   GlueDialog          — 로그인 글루 화면 다이얼로그
--   Dialog%d            — 일반 다이얼로그 패턴 (DialogBox1 등)
-- FontString 본인 + 조상 chain 둘 다 확인. 한 번이라도 매칭되면 영구 캐시(true)
-- → 해당 fs 는 영원히 번역 시도 안 함 → taint 가능성 0.
-- ⚠ FontString 자신에 *어떤 필드도 쓰지 않음* (taint 우회). 외부 weak 캐시 사용.
local _spCheckCache = setmetatable({}, { __mode = "k" })
-- 최소 가드 — Dialog/Popup/Confirm 만. 이전 정상 동작 상태로 회귀.
-- (Blizzard_ prefix, _BLIZZ_CORE prefix 매칭은 회귀 가능성 있어 제거.
--  ItemUpgrade 등 개별 frame 도 _BLIZZ_CORE 안 씀 — 회귀 의심분 제거.)
local function _NameLooksProtected(name)
    if type(name) ~= "string" or name == "" then return false end
    if name:find("Dialog") or name:find("Popup") or name:find("Confirm") then return true end
    if name == "PVPMatchResults" then return true end
    -- ★ 워밴드 화폐 이전 UI(CurrencyTransferMenu): 자식 FontString 에 우리 사용자 필드만 써도
    --   taint → 확인 시 보호함수 RequestCurrencyFromAccountCharacter() 차단(ADDON_ACTION_FORBIDDEN).
    --   트리 전체 회피 → 손대지 않음(화폐명은 클라이언트가 이미 한글).
    if name:find("CurrencyTransfer") then return true end
    return false
end
local function _IsInStaticPopup(fs)
    if not fs or not fs.GetParent then return false end
    local cached = _spCheckCache[fs]
    if cached ~= nil then return cached end
    local result = false
    local okN, fsName = pcall(fs.GetName, fs)
    if okN and _NameLooksProtected(fsName) then result = true end
    if not result then
        local p = fs:GetParent()
        local d = 0
        while p and type(p) == "table" and d < 16 do   -- 깊이 16: 깊게 중첩된 보호 프레임(CurrencyTransfer 등) 글자도 확실히 회피
            local okN2, name = pcall(p.GetName, p)
            if okN2 and _NameLooksProtected(name) then
                result = true; break
            end
            local okP, np = pcall(p.GetParent, p)
            if not okP or np == p or np == UIParent or np == nil then break end
            p = np
            d = d + 1
        end
    end
    _spCheckCache[fs] = result
    return result
end

-- ★ StatusBar 의 TextString/LeftText/RightText FontString 회피.
--   PlayerFrame/TargetFrame 등의 체력/자원 표시 텍스트는 *secret number* 를
--   포함할 수 있어 GetText 호출만으로 우리 addon 이 taint 됨 → 이후
--   CharacterFrame 등 보호 함수 호출 시 ADDON_ACTION_FORBIDDEN.
--   FontString 의 부모가 StatusBar(GetObjectType=="StatusBar")이면 skip.
--   외부 weak 캐시.
local _sbCheckCache = setmetatable({}, { __mode = "k" })
local function _IsInStatusBar(fs)
    if not fs or not fs.GetParent then return false end
    local cached = _sbCheckCache[fs]
    if cached ~= nil then return cached end
    local result = false
    local p = fs:GetParent()
    if p and type(p) == "table" and p.GetObjectType then
        local ok, ot = pcall(p.GetObjectType, p)
        if ok and ot == "StatusBar" then result = true end
    end
    _sbCheckCache[fs] = result
    return result
end

-- ★ per-FontString 상태를 외부 weak 테이블에 보관 (fs 에 직접 쓰면 taint →
--   CurrencyTransfer/팝업 등 보호 프레임에서 ADDON_ACTION_FORBIDDEN). fs 에 안 쓴다.
local _wkMeter     = setmetatable({}, { __mode = "k" })
local _wkFSReHook  = setmetatable({}, { __mode = "k" })
local _wkFSReEntry = setmetatable({}, { __mode = "k" })
local _wkLastTxt   = setmetatable({}, { __mode = "k" })
local _wkCtxKey    = setmetatable({}, { __mode = "k" })
local _wkCtxVal    = setmetatable({}, { __mode = "k" })
-- ★ 프레임(메뉴)에 직접 필드를 쓰면 그 프레임이 taint → 프레임 키도 외부 weak 테이블로.
local _wkDDHide    = setmetatable({}, { __mode = "k" })
-- ★ [전수] 모든 객체 필드 쓰기를 외부 weak 테이블로 (frame/fs 에 사용자 필드 안 씀 = taint 0).
local _wk_cimBagHook = setmetatable({}, { __mode = "k" })
local _wk_cimBagShowHook = setmetatable({}, { __mode = "k" })
local _wk_cimBagTr = setmetatable({}, { __mode = "k" })
local _wk_cimCSDyn = setmetatable({}, { __mode = "k" })
local _wk_cimCSReent = setmetatable({}, { __mode = "k" })
local _wk_cimMMScanHook = setmetatable({}, { __mode = "k" })
local _wk_cimMMTip = setmetatable({}, { __mode = "k" })
local _wk_cimkrDDLblHook = setmetatable({}, { __mode = "k" })
local _wk_cimkrDDReentry = setmetatable({}, { __mode = "k" })
local _wk_cimkrFSPatched = setmetatable({}, { __mode = "k" })
local _wk_cimkrKoParse = setmetatable({}, { __mode = "k" })
local _wk_cimkrPatched = setmetatable({}, { __mode = "k" })
local _wk_cimkrPopupShowHook = setmetatable({}, { __mode = "k" })
local _wk_cimkrQoLHooked = setmetatable({}, { __mode = "k" })
local _wk_cimkrQoLTick = setmetatable({}, { __mode = "k" })
local _wk_cimkrSTBusy = setmetatable({}, { __mode = "k" })
local _wk_cimkrSTHooked = setmetatable({}, { __mode = "k" })
local _wk_cimkrScanHooked = setmetatable({}, { __mode = "k" })
local _wk_cimkrShowHooked = setmetatable({}, { __mode = "k" })
local _wk_cimkrTipPatched = setmetatable({}, { __mode = "k" })
local _wk_cimkrTipTxtHook = setmetatable({}, { __mode = "k" })
local _wk_cimkrTipTxtRe = setmetatable({}, { __mode = "k" })
local _wk_cimkrUMHook = setmetatable({}, { __mode = "k" })
local _wk_cimkrUMReentry = setmetatable({}, { __mode = "k" })
local _wk_cimkrUMTickerHooked = setmetatable({}, { __mode = "k" })
local _wk_cimkrUnlockClick = setmetatable({}, { __mode = "k" })
local _wk_cimkrVaultHook = setmetatable({}, { __mode = "k" })
local _wk_cimkrVaultReentry = setmetatable({}, { __mode = "k" })
-- ★ CharacterFrame 버튼/라벨 마커도 weak 테이블로 (필드 쓰기 taint 회피).
local _wk_euiKoEnchHover = setmetatable({}, { __mode = "k" })
local _wk_euiKoILvlTip = setmetatable({}, { __mode = "k" })
local _wk_euiKoLbl = setmetatable({}, { __mode = "k" })
local _wk_euiKoStatTip = setmetatable({}, { __mode = "k" })
local function RetranslateFontString(fs)
    if not fs or not fs.GetText then return end
    if fs.IsForbidden and fs:IsForbidden() then return end
    -- ★ StaticPopup/GameDialog 트리는 어떤 작업도 *완전히* 회피.
    --   _IsInStaticPopup 는 외부 weak 캐시(_spCheckCache) 사용 — fs 에 쓰지 않음.
    --   fs 에 사용자 필드 쓰는 즉시 taint 됨(이전 시도가 이 점을 놓쳐 fix 가 실패).
    if _IsInStaticPopup(fs) then return end
    -- ★ StatusBar TextString 차단 — secret number taint.
    if _IsInStatusBar(fs) then return end
    -- IsProtected 체크 제거: FontString 의 IsProtected 는 부모(secure 버튼) 상속이지만
    -- FontString:SetText/SetFont 자체는 secure 작업이 아님 → 호출 안전. 이 체크가
    -- 미니맵 micro menu 의 SecureActionButtonTemplate 자식 라벨 번역을 막던 원인.
    -- ★ per-fs 캐시: 미터기 frame 자식 판정은 부모 체인 변하지 않는 한 영구.
    --   매 호출마다 walk 하지 않도록 첫 회 결과 캐싱.
    if _wkMeter[fs] == nil then
        _wkMeter[fs] = _IsInMeterFrame(fs) or false
    end
    if InCombatLockdown() and _wkMeter[fs] then return end
    -- ★ 폰트는 일절 강제하지 않는다(SetFont = FontString/프레임 taint → 보호 UI 차단).
    --   폰트는 EllesmereUI 원본 로직(koKR=2002.TTF) 그대로. 우리는 SetText(번역)만 한다.
    -- ★ SetText 후크 1회 설치 (per-fs __cimkrFSReHook 가드) → 본체가 새 영문으로
    --   SetText 할 때마다 *즉시* 재번역. 커스텀 드롭다운(WidgetFactory _ddMenu 아닌
    --   zoneDDBtn 등)의 선택값 라벨이 선택 변경 후 영문으로 회귀하던 증상 해결.
    --   __cimkrLastTxt 캐시는 SetText 후 nil 로 비워 동일 새 텍스트도 재처리 가능.
    --   미터기(__cimkrMeter) FontString 은 SetText 가 분당 수백회 → 후크 비용 피해 skip.
    if not _wkFSReHook[fs] and fs.SetText and not _wkMeter[fs] then
        _wkFSReHook[fs] = true
        pcall(hooksecurefunc, fs, "SetText", function(self)
            -- ★ secure 컨텍스트(StaticPopup_OnClick 등)에서 발화하면 즉시 bail.
            --   우리가 어떤 작업이라도 하면 secure 호출 체인이 우리 addon 으로 taint
            --   → OnAccept 의 보호함수(EndBoundTradeable 등) ADDON_ACTION_FORBIDDEN.
            if issecure and not issecure() then
                -- insecure 컨텍스트(EUI 자체/우리 코드 등) — 안전. 정상 재번역.
                if _wkFSReEntry[self] then return end
                _wkFSReEntry[self] = true
                _wkLastTxt[self] = nil
                pcall(RetranslateFontString, self)
                _wkFSReEntry[self] = false
            elseif not issecure then
                -- API 없는 경우 fallback (구버전) — 기존 경로
                if _wkFSReEntry[self] then return end
                _wkFSReEntry[self] = true
                _wkLastTxt[self] = nil
                pcall(RetranslateFontString, self)
                _wkFSReEntry[self] = false
            end
        end)
    end
    local ok, txt = pcall(fs.GetText, fs)
    if not ok or type(txt) ~= "string" then return end
    -- ★ secret 값(화폐 금액·보호 텍스트) 격리 — 비교/인덱싱이 실패하면 secret → 만지지 않고 skip.
    --   (메모리: 12.x GetText 반환이 secret 이면 ==/[k]/+ 연산 시 taint → 연산 자체를 pcall 로 가둠.)
    if not pcall(function() return txt == "" or EUI_L[txt] ~= nil end) then return end
    -- ★ 같은 텍스트 중복 처리 회피 — 텍스트 바뀔 때만 재번역.
    if _wkLastTxt[fs] == txt then return end
    -- 1) 컨텍스트 override (per-fs 캐시 가능 — 첫 회만 walk)
    if _wkCtxKey[fs] ~= txt then
        _wkCtxKey[fs] = txt
        _wkCtxVal[fs] = _findCtxOverride(fs, txt)
    end
    if _wkCtxVal[fs] then
        pcall(fs.SetText, fs, _wkCtxVal[fs])
        _wkLastTxt[fs] = txt
        return
    end
    -- 2) 일반 EUI_L lookup
    local hit, isEmpty
    ok, hit, isEmpty = pcall(_safeLookup, txt)
    if not ok or isEmpty then return end
    if hit then
        pcall(fs.SetText, fs, hit)
        _wkLastTxt[fs] = txt
        return
    end
    -- 동적 prefix: "Reset 모듈명"
    local matchOk, rest = pcall(txt.match, txt, "^Reset (.+)$")
    if matchOk and rest then
        pcall(fs.SetText, fs, rest .. " 초기화")
        _wkLastTxt[fs] = txt
        return
    end
    -- ★ 색코드로 감싼 단일 라벨 "|cAARRGGBB본문|r" → 본문만 EUI_L 번역 후 재조립.
    --   (토템바 "활성화 직업" 드롭다운의 직업명 "|cff..Warrior|r" 등 — 베이스가
    --    localizedName 누락으로 영문 직업파일명 폴백 → 색코드 벗겨 번역)
    local cpre, cbody, csuf = txt:match("^(|c%x%x%x%x%x%x%x%x)(.-)(|r)$")
    if cbody and cbody ~= "" then
        local hb = EUI_L[cbody]
        if hb then
            pcall(fs.SetText, fs, cpre .. hb .. csuf)
            _wkLastTxt[fs] = txt
            return
        end
    end
    -- ★ 콤마 구분 다중선택 요약 ("Mythic Raid, Heroic Raid" 등 CB 드롭다운).
    --   모든 조각이 EUI_L 에 있을 때만 전체 번역 (부분 번역 방지).
    if txt:find(", ", 1, true) then
        local parts, allHit = {}, true
        for piece in (txt .. ", "):gmatch("(.-), ") do
            local h = EUI_L[piece]
            if not h then
                -- 색코드로 감싼 조각("|cff..Shaman|r") — 본문만 번역 후 재조립
                local p2, b2, s2 = piece:match("^(|c%x%x%x%x%x%x%x%x)(.-)(|r)$")
                if b2 then local hb = EUI_L[b2]; if hb then h = p2 .. hb .. s2 end end
            end
            if h then parts[#parts + 1] = h
            else allHit = false; break end
        end
        if allHit and #parts > 0 then
            local joined = table.concat(parts, ", ")
            pcall(fs.SetText, fs, joined)
            _wkLastTxt[fs] = txt
            return
        end
    end
    -- ★ "This option/feature requires X to be enabled/disabled" 형식의 동적
    --   disabledTooltip (Widgets DisabledTooltip 합성). 래퍼(앞뒤)가 영문이라
    --   가운데만 번역되면 영문이 섞여 보임 → 통째 한글 어순으로 재조립.
    --   가운데 X 는 EUI_L 에 있으면 번역(없으면 원문 유지). X 가 이미 완결형
    --   한글 문장이면 군더더기 없이 그대로 표시(이중 "사용 가능" 방지).
    do
        local inner, mode = txt:match("^This option requires (.+) to be (%a+)$")
        if not inner then inner, mode = txt:match("^This feature requires (.+) to be (%a+)$") end
        if inner then
            local ti = EUI_L[inner]
            if not ti then
                local p2, b2, s2 = inner:match("^(|c%x%x%x%x%x%x%x%x)(.-)(|r)$")
                if b2 and EUI_L[b2] then ti = p2 .. EUI_L[b2] .. s2 end
            end
            ti = ti or inner
            local out
            if ti:find("사용", 1, true) or ti:find("필요", 1, true) or ti:find("합니다", 1, true) then
                out = ti
            else
                local verb = (mode == "disabled") and "꺼야" or "켜야"
                out = "'" .. ti .. "' 옵션을 " .. verb .. " 사용 가능합니다"
            end
            pcall(fs.SetText, fs, out)
            _wkLastTxt[fs] = txt
            return
        end
    end
    -- ★ UpgradeCalc 등 합성 문자열 (숫자/색코드 섞임) — 라벨만 gsub 치환.
    local okR, replaced = pcall(_TranslateComposite, txt)
    if okR and replaced then
        pcall(fs.SetText, fs, replaced)
        -- 합성 문자열은 매번 숫자 바뀌므로 LastTxt 캐시 안 함 (재진입 시 재치환)
        return
    end
    -- miss — 다음 호출에 같은 txt 면 즉시 skip
    _wkLastTxt[fs] = txt
end

-- ============================================================
--  Phase 3 헬퍼: 프레임 재귀 스캔 (forward decl 없이 모두 사용)
-- ============================================================
-- ★ 프레임 재방문 캐시 — weak keys 로 GC 자유. 이미 스캔한 frame 은 ticker
--   재진입 시 skip. 모듈/페이지 변경 hook 에서 _ScanInvalidate() 호출로 무효화.
--   BuzzardFrames 의 _done 패턴 차용.
local _scanned = setmetatable({}, { __mode = "k" })
local function _ScanInvalidate()
    _scanned = setmetatable({}, { __mode = "k" })
end

-- ★ 드롭다운 버튼 레지스트리 (선택값 라벨 재번역용). _scanned 캐시가 버튼
--   재스캔을 막아 *선택으로 바뀐 선택값*이 영문으로 남는 문제 → 패널 열린
--   동안 ticker 가 이 레지스트리의 버튼 라벨을 매번 캐시 무시 재번역.
--   _ScanInvalidate 와 무관하게 유지(버튼은 페이지 살아있는 한 유효).
local _ddButtons = setmetatable({}, { __mode = "k" })

-- forward decl — 드롭다운 버튼 라벨 캐시 무시 재번역 (정의는 아래쪽)
local _RescanDDButtonLabel
-- forward decl — 옵션 패널 버튼 OnClick → 즉시 팝업 스캔 (PLAYER_LOGIN 에서 채워짐).
-- 클릭 후 1프레임 안에 생성되는 팝업/메뉴 한글로 즉시 적용 → 500ms ticker 대기 회피.
local _TriggerPopupScan
-- forward decl — 편집모드 더미 SetText 후크 설치 (정의는 PLAYER_LOGIN 핸들러 안).
-- ticker 가 편집모드 동안 매 tick 호출해 *진입 경로·생성 타이밍 무관* 하게
-- 더미가 호버해도 영문으로 안 돌아가게 보장.
local _umReHook

-- 드롭다운 버튼 라벨 FontString 에 SetText 후크 → 체크박스형 드롭다운에서
-- 항목 선택 시 ddLbl:SetText("|cff..Shaman|r") 로 영문 들어오는 *즉시* 재번역
-- (ticker/OnHide 지연 없이 0). 편집모드 더미와 같은 기법.
local function _HookDDLabelFS(fs)
    if not fs or _wk_cimkrDDLblHook[fs] or not fs.SetText then return end
    if fs.IsForbidden and fs:IsForbidden() then return end
    _wk_cimkrDDLblHook[fs] = true
    hooksecurefunc(fs, "SetText", function(self)
        if _wk_cimkrDDReentry[self] then return end
        _wk_cimkrDDReentry[self] = true
        _wkLastTxt[self] = nil   -- 캐시 무효화 → 새 선택값 즉시 재번역
        pcall(RetranslateFontString, self)
        _wk_cimkrDDReentry[self] = false
    end)
end

local function ScanFrameRecursive(frame, depth, noCache)
    -- 깊이 14: 옵션 페이지의 깊숙한 드롭다운 버튼(_ddMenu)까지 닿아야 메뉴 닫힘
    -- 재번역 OnHide 후크가 설치됨 (안 닿으면 리스트는 한글인데 선택값은 영문).
    -- _scanned 캐시로 페이지 전환당 1회만 깊게 walk → 설정창 열렸을 때만이라 비용 OK.
    -- noCache=true: 팝업/서브메뉴처럼 *내용이 바뀌는* 트리는 캐시 무시 (서브 항목 lazy
    --   생성·재구성 잡기 위함). FS 자체의 __cimkrLastTxt 캐시로 재번역 비용은 낮음.
    -- 보호(secure) 프레임: HookScript/자식 재귀는 skip (taint 위험), region(FontString)
    --   번역은 허용 (SetText 는 FontString 에서 secure 아님). 미니맵 micro menu 등
    --   SecureActionButtonTemplate 자식 라벨 한글화 위해 필요.
    if not frame or depth > 14 then return end
    if not noCache and _scanned[frame] then return end
    if frame.IsForbidden and frame:IsForbidden() then return end
    -- ★ 통합 가드 — _NameLooksProtected 가 Dialog/Popup/Confirm 다 잡음.
    local okN, fnm = pcall(frame.GetName, frame)
    if okN and _NameLooksProtected(fnm) then return end
    local _isProtected = frame.IsProtected and frame:IsProtected()
    if not noCache then _scanned[frame] = true end   -- 가드 통과 시점에 표시 (forbidden 은 매번 재확인)
    -- ★ 드롭다운(_ddMenu 보유) — 메뉴 닫힘 시 선택값/요약 라벨 즉시 재번역.
    --   _scanned 캐시가 버튼 재스캔을 막으므로 캐시 무시 직접 경로 사용.
    --   WidgetFactory / CB / VisOpts 드롭다운 모두 _ddMenu 를 가짐.
    if frame._ddMenu and not _isProtected then
        _ddButtons[frame] = true   -- ticker 가 매 tick 선택값 재번역 (안전망)
        -- ★ 버튼 라벨 FontString 에 SetText 후크 → 항목 선택 시 *즉시* 재번역
        --   (체크박스형 드롭다운: 선택하면 ddLbl 이 영문으로 바뀌는데 바로 한글로).
        if frame.GetRegions then
            local rok, regs = pcall(function() return { frame:GetRegions() } end)
            if rok and type(regs) == "table" then
                for i = 1, #regs do
                    local rg = regs[i]
                    local tok, ot = pcall(function() return rg and rg.GetObjectType and rg:GetObjectType() end)
                    if tok and ot == "FontString" then _HookDDLabelFS(rg) end
                end
            end
        end
        if not _wkDDHide[frame] then
            local menu = frame._ddMenu
            if type(menu) == "table" and menu.HookScript then
                _wkDDHide[frame] = true
                menu:HookScript("OnHide", function()
                    if _RescanDDButtonLabel then _RescanDDButtonLabel(frame) end
                    if C_Timer and C_Timer.After then
                        C_Timer.After(0, function() if _RescanDDButtonLabel then _RescanDDButtonLabel(frame) end end)
                    end
                end)
            end
        end
    end
    if frame._label and not _isProtected then pcall(RetranslateFontString, frame._label) end
    -- ★ [제거] 버튼 OnClick 후크 — 비보호처럼 보이는 블리자드 버튼(화폐 이전 확인 등)에
    --   HookScript 를 걸면 그 버튼이 taint → 클릭 시 보호함수
    --   (RequestCurrencyFromAccountCharacter) 차단. 텍스트 치환과 무관한 스캔 트리거라 통째로
    --   제거. 클릭으로 뜨는 팝업은 OnShow 후크가 잡는다(영문 한 프레임 깜빡임은 감수).
    -- ★ pcall 가드. 보호(secure) frame 의 region 도 만지지 않음 — SetText/SetFont
    --   호출이 frame 을 taint → 이후 secure 콜백(OnAccept→UpgradeItem 등) 차단.
    --   미니맵 micro menu 라벨 등 보호 frame 내 라벨은 영문 노출 트레이드오프.
    if not _isProtected and frame.GetRegions then
        local ok, regions = pcall(function() return { frame:GetRegions() } end)
        if ok and type(regions) == "table" then
            for i = 1, #regions do
                local rgn = regions[i]
                local tok, ot = pcall(function() return rgn and rgn.GetObjectType and rgn:GetObjectType() end)
                if tok and ot == "FontString" then pcall(RetranslateFontString, rgn) end
            end
        end
    end
    -- 보호 프레임의 자식 재귀는 skip (위 region 처리로 FontString 라벨은 이미 번역됨).
    -- 보호 프레임 자식을 깊이 walk 하면 child 도 보호인 경우가 많고 HookScript/추가 작업
    -- taint 위험 → 비용 대비 이득 없음.
    if not _isProtected and frame.GetChildren then
        local ok, children = pcall(function() return { frame:GetChildren() } end)
        if ok and type(children) == "table" then
            for i = 1, #children do
                ScanFrameRecursive(children[i], depth + 1, noCache)
            end
        end
    end
end

-- 외부 노출 (Additions.lua 의 이름표 cog popup 처리 등에서 사용).
_G.EllesmereUI_KR_Scan = function(frame) pcall(ScanFrameRecursive, frame, 0, true) end

-- ============================================================
--  Phase 1: WidgetFactory 후킹
-- ============================================================
local function ApplyWidgetHooks()
    local W = EllesmereUI and EllesmereUI.Widgets
    if not W then return false end
    if W._koKR_hooked then return true end
    W._koKR_hooked = true

    -- (parent, text, yOffset, ...) 시그니처용 텍스트 위젯 후크 헬퍼
    local function hookText(name)
        local orig = W[name]
        if type(orig) ~= "function" then return end
        W[name] = function(self, parent, text, yOffset, ...)
            return orig(self, parent, TR(text), yOffset, ...)
        end
    end
    -- (parent, text, yOffset, values, ...) Dropdown 시그니처
    local function hookDropdown(name)
        local orig = W[name]
        if type(orig) ~= "function" then return end
        W[name] = function(self, parent, text, yOffset, values, ...)
            return orig(self, parent, TR(text), yOffset, TRValues(values), ...)
        end
    end
    -- (parent, yOffset, cfg) Cfg 시그니처
    local function hookCfg(name)
        local orig = W[name]
        if type(orig) ~= "function" then return end
        W[name] = function(self, parent, yOffset, cfg, ...)
            return orig(self, parent, yOffset, TRCfg(cfg), ...)
        end
    end

    hookText("SectionHeader"); hookText("Toggle"); hookText("Slider"); hookText("Checkbox")
    hookText("Button");        hookText("WideButton")
    hookDropdown("Dropdown");   hookDropdown("WideDropdown")
    hookCfg("DropdownWithOffsets")

    -- 다중 cfg 위젯
    if type(W.DualRow) == "function" then
        local _DR = W.DualRow
        W.DualRow = function(self, parent, yOffset, l, r, ...)
            return _DR(self, parent, yOffset, TRCfg(l), TRCfg(r), ...)
        end
    end
    if type(W.TripleRow) == "function" then
        local _TR3 = W.TripleRow
        W.TripleRow = function(self, parent, yOffset, l, m, r, ...)
            return _TR3(self, parent, yOffset, TRCfg(l), TRCfg(m), TRCfg(r), ...)
        end
    end
    -- (parent, configs[], yOffset) — 배열 형식
    local function hookCfgArray(name)
        local orig = W[name]
        if type(orig) ~= "function" then return end
        W[name] = function(self, parent, configs, yOffset)
            if type(configs) == "table" then
                local out = {}
                for i, cfg in ipairs(configs) do out[i] = TRCfg(cfg) end
                return orig(self, parent, out, yOffset)
            end
            return orig(self, parent, configs, yOffset)
        end
    end
    hookCfgArray("TripleDropdown"); hookCfgArray("TripleSlider")

    -- MultiSwatchRow — 중첩 swatch 배열 처리
    if type(W.MultiSwatchRow) == "function" then
        local _MSR = W.MultiSwatchRow
        W.MultiSwatchRow = function(self, parent, yOffset, cfg)
            if type(cfg) ~= "table" then return _MSR(self, parent, yOffset, cfg) end
            local out = {}
            for k, v in pairs(cfg) do out[k] = v end
            if out.text then out.text = TR(out.text) end
            if type(out.swatches) == "table" then
                local swOut = {}
                for i, sw in ipairs(out.swatches) do
                    local s = {}
                    for k, v in pairs(sw) do s[k] = v end
                    if s.tooltip then s.tooltip = TR(s.tooltip) end
                    swOut[i] = s
                end
                out.swatches = swOut
            end
            return _MSR(self, parent, yOffset, out)
        end
    end

    -- MakeStyledButton (text 인자 2번째)
    if type(EllesmereUI.MakeStyledButton) == "function"
       and not EllesmereUI._koKR_msb_hooked then
        EllesmereUI._koKR_msb_hooked = true
        local _MSB = EllesmereUI.MakeStyledButton
        EllesmereUI.MakeStyledButton = function(button, text, fontSize, ...)
            return _MSB(button, TR(text), fontSize, ...)
        end
    end

    return true
end

-- ============================================================
--  Phase 2: 사이드바 데이터 사전 mutate
-- ============================================================
local function TranslateRoster()
    local info = EllesmereUI and EllesmereUI._addonInfoByFolder
    if type(info) == "table" then
        for _, e in pairs(info) do
            if type(e) == "table" then
                if e.display     then e.display     = TR(e.display)     end
                if e.search_name then e.search_name = TR(e.search_name) end
            end
        end
    end

    local groups = EllesmereUI and EllesmereUI.ADDON_GROUPS
    if type(groups) == "table" then
        for _, g in ipairs(groups) do
            if g.label then g.label = TR(g.label) end
        end
    end

    for _, name in ipairs({ "VIS_VALUES", "VIS_VALUES_CDM" }) do
        local t = EllesmereUI and EllesmereUI[name]
        if type(t) == "table" then
            for k, v in pairs(t) do
                if type(v) == "string" and EUI_L[v] then t[k] = EUI_L[v] end
            end
        end
    end

    local viOpts = EllesmereUI and EllesmereUI.VIS_OPT_ITEMS
    if type(viOpts) == "table" then
        for _, item in ipairs(viOpts) do
            if type(item) == "table" then
                if item.label   then item.label   = TR(item.label)   end
                if item.tooltip then item.tooltip = TR(item.tooltip) end
            end
        end
    end
end

-- ============================================================
--  Phase 3: mainFrame 빌드 후 라벨 후처리
-- ============================================================
local function TranslateBuiltUI()
    local unlock = EllesmereUI and EllesmereUI._unlockSidebarBtn
    if unlock and unlock._label then RetranslateFontString(unlock._label) end

    -- 사이드바 네비 버튼 전수 처리. EUI 의 sidebarButtons 는 파일 로컬
    -- (미노출)이라 특수 "Global Settings" 버튼(EllesmereUI.lua:4279
    -- label:SetText("Global Settings"), sidebarButtons["_EUIGlobal"])을
    -- ScanFrameRecursive 가 깊이/타이밍으로 놓침. _unlockSidebarBtn(노출)
    -- 은 같은 사이드바의 형제 → 그 부모의 자식 = 모든 네비 버튼.
    -- 그걸 직접 훑어 결정적으로 커버(EUI_L["Global Settings"]="전체 설정"
    -- 이미 존재). 가산·pcall — 회귀 0.
    if unlock and unlock.GetParent then
        local okP, sb = pcall(unlock.GetParent, unlock)
        if okP and sb and sb.GetChildren then
            local kids = { sb:GetChildren() }
            for i = 1, #kids do
                local b = kids[i]
                if b and b._label then RetranslateFontString(b._label) end
            end
        end
    end

    local gh = EllesmereUI and EllesmereUI._sidebarGroupButtons
    if type(gh) == "table" then
        for _, hdr in pairs(gh) do
            if hdr and hdr._label then RetranslateFontString(hdr._label) end
        end
    end

    local mf = EllesmereUI and EllesmereUI._mainFrame
    if mf then ScanFrameRecursive(mf, 0) end
end

-- ============================================================
--  Phase 4a: Confirm/Info/Input Popup 후킹
-- ============================================================
local POPUP_FIELDS = { "title", "message", "confirmText", "cancelText",
                      "disclaimer", "content", "placeholder", "text" }

local function _trPopupOpts(opts)
    if type(opts) ~= "table" then return opts end
    local out = {}
    for k, v in pairs(opts) do out[k] = v end
    for i = 1, #POPUP_FIELDS do
        local f = POPUP_FIELDS[i]
        if type(out[f]) == "string" then
            out[f] = TR(out[f])
            local s = out[f]
            local rest = s:match("^Reset (.+)$");                                                                       if rest then out[f] = rest .. " 초기화" end
            rest = s:match("^Disable (.+) Module$");                                                                    if rest then out[f] = rest .. " 모듈 비활성화" end
            rest = s:match("^Enable (.+) Module$");                                                                     if rest then out[f] = rest .. " 모듈 활성화" end
            rest = s:match("^Are you sure you want to reset all (.+) settings to their defaults%? This will reload your UI%.$"); if rest then out[f] = rest .. " 설정을 모두 기본값으로 초기화하시겠습니까? UI가 리로드됩니다." end
            rest = s:match("^Are you sure you want to disable (.+)%?$");                                                if rest then out[f] = rest .. "을(를) 비활성화하시겠습니까?" end
            rest = s:match("^Are you sure you want to enable (.+)%?$");                                                 if rest then out[f] = rest .. "을(를) 활성화하시겠습니까?" end
            rest = s:match('^Are you sure you want to delete "(.+)"%?$');                                               if rest then out[f] = '"' .. rest .. '"을(를) 삭제하시겠습니까?' end
            rest = s:match('^Delete "(.+)"%?$');                                                                        if rest then out[f] = '"' .. rest .. '" 삭제?' end
            rest = s:match('^Enter a new name for "(.+)":$');                                                           if rest then out[f] = '"' .. rest .. '"의 새 이름을 입력하세요:' end
            rest = s:match("^EllesmereUI Update for (.+)$");                                                            if rest then out[f] = "EllesmereUI 업데이트: " .. rest end
        end
    end
    -- content 멀티라인: 줄 단위 EUI_L 정확매칭(통짜 키가 없는 거대 concat 대비,
    -- 미등록 줄은 영문 유지). ShowInfoPopup 안내문 등.
    if type(out.content) == "string" and out.content:find("\n", 1, true) then
        local lines, ch = {}, false
        for line in (out.content .. "\n"):gmatch("(.-)\n") do
            local tr = EUI_L[line]
            if type(tr) == "string" then ch = true; lines[#lines + 1] = tr
            else lines[#lines + 1] = line end
        end
        if ch then out.content = table.concat(lines, "\n") end
    end
    if type(out.extraButton) == "table" then
        local eb = {}
        for k, v in pairs(out.extraButton) do eb[k] = v end
        if type(eb.text) == "string" then eb.text = TR(eb.text) end
        out.extraButton = eb
    end
    return out
end

local POPUP_FRAMES = {
    "EUIConfirmPopup", "EUIInfoPopup", "EUIInputPopup", "EllesmereUnlockMode",
    "EUIWelcomePopup", "EUIFirstInstallPopup", "EUISpecAssignPopup",
}
local _popupCache = {}

local function ScanPopupFrames()
    for i = 1, #POPUP_FRAMES do
        local name = POPUP_FRAMES[i]
        local f = _popupCache[name] or _G[name]
        if f then
            _popupCache[name] = f
            ScanFrameRecursive(f, 0)
        end
    end
end

-- ============================================================
--  ShowWidgetTooltip 후킹 — 마우스 오버 툴팁
-- ============================================================
-- 툴팁 한 줄 번역: exact → 색코드-감싼 문장 → "~Nx  문장명" 합성 → 라벨 합성.
-- (강화 가능 타일 호버 툴팁: "80x  Hero Crest", "~80x  Hero Crest",
--  "|cff20c020At maximum item level|r" 등)
local function _trTipLine(line)
    if type(line) ~= "string" or line == "" then return line, false end
    local hit = EUI_L[line]
    if hit then return hit, true end
    -- 색코드로 감싼 전체 문장: "|cAARRGGBB...|r"
    local pre, body, suf = line:match("^(|c%x%x%x%x%x%x%x%x)(.-)(|r)$")
    if body and body ~= "" then
        local hb = EUI_L[body]
        if hb then return pre .. hb .. suf, true end
        local repB = _TranslateComposite(body)
        if repB then return pre .. repB .. suf, true end
    end
    -- "~Nx  Hero Crest" / "Nx  Hero Crest" → 한글 어순 "영웅 문장 ~N개"
    local tilde, num, _, crest = line:match("^(~?)(%d+)x(%s+)(.+)$")
    if num and crest then
        local tc = EUI_L[crest] or crest
        return tc .. " " .. tilde .. num .. "개", true
    end
    -- 동적 disabledTooltip "This option/feature requires X to be enabled/disabled"
    -- → 한글 어순 재조립. 가운데 X 는 EUI_L 번역(색코드 처리), 이미 완결형
    --   한글 문장이면 그대로(이중어미 방지).
    local inner, mode = line:match("^This option requires (.+) to be (%a+)$")
    if not inner then inner, mode = line:match("^This feature requires (.+) to be (%a+)$") end
    if inner then
        local ti = EUI_L[inner]
        if type(ti) ~= "string" then
            local p2, b2, s2 = inner:match("^(|c%x%x%x%x%x%x%x%x)(.-)(|r)$")
            if b2 and type(EUI_L[b2]) == "string" then ti = p2 .. EUI_L[b2] .. s2 end
        end
        if type(ti) ~= "string" then ti = inner end
        if ti:find("사용", 1, true) or ti:find("필요", 1, true) or ti:find("합니다", 1, true) then
            return ti, true
        end
        local verb = (mode == "disabled") and "꺼야" or "켜야"
        return "'" .. ti .. "' 옵션을 " .. verb .. " 사용 가능합니다", true
    end
    -- 동적 "Already on X" (CDM 약품/장신구 드롭다운: 이미 다른 바에 할당된 항목 호버 시).
    --   X 가 바 이름이라 정확매칭 불가 → 패턴 분리 후 EUI_L 로 X 만 번역.
    local onBar = line:match("^Already on (.+)$")
    if onBar then
        local tx = EUI_L[onBar] or onBar
        return "이미 " .. tx .. " 에 있음", true
    end
    local rep = _TranslateComposite(line)
    if rep then return rep, true end
    return line, false
end

local function _trTooltipText(text)
    if type(text) ~= "string" then return text end
    local ok, isEmpty = pcall(function() return text == "" end)
    if not ok or isEmpty then return text end
    local hit = EUI_L[text]
    if hit then return hit end
    -- ★ 멀티라인 합성 툴팁: \n 으로 분리해 각 줄을 번역(_trTipLine) 후 재결합.
    --   (UpgradeCalc refresh/scan 버튼, 아이템 타일 툴팁 등이 여러 줄 literal
    --    concat) 한 줄이라도 번역되면 적용.
    if text:find("\n", 1, true) then
        local out, changed = {}, false
        for line in (text .. "\n"):gmatch("(.-)\n") do
            local tl, ch = _trTipLine(line)
            if ch then changed = true end
            out[#out + 1] = tl
        end
        if changed then return table.concat(out, "\n") end
    else
        -- 단일 줄 합성 툴팁 ("120x  Hero Crest", 색코드 문장 등) — _trTipLine
        local tl, ch = _trTipLine(text)
        if ch then return tl end
    end
    -- "name\n색상wrap메시지"
    local nm, msg = text:match("^(.-)\n(|cff%x%x%x%x%x%x.+|r)$")
    if nm and msg then
        local trN = EUI_L[nm] or nm
        local trM = EUI_L[msg] or msg
        if trN ~= nm or trM ~= msg then return trN .. "\n" .. trM end
    end
    local req = text:match("^This option requires (.+) to be enabled$")
    if req then return "이 옵션은 \"" .. req .. "\"가 활성화되어야 합니다" end
    local r2 = text:match("^Required: (.+)$")
    if r2 then return "필수: " .. r2 end
    -- |cffXXXXXX prefix
    local prefix, body = text:match("^(|cff%x%x%x%x%x%x)(.+)$")
    if prefix and body then
        local h = EUI_L[body]
        if h then return prefix .. h end
        local head, tail = body:match("^(.-|r)(.*)$")
        if head and tail then
            local hb, ht = EUI_L[head], EUI_L[tail]
            if hb or ht then return prefix .. (hb or head) .. (ht or tail) end
            local hAll = EUI_L[head .. tail]
            if hAll then return prefix .. hAll end
        end
    end
    return text
end

local function PatchTooltipFrame(tt)
    if not tt or _wk_cimkrPatched[tt] then return end
    _wk_cimkrPatched[tt] = true

    local function patchFS(fs)
        if not fs or _wk_cimkrFSPatched[fs] then return end
        _wk_cimkrFSPatched[fs] = true
        if fs.SetText then
            local origSet = fs.SetText
            fs.SetText = function(self, text, ...)
                return origSet(self, _trTooltipText(text), ...)
            end
        end
        if fs.SetFormattedText then
            local origFmt = fs.SetFormattedText
            fs.SetFormattedText = function(self, fmt, ...)
                return origFmt(self, _trTooltipText(fmt), ...)
            end
        end
    end

    if tt.text  then patchFS(tt.text)  end
    if tt.title then patchFS(tt.title) end
    if tt.desc  then patchFS(tt.desc)  end

    if tt.GetRegions then
        for _, rgn in ipairs({ tt:GetRegions() }) do
            if rgn and rgn.GetObjectType and rgn:GetObjectType() == "FontString" then
                patchFS(rgn)
            end
        end
    end

    if tt.HookScript then
        tt:HookScript("OnShow", function(self)
            if not self.GetRegions then return end
            for _, rgn in ipairs({ self:GetRegions() }) do
                if rgn and rgn.GetObjectType and rgn:GetObjectType() == "FontString" then
                    local ok, txt = pcall(rgn.GetText, rgn)
                    if ok and type(txt) == "string" then
                        local tr = _trTooltipText(txt)
                        if tr ~= txt then pcall(rgn.SetText, rgn, tr) end
                    end
                end
            end
        end)
    end
end

-- 미니맵 거대한 금고 상세 툴팁: 본체 EllesmereUIMinimap.lua:1152 GetVaultTooltip
-- 가 CreateFrame("Frame", nil, UIParent, "BackdropTemplate") 로 무명 프레임
-- 만들고 strata=TOOLTIP. 타이틀 "Great Vault" + 3행("Raids", "Mythic+",
-- "World") + 토큰 FontString. UIParent 자식 중 strata=TOOLTIP, GameTooltip
-- 아님, FontString 에 "Great Vault" 또는 "위대한 금고" 들어있으면 vault 후보.
-- 1) 발견 즉시 ScanFrameRecursive 로 번역
-- 2) 그 프레임의 OnShow 에 hook 걸어 매번 show 시 row 라벨 재번역 (Raids/
--    Mythic+/World 가 ShowVaultTooltip 호출마다 SetText)
local _hookedVaultFrames = setmetatable({}, { __mode = "k" })

-- secret string 안전 동등 비교 (taint context 에서도 throw 안 함)
local function _safeEq(a, b)
    local ok, r = pcall(function() return a == b end)
    if not ok then return false end
    return r == true
end

local function _IsVaultTip(c)
    if type(c) ~= "table" or not c.GetRegions then return false end
    local ok, regs = pcall(function() return { c:GetRegions() } end)
    if not ok then return false end
    for i = 1, #regs do
        local rg = regs[i]
        local oko, ot = pcall(function() return rg and rg.GetObjectType and rg:GetObjectType() end)
        if oko and ot == "FontString" then
            local okT, t = pcall(rg.GetText, rg)
            -- secret string (HoverNameFrame 등 Blizzard secure 데이터 frame 자식)
            -- 인 경우 t == "..." 직접 비교가 throw → _safeEq 로 보호
            if okT and type(t) == "string"
               and (_safeEq(t, "Great Vault") or _safeEq(t, "위대한 금고")) then
                return true
            end
        end
    end
    return false
end

-- 금고 툴팁 FontString 에 SetText 후크 → 호버마다 본체가 영문으로 다시 SetText
-- 해도 즉시 재번역(캐시 비우고). 이게 없으면 캐시 때문에 "됐다 안됐다" 함.
-- secret string 위험은 pcall 로 흡수.
local function _HookVaultFS(fs)
    if not fs or _wk_cimkrVaultHook[fs] or not fs.SetText then return end
    if fs.IsForbidden and fs:IsForbidden() then return end
    _wk_cimkrVaultHook[fs] = true
    hooksecurefunc(fs, "SetText", function(self)
        if _wk_cimkrVaultReentry[self] then return end
        _wk_cimkrVaultReentry[self] = true
        _wkLastTxt[self] = nil
        pcall(RetranslateFontString, self)
        _wk_cimkrVaultReentry[self] = false
    end)
end
local function _HookVaultTree(frame, depth)
    if not frame or depth > 6 then return end
    local rok, regs = pcall(function() return { frame:GetRegions() } end)
    if rok and type(regs) == "table" then
        for i = 1, #regs do
            local rg = regs[i]
            local oko, ot = pcall(function() return rg and rg.GetObjectType and rg:GetObjectType() end)
            if oko and ot == "FontString" then _HookVaultFS(rg) end
        end
    end
    local cok, kids = pcall(function() return { frame:GetChildren() } end)
    if cok and type(kids) == "table" then
        for i = 1, #kids do _HookVaultTree(kids[i], depth + 1) end
    end
end

-- 금고 툴팁 *프레임 스케일* (글자만 키우면 배열이 깨져서 SetScale 로 통째 확대).
-- K메뉴 "툴팁 크기"(EllesmereUIKorAdditionsDB.settingsTipScale, 배율 1.0~2.0)에 연동.
local function _ApplyVaultScale(c)
    if not c or not c.SetScale then return end
    -- ★ 툴팁 *스케일* 은 추가기능(Additions) 기능 → Additions 가 로드돼 있을 때만
    --   적용. 꺼져 있으면 기본 크기(1). (번역은 추가기능과 무관하게 항상 동작.)
    local scale = 1.0
    if _G.__cimkrAdditionsActive then
        local db = _G.EllesmereUIKorAdditionsDB
        scale = (type(db) == "table" and tonumber(db.settingsTipScale)) or 1.0
        if scale < 1 then scale = 1 elseif scale > 2 then scale = 2 end
    end
    pcall(c.SetScale, c, scale)
end

local function _HookVaultFrameOnce(c)
    if _hookedVaultFrames[c] then return end
    _hookedVaultFrames[c] = true
    pcall(ScanFrameRecursive, c, 0)
    pcall(_HookVaultTree, c, 0)   -- SetText 후크 설치 → 재호버 시 영문 재유입 방지
    _ApplyVaultScale(c)
    if c.HookScript then
        c:HookScript("OnShow", function(self)
            pcall(ScanFrameRecursive, self, 0)
            pcall(_HookVaultTree, self, 0)
            _ApplyVaultScale(self)
        end)
    end
end

-- 미니맵 "접속 친구" 인디케이터 호버 친구 목록 툴팁(EllesmereUIMinimap.lua
-- GetFriendsTT). 금고 툴팁과 같은 무명 strata=TOOLTIP 프레임이나 .text 없음.
-- 섹션 헤더(Favorites/Guild/Friends 또는 한글) 또는 빈 목록 마커로 식별 후
-- 금고와 동일하게 *프레임 스케일*(settingsTipScale) + OnShow 후크 + 번역 스캔.
local _hookedFriendsFrames = setmetatable({}, { __mode = "k" })
local function _IsFriendsTip(c)
    if type(c) ~= "table" or not c.GetRegions then return false end
    local ok, regs = pcall(function() return { c:GetRegions() } end)
    if not ok then return false end
    for i = 1, #regs do
        local rg = regs[i]
        local oko, ot = pcall(function() return rg and rg.GetObjectType and rg:GetObjectType() end)
        if oko and ot == "FontString" then
            local okT, t = pcall(rg.GetText, rg)
            if okT and type(t) == "string" and t ~= "" then
                -- ★ 헤더는 "Favorites (|cff..3|r)" 처럼 개수·색코드가 붙음 →
                --   정확매칭 불가. 부분/접두사 일치로 식별.
                local okF, hit = pcall(function()
                    return t:find("Favorites", 1, true) or t:find("즐겨찾기", 1, true)
                        or t:find("No friends online", 1, true) or t:find("접속한 친구 없음", 1, true)
                        or t:find("^Guild %(") or t:find("^길드 %(")
                        or t:find("^Friends %(") or t:find("^친구 %(")
                end)
                if okF and hit then return true end
            end
        end
    end
    return false
end

local function _HookFriendsFrameOnce(c)
    if _hookedFriendsFrames[c] then return end
    _hookedFriendsFrames[c] = true
    pcall(ScanFrameRecursive, c, 0)
    pcall(_HookVaultTree, c, 0)   -- SetText 후크(헤더 영문 재유입 시 재번역)
    _ApplyVaultScale(c)           -- settingsTipScale 로 프레임 통째 스케일
    if c.HookScript then
        c:HookScript("OnShow", function(self)
            pcall(ScanFrameRecursive, self, 0)
            pcall(_HookVaultTree, self, 0)
            _ApplyVaultScale(self)
        end)
    end
end

local function _ScanVaultTip()
    if not (UIParent and UIParent.GetChildren) then return end
    local gtt = _G.GameTooltip
    local ok, kids = pcall(function() return { UIParent:GetChildren() } end)
    if not ok then return end
    for i = 1, #kids do
        local c = kids[i]
        if type(c) == "table" and c ~= gtt and c ~= GameTooltip
           and c ~= (EllesmereUI and EllesmereUI._mainFrame) then
            local fok, forb = pcall(function()
                return (c.IsForbidden and c:IsForbidden())
                    or (c.IsProtected and c:IsProtected())
            end)
            if not fok or not forb then
                local stok, isTT = pcall(function() return c:GetFrameStrata() == "TOOLTIP" end)
                if stok and isTT then
                    -- vault tip 인지 식별 후 1회 hook 설치 + 즉시 스캔
                    if _IsVaultTip(c) then
                        _HookVaultFrameOnce(c)
                    elseif _IsFriendsTip(c) then
                        _HookFriendsFrameOnce(c)
                    else
                        -- 여전히 다른 TOOLTIP strata 무명 프레임도 보일 때
                        -- (이전 동작 유지)는 IsShown 일 때만 스캔
                        local sok, shown = pcall(c.IsShown, c)
                        if sok and shown then
                            pcall(ScanFrameRecursive, c, 0)
                        end
                    end
                end
            end
        end
    end
end

-- EllesmereUI 미니맵 버튼(전역명 EllesmereUIMinimapButton) 자체 툴팁:
-- 애드온 OnEnter 가 GameTooltip 에 평문 라인 AddLine(secret 아님).
-- *그 버튼의 OnEnter 를 HookScript*(글로벌 GameTooltip 후크 아님 →
-- 문서화된 secure/secret 크래시 클래스 회피) → 원본 OnEnter 직후
-- owner==그버튼 일 때만 GameTooltipTextLeftN 을 EUI_L 로 재작성.
-- 전 구간 pcall. 다른 애드온도 owner 고정 OnEnter 후처리로 함.
local function _HookMMBtn()
    local b = _G.EllesmereUIMinimapButton
    if not b then return false end
    if _wk_cimMMTip[b] or not b.HookScript then return true end
    _wk_cimMMTip[b] = true
    b:HookScript("OnEnter", function()
        local gtt = _G.GameTooltip
        local ok, owner = pcall(gtt.GetOwner, gtt)
        if not ok or owner ~= b then return end
        local nok, n = pcall(gtt.NumLines, gtt)
        if not nok or not n then return end
        for i = 1, n do
            local fs = _G["GameTooltipTextLeft" .. i]
            if fs and fs.GetText then
                local gok, t = pcall(fs.GetText, fs)
                if gok and type(t) == "string" and t ~= "" then
                    local h = EUI_L[t]
                    if h then pcall(fs.SetText, fs, h) end
                end
            end
        end
        pcall(gtt.Show, gtt)
    end)
    return true
end
-- ── QoL 별도 창 (UpgradeCalc / Keys popup) OnShow 스캔 ───────────────
-- 두 창은 UIParent 직속 별도 frame (EUIUpgCalcFrame: file-body 생성,
-- EUIKeysPopup: lazy). EUI_L 안 읽고 영문 하드코딩 + 우리 mainFrame 스캔
-- 영역 밖 → 직접 OnShow 후크 + 즉시 스캔. 동적 합성 문자열은
-- RetranslateFontString 의 gsub 패턴이 처리.
-- QoL 창의 FontString 에 SetText 후크 설치 → 내용 갱신(숫자 채워짐, 대기열
-- 변경 등)될 때마다 자동 재번역. 창이 addon 소유(비-secure)라 taint 무관.
-- 재진입 가드 __cimkrSTBusy 로 무한 루프 방지.
local function _qolSetTextHook(self)
    if _wk_cimkrSTBusy[self] then return end
    local ok, txt = pcall(self.GetText, self)
    if not ok or type(txt) ~= "string" or txt == "" then return end
    -- 1) 정확 매칭
    local hit = EUI_L[txt]
    if hit then
        _wk_cimkrSTBusy[self] = true
        pcall(self.SetText, self, hit)
        _wk_cimkrSTBusy[self] = false
        return
    end
    -- 2) 색코드로 감싼 단일 라벨: "|cffXXXXXXXX본문|r" → 본문만 EUI_L 매칭 후
    --    색코드 보존하여 재조립. (UpgradeCalc 문장 표의 트랙명 = 색상 트랙명)
    local pre, body, suf = txt:match("^(|c%x%x%x%x%x%x%x%x)(.-)(|r)$")
    if body and body ~= "" then
        local h2 = EUI_L[body]
        if h2 then
            _wk_cimkrSTBusy[self] = true
            pcall(self.SetText, self, pre .. h2 .. suf)
            _wk_cimkrSTBusy[self] = false
            return
        end
    end
    -- 3) 큐 항목 "부위명 |cff888888+N|r" — 앞 부위명만 EUI_L 매칭, 뒤 보존.
    local slot, tail = txt:match("^(.-) (|cff%x%x%x%x%x%x%x?%x?%+.+)$")
    if slot and tail then
        local ts = EUI_L[slot]
        if ts then
            _wk_cimkrSTBusy[self] = true
            pcall(self.SetText, self, ts .. " " .. tail)
            _wk_cimkrSTBusy[self] = false
            return
        end
    end
    -- 4) 합성 라벨 (숫자/색코드 섞임 — 라벨만 gsub)
    local rep = _TranslateComposite(txt)
    if rep then
        _wk_cimkrSTBusy[self] = true
        pcall(self.SetText, self, rep)
        _wk_cimkrSTBusy[self] = false
    end
end

local function _InstallFSHooks(frame, depth)
    if not frame or depth > 8 then return end
    if frame.GetRegions then
        local ok, regs = pcall(function() return { frame:GetRegions() } end)
        if ok then
            for i = 1, #regs do
                local rg = regs[i]
                local oko, ot = pcall(function() return rg and rg.GetObjectType and rg:GetObjectType() end)
                if oko and ot == "FontString" and not _wk_cimkrSTHooked[rg] then
                    _wk_cimkrSTHooked[rg] = true
                    pcall(function() hooksecurefunc(rg, "SetText", _qolSetTextHook) end)
                    -- 후크 설치 시점의 현재 글자도 즉시 번역 (비동기로 늦게 채워진
                    -- 타일 = 트랙명 "Hero"/"Champion" 등 — 후크는 다음 SetText 부터라
                    -- 현재 텍스트는 직접 1회 변환해야 영문 잔존 방지).
                    pcall(_qolSetTextHook, rg)
                end
            end
        end
    end
    if frame.GetChildren then
        local ok, kids = pcall(function() return { frame:GetChildren() } end)
        if ok then for i = 1, #kids do _InstallFSHooks(kids[i], depth + 1) end end
    end
end

local function _HookQoLWindow(name)
    local f = _G[name]
    if not f or _wk_cimkrQoLHooked[f] then return (f ~= nil) end
    _wk_cimkrQoLHooked[f] = true
    if f.HookScript then
        f:HookScript("OnShow", function(self)
            pcall(ScanFrameRecursive, self, 0)
            -- 동적 라벨 FontString 에 SetText 후크 (1회만 — 가드) → 이후 숫자
            -- 갱신·대기열 변경 시 자동 재번역. + 즉시 1회 스캔.
            pcall(_InstallFSHooks, self, 0)
            if C_Timer and C_Timer.After then
                C_Timer.After(0.1, function() pcall(ScanFrameRecursive, self, 0); pcall(_InstallFSHooks, self, 0) end)
                C_Timer.After(0.5, function() pcall(_InstallFSHooks, self, 0) end)
            end
            -- ★ 창-표시 게이트 ticker: 장비강화 타일(트랙명 "Hero" 등)은 아이템
            --   정보 비동기 로딩 뒤 *한참 늦게* 채워져 OnShow 패스(0.5초)로는
            --   놓침. 설정창과 독립적으로 열려 메인 ticker 도 안 돌 수 있음 →
            --   창 자체에 0.5초 ticker 부착, 새 FontString 만 후크+현재글자 번역
            --   (가드로 기존건 즉시 skip). 창 숨기면 스스로 Cancel = 비용 0.
            if not _wk_cimkrQoLTick[self] and C_Timer and C_Timer.NewTicker then
                _wk_cimkrQoLTick[self] = C_Timer.NewTicker(0.5, function(tk)
                    if not (self.IsShown and self:IsShown()) then
                        tk:Cancel(); _wk_cimkrQoLTick[self] = nil; return
                    end
                    pcall(_InstallFSHooks, self, 0)
                end)
            end
        end)
    end
    return true
end
-- /keys 팝업 스캔+후크 1회 (열렸을 때 호출). lazy 라 후크 재시도(60초)가
-- 만료된 뒤 열어도 잡히게, 슬래시/RefreshPopupIfOpen 후크에서 직접 호출.
local function _ScanKeysPopup()
    local f = _G.EUIKeysPopup
    if not f then return end
    _HookQoLWindow("EUIKeysPopup")             -- OnShow 후크 + 게이트 ticker (가드로 1회)
    pcall(ScanFrameRecursive, f, 0)
    pcall(_InstallFSHooks, f, 0)
    if C_Timer and C_Timer.After then
        C_Timer.After(0, function() pcall(ScanFrameRecursive, f, 0); pcall(_InstallFSHooks, f, 0) end)
        C_Timer.After(0.1, function() pcall(ScanFrameRecursive, f, 0); pcall(_InstallFSHooks, f, 0) end)
    end
end

local function HookQoLWindows()
    local upg = _HookQoLWindow("EUIUpgCalcFrame")   -- file-body 생성 (즉시 존재)
    local keys = _HookQoLWindow("EUIKeysPopup")      -- lazy (첫 /keys 시 생성)
    -- Keys 는 lazy → 재시도
    if not keys and C_Timer and C_Timer.After then
        local tries = 0
        local function retry()
            tries = tries + 1
            if _HookQoLWindow("EUIKeysPopup") or tries > 30 then return end
            C_Timer.After(2, retry)
        end
        C_Timer.After(2, retry)
    end
    -- ★ lazy 팝업 확실히 잡기: RefreshPopupIfOpen(전역) + /keys 슬래시 후크.
    --   재시도 만료 후 열어도 *열 때마다* 스캔/후크 (taint-safe — 평범 함수/테이블).
    if type(_G._EUI_RefreshKeystonePopup) == "function" and not _G.__cimkrKeysRefHooked then
        _G.__cimkrKeysRefHooked = true
        hooksecurefunc(_G, "_EUI_RefreshKeystonePopup", _ScanKeysPopup)
    end
    if SlashCmdList and type(SlashCmdList.EUIKEYS) == "function" and not _G.__cimkrKeysSlashHooked then
        _G.__cimkrKeysSlashHooked = true
        hooksecurefunc(SlashCmdList, "EUIKEYS", function()
            if C_Timer and C_Timer.After then
                C_Timer.After(0,   _ScanKeysPopup)
                C_Timer.After(0.1, _ScanKeysPopup)
            else
                _ScanKeysPopup()
            end
        end)
    end
end

-- ============================================================
--  UpgradeCalc 한글 클라 지원 (원본 미수정 — 전역 EUIUpgCalc 메서드 교체)
--  원본 GetItemTrackAndRank 는 영문 툴팁("Upgrade Level: Hero 2/6")만 파싱
--  → 한글 클라("레벨 강화: 영웅 2/6")에선 트랙 미파싱 → 필요 문장 계산 0.
--  영문 파싱 실패 시 한글 툴팁을 읽어 한글 트랙명을 *영문 키*로 매핑해 반환
--  (애드온 나머지는 영문 키 Data.tracks["Hero"] 로 작동하므로 키 변환 필수).
--  제작/공허는 업그레이드 대상이 아니라 처리 불필요.
-- ============================================================
local _UPG_KOTRACK = {
    ["모험가"] = "Adventurer",
    ["노련가"] = "Veteran",
    ["챔피언"] = "Champion",
    ["영웅"]   = "Hero",
    ["신화"]   = "Myth",
}
local _upgKoCache, _upgKoCount = {}, 0   -- link -> { t=영문키|false, r=랭크 }
local function _UpgKoParse(link)
    if type(link) ~= "string" or link == "" then return nil end
    local c = _upgKoCache[link]
    if c then return c.t or nil, c.r end
    local en, rnum
    if C_TooltipInfo and C_TooltipInfo.GetHyperlink then
        local data = C_TooltipInfo.GetHyperlink(link)
        if data and data.lines then
            for _, line in ipairs(data.lines) do
                local txt = line and line.leftText
                if type(txt) == "string" and txt:find("레벨 강화", 1, true) then
                    for ko, eng in pairs(_UPG_KOTRACK) do
                        local r = txt:match(ko .. "%s*(%d+)/%d+")
                        if r then en, rnum = eng, tonumber(r); break end
                    end
                    break
                end
            end
        end
    end
    -- ★ 상한: 살펴보기/거래 등으로 본 아이템 링크가 무한 누적되지 않게
    --   200개 넘으면 통째로 비움(다음 조회 시 재파싱 — 비용 미미).
    if _upgKoCount >= 200 then _upgKoCache, _upgKoCount = {}, 0 end
    _upgKoCache[link] = { t = en or false, r = rnum }
    _upgKoCount = _upgKoCount + 1
    return en, rnum
end
local function _HookUpgradeCalcParser()
    local C = _G.EUIUpgCalc
    if type(C) ~= "table" or type(C.GetItemTrackAndRank) ~= "function" then return false end
    if _wk_cimkrKoParse[C] then return true end
    _wk_cimkrKoParse[C] = true
    local orig = C.GetItemTrackAndRank
    C.GetItemTrackAndRank = function(self, link)
        local t, r = orig(self, link)
        if t then return t, r end          -- 영문 파싱 성공 시 그대로
        local kt, kr = _UpgKoParse(link)    -- 한글 폴백
        if kt then return kt, kr end
        return t, r
    end
    return true
end

local function HookMinimapBtnTip()
    if _HookMMBtn() then return end
    if not (C_Timer and C_Timer.After) then return end
    local tries = 0
    local function retry()
        tries = tries + 1
        if _HookMMBtn() or tries > 20 then return end
        C_Timer.After(1, retry)
    end
    C_Timer.After(1, retry)
end

-- 미니맵 가운데 클릭 micro menu (EllesmereUIMicroMenu) 한글화:
-- 본체는 PLAYER_LOGIN 에서 CreateMenuFrame 자동 실행 (EllesmereUIMinimap.lua:3501).
-- ScanFrameRecursive 우회 → menuFrame 자식(secure 버튼들) 직접 walk → FontString
-- 라벨에 직접 EUI_L 매칭/SetText. SetText 후크도 설치(향후 변경 대비).
local function _kokrTranslateMM(mm)
    if not mm or not mm.GetChildren then return end
    local ok, kids = pcall(function() return { mm:GetChildren() } end)
    if not ok then return end
    for _, btn in ipairs(kids) do
        if type(btn) == "table" and btn.GetRegions then
            local rok, regs = pcall(function() return { btn:GetRegions() } end)
            if rok then
                for _, r in ipairs(regs) do
                    local tok, ot = pcall(function() return r.GetObjectType and r:GetObjectType() end)
                    if tok and ot == "FontString" then
                        local gok, t = pcall(r.GetText, r)
                        if gok and type(t) == "string" and t ~= "" then
                            local hit = EUI_L[t]
                            if hit then pcall(r.SetText, r, hit) end
                        end
                    end
                end
            end
        end
    end
end
local function HookMinimapMicroMenu()
    if not (C_Timer and C_Timer.NewTicker) then return end
    if _G.__cimkrMMMenuScanned then return end
    local tries = 0
    local ticker
    ticker = C_Timer.NewTicker(0.2, function()
        tries = tries + 1
        local mm = _G.EllesmereUIMicroMenu
        if mm then
            _G.__cimkrMMMenuScanned = true
            pcall(_kokrTranslateMM, mm)
            ticker:Cancel()
        elseif tries > 50 then  -- 10초 후 포기
            ticker:Cancel()
        end
    end)
end

-- Minimap 영역 hover 시 vault tooltip 후보 탐색. 본체 vault tooltip 은
-- 첫 vault 버튼 hover 시 lazy 생성되므로 Minimap OnEnter 1회로는 못 잡을
-- 수도 있지만, 두 번째 hover 부턴 _IsVaultTip 매칭 → OnShow 후크 설치 →
-- 그 이후 매 show 마다 자동 번역.
local function HookMinimapForVaultScan()
    if EllesmereUI and EllesmereUI._koKR_mmVaultHooked then return end
    local mm = _G.Minimap
    if not mm or not mm.HookScript then return end
    if EllesmereUI then EllesmereUI._koKR_mmVaultHooked = true end
    -- ★ 금고 툴팁은 미니맵 진입 *후* 금고 버튼으로 이동했을 때 떠서, 진입 직후
    --   1~2회 스캔으론 놓침("됐다 안됐다"). 여러 시점 재시도로 확실히 탐지 →
    --   한 번 탐지되면 OnShow 후크 + 캐시 weak 로 이후엔 자동(번역+스케일).
    local function _vaultScanBurst()
        if not (C_Timer and C_Timer.After) then _ScanVaultTip(); return end
        for _, d in ipairs({ 0, 0.1, 0.25, 0.45, 0.7 }) do
            C_Timer.After(d, _ScanVaultTip)
        end
    end
    -- ★ 금고/친구 등 *인디케이터 버튼*은 미니맵 진입 *후* 호버 → 진입 직후
    --   0.7초 버스트로는 놓침(특히 친구 툴팁). 미니맵 자식 버튼들의 OnEnter
    --   자체도 스캔 트리거로 후크(1회씩) → 그 버튼 호버 시 툴팁 떠도 확실히 탐지.
    --   버튼은 lazy 생성/재부모화될 수 있어 진입마다 재-walk(이미 후크는 skip).
    local function _hookMMKids(fr, depth)
        if not fr or depth > 2 or not fr.GetChildren then return end
        local ok, kids = pcall(function() return { fr:GetChildren() } end)
        if not ok then return end
        for _, ch in ipairs(kids) do
            if type(ch) == "table" then
                -- ★ EUI 인디케이터(vault/tracking/calendar/mail/crafting/friends)만 후크.
                --   블리자드 추적 POI(광석/약초) 핀은 _indicatorKey 없음 → HookScript
                --   하면 secure 경로 taint 되어 호버 GameTooltip 안 뜸. 필터 필수.
                if ch._indicatorKey and ch.HookScript and not _wk_cimMMScanHook[ch] then
                    local oko, ot = pcall(ch.GetObjectType, ch)
                    if oko and (ot == "Button" or ot == "Frame") then
                        _wk_cimMMScanHook[ch] = true
                        pcall(ch.HookScript, ch, "OnEnter", _vaultScanBurst)
                    end
                end
                _hookMMKids(ch, depth + 1)
            end
        end
    end
    mm:HookScript("OnEnter", function()
        _vaultScanBurst()
        _hookMMKids(mm, 0)
    end)
    _hookMMKids(mm, 0)
    -- 인디게이터 버튼은 미니맵 setup 에서 늦게 생성될 수 있어, 로그인 후
    -- 몇 시점에 재-walk 해 미리 후크(미니맵을 안 거치고 버튼 직행해도 1회차 탐지).
    if C_Timer and C_Timer.After then
        for _, d in ipairs({ 2, 5, 10 }) do
            C_Timer.After(d, function() _hookMMKids(mm, 0) end)
        end
    end
end

local function HookShowWidgetTooltip()
    if not EllesmereUI or EllesmereUI._koKR_swtHooked then return end
    if type(EllesmereUI.ShowWidgetTooltip) ~= "function" then return end
    EllesmereUI._koKR_swtHooked = true
    local orig = EllesmereUI.ShowWidgetTooltip
    EllesmereUI.ShowWidgetTooltip = function(anchor, text, opts)
        local r = orig(anchor, _trTooltipText(text), opts)
        -- 모든 ShowWidgetTooltip 호출 시 vault 후보 스캔 (현재 본체는
        -- 금고 hover 때 ShowWidgetTooltip 안 부르고 커스텀 ShowVaultTooltip
        -- 사용 — 그래도 안전망. 실제 식별/hook 은 _ScanVaultTip 안에서.)
        if C_Timer and C_Timer.After then
            C_Timer.After(0, _ScanVaultTip)
        end
        return r
    end

    -- file-local 호출 잡기 위해 tooltipFrame 직접 패치
    if debug and debug.getupvalue then
        local i = 1
        while true do
            local name, val = debug.getupvalue(orig, i)
            if not name then break end
            if name == "GetTooltipFrame" and type(val) == "function" then
                local ok, tt = pcall(val)
                if ok and tt then PatchTooltipFrame(tt) end
                break
            elseif name == "tooltipFrame" and type(val) == "table" then
                PatchTooltipFrame(val)
                break
            end
            i = i + 1
        end
    end
end

-- 위젯 툴팁 싱글톤(.text 보유 무명 TOOLTIP 프레임)의 .text/.title/.desc SetText
-- 후크 → *로컬 경로*(EUI 의 file-local ShowWidgetTooltip 직접 호출, 필드 래퍼·
-- getupvalue 패치를 우회) 툴팁도 확실히 번역. PatchTooltipFrame(getupvalue) 보조.
local function _hookTipFS(fs)
    if not fs or _wk_cimkrTipTxtHook[fs] or not fs.SetText then return end
    if fs.IsForbidden and fs:IsForbidden() then return end
    _wk_cimkrTipTxtHook[fs] = true
    _G.__cimTipTxtHooked = true   -- 번역 싱글톤 후크 확보 → 로그인 폴링 티커 종료 신호
    hooksecurefunc(fs, "SetText", function(self, text)
        if _wk_cimkrTipTxtRe[self] or type(text) ~= "string" then return end
        local tr = _trTooltipText(text)
        if tr ~= text then
            _wk_cimkrTipTxtRe[self] = true
            pcall(self.SetText, self, tr)
            _wk_cimkrTipTxtRe[self] = false
        end
    end)
    local okT, cur = pcall(fs.GetText, fs)
    if okT and type(cur) == "string" and cur ~= "" then
        local tr = _trTooltipText(cur)
        if tr ~= cur then pcall(fs.SetText, fs, tr) end
    end
end
local function _ScanTipSingleton()
    if not (UIParent and UIParent.GetChildren) then return end
    local ok, kids = pcall(function() return { UIParent:GetChildren() } end)
    if not ok then return end
    for _, c in ipairs(kids) do
        if type(c) == "table" and c.GetObjectType then
            local okN, nm = pcall(c.GetName, c)
            local okS, isTT = pcall(function() return c:GetFrameStrata() == "TOOLTIP" end)
            if okN and nm == nil and okS and isTT and type(c.text) == "table" then
                _hookTipFS(c.text)
                if type(c.title) == "table" then _hookTipFS(c.title) end
                if type(c.desc) == "table" then _hookTipFS(c.desc) end
            end
        end
    end
end

local function HookPopups()
    if not EllesmereUI or EllesmereUI._koKR_popupHooked then return end
    EllesmereUI._koKR_popupHooked = true

    for _, fn in ipairs({ "ShowConfirmPopup", "ShowInfoPopup", "ShowInputPopup" }) do
        if type(EllesmereUI[fn]) == "function" then
            local orig = EllesmereUI[fn]
            EllesmereUI[fn] = function(self, opts, ...)
                local r = { orig(self, _trPopupOpts(opts), ...) }
                ScanPopupFrames()
                return unpack(r)
            end
        end
    end

    -- ★ BuildVisOptsCBDropdown: items[i].label 사전 번역 (lazy 생성 드롭다운 — 일반 스캔
    --   타이밍 못 잡음). 자원바 토템 "Enabled Classes" 의 |c..Warrior|r 같은 색코드+영문
    --   라벨을 색코드 안 본문만 EUI_L 로 번역 후 재조립.
    if type(EllesmereUI.BuildVisOptsCBDropdown) == "function"
       and not EllesmereUI._koKR_visOptsCBHooked then
        EllesmereUI._koKR_visOptsCBHooked = true
        local origVisCB = EllesmereUI.BuildVisOptsCBDropdown
        EllesmereUI.BuildVisOptsCBDropdown = function(parentFrame, ddW, fLevel, items, ...)
            if type(items) == "table" then
                for i = 1, #items do
                    local it = items[i]
                    if type(it) == "table" and type(it.label) == "string" then
                        local pre, body, suf = it.label:match("^(|c%x%x%x%x%x%x%x%x)(.-)(|r)$")
                        if body and body ~= "" then
                            local t = EUI_L and EUI_L[body]
                            if t then it.label = pre .. t .. suf end
                        else
                            local t = EUI_L and EUI_L[it.label]
                            if t then it.label = t end
                        end
                    end
                end
            end
            return origVisCB(parentFrame, ddW, fLevel, items, ...)
        end
    end

    if type(EllesmereUI.BuildCogPopup) == "function" then
        local origCog = EllesmereUI.BuildCogPopup
        EllesmereUI.BuildCogPopup = function(opts, ...)
            if type(opts) ~= "table" then return origCog(opts, ...) end
            local out = {}
            for k, v in pairs(opts) do out[k] = v end
            if type(out.title) == "string" then out.title = TR(out.title) end
            if type(out.rows) == "table" then
                local rows = {}
                for i, row in ipairs(out.rows) do
                    local r = {}
                    for k, v in pairs(row) do r[k] = v end
                    if type(r.label)   == "string" then r.label   = TR(r.label)   end
                    if type(r.tooltip) == "string" then r.tooltip = TR(r.tooltip) end
                    if type(r.disabledTooltip) == "string" then r.disabledTooltip = TR(r.disabledTooltip) end
                    if type(r.values) == "table" then r.values = TRValues(r.values) end
                    rows[i] = r
                end
                out.rows = rows
            end
            local result = { origCog(out, ...) }
            ScanPopupFrames()
            return unpack(result)
        end
    end

    -- 잠금 해제 / 옵션 popup — OnShow 즉시 번역 (영문 깜빡임 방지)
    local function HookPopupShow(name)
        local f = _G[name]
        if not f or _wk_cimkrShowHooked[f] then return end
        _wk_cimkrShowHooked[f] = true
        if f.HookScript then
            f:HookScript("OnShow", function(self) ScanFrameRecursive(self, 0) end)
        end
    end
    for i = 1, #POPUP_FRAMES do HookPopupShow(POPUP_FRAMES[i]) end
    if C_Timer and C_Timer.After then
        C_Timer.After(1, function()
            for i = 1, #POPUP_FRAMES do HookPopupShow(POPUP_FRAMES[i]) end
        end)
    end
end

-- ============================================================
--  Phase 4: RegisterModule 후킹
-- ============================================================
local function HookRegisterModule()
    if not (EllesmereUI and EllesmereUI.RegisterModule) then return end
    if EllesmereUI._koKR_registerHooked then return end
    EllesmereUI._koKR_registerHooked = true

    hooksecurefunc(EllesmereUI, "RegisterModule", function(self, folder, config)
        if type(config) ~= "table" then return end
        if config.title       then config.title       = TR(config.title)       end
        if config.description then config.description = TR(config.description) end
        -- pages / disabledPageTooltips 는 내부 lookup 키로도 사용 → mutate 금지
    end)
end

-- ============================================================
--  Phase 4b: AuraBuff Reminders 한글화 (WoW API 사용)
-- ============================================================
local EABR_SPELL_TABLES = {
    "_EABR_RAID_BUFFS", "_EABR_AURAS",
    "_EABR_ROGUE_POISONS", "_EABR_PALADIN_RITES",
    "_EABR_SHAMAN_IMBUES", "_EABR_SHAMAN_SHIELDS",
}
local EABR_ITEM_TABLES = {
    "_EABR_FLASK_ITEMS", "_EABR_FOOD_ITEMS", "_EABR_WEAPON_ENCHANT_ITEMS",
}
local EABR_ZONE_TABLES = { "_EABR_TALENT_REMINDER_ZONES" }

local function _GetSpellName(id)
    if not id then return nil end
    if C_Spell and C_Spell.GetSpellName then
        local n = C_Spell.GetSpellName(id)
        if type(n) == "string" and n ~= "" then return n end
    end
    if GetSpellInfo then
        local n = GetSpellInfo(id)
        if type(n) == "string" and n ~= "" then return n end
    end
end

local function _GetItemName(id)
    if not id then return nil end
    if C_Item and C_Item.GetItemNameByID then
        local n = C_Item.GetItemNameByID(id)
        if type(n) == "string" and n ~= "" then return n end
    end
    if GetItemInfo then
        local n = GetItemInfo(id)
        if type(n) == "string" and n ~= "" then return n end
    end
end

local function LocalizeEABR()
    for _, name in ipairs(EABR_SPELL_TABLES) do
        local t = _G[name]
        if type(t) == "table" then
            for _, e in ipairs(t) do
                if type(e) == "table" then
                    local sid = e.castSpell or (type(e.buffIDs) == "table" and e.buffIDs[1])
                    local n = _GetSpellName(sid)
                    if n then e.name = n end
                end
            end
        end
    end
    for _, name in ipairs(EABR_ITEM_TABLES) do
        local t = _G[name]
        if type(t) == "table" then
            for _, e in ipairs(t) do
                if type(e) == "table" then
                    local iid = e.itemID or (type(e.items) == "table" and e.items[1])
                    local n = _GetItemName(iid)
                    if n then e.name = n end
                end
            end
        end
    end
    for _, name in ipairs(EABR_ZONE_TABLES) do
        local t = _G[name]
        if type(t) == "table" then
            for _, e in ipairs(t) do
                if type(e) == "table" and type(e.name) == "string" then
                    local newName
                    if e.mapID and C_Map and C_Map.GetMapInfo then
                        local info = C_Map.GetMapInfo(e.mapID)
                        if info and type(info.name) == "string" and info.name ~= "" then
                            newName = info.name
                        end
                    end
                    if not newName then
                        local hit = EUI_L[e.name]
                        if hit then newName = hit end
                    end
                    if newName then e.name = newName end
                end
            end
        end
    end
end

-- ============================================================
--  Phase 4c: GameTooltip / LibDBIcon 툴팁
--  AddLine wrapper + OnShow 시 모든 FontString 강제 번역 (영문 깜빡임 차단)
-- ============================================================
-- 11.x: 툴팁 FontString 텍스트가 'secret' 값이면 비교(txt~="")·인덱싱
-- ·SetText 가 taint 중엔 전부 throw. 모든 secret-위험 연산을 이 헬퍼
-- *안* 에 모으고 호출부에서 pcall → secret 이면 그 region 만 조용히 skip.
local function _tipRegionTrans(rgn)
    if not (rgn and rgn.GetObjectType) then return end
    if rgn:GetObjectType() ~= "FontString" then return end
    local txt = rgn.GetText and rgn:GetText()
    if type(txt) == "string" and txt ~= "" then     -- secret 이면 여기서 throw → pcall 포착
        local hit = EUI_L[txt]
        if hit then rgn:SetText(hit) end
    end
    -- 폰트 강제 안 함 (SetFont taint 방지). EUI 텍스트는 EUI 폰트 변수로 이미 한글 폰트.
end

local function _scanTooltipRegions(tip)
    if not tip.GetRegions then return end
    local ok, rs = pcall(function() return { tip:GetRegions() } end)
    if not ok then return end
    for i = 1, #rs do
        pcall(_tipRegionTrans, rs[i])   -- secret region 은 throw→skip
    end
end

local function HookGameTooltip()
    if _G.__cimkr_gtt_hooked then return end
    _G.__cimkr_gtt_hooked = true
    local function patchTip(tip)
        if not tip or _wk_cimkrTipPatched[tip] then return end
        -- 글로벌 GameTooltip 은 어떤 경우에도 후크 금지(secure/secret 크래시).
        if tip == GameTooltip or tip == _G.GameTooltip then return end
        _wk_cimkrTipPatched[tip] = true
        -- ⚠ WoW 11.x taint 수정: tip.AddLine/AddDoubleLine 을 애드온
        -- 함수로 plain 교체하면, Blizzard *secure* 툴팁 경로
        -- (SetWorldCursor/CooldownViewer → TooltipDataHandler →
        --  GameTooltip_AddColoredLine)가 우리 taint 를 타고 실행 →
        -- 그 안 secret 값 비교/인덱싱이 차단되어 크래시
        -- ("tainted by 'EllesmereUI_koKR'"). 메서드 교체는 11.x 에서
        -- 근본적으로 불가 → 제거. 번역은 OnShow 시 region 스캔
        -- (_scanTooltipRegions, 완전 pcall-안전)으로 대체 — taint 안 함.
        if tip.HookScript then
            tip:HookScript("OnShow", _scanTooltipRegions)
        end
    end
    -- ⚠ WoW 11.x: 글로벌 GameTooltip 은 Blizzard *secure* TooltipDataHandler
    -- 가 secret 값으로 채움. 그 OnShow/PostCall 안에서 region 을 읽으면
    -- (securecallfunction 컨텍스트) secret 비교·인덱싱이 pcall 로도 안 막혀
    -- 크래시("tainted by 'EllesmereUI_koKR'", 44x). koKR 클라이언트는
    -- Blizzard 툴팁이 이미 한글이라 번역 가치도 낮음 → **GameTooltip 및
    -- TooltipDataProcessor PostCall 스캔 전면 제거**. (EllesmereUI 자체
    -- 위젯 툴팁은 별도 ShowWidgetTooltip 네임스페이스 경로로 처리됨)
    -- LibDBIconTooltip(미니맵 버튼, 애드온 소유 — secure-fed 아님)만 유지.
    patchTip(_G.LibDBIconTooltip)
end

-- ============================================================
--  Phase 4d: BuildDropdownMenu 후크 — 드롭다운 메뉴 생성 시 즉시 한글화
--  메뉴 프레임의 OnShow 에도 스캔 부착 → 매 열림 마다 보장
-- ============================================================
-- 드롭다운 버튼의 선택값 라벨(ddLbl)을 *캐시 무시하고 직접* 재번역.
-- _scanned 캐시는 ScanFrameRecursive 가 frame 단위로 skip 하므로, 선택 변경
-- 후 버튼이 재스캔 안 됨. RetranslateFontString 의 __cimkrLastTxt 가 텍스트
-- 변경을 감지하므로 region 을 직접 넘기면 새 선택값이 즉시 번역됨.
-- forward 선언(ScanFrameRecursive 위)된 local 에 *할당* (function name() 은
-- 전역이 되므로 금지). RetranslateFontString 의 __cimkrLastTxt 가 텍스트
-- 변경 감지하므로 캐시 무시하고 region 직접 재번역.
_RescanDDButtonLabel = function(ddBtn)
    if not ddBtn or not ddBtn.GetRegions then return end
    local ok, regs = pcall(function() return { ddBtn:GetRegions() } end)
    if not ok then return end
    for i = 1, #regs do
        local rg = regs[i]
        local oko, ot = pcall(function() return rg and rg.GetObjectType and rg:GetObjectType() end)
        if oko and ot == "FontString" then pcall(RetranslateFontString, rg) end
    end
end

local function HookBuildDropdownMenu()
    if not EllesmereUI or EllesmereUI._koKR_bdmHooked then return end
    if type(EllesmereUI.BuildDropdownMenu) ~= "function" then return end
    EllesmereUI._koKR_bdmHooked = true

    hooksecurefunc(EllesmereUI, "BuildDropdownMenu", function(ddBtn)
        local menu = ddBtn and ddBtn._ddMenu
        if not menu or _wk_cimkrScanHooked[menu] then return end
        _wk_cimkrScanHooked[menu] = true
        _RescanDDButtonLabel(ddBtn)   -- 빌드 시점 선택값 즉시 번역
        ScanFrameRecursive(menu, 0)
        if menu.HookScript then
            menu:HookScript("OnShow", function(self)
                ScanFrameRecursive(self, 0)
                -- 메뉴 행이 OnShow 직후 지연 생성되는 경우 한 번 더 (0 프레임)
                if C_Timer and C_Timer.After then
                    C_Timer.After(0, function() pcall(ScanFrameRecursive, self, 0) end)
                end
            end)
            -- ★ 메뉴 닫힘 = 선택 완료 → 버튼 선택값 라벨 즉시 재번역 (ticker 지연 회피)
            menu:HookScript("OnHide", function()
                _RescanDDButtonLabel(ddBtn)
                if C_Timer and C_Timer.After then
                    C_Timer.After(0, function() _RescanDDButtonLabel(ddBtn) end)
                end
            end)
        end
    end)
end

-- ============================================================
--  Phase 4e: BuildMacroFactory 후크 — 매크로 공장(QoL) 한글화
--  매크로 공장은 QoL 옵션 깊숙이 빌드되어 _mainFrame 기준 depth>8 →
--  ScanFrameRecursive(깊이 8)가 못 미침(Core 주석: 깊은 버튼 별도 타겟).
--  빌드 직후 parent 서브트리를 전용 깊은(16) 스캔으로 EUI_L 적용.
--  비영문 클라에선 EUI 가 스펙 매크로 미표시 → GENERAL/UI 만 노출.
-- ============================================================
local function HookBuildMacroFactory()
    if not EllesmereUI or EllesmereUI._koKR_mfHooked then return end
    if type(EllesmereUI.BuildMacroFactory) ~= "function" then return end
    EllesmereUI._koKR_mfHooked = true

    local function DeepScan(fr, d)
        if not fr or d > 16 then return end
        if fr.IsForbidden and fr:IsForbidden() then return end
        if fr.IsProtected and fr:IsProtected() then return end
        if fr._label then RetranslateFontString(fr._label) end
        if fr.GetRegions then
            for _, rgn in ipairs({ fr:GetRegions() }) do
                if rgn and rgn.GetObjectType
                   and rgn:GetObjectType() == "FontString" then
                    RetranslateFontString(rgn)
                end
            end
        end
        if fr.GetChildren then
            for _, c in ipairs({ fr:GetChildren() }) do DeepScan(c, d + 1) end
        end
    end

    hooksecurefunc(EllesmereUI, "BuildMacroFactory", function(parent)
        if not parent then return end
        pcall(DeepScan, parent, 0)
        if C_Timer and C_Timer.After then
            C_Timer.After(0,   function() pcall(DeepScan, parent, 0) end)
            C_Timer.After(0.2, function() pcall(DeepScan, parent, 0) end)
        end
    end)
end

-- ============================================================
--  Phase 5: [폐지] 폰트 강제 — 제거됨
-- ============================================================
-- ★ [제거] 폰트 강제를 전부 제거. 애드온은 "글자 치환(SetText)"만 한다.
--   - SetFont 호출 없음(이미 제거), 전역 폰트 변수/폰트오브젝트/EUI 폰트변수도 안 건드림.
--   - EUI 전용 폰트(Expressway: CJK 글리프 없음)를 쓰는 일부 프레임은 한글이 □ 로 보일 수 있음.
--     → 사용자가 CJK 폰트 파일을 EUI 폰트 자리에 *수동 복사*해 해결(폰트 코드로 강제하지 않음).

-- ============================================================
--  진입점 — PLAYER_LOGIN
-- ============================================================
-- ── 캐릭터창(CharacterFrame) 스탯/탭/라벨 번역 ─────────────────────────
--  엘스미어 캐릭터창은 블리자드 CharacterFrame 리스킨이라 일반 스캔이 못 잡음
--  → 전용 처리. *추가기능(Additions)과 무관하게 항상 동작*해야 하므로 Core 에
--  둠(장비 등급 표시 *기능*은 Additions 에 별도 유지).
-- 스탯/카테고리는 *블리자드 글로벌*(이미 공식 koKR) 우선 사용 —
-- Chonky 와 동일(STAT_LIFESTEAL="생명력 흡수" 등). 하드코딩 X =
-- 로캘/패치 무관 자동 정확. 글로벌 없으면 EUI_L 폴백.
local function _G_str(k)
    local v = _G[k]
    return (type(v) == "string" and v ~= "") and v or nil
end
local _STAT_MAP = {
    ["Strength"]      = _G_str("SPELL_STAT1_NAME"),
    ["Agility"]       = _G_str("SPELL_STAT2_NAME"),
    ["Stamina"]       = _G_str("SPELL_STAT3_NAME"),
    ["Intellect"]     = _G_str("SPELL_STAT4_NAME"),
    ["Crit"]          = _G_str("STAT_CRITICAL_STRIKE"),
    ["Critical Strike"] = _G_str("STAT_CRITICAL_STRIKE"),
    ["Haste"]         = _G_str("STAT_HASTE"),
    ["Mastery"]       = _G_str("STAT_MASTERY"),
    ["Versatility"]   = _G_str("STAT_VERSATILITY"),
    ["Leech"]         = _G_str("STAT_LIFESTEAL"),
    ["Avoidance"]     = _G_str("STAT_AVOIDANCE"),
    ["Speed"]         = _G_str("STAT_SPEED"),
    ["Armor"]         = _G_str("ARMOR"),
    ["Attack Power"]  = _G_str("ATTACK_POWER") or _G_str("STAT_ATTACK_POWER"),
    ["Spell Power"]   = _G_str("STAT_SPELLPOWER") or _G_str("SPELL_POWER"),
    ["Health"]        = _G_str("HEALTH"),
    ["Mana"]          = _G_str("MANA"),
    ["Item Level"]    = _G_str("ITEM_LEVEL_ABBR") or _G_str("STAT_AVERAGE_ITEM_LEVEL"),
    ["Attack"]        = _G_str("STAT_CATEGORY_ATTACK"),
    ["Defense"]       = _G_str("STAT_CATEGORY_DEFENSE"),
    ["Secondary"]     = _G_str("STAT_CATEGORY_ENHANCEMENTS"),
    ["Primary"]       = _G_str("STAT_CATEGORY_ATTRIBUTES"),
    -- 섹션 헤더(EUI 리터럴 제목) + 문장/PvP 행 라벨(패널 표시).
    ["Attributes"]    = _G_str("STAT_CATEGORY_ATTRIBUTES") or "능력치",
    ["Tertiary"]      = "3차 능력치",
    ["Crests"]        = "문장",
    ["PvP"]           = _G_str("PVP") or "PvP",
    ["Myth"]          = "신화",
    ["Hero"]          = "영웅",
    ["Champion"]      = "챔피언",
    ["Veteran"]       = "노련가",
    ["Adventurer"]    = "모험가",
    -- ⚠ HONOR_LEVEL_LABEL 은 "%d" 포함 *포맷 문자열* → 절대 라벨로 쓰지 말 것
    -- (패널/툴팁에 "%d" 그대로 노출됨). 평문 한글 고정.
    ["Honor Level"]   = "명예 등급",
    ["Honor"]         = _G_str("HONOR") or "명예",
    ["Conquest"]      = _G_str("CONQUEST") or "정복 점수",
}

-- 캐릭터창 *전용* 라벨(컨텍스트 분리). _CSXlat 은 CharacterFrame 안에서만
-- 돌므로, 전역 EUI_L 에 넣으면 충돌하는 단어(계산기 창의 "Upgrades" 등)를
-- 여기 두면 캐릭터창에서만 적용됨(가방 BAG_CAT_KR 와 동일 발상).
local _CS_LABEL = {
    ["Upgrades"]  = "계산기 열기",   -- 캐릭터 시트 우하단 강화 계산기 탭
    -- 장비 세트(Gear Sets) 패널 링크. ★"New"는 전역 EUI_L["New"]="새 색상"
    --  (색상 미리보기용)을 캐릭터창에서만 " 만들기"로 오버라이드(컨텍스트 분리).
    ["New"]       = "만들기",
    ["Equip"]     = "장착",
    ["Equipped!"] = "장착됨!",
    ["Save"]      = "저장",
    ["Saved!"]    = "저장됨!",
    ["Gear Sets"] = "장비 세트",
}
-- 클릭 시 텍스트가 토글되는 동적 링크의 *초기(영문) 상태*. 첫 스캔에서 영문일 때
-- SetText 후크를 1회 설치하면, 이후 "Saved!"↔"Save" 등 변경은 후크가 재번역
-- (한글 상태로 다시 매칭할 필요 없음 → 무관한 "저장/장착" 글자 오후크 방지).
local _CS_DYN = {
    ["New"] = true, ["Equip"] = true, ["Save"] = true,
}

-- 캐릭터창 FontString 1개 번역값 계산(전용라벨→스탯글로벌→EUI_L→접두사형).
local function _csTransText(txt)
    if type(txt) ~= "string" or txt == "" then return nil end
    local h = _CS_LABEL[txt] or _STAT_MAP[txt] or (EUI_L and EUI_L[txt])
    if not h then
        if txt:find("^M%+ Score:") then h = (txt:gsub("^M%+ Score:", "쐐기 점수:"))
        elseif txt:find("^PvP iLvl:") then h = (txt:gsub("^PvP iLvl:", "PvP 아이템 레벨:")) end
    end
    return h
end

-- 동적 링크 FontString SetText 후크(1회) → 클릭 후 "Saved!"→"Save" 리셋 등
-- 본체가 영문으로 다시 SetText 해도 즉시 재번역.
local function _csHookDynFS(rgn)
    if _wk_cimCSDyn[rgn] or not rgn.SetText then return end
    _wk_cimCSDyn[rgn] = true
    hooksecurefunc(rgn, "SetText", function(self)
        if _wk_cimCSReent[self] then return end
        local okT, t = pcall(self.GetText, self)
        if not okT or type(t) ~= "string" then return end
        local h = _csTransText(t)
        if h and h ~= t then
            _wk_cimCSReent[self] = true
            pcall(self.SetText, self, h)
            _wk_cimCSReent[self] = false
        end
    end)
end

-- CharacterFrame FontString 재귀 번역(전용라벨→스탯=글로벌→EUI_L). 장비 세트
-- 링크는 스크롤 자식 아래 깊어서 깊이 12 까지 탐색.
local function _CSXlat(fr, d)
    if not fr or d > 12 then return end
    if fr.IsForbidden and fr:IsForbidden() then return end
    -- ★ 화폐/통화 탭·이전 메뉴(Token/Currency/Transfer) 서브트리 회피 — 이 안의 이전 버튼을
    --   건드리면(SetText/후크/secret 금액 읽기) taint → 클릭 시 RequestCurrencyFromAccountCharacter
    --   차단. 화폐는 블리자드 한글이라 번역도 불필요 → 통째로 skip.
    local _okn, _nm = pcall(fr.GetName, fr)
    if _okn and type(_nm) == "string" and (_nm:find("Token") or _nm:find("Currency") or _nm:find("Transfer")) then return end
    if fr.GetRegions then
        for _, rgn in ipairs({ fr:GetRegions() }) do
            if rgn and rgn.GetObjectType and rgn:GetObjectType() == "FontString" then
                local ok, txt = pcall(rgn.GetText, rgn)
                if ok and type(txt) == "string" and txt ~= "" then
                    if _CS_DYN[txt] then _csHookDynFS(rgn) end
                    local h = _csTransText(txt)
                    if h and h ~= txt then
                        pcall(rgn.SetText, rgn, h)
                        -- 본체가 영문으로 SetText 재호출(M+ Score 점수갱신 등)
                        -- 에 즉시 재번역되도록 후크 1회 설치.
                        _csHookDynFS(rgn)
                    end
                end
            end
        end
    end
    if fr.GetChildren then
        for _, c in ipairs({ fr:GetChildren() }) do _CSXlat(c, d + 1) end
    end
end
-- ── 캐릭터창 스탯 % 마우스오버 툴팁 한글화 ───────────────────────────
--  EllesmereUIBlizzardSkin_CharacterSheet.lua 가 스탯마다 자체
--  valueButton(이름없는 Button, 90x16) 을 만들어 OnEnter 에서 *전역
--  GameTooltip* 에 영문 문자열을 직접 조립함(블리자드 STAT_* 글로벌이
--  아니라 하드코딩 영문). 전역 GameTooltip 스크립트 후크는 금지(taint/
--  secret 크래시) 이므로, *애드온 소유* valueButton 의 OnEnter 에
--  소유자-게이트 후처리 후크만 추가(미니맵 버튼과 동일 검증 패턴):
--  EUI 가 채운 뒤 GameTooltipTextLeftN 을 읽어 알려진 영문만 치환.
local _CS_TIP_STATIC = {
    ["Increases the power of your spells and abilities"] = "주문과 능력의 위력을 증가시킵니다.",
    ["Attacks per second"] = "초당 공격 횟수",
    ["Reduces physical damage taken"] = "받는 물리 피해를 감소시킵니다.",
    ["Chance to avoid melee attacks"] = "근접 공격을 회피할 확률",
    ["Chance to deflect melee attacks"] = "근접 공격을 무기 막기할 확률",
    ["Chance to block incoming attacks with a shield"] = "방패로 공격을 막아낼 확률",
    ["Converts damage into a delayed effect"] = "받는 피해를 지연 효과로 전환합니다.",
    -- 능력치(Attributes) 섹션: 힘/민첩/지능/체력/생명력 설명
    ["Increases melee attack power"] = "근접 공격력을 증가시킵니다.",
    ["Increases dodge chance and melee attack power"] = "회피 확률과 근접 공격력을 증가시킵니다.",
    ["Increase the magnitude of your attacks and Abilities"] = "공격과 능력의 위력을 증가시킵니다.",
    ["Primary stat"] = "주 능력치",
    ["Increases health"] = "생명력을 증가시킵니다.",
    ["The amount of damage you can take"] = "버틸 수 있는 피해량",
}
local _CS_TIP_TPL = {
    { "^Increases your chance to critically hit by ([%d%.,]+)%%%.$",
      "치명타 적중 확률이 %s%% 증가합니다." },
    { "^Increases attack and casting speed by ([%d%.,]+)%%%.$",
      "공격 및 시전 속도가 %s%% 증가합니다." },
    { "^Increases the effectiveness of your Mastery by ([%d%.,]+)%%%.$",
      "특화 효과가 %s%% 증가합니다." },
    { "^Increases damage and healing done by ([%d%.,]+)%% and reduces damage taken by ([%d%.,]+)%%%.$",
      "주는 피해 및 치유량이 %s%% 증가하고 받는 피해가 %s%% 감소합니다." },
    { "^Heals for ([%d%.,]+)%% of damage and healing done%.$",
      "입힌 피해 및 치유량의 %s%%만큼 생명력을 회복합니다." },
    { "^Reduces damage taken from area attacks by ([%d%.,]+)%%%.$",
      "광역 공격으로 받는 피해가 %s%% 감소합니다." },
    { "^Increases movement speed by ([%d%.,]+)%%%.$",
      "이동 속도가 %s%% 증가합니다." },
}
-- 헤더/제목 라인 선두의 영문 스탯명 → koKR(블리자드 글로벌 우선).
local _CS_NAME, _CS_NAME_ORDER
local function _csNameMap()
    if _CS_NAME then return _CS_NAME end
    _CS_NAME = {
        ["Stagger Effect"] = "취기 효과",
        ["Attack Speed"]   = "공격 속도",
        ["Attack Power"]   = _G_str("ATTACK_POWER") or _G_str("STAT_ATTACK_POWER") or "공격력",
        ["Spell Power"]    = _G_str("STAT_SPELLPOWER") or _G_str("SPELL_POWER") or "주문력",
        ["Critical Strike"] = _G_str("STAT_CRITICAL_STRIKE") or "치명타 적중",
        ["Versatility"]    = _G_str("STAT_VERSATILITY") or "유연성",
        ["Avoidance"]      = _G_str("STAT_AVOIDANCE") or "회피",
        ["Mastery"]        = _G_str("STAT_MASTERY") or "특화",
        ["Haste"]          = _G_str("STAT_HASTE") or "가속",
        ["Armor"]          = _G_str("ARMOR") or "방어도",
        ["Dodge"]          = _G_str("DODGE") or "회피",
        ["Parry"]          = _G_str("PARRY") or "무기 막기",
        ["Block"]          = _G_str("BLOCK") or "방패 막기",
        ["Leech"]          = _G_str("STAT_LIFESTEAL") or "생명력 흡수",
        ["Speed"]          = _G_str("STAT_SPEED") or "이동 속도",
        ["Crit"]           = _G_str("STAT_CRITICAL_STRIKE") or "치명타 적중",
        -- 능력치 섹션: 생명력 행은 generic 분기("Health N") → 헤더 영문.
        ["Health"]         = _G_str("HEALTH") or "생명력",
        ["Mana"]           = _G_str("MANA") or "마나",
        -- 문장(Crests) 섹션: 툴팁 헤더 "<등급> Crests"
        ["Myth"]           = "신화",
        ["Hero"]           = "영웅",
        ["Champion"]       = "챔피언",
        ["Veteran"]        = "노련가",
        ["Adventurer"]     = "모험가",
        -- PvP 섹션: generic 분기 "<name> <value>"
        ["Honor Level"]    = "명예 등급",   -- HONOR_LEVEL_LABEL 은 %d 포맷 → 사용 금지
        ["Honor"]          = _G_str("HONOR") or "명예",
        ["Conquest"]       = _G_str("CONQUEST") or "정복 점수",
    }
    -- 긴 이름 먼저(접두 매칭 충돌 방지: "Attack Speed" > "Speed",
    -- "Honor Level" > "Honor").
    _CS_NAME_ORDER = {
        "Stagger Effect", "Attack Speed", "Attack Power", "Spell Power",
        "Critical Strike", "Honor Level", "Versatility", "Avoidance",
        "Adventurer", "Conquest", "Champion", "Veteran", "Mastery",
        "Haste", "Armor", "Dodge", "Parry", "Block", "Leech", "Speed",
        "Crit", "Health", "Mana", "Honor", "Myth", "Hero",
    }
    return _CS_NAME
end
local function _statTipXlat(s)
    if type(s) ~= "string" or s == "" then return nil end
    local st = _CS_TIP_STATIC[s]
    if st then return st end
    for _, tp in ipairs(_CS_TIP_TPL) do
        local a, b = s:match(tp[1])
        if a then
            if b then return tp[2]:format(a, b) else return tp[2]:format(a) end
        end
    end
    local nm = _csNameMap()
    for _, en in ipairs(_CS_NAME_ORDER) do
        local L = #en
        if s:sub(1, L) == en then
            local nx = s:sub(L + 1, L + 1)
            if nx == "" or nx == " " then
                local ko = nm[en]
                if ko and ko ~= en then
                    local rest = s:sub(L + 1)
                    rest = rest:gsub(" rating%)", " 평점)")
                                :gsub("^ Crests$", " 문장")
                    return ko .. rest
                end
                return nil
            end
        end
    end
    return nil
end
-- 능력치 행 설명(라벨 텍스트 → 한글 설명). EUI 가 UnitStat=secret 으로
-- 툴팁을 못 만든 힘/체력/민첩/지능 행을 koKR 이 직접 구성할 때 사용.
-- 키: 블리자드 글로벌 koKR 이름(라벨은 _CSXlat 가 이미 한글화) + 영문.
local _ATTR_DESC
local function _attrDesc(txt)
    if not _ATTR_DESC then
        local function K(g, fb) return _G_str(g) or fb end
        _ATTR_DESC = {
            [K("STAT_STRENGTH",  "\1")] = "근접 공격력을 증가시킵니다.",
            [K("STAT_AGILITY",   "\2")] = "회피 확률과 근접 공격력을 증가시킵니다.",
            [K("STAT_INTELLECT", "\3")] = "공격과 능력의 위력을 증가시킵니다.",
            [K("STAT_STAMINA",   "\4")] = "생명력을 증가시킵니다.",
            ["Strength"]  = "근접 공격력을 증가시킵니다.",
            ["Agility"]   = "회피 확률과 근접 공격력을 증가시킵니다.",
            ["Intellect"] = "공격과 능력의 위력을 증가시킵니다.",
            ["Stamina"]   = "생명력을 증가시킵니다.",
        }
    end
    return _ATTR_DESC[txt]
end
-- 후처리: ① EUI 가 만든 툴팁(소유자=우리 버튼) → 영문 라인 치환.
-- ② EUI 가 secret(UnitStat) 로 SetOwner 전에 조기 반환해 툴팁이 아예
--    없는 힘/체력 등 → 형제 라벨 텍스트로 koKR 이 직접 구성(우리 소유
--    버튼 OnEnter 안, secret 미열람, 정적 한글만 → taint 제약 준수).
local function _statTipPost(self)
    local gt = _G.GameTooltip
    if not gt or not gt.GetOwner then return end
    local okOwn, ownerFr = pcall(gt.GetOwner, gt)
    if okOwn and ownerFr == self then
        local okN, n = pcall(gt.NumLines, gt)
        if not okN or type(n) ~= "number" then return end
        local changed
        for i = 1, n do
            local fs = _G["GameTooltipTextLeft" .. i]
            if fs and fs.GetText then
                local okT, t = pcall(fs.GetText, fs)
                if okT and type(t) == "string" and t ~= "" then
                    local nt = _statTipXlat(t)
                    if nt and nt ~= t then
                        pcall(fs.SetText, fs, nt)
                        changed = true
                    end
                end
            end
        end
        if changed then pcall(gt.Show, gt) end
        return
    end
    -- EUI 미생성 케이스: 형제 라벨이 알려진 능력치면 직접 구성.
    -- 라벨은 호버 시점에 지연 캡처(같은 부모·같은 Y·우측정렬 아님).
    local lbl = _wk_euiKoLbl[self]
    if not lbl then
        local okP, par = pcall(self.GetParent, self)
        if okP and par and par.GetRegions and self.GetCenter then
            local _, bcy = self:GetCenter()
            if bcy then
                for _, rg in ipairs({ par:GetRegions() }) do
                    if rg and rg.GetObjectType
                       and rg:GetObjectType() == "FontString" and rg.GetCenter then
                        local _, rcy = rg:GetCenter()
                        local jh = rg.GetJustifyH and rg:GetJustifyH()
                        if rcy and math.abs(bcy - rcy) < 3 and jh ~= "RIGHT" then
                            lbl = rg
                            _wk_euiKoLbl[self] = rg
                            break
                        end
                    end
                end
            end
        end
    end
    if not lbl or not lbl.GetText then return end
    local okL, lt = pcall(lbl.GetText, lbl)
    if not okL or type(lt) ~= "string" or lt == "" then return end
    local desc = _attrDesc(lt)
    if not desc then return end
    if not (self.IsMouseOver and self:IsMouseOver()) then return end
    -- 라벨이 영문("Strength")일 수 있으니 한글 이름으로 변환 후 출력.
    local koName = _STAT_MAP[lt] or (EUI_L and EUI_L[lt]) or lt
    if not pcall(gt.SetOwner, gt, self, "ANCHOR_RIGHT") then return end
    pcall(gt.AddLine, gt, koName, 1, 0.82, 0, 1)
    pcall(gt.AddLine, gt, desc, 1, 1, 1, true)
    pcall(gt.Show, gt)
end

-- ── 착용 아이템레벨 호버 툴팁 한글화 ────────────────────────────────────
--  EllesmereUIBlizzardSkin_CharacterSheet.lua 가 iLvl 텍스트 위 버튼 OnEnter 에서
--  *전역 GameTooltip* 에 영문을 직접 조립("Equipped Item Level" / "You have N
--  better items..." 등). 90x16 스탯 버튼이 아니라 _CSHookStatTips 가 못 잡음.
--  → 같은 후처리 기법(소유자=우리버튼 게이트 + 알려진 영문만 치환)으로 별도 처리.
local _CS_ILVL_STATIC = {
    ["Equipped Item Level"]          = "착용 아이템 레벨",
    ["No better items in inventory"] = "가방에 더 좋은 아이템 없음",
    ["Enchant missing"]              = "마법부여 없음",  -- 마법부여 슬롯 호버
}
local function _ilvlTipXlat(s)
    if type(s) ~= "string" or s == "" then return nil end
    local st = _CS_ILVL_STATIC[s]
    if st then return st end
    local cnt = s:match("^You have (%d+) better items? in inventory$")
    if cnt then return "가방에 더 좋은 아이템이 " .. cnt .. "개 있습니다" end
    local more = s:match("^%s*%.%.%. and (%d+) more$")
    if more then return "  ... 외 " .. more .. "개 더" end
    -- 아이템 행 "|T..|t 이름 (iLvl N)" — 이름은 이미 한글, iLvl 태그만 치환.
    if s:find("(iLvl ", 1, true) then
        local out = s:gsub("iLvl", "아이템레벨")
        return out
    end
    return nil
end
-- rewrite-only 후처리(소유자=self 일 때 알려진 영문 라인만 치환). 스탯버튼용
-- _statTipPost 와 달리 *생성 폴백 없음* → 임의 버튼에 걸어도 안전.
local function _iLvlTipPost(self)
    local gt = _G.GameTooltip
    if not gt or not gt.GetOwner then return end
    local okOwn, owner = pcall(gt.GetOwner, gt)
    if not okOwn or owner ~= self then return end
    local okN, n = pcall(gt.NumLines, gt)
    if not okN or type(n) ~= "number" then return end
    local changed
    for i = 1, n do
        local fs = _G["GameTooltipTextLeft" .. i]
        if fs and fs.GetText then
            local okT, t = pcall(fs.GetText, fs)
            if okT and type(t) == "string" and t ~= "" then
                local nt = _ilvlTipXlat(t)
                if nt and nt ~= t then pcall(fs.SetText, fs, nt); changed = true end
            end
        end
    end
    if changed then pcall(gt.Show, gt) end
end

-- CharacterFrame 하위에서 valueButton(Button, 90x16) 탐색 → OnEnter 후크 1회.
-- 그 외 버튼(아이템레벨 버튼 등)에는 rewrite-only 후크(_iLvlTipPost) 1회.
local function _CSHookStatTips(fr, d)
    if not fr or d > 7 then return end
    if fr.IsForbidden and fr:IsForbidden() then return end
    -- ★ 화폐/통화 탭·이전 메뉴(Token/Currency/Transfer) 서브트리 회피 — 이전 버튼에 필드쓰기/
    --   OnEnter 후크 걸면 taint → RequestCurrencyFromAccountCharacter 차단. 통째로 skip.
    local _okn, _nm = pcall(fr.GetName, fr)
    if _okn and type(_nm) == "string" and (_nm:find("Token") or _nm:find("Currency") or _nm:find("Transfer")) then return end
    if fr.GetChildren then
        for _, c in ipairs({ fr:GetChildren() }) do
            if type(c) == "table" and c.GetObjectType then
                local okO, ot = pcall(c.GetObjectType, c)
                if okO and ot == "Button" and c.HookScript then
                    local okW, w = pcall(c.GetWidth, c)
                    local okH, h = pcall(c.GetHeight, c)
                    local is9016 = okW and okH and w and h
                        and math.abs(w - 90) < 1.5 and math.abs(h - 16) < 1.5
                    if is9016 and not _wk_euiKoStatTip[c] then
                        _wk_euiKoStatTip[c] = true
                        c:HookScript("OnEnter", function(self)
                            pcall(_statTipPost, self)  -- HookScript=EUI 핸들러 뒤 실행
                        end)
                    elseif not is9016 and not _wk_euiKoILvlTip[c] then
                        _wk_euiKoILvlTip[c] = true
                        c:HookScript("OnEnter", function(self)
                            pcall(_iLvlTipPost, self)
                        end)
                    end
                elseif okO and ot == "Frame" and c.HookScript
                       and c.IsMouseEnabled and c:IsMouseEnabled()
                       and c.GetScript and c:GetScript("OnEnter")
                       and not _wk_euiKoEnchHover[c] then
                    -- 마우스+OnEnter 달린 Frame (enchantHoverFrame 등) → 동일 후처리
                    _wk_euiKoEnchHover[c] = true
                    c:HookScript("OnEnter", function(self)
                        pcall(_iLvlTipPost, self)
                    end)
                end
                _CSHookStatTips(c, d + 1)
            end
        end
    end
end

-- 캐릭터창 번역 적용 + 설치(OnShow + 비동기 타이머 + 스탯/장비 변경 이벤트).
local function _CSTranslate()
    local cf = _G.CharacterFrame
    if not cf or not cf.IsShown or not cf:IsShown() then return end
    pcall(_CSXlat, cf, 0)
    pcall(_CSHookStatTips, cf, 0)
end
local function _InstallCharSheetTrans()
    local cf = _G.CharacterFrame
    if not cf or _G.__cimCSTransHooked then return end
    _G.__cimCSTransHooked = true
    -- CharacterFrame 이 *열릴 때만* 번역. 보호 다이얼로그가 떠 있는 동안 다른
    -- 이벤트가 _CSXlat 트리거하면 secret stat FontString 읽기로 addon 오염 →
    -- 직후 보호 함수 (AcceptSpellConfirmationPrompt 등) 차단.
    -- (PaperDollFrame_UpdateStats hook + PLAYER_EQUIPMENT_CHANGED / UNIT_INVENTORY_CHANGED
    --  이벤트 + 0.2/0.6/1.5초 지연 호출 모두 제거. 장비 변경 후 새 스탯 한글화는
    --  창 닫았다 다시 열어야 반영됨.)
    cf:HookScript("OnShow", function() _CSTranslate() end)
    if cf:IsShown() then _CSTranslate() end
end
-- 가방 라이브창 번역에 쓰는 보호 헬퍼(secret string 안전 — 호출부 pcall).
local function _safeIdx(t, k)   return t[k] end
local function _safeMatch(s, p) return s:match(p) end

-- ============================================================
--  EllesmereUIBags 라이브 창 한글화 (2026-05-17)
--  Core.lua 의 라이브 스캔은 EUI 옵션 패널이 열려 있을 때만(ticker
--  PanelOpen 게이트) 돌고, 가방창은 별도 전역 프레임이라 안 잡혀서
--  옵션창엔 한글이 떠도 실제 가방을 열면 영문이었음. 미터처럼 가방
--  전용 후크로 해결: 전역 프레임(EUI_Bags/EUI_BagsReagent/
--  EUI_BagsWindow) FontString 을 EUI_L 로 변환 + 열릴 때마다 재스캔.
--  (EUI_L 정확매칭만 — K/M/B 변환 없음. 핫패스 클로저 0: 명명 헬퍼)
-- ============================================================
-- 가방 동적 concat 문자열 패턴 ("Bank Tab " .. n 등은 exact 키로 못 잡음).
-- ⚠ _BAG_HookFS 보다 *먼저* 선언 (Lua 스코프 — 뒤에 두면 nil 크래시).
-- 구체적 패턴을 앞에 (Warbank Tab 이 Warbank 보다 먼저 매치되게).
-- WoW 공식 용어 기준(2026-05-17): Bank Tab=은행 보관함, Tab=보관함,
-- Warband/Warbank=전투부대 은행, Reagent Bank=재료 은행 (전쟁부대·은행 칸
-- ·전쟁 은행 = 오역 → 교정).
local _BAG_PATTERNS = {
    { "^Bank Tab (%d+)$",          "보관함 %s" },
    { "^Warbank Tab (%d+)$",       "전투부대 %s번" },
    { "^Bag (%d+)$",               "가방 %s" },
    { "^Tab (%d+)$",               "보관함 %s" },
    { "^(%d+) / (%d+) Items$",     "아이템 %s / %s개" },
    { "^(%d+) Items$",             "아이템 %s개" },
    { "^Reagent Bank %((.+)%)$",   "재료 은행 (%s)" },
    { "^Warband Bank %((.+)%)$",   "전투부대 은행 (%s)" },
    { "^Bank %((.+)%)$",           "은행 (%s)" },
    { "^Main Bags %((.+)%)$",      "주 가방 (%s)" },
    { "^Reagent Bag %((.+)%)$",    "재료 가방 (%s)" },
    { "^Warbank (.+)$",            "전투부대 %s" },
    { "^Ungroup (.+)$",            "%s 그룹 해제" },
}
local function _BAG_Translate(text)
    if type(text) ~= "string" or text == "" then return nil end
    local okL, hit = pcall(_safeIdx, EUI_L, text)
    if okL and hit and hit ~= text then return hit end
    -- "<카테고리> (개수)" 그리드 그룹 헤더 — 이름부만 공식 EUI_L 로
    -- 조합 번역, 개수는 그대로. 이름이 *알려진 EUI_L 키일 때만* 반환
    -- (모르는 문자열은 건드리지 않음 = 안전). 예: "The Armory (30)"
    -- → EUI_L["The Armory"]="무기고" → "무기고 (30)".
    local okC, nm, cnt = pcall(_safeMatch, text, "^(.-) %((%d+)%)$")
    if okC and nm and nm ~= "" and cnt then
        local okT, tnm = pcall(_safeIdx, EUI_L, nm)
        if okT and tnm and tnm ~= nm then
            return tnm .. " (" .. cnt .. ")"
        end
    end
    for i = 1, #_BAG_PATTERNS do
        local pr = _BAG_PATTERNS[i]
        -- 다중 캡처 지원: m = { ok, cap1, cap2, ... }
        -- (옛 코드는 cap1 만 넘겨 "%s / %s개" 같은 2캡처 패턴이 format
        --  인자 부족으로 실패 → "N / M Items" 미번역 버그였음)
        local m = { pcall(_safeMatch, text, pr[1]) }
        if m[1] and m[2] ~= nil then
            if pr[1] == "^Ungroup (.+)$" then
                local ok2, h2 = pcall(_safeIdx, EUI_L, m[2])
                if ok2 and h2 then m[2] = h2 end
            end
            local okF, r = pcall(string.format, pr[2], unpack(m, 2, #m))
            if okF and r then return r end
        end
    end
    return nil
end

local function _BAG_HookFS(fs)
    if not fs or _wk_cimBagHook[fs] or not fs.SetText then return end
    if fs.IsForbidden and fs:IsForbidden() then return end
    _wk_cimBagHook[fs] = true
    hooksecurefunc(fs, "SetText", function(self, text)
        if _wk_cimBagTr[self] or type(text) ~= "string" then return end
        local tr = _BAG_Translate(text)
        if not tr or tr == text then return end
        _wk_cimBagTr[self] = true
        pcall(self.SetText, self, tr)
        _wk_cimBagTr[self] = false
    end)
    local okT, txt = pcall(fs.GetText, fs)
    if okT and type(txt) == "string" then
        local tr = _BAG_Translate(txt)
        if tr and tr ~= txt then
            _wk_cimBagTr[fs] = true
            pcall(fs.SetText, fs, tr)
            _wk_cimBagTr[fs] = false
        end
    end
end

local function _BAG_ScanFrame(frame, depth)
    if not frame or depth > 12 then return end
    if frame.GetRegions then
        for _, rgn in ipairs({ frame:GetRegions() }) do
            if rgn and rgn.GetObjectType and rgn:GetObjectType() == "FontString" then
                _BAG_HookFS(rgn)
            end
        end
    end
    if frame.GetChildren then
        for _, ch in ipairs({ frame:GetChildren() }) do
            _BAG_ScanFrame(ch, depth + 1)
        end
    end
end

local _bagFound = false
local function _BAG_Apply()
    -- ⚠ 실제 EUI Bags 프레임명(소스 확인): EUI_MainBagFrame /
    -- EUI_BagsWindowFrame / EUI_BankFrame / EUI_ReagentBagFrame /
    -- EUI_BagMoneyFrame. 옛 목록(EUI_Bags/EUI_BagsReagent/EUI_BagsWindow)
    -- 은 *틀린 이름* → 은행창(별도 EUI_BankFrame)·헤더 수량이 통째로
    -- 미적용이던 근본원인. EUI_Bags/EUI_CategoryManager 는 있으면 보너스.
    for _, gn in ipairs({
        "EUI_MainBagFrame", "EUI_BagsWindowFrame", "EUI_BankFrame",
        "EUI_ReagentBagFrame", "EUI_BagMoneyFrame", "EUI_CategoryManager",
        "EUI_Bags",
    }) do
        local fr = _G[gn]
        if type(fr) == "table" and fr.GetObjectType then
            _bagFound = true
            _BAG_ScanFrame(fr, 0)
            -- 가방 열릴 때마다 재스캔 (새 분류/행) — 1회만 설치.
            -- ⚠ 사이드바/카테고리 라벨은 OnShow *이후* 같은/다음 프레임에
            --   지연 생성됨 → OnShow 시점 스캔만으론 영문 깜빡임("조금 있어야
            --   한글"). 즉시 + 다음프레임(0) + 0.1/0.3초 재스캔으로 라벨이
            --   만들어지는 즉시 후크/변환 → 깜빡임 제거. (__cimBagHook 가
            --   중복 후크 막아 비용 0, 폴링 아님 — OnShow 당 유한 회)
            if not _wk_cimBagShowHook[fr] and fr.HookScript then
                _wk_cimBagShowHook[fr] = true
                pcall(fr.HookScript, fr, "OnShow", function(self)
                    _BAG_ScanFrame(self, 0)
                    if C_Timer and C_Timer.After then
                        C_Timer.After(0,   function() _BAG_ScanFrame(self, 0) end)
                        C_Timer.After(0.1, function() _BAG_ScanFrame(self, 0) end)
                        C_Timer.After(0.3, function() _BAG_ScanFrame(self, 0) end)
                    end
                end)
            end
        end
    end
end

-- 로그인 후 가방 애드온이 프레임을 늦게 만들 수 있으니 찾을 때까지 재시도
-- (DM 과 동일 패턴). 찾으면 즉시 종료 — 이후 OnShow/SetText 훅이 담당.
local _bagInitTicker
local function _BAG_InitRetry()
    if _bagFound or _bagInitTicker then return end
    if not (C_Timer and C_Timer.NewTicker) then
        if C_Timer and C_Timer.After then C_Timer.After(2.0, _BAG_Apply) end
        return
    end
    local tries = 0
    _bagInitTicker = C_Timer.NewTicker(1.0, function(tk)
        tries = tries + 1
        _BAG_Apply()
        if _bagFound or tries >= 60 then
            tk:Cancel()
            _bagInitTicker = nil
        end
    end)
end
-- ── StaticPopup(EUI 커스텀 대화상자) 한글화 ────────────────────────────
--  EUI 가 StaticPopupDialogs 에 영문 text/button 으로 대화상자를 띄움(예: 장비
--  세트 "New equipment set name:" + Create/Cancel). StaticPopup 프레임은 일반
--  스캔이 못 잡아 OnShow 후크로 EUI_L 정확매칭 번역(블리자드 기본 팝업은 이미
--  현지화 텍스트라 키 미스 → 무변경, 안전). SetText 는 비보안 → taint 무관.
-- Blizzard 보호 다이얼로그는 후크 시 GetText 자체가 secret string 읽기 → addon 오염
-- → 이후 OnAccept 에서 AcceptSpellConfirmationPrompt 등 보호 함수 호출 차단.
-- 블랙리스트 매칭되면 번역 시도 X (원래 영문 그대로 노출, 기능은 정상 동작).
local _PROTECTED_POPUP_WHICH = {
    SPELL_CONFIRMATION_PROMPT          = true,
    SPELL_CONFIRMATION_PROMPT_NO_COST  = true,
    SPELL_CONFIRMATION_PROMPT_ALERT    = true,
    SPELL_CONFIRMATION_PROMPT_BONUS    = true,
    SPELL_CONFIRMATION_WARNING         = true,
    BIND_ENCHANT                       = true,
    REPLACE_ENCHANT                    = true,
    EQUIP_BIND                         = true,
    USE_BIND                           = true,
    CONFIRM_REFUND_TOKEN_REROLL        = true,
    DEATH                              = true,
    RESURRECT                          = true,
    RESURRECT_NO_TIMER                 = true,
    RESURRECT_NO_SICKNESS              = true,
    RECOVER_CORPSE                     = true,
    RECOVER_CORPSE_INSTANCE            = true,
    XP_LOSS                            = true,
    AREA_SPIRIT_HEAL                   = true,
    DEATH_RELEASE_TIMER                = true,
}
local function _trStaticPopup(self)
    if self and self.which and _PROTECTED_POPUP_WHICH[self.which] then return end
    local L = _G.EUI_L
    if not (self and self.GetName and L) then return end
    local n = self:GetName()
    if not n then return end
    local txtFS = _G[n .. "Text"]
    if txtFS and txtFS.GetText then
        local t = txtFS:GetText()
        if type(t) == "string" and type(L[t]) == "string" then pcall(txtFS.SetText, txtFS, L[t]) end
    end
    for i = 1, 4 do
        local b = _G[n .. "Button" .. i]
        if b and b.GetText then
            local t = b:GetText()
            if type(t) == "string" and type(L[t]) == "string" then pcall(b.SetText, b, L[t]) end
        end
    end
end
local function _InstallStaticPopupHook()
    -- (StaticPopup OnShow 후크 제거 — Blizzard 보호 다이얼로그 (SPELL_CONFIRMATION_PROMPT,
    --  아이템 사용 확인 등) 의 텍스트가 secret string 이면 GetText 호출만으로 koKR
    --  애드온 오염 → 직후 OnAccept 의 보호 함수 호출 차단.
    --  EUI 커스텀 다이얼로그 (장비세트 생성 창 등) 번역은 잃음.)
    if _G.__cimSPHooked then return end
    _G.__cimSPHooked = true
end

-- (ShowInfoPopup 번역은 HookPopups + _trPopupOpts 가 처리 — content 줄단위 보강 포함.)

-- ★ EllesmereUI._SPEC_DATA 한글화 (자원바 등 spec assign popup 의 직업/전문화 이름).
--   원본은 SPEC_DATA 에 영문 하드코딩 ("Warrior" / "Beast Mastery" 등) →
--   ShowSpecAssignPopup 이 popup 띄울 때 그대로 사용 → 영문 노출.
--   Blizzard API (LOCALIZED_CLASS_NAMES_MALE / GetSpecializationInfoByID) 가 이미
--   한글명 제공하므로 직접 매핑 불필요. SPEC_DATA 의 name 필드만 mutate.
local function _TranslateSpecData()
    local data = _G.EllesmereUI and _G.EllesmereUI._SPEC_DATA
    if type(data) ~= "table" then return false end
    local LCM = _G.LOCALIZED_CLASS_NAMES_MALE
    local GSI = _G.GetSpecializationInfoByID
    for _, cls in ipairs(data) do
        if type(cls.class) == "string" and LCM and LCM[cls.class] then
            cls.name = LCM[cls.class]
        end
        if type(cls.specs) == "table" and GSI then
            for _, spec in ipairs(cls.specs) do
                if type(spec.id) == "number" then
                    local ok, _, name = pcall(GSI, spec.id)
                    if ok and type(name) == "string" and name ~= "" then
                        spec.name = name
                    end
                end
            end
        end
    end
    return true
end

-- ★ ShowSpecAssignPopup 후크: popup 이 lazy 생성되어 HookPopupShow 가 못 잡음.
--   호출 직후 EUISpecAssignPopup 의 모든 FontString 라벨을 번역(SetText)한다.
--   (폰트는 안 건드림 — Expressway 가 한글 글리프 없어 □ 면 사용자가 폰트 파일 교체로 해결.)
--
-- ⚠ ScanFrameRecursive 는 frame 이름이 "Popup"/"Dialog"/"Confirm" 이면 _NameLooksProtected
--   가드(Blizzard secure StaticPopup taint 방지)로 *즉시 return* → 이름이 "EUISpecAssignPopup"
--   인 이 팝업은 title/subtitle/Check All/Apply 가 전부 영문으로 남았다(EUI_L 엔 다 등록돼
--   있는데도). 이 팝업은 EllesmereUI 소유 *비-secure* frame 이라 직접 순회·SetText 가 안전
--   → 이름 가드 없는 전용 스캐너로 FontString 을 EUI_L 직접 매칭해 SetText(번역)만 한다.
local function _ScanSpecPopupFS(frame, depth)
    if not frame or depth > 10 then return end
    if frame.GetRegions then
        local ok, regs = pcall(function() return { frame:GetRegions() } end)
        if ok and type(regs) == "table" then
            for i = 1, #regs do
                local rgn = regs[i]
                local tok, ot = pcall(function() return rgn and rgn.GetObjectType and rgn:GetObjectType() end)
                if tok and ot == "FontString" then
                    local gok, t = pcall(rgn.GetText, rgn)
                    if gok and type(t) == "string" and t ~= "" then
                        local h = EUI_L[t]
                        if h and h ~= t then pcall(rgn.SetText, rgn, h) end
                    end
                    -- 폰트는 일절 안 건드림(글자 치환만). 한글 폰트는 사용자가 Expressway.TTF 파일 교체로 해결.
                end
            end
        end
    end
    if frame.GetChildren then
        local ok, kids = pcall(function() return { frame:GetChildren() } end)
        if ok and type(kids) == "table" then
            for i = 1, #kids do _ScanSpecPopupFS(kids[i], depth + 1) end
        end
    end
end
local function _InstallSpecPopupHook()
    if not _G.EllesmereUI or type(_G.EllesmereUI.ShowSpecAssignPopup) ~= "function" then return end
    if _G.EllesmereUI._koKR_specPopupHooked then return end
    _G.EllesmereUI._koKR_specPopupHooked = true
    hooksecurefunc(_G.EllesmereUI, "ShowSpecAssignPopup", function()
        local popup = _G.EUISpecAssignPopup
        if not popup then return end
        if C_Timer and C_Timer.After then
            C_Timer.After(0, function() pcall(_ScanSpecPopupFS, popup, 0) end)
        else
            pcall(_ScanSpecPopupFS, popup, 0)
        end
    end)
end

local coreFrame = CreateFrame("Frame")
coreFrame:RegisterEvent("PLAYER_LOGIN")
coreFrame:SetScript("OnEvent", function(self)
    self:UnregisterEvent("PLAYER_LOGIN")

    _InstallStaticPopupHook()
    _BAG_InitRetry()               -- 가방 라이브창 번역(추가기능과 독립)
    _InstallCharSheetTrans()       -- 캐릭터창 스탯/탭/라벨 번역(추가기능과 독립)
    _TranslateSpecData()           -- SPEC 데이터(spec assign popup) 한글화
    _InstallSpecPopupHook()        -- ShowSpecAssignPopup 후크 — popup 폰트/번역
    -- 위젯 툴팁 싱글톤은 첫 호버 때 lazy 생성 → 잡힐 때까지 빠르게 폴링(0.5초).
    -- 느린 폴링(기존 2초)이면 "처음엔 영문, 0.5~2초 뒤 한글"로 늦게 보임 → 0.5초로 단축.
    -- 또 EUI.ShowWidgetTooltip 이 로그인 *후* 늦게 정의되면 텍스트 래퍼가 안 걸리므로
    -- 매 틱 HookShowWidgetTooltip 재시도(가드라 1회만 실효). 래퍼+싱글톤 둘 다 확보 시 종료.
    -- (설정창을 안 열어도 첫 호버 직후 자동 번역 — 더는 설정창 열 필요 없음.)
    if C_Timer and C_Timer.NewTicker and not _G.__cimTipTxtTicker then
        local n = 0
        _G.__cimTipTxtTicker = C_Timer.NewTicker(0.5, function(tk)
            n = n + 1
            HookShowWidgetTooltip()                 -- 래퍼 늦은 설치 재시도(가드)
            pcall(_ScanTipSingleton)                -- 싱글톤 SetText 후크(로컬 경로 번역)
            if _G.EllesmereUI_KR_ApplyTipScale then _G.EllesmereUI_KR_ApplyTipScale() end  -- 툴팁 배율(추가기능)
            local swt = EllesmereUI and EllesmereUI._koKR_swtHooked
            if (_G.__cimTipTxtHooked and swt) or n >= 120 then
                tk:Cancel(); _G.__cimTipTxtTicker = nil
            end
        end)
    end
    if C_Timer and C_Timer.After then
        C_Timer.After(3, _InstallCharSheetTrans)   -- CharacterFrame 늦은 생성 대비
    end
    local ok = ApplyWidgetHooks()
    TranslateRoster()
    HookRegisterModule()
    HookPopups()
    HookShowWidgetTooltip()
    HookGameTooltip()
    HookMinimapBtnTip()
    HookMinimapMicroMenu()
    HookMinimapForVaultScan()
    HookQoLWindows()
    HookBuildDropdownMenu()
    -- UpgradeCalc 한글 클라 트랙 파서 — EUIUpgCalc 늦게 로드될 수 있어 재시도.
    if not _HookUpgradeCalcParser() and C_Timer and C_Timer.After then
        C_Timer.After(2, _HookUpgradeCalcParser)
        C_Timer.After(5, _HookUpgradeCalcParser)
    end
    -- HookBuildMacroFactory()  -- 회귀 격리용 일시 분리(엔진 정상화 우선)
    -- (ApplyKoreanFont 제거됨 — 폰트 강제 안 함)
    LocalizeEABR()

    -- ItemInfo 캐시 늦게 채워짐 → 디바운스 재시도.
    -- ⚠ GET_ITEM_INFO_RECEIVED 는 게임 내내 발화 (인벤 인스펙트/상점/거래 등).
    --    영구 listener 면 분당 수십 회 진입 누적. PLAYER_ENTERING_WORLD 후
    --    60초 안에 ItemInfo 가 거의 다 캐싱되므로 그 시점에 자동 unregister.
    local eabrFrame = CreateFrame("Frame")
    eabrFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
    eabrFrame:RegisterEvent("ADDON_LOADED")
    local eabrPending = false
    eabrFrame:SetScript("OnEvent", function(self, event, arg1)
        if event == "ADDON_LOADED" and arg1 ~= "EllesmereUIAuraBuffReminders" then return end
        if eabrPending then return end
        eabrPending = true
        C_Timer.After(0.5, function() eabrPending = false; LocalizeEABR() end)
    end)
    -- 60초 후 listener 자동 해제 — 이후 영구 0 비용
    C_Timer.After(60, function() eabrFrame:UnregisterAllEvents() end)

    if not EllesmereUI then
        print("|cff00ff88[EUI koKR]|r " .. (EUI_L["_VERSION"] or "v1.3") .. " (Widgets 미감지)")
        return
    end

    -- ========================================================
    --  ★ 폴링 ticker 제거 → 이벤트 기반 burst 스캔.
    --    설정창은 "열 때 한 번 다 번역" 모델 (사용자 요청). 페이지/모듈
    --    전환·드롭다운·팝업 등 이벤트마다 burst(즉시 + 0.1/0.3/0.6초 후속
    --    몇 번)로 비동기 생성 위젯까지 잡고 *정지*. 상시 폴링 0.
    --    닫으면 비용 0 (예약된 burst 도 PanelOpen 가드로 no-op).
    -- ========================================================
    local _mfRef, _umRef

    -- 별도 QoL 창(설정창과 독립적으로 열릴 수 있음) — 열려 있으면 ticker 유지.
    local _QOL_WINDOWS = { "EUIUpgCalcFrame", "EUIKeysPopup" }
    local function _AnyQoLShown()
        for i = 1, #_QOL_WINDOWS do
            local w = _G[_QOL_WINDOWS[i]]
            if w and w.IsShown and w:IsShown() then return true end
        end
        return false
    end
    local function PanelOpen()
        _mfRef = _mfRef or EllesmereUI._mainFrame
        _umRef = _umRef or _G.EllesmereUnlockMode
        return (_mfRef and _mfRef:IsShown()) or (_umRef and _umRef:IsShown())
               or _AnyQoLShown()
    end

    local function ScanPopupChildren()
        if not (UIParent and UIParent.GetChildren) then return end
        local children = { UIParent:GetChildren() }
        for i = 1, #children do
            local child = children[i]
            if type(child) == "table" and child ~= _mfRef and child ~= _umRef then
                local skip = (child.IsForbidden and child:IsForbidden())
                          or (child.IsProtected and child:IsProtected())
                -- ★★ Whitelist: EUI/Ellesmere/EUIKR 접두 이름만 스캔.
                --   Blizzard StaticPopup/GameDialog (legacy 명명 OR 신규 익명 wrapper)
                --   에 frame 필드 쓰기 시 그 popup 의 OnUpdate/OnAccept/onCancel 등
                --   secure 호출 모두 차단(CancelLogout, EndBoundTradeable 등).
                --   *익명 frame 도 모두 skip* — Blizzard 가 만든 익명 popup 가능성.
                --   Blizzard 자체 popup 의 번역 손실은 의도된 trade-off (클라가 이미 한글화).
                if not skip then
                    local okN, nm = pcall(child.GetName, child)
                    if not (okN and type(nm) == "string"
                            and (nm:find("^Ellesmere") or nm:find("^EUI")
                                 or nm:find("^CIM"))) then
                        skip = true
                    end
                end
                if not skip then
                    local sok, shown = pcall(child.IsShown, child)
                    if sok and shown then
                        local stok, isMatch = pcall(function() local s = child:GetFrameStrata(); return s == "FULLSCREEN_DIALOG" or s == "TOOLTIP" or s == "DIALOG" end)
                        if stok and isMatch then
                            local hok, h = pcall(child.GetHeight, child)
                            if hok and h and h > 0 and h <= 700 then
                                -- ★ 팝업/서브메뉴: 캐시 무시 (noCache=true) → 호버로 늦게
                                --   생성되는 서브 항목(예: Potions & Healthstone 하위)이
                                --   재구성될 때도 매번 잡힘.
                                ScanFrameRecursive(child, 0, true)
                                -- ★ 첫 감지 시 OnShow 후크 설치 → 다음에 popup 이 *열리는
                                --   즉시* 동기 스캔 → "영문 깜빡이다 한글로" 플리커 제거.
                                if not _wk_cimkrPopupShowHook[child] and child.HookScript then
                                    _wk_cimkrPopupShowHook[child] = true
                                    pcall(child.HookScript, child, "OnShow", function(self)
                                        pcall(ScanFrameRecursive, self, 0, true)
                                    end)
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    -- 1회 스캔 패스 (패널 열림 + 비전투 일 때만 실제 작업)
    local function _DoScan()
        if not PanelOpen() then return end
        if InCombatLockdown() then return end
        if _umRef and _umRef:IsShown() then ScanFrameRecursive(_umRef, 0) end
        TranslateBuiltUI()
        ScanPopupChildren()
        -- 검색형 드롭다운 목록 글자 크기 맞춤(Additions.lua, 옵션). 끔이면 즉시 return.
        if _G.EUIKR_ApplyDropdownFonts then _G.EUIKR_ApplyDropdownFonts() end
    end

    -- ★ ScanFrameRecursive 의 버튼 OnClick 후크가 호출 → 1프레임 후 팝업 스캔.
    --   클릭으로 생성되는 메뉴/팝업이 ticker(0.5s) 안 기다리고 즉시 한글화.
    --   C_Timer.After(0,...) 만으로 충분 (다음 프레임에 click handler 가 만든 팝업 보임).
    _TriggerPopupScan = function()
        if C_Timer and C_Timer.After then
            C_Timer.After(0, function() pcall(ScanPopupChildren) end)
        else
            pcall(ScanPopupChildren)
        end
    end

    -- ★ 패널-열림 게이트 ticker (회귀 수정). burst-만 방식은 바 선택 등
    --   *동적으로 열리는 드롭다운 메뉴*를 못 잡아 영문으로 남았음. 이 ticker는
    --   PanelOpen() 일 때만 돌고, 닫히면 스스로 Cancel → 닫으면 메모리/사용량 0
    --   (사용자 요구: "설정창 안 열면 안 늘어남" 충족). 열린 동안은 _scanned
    --   캐시로 정적 트리 즉시 skip → 비용 낮음.
    local _ticker, _tickCount = nil, 0
    local function ScanCycle()
        if not PanelOpen() then
            if _ticker then _ticker:Cancel(); _ticker = nil end
            return
        end
        if InCombatLockdown() then return end
        _tickCount = _tickCount + 1
        if _umRef and _umRef:IsShown() then
            ScanFrameRecursive(_umRef, 0)
            -- ★ 더미 SetText 후크 보장 (진입 경로/생성 타이밍 무관). 설정창 경유
            --   진입은 _openUnlockMode 가 지연 호출이라 더미가 늦게 생겨 후크가
            --   안 걸릴 수 있음 → ticker 가 매 tick 재시도(__cimkrUMHook 가드로 저렴).
            if _umReHook then _umReHook() end
        end
        if _tickCount % 2 == 0 then TranslateBuiltUI() end   -- 1초마다 풀스캔
        ScanPopupChildren()                                   -- 매 0.5초 — 드롭다운 메뉴 즉시
        -- ★ 위젯 cog popup 들 OnShow 후크 보장 (lazy 생성 popup 도 0.5s 안에 잡힘).
        --   Additions.lua 의 ApplyWidgetScale 이 _popupFrames 순회 + OnShow 후크 설치.
        if _G.EllesmereUI_KR_ApplyWidgetScale then _G.EllesmereUI_KR_ApplyWidgetScale() end
        -- QoL 창(장비강화/쐐기) — 비동기 아이템 정보로 타일(트랙명 "Hero" 등)이
        -- OnShow 한참 뒤 채워짐 → 매 tick _InstallFSHooks 로 새 FontString 후크
        -- + 현재 글자 즉시 번역(가드로 기존건 즉시 skip → 저렴).
        for i = 1, #_QOL_WINDOWS do
            local w = _G[_QOL_WINDOWS[i]]
            if w and w.IsShown and w:IsShown() then pcall(_InstallFSHooks, w, 0) end
        end
        -- ★ 드롭다운 버튼 선택값 재번역 (캐시 무시) — 선택으로 바뀐 선택값이
        --   영문으로 남던 "타이밍 문제" 해결. 등록된 버튼만(저렴), 매 0.5초.
        if _RescanDDButtonLabel then
            for btn in pairs(_ddButtons) do
                if type(btn) == "table" and btn.IsShown and btn:IsShown() then
                    pcall(_RescanDDButtonLabel, btn)
                end
            end
        end
        if _G.EUIKR_ApplyDropdownFonts then _G.EUIKR_ApplyDropdownFonts() end
    end
    local function StartTicker()
        -- 즉시 1회 (페이지 전환 직후 반응) + 패널 열림 동안 0.5초 ticker
        _DoScan()
        if _ticker then return end
        _tickCount = 0
        if C_Timer and C_Timer.NewTicker then
            _ticker = C_Timer.NewTicker(0.5, ScanCycle)
        end
    end

    -- 페이지/모듈 전환 시 캐시 무효화 후 burst 스캔. StartTicker 가 즉시 +
    -- 0.1/0.3/0.6초 후속 스캔을 수행하므로 비동기 생성 위젯도 빠르게 잡힘.
    local function _PageRescan()
        _ScanInvalidate()
        StartTicker()
    end
    if EllesmereUI.Show then
        hooksecurefunc(EllesmereUI, "Show", _PageRescan)
    end
    if EllesmereUI.Toggle then
        hooksecurefunc(EllesmereUI, "Toggle", function()
            if PanelOpen() then _PageRescan() end
        end)
    end
    if EllesmereUI.SelectModule then
        hooksecurefunc(EllesmereUI, "SelectModule", _PageRescan)
    end
    if EllesmereUI.SelectPage then
        hooksecurefunc(EllesmereUI, "SelectPage", _PageRescan)
    end
    if EllesmereUI.RefreshPage then
        hooksecurefunc(EllesmereUI, "RefreshPage", _PageRescan)
    end

    -- 미니맵 아이콘으로 직접 UnlockMode 진입 시 dummy 미번역 회피.
    -- 설정창 경유(Toggle/Show)는 StartTicker 가 걸려 dummy 가 늦게 생성돼도
    -- ticker 가 스캔하지만, EllesmereUI:ToggleUnlockMode 직접 호출은 그 경로를
    -- 안 탐 → EllesmereUnlockMode OnShow 에서도 ticker 시동 + 즉시 스캔.
    -- UnlockMode 진입 시 1회 풀스캔 (캐시 무효화 + dummy frame 까지).
    --  ★ 회귀 수정: _scanned 캐시가 이전 edit mode 세션의 frame 을 기억해
    --    재진입 시 skip → 매번 _ScanInvalidate() 로 강제 재스캔.
    --  ★ dummy frame 은 UnlockMode 자식이 아니라 UIParent 자식(strata DIALOG/
    --    TOOLTIP)일 수 있어 ScanPopupChildren 도 즉시 호출 (5-tick 대기 회피).
    -- ★ 편집모드 더미 영문 잔존 수정: 더미에 마우스오버하면 OnEnter 가
    --   영문으로 SetText → RetranslateFontString 의 __cimkrLastTxt 캐시가
    --   '이미 본 영문' 이라 재번역을 건너뛰어 영문이 남았음. 더미 FontString
    --   에 SetText 후크를 1회 설치 → 영문이 들어올 때마다 캐시를 비우고 즉시
    --   재번역 → 한글 고정(호버해도 안 바뀜). EUI 자체 프레임이라 taint 안전.
    local function _HookUMFontString(fs)
        if not fs or _wk_cimkrUMHook[fs] or not fs.SetText then return end
        if fs.IsForbidden and fs:IsForbidden() then return end
        if fs.IsProtected and fs:IsProtected() then return end
        _wk_cimkrUMHook[fs] = true
        hooksecurefunc(fs, "SetText", function(self)
            if _wk_cimkrUMReentry[self] then return end
            _wk_cimkrUMReentry[self] = true
            _wkLastTxt[self] = nil   -- 캐시 무효화 → 영문 재유입 시 재번역
            pcall(RetranslateFontString, self)
            _wk_cimkrUMReentry[self] = false
        end)
        -- 설치 시점에 더미가 이미 영문(호버로 되돌아간 상태)일 수 있으니 캐시 비우고
        -- 현재 글자도 즉시 번역. (__cimkrUMHook 가드로 설치는 1회뿐이라 비용 X)
        _wkLastTxt[fs] = nil
        pcall(RetranslateFontString, fs)
    end
    -- um 서브트리 + UIParent 의 더미(strata DIALOG/TOOLTIP/FULLSCREEN_DIALOG)
    -- FontString 에 SetText 후크 설치 (한 번만 — __cimkrUMHook 가드).
    local function _HookUMTree(frame, depth)
        if not frame or depth > 8 then return end
        if frame.IsForbidden and frame:IsForbidden() then return end
        if frame.GetRegions then
            local ok, rs = pcall(function() return { frame:GetRegions() } end)
            if ok and type(rs) == "table" then
                for _, r in ipairs(rs) do
                    local tok, ot = pcall(function() return r and r.GetObjectType and r:GetObjectType() end)
                    if tok and ot == "FontString" then _HookUMFontString(r) end
                end
            end
        end
        if frame.GetChildren then
            local ok, cs = pcall(function() return { frame:GetChildren() } end)
            if ok and type(cs) == "table" then
                for _, c in ipairs(cs) do _HookUMTree(c, depth + 1) end
            end
        end
    end
    -- ★ 더미(mover)는 전부 EllesmereUnlockMode(=um)의 자식임
    --   (EUI_UnlockMode.lua: CreateMover → CreateFrame("Button", nil, unlockFrame),
    --    unlockFrame = CreateFrame(..,"EllesmereUnlockMode", UIParent)).
    --   따라서 um 서브트리만 후크. ★ UIParent 전체를 훑으면 설정창
    --   _mainFrame 의 사이드바 버튼 FontString 까지 SetText/SetFont 가 걸려
    --   그 버튼이 오염→ 클릭 시 EllesmereUI.lua:4581(C_Timer.After) 크래시.
    --   um 한정으로 그 경로를 완전히 회피.
    local function _HookUMDummies()
        local um = _G.EllesmereUnlockMode
        if um then pcall(_HookUMTree, um, 0) end
    end
    _umReHook = _HookUMDummies   -- ticker(ScanCycle)가 편집모드 동안 호출 (forward decl 채움)

    local function _ScanUMNow()
        _ScanInvalidate()
        local um = _G.EllesmereUnlockMode
        if um then pcall(ScanFrameRecursive, um, 0) end
        pcall(ScanPopupChildren)
        if EllesmereUI._mainFrame then pcall(TranslateBuiltUI) end
        -- 더미 FontString SetText 후크 → 호버해도 영문으로 안 돌아가고 한글 고정.
        -- (EllesmereUnlockMode 서브트리 한정 = EUI 자체 프레임, taint 무관.
        --  4581 크래시는 _openUnlockMode 래핑이 원인이었고 이 후크와 무관함이 확인됨)
        _HookUMDummies()
    end
    local function _StartTickerOnUM()
        local um = _G.EllesmereUnlockMode
        if not um then return end
        if _wk_cimkrUMTickerHooked[um] then return end
        _wk_cimkrUMTickerHooked[um] = true
        if um.HookScript then
            um:HookScript("OnShow", function(self)
                StartTicker()   -- 편집모드 동안 0.5초 ticker (드롭다운/메뉴 포착, 닫으면 취소)
                _ScanUMNow()
                if C_Timer and C_Timer.After then
                    C_Timer.After(0.1, _ScanUMNow)
                    C_Timer.After(0.5, _ScanUMNow)
                end
            end)
        end
    end
    _StartTickerOnUM()
    -- 본체가 늦게 생성하는 경우 대비
    if C_Timer and C_Timer.After then
        C_Timer.After(1, _StartTickerOnUM)
        C_Timer.After(3, _StartTickerOnUM)
    end

    -- ★ 핵심 회귀 수정: EllesmereUnlockMode 는 첫 편집모드 진입 시 *lazy 생성*
    --   (EUI_UnlockMode.lua:8227). PLAYER_LOGIN 시점엔 frame 이 없어
    --   _StartTickerOnUM 의 OnShow 후크가 설치조차 안 됨. 미니맵 경로는
    --   ticker 도 안 돌아 스캔 0 → dummy 영문 잔존.
    --   해결: unlock mode 를 *여는 함수* (_openUnlockMode / ToggleUnlockMode)
    --   를 hooksecurefunc → 호출 직후 frame 이 존재하므로 OnShow 후크 설치 +
    --   타이머로 즉시 스캔 (첫 진입은 OnShow 가 이미 지나갔으므로 타이머 필수).
    local function _OnUnlockOpened()
        _StartTickerOnUM()           -- 다음 진입을 위한 OnShow 후크 설치
        StartTicker()                -- 편집모드 동안 ticker (PanelOpen 가 um 도 포함)
        _ScanUMNow()
        if C_Timer and C_Timer.After then
            C_Timer.After(0.2, _ScanUMNow)
            C_Timer.After(0.6, _ScanUMNow)
        end
    end
    -- ★ 크래시 수정: 예전엔 hooksecurefunc(EllesmereUI,"_openUnlockMode",..) 로
    --   여는 함수를 *감쌌는데*, 사이드바 버튼이 그 값을 C_Timer.After(0, _openUnlockMode)
    --   에 넘기는 경로(EllesmereUI.lua:4581)에서 "bad argument #2 (not a function)"
    --   크래시가 났음. 함수 자체를 감싸지 않고, *버튼 OnClick post-hook* (taint-safe)
    --   로만 진입을 감지 → C_Timer.After 가 받는 값은 베이스 원본 그대로.
    if not EllesmereUI._koKR_unlockOpenHooked then
        EllesmereUI._koKR_unlockOpenHooked = true
        local function _hookSidebarBtn()
            local sb = EllesmereUI._unlockSidebarBtn
            if sb and sb.HookScript and not _wk_cimkrUnlockClick[sb] then
                _wk_cimkrUnlockClick[sb] = true
                sb:HookScript("OnClick", function()
                    if C_Timer and C_Timer.After then
                        C_Timer.After(0.05, _OnUnlockOpened)
                        C_Timer.After(0.3,  _OnUnlockOpened)
                    else
                        _OnUnlockOpened()
                    end
                end)
            end
        end
        _hookSidebarBtn()
        -- 사이드바는 설정창 첫 표시 때 생성될 수 있어 약간의 재시도 + 패널 표시 후크
        if C_Timer and C_Timer.After then
            C_Timer.After(1, _hookSidebarBtn)
            C_Timer.After(3, _hookSidebarBtn)
        end
        if EllesmereUI.Show then hooksecurefunc(EllesmereUI, "Show", _hookSidebarBtn) end

        -- ★ 미니맵 우클릭 진입: EllesmereUIMinimapButton OnClick 직접 호출
        --   (EllesmereUI.lua:8266 EllesmereUI._openUnlockMode()). 버튼 OnClick
        --   post-hook (taint-safe) → 우클릭 시 _OnUnlockOpened 로 스캔/OnShow 설치.
        local function _hookMinimapBtn()
            local mb = _G.EllesmereUIMinimapButton
            if mb and mb.HookScript and not _wk_cimkrUnlockClick[mb] then
                _wk_cimkrUnlockClick[mb] = true
                mb:HookScript("OnClick", function(_, button)
                    if button ~= "RightButton" then return end
                    if C_Timer and C_Timer.After then
                        C_Timer.After(0.05, _OnUnlockOpened)
                        C_Timer.After(0.3,  _OnUnlockOpened)
                    else
                        _OnUnlockOpened()
                    end
                end)
            end
        end
        _hookMinimapBtn()
        if C_Timer and C_Timer.After then
            C_Timer.After(1, _hookMinimapBtn)
            C_Timer.After(3, _hookMinimapBtn)
        end

        -- ★ ToggleUnlockMode 경유(ESC 게임메뉴·위젯 잠금버튼)는 *값으로 C_Timer.After
        --   에 안 넘어가므로* 래핑해도 안전(크래시는 _openUnlockMode 값 전달만 해당).
        --   한 번이라도 진입하면 _StartTickerOnUM 이 OnShow 후크를 설치 → 이후 모든
        --   경로(_openUnlockMode 직접 호출 포함) 자동 커버.
        if type(EllesmereUI.ToggleUnlockMode) == "function" then
            hooksecurefunc(EllesmereUI, "ToggleUnlockMode", function()
                if C_Timer and C_Timer.After then
                    C_Timer.After(0.05, _OnUnlockOpened)
                    C_Timer.After(0.3,  _OnUnlockOpened)
                else
                    _OnUnlockOpened()
                end
            end)
        end

        -- ★ 설정창 사이드바 "잠금 해제" 진입은 SelectPage("Unlock Mode") 경로
        --   (EllesmereUI.lua:6899) — 위 버튼 후크가 못 잡던 경로. 이게 미설치라
        --   "설정창 편집모드 = 영문, 미니맵 = 한글, 미니맵 한번 후 설정창도 한글"
        --   증상의 원인. 여기서 _OnUnlockOpened → OnShow 후크 설치 + 스캔.
        --   (SelectPage 는 값으로 C_Timer.After 에 안 넘어가므로 래핑 안전)
        if type(EllesmereUI.SelectPage) == "function" then
            hooksecurefunc(EllesmereUI, "SelectPage", function(_, pageName)
                if pageName ~= "Unlock Mode" then return end
                if C_Timer and C_Timer.After then
                    C_Timer.After(0.05, _OnUnlockOpened)
                    C_Timer.After(0.3,  _OnUnlockOpened)
                    C_Timer.After(0.8,  _OnUnlockOpened)
                else
                    _OnUnlockOpened()
                end
            end)
        end
    end

    print("|cff00ff88[EUI koKR]|r " .. (EUI_L["_VERSION"] or "v1.3")
        .. (ok and " 준비 완료" or " (Widgets 미감지)"))
end)

-- ── /euikr 진단 슬래시 ───────────────────────────────────────────────
SLASH_EUIKR1 = "/euikr"
-- ★ SlashCmdList 를 재할당(= SlashCmdList or {})하면 전역 SlashCmdList 가 taint 되어
--   매크로의 슬래시 실행(SetRaidTarget 등 보호 함수)이 차단됨. 키만 추가하면 충분.
SlashCmdList["EUIKR"] = function()
    local function p(s) print("|cff00ff88[EUI koKR]|r " .. s) end
    local n = 0
    for _ in pairs(EUI_L) do n = n + 1 end
    p("EUI_L 키 수: |cffffff00" .. n .. "|r")
    local W = EllesmereUI and EllesmereUI.Widgets
    p("Widgets: " .. (W and "|cff00ff00OK|r" or "|cffff0000nil|r")
        .. "  WidgetHook: " .. (W and W._koKR_hooked and "|cff00ff00OK|r" or "|cffff0000NO|r")
        .. "  PopupHook: " .. (EllesmereUI and EllesmereUI._koKR_popupHooked and "|cff00ff00OK|r" or "|cffff0000NO|r")
        .. "  RegModuleHook: " .. (EllesmereUI and EllesmereUI._koKR_registerHooked and "|cff00ff00OK|r" or "|cffff0000NO|r"))
    p("ShowWidgetTipHook: " .. (EllesmereUI and EllesmereUI._koKR_swtHooked and "|cff00ff00OK|r" or "|cffff0000NO|r")
        .. "  BuildDDM: " .. (EllesmereUI and EllesmereUI._koKR_bdmHooked and "|cff00ff00OK|r" or "|cffff0000NO|r")
        .. "  GTT: " .. (_G.__cimkr_gtt_hooked and "|cff00ff00OK|r" or "|cffff0000NO|r"))
    p("mainFrame: " .. (EllesmereUI and EllesmereUI._mainFrame and "|cff00ff00OK|r" or "|cffff0000nil|r")
        .. "  MMBtn: " .. (_G.EllesmereUIMinimapButton and "|cff00ff00OK|r" or "|cffff0000nil|r"))
    for _, k in ipairs({ "Global Settings", "Reset", "Disable", "Modules", "Font" }) do
        local v = EUI_L[k]
        p("  EUI_L[\"" .. k .. "\"] = " .. (v and ("|cffaaaaff" .. tostring(v) .. "|r") or "|cffff0000nil|r"))
    end
end

-- ═════════════════════════════════════════════════════════════════════
-- [POI 호버 툴팁 복원] — 이 블록 통째로 지워도 됨 (자기완결).
-- EllesmereUIMinimap 의 pingBlocker (line 2718) 가 EnableMouse(true) 라서
-- 미니맵 전체에서 호버를 가로채 광석/약초 POI 핀 OnEnter 가 안 옴 → 툴팁
-- 미발생. 그 frame 만 찾아 모션만 차단 해제(클릭은 유지). 원본 비건드림.
-- ═════════════════════════════════════════════════════════════════════
do
    local function _FixMinimapBlocker()
        local mm = _G.Minimap
        if not mm or not mm.GetChildren then return end
        local ok, kids = pcall(function() return { mm:GetChildren() } end)
        if not ok then return end
        for _, ch in ipairs(kids) do
            if type(ch) == "table"
               and not ch._indicatorKey                -- EUI 인디케이터 제외
               and ch.SetMouseMotionEnabled
               and ch.IsMouseMotionEnabled
               and ch.GetObjectType
            then
                local okT, ot = pcall(ch.GetObjectType, ch)
                if okT and ot == "Frame" then
                    local okM, motion = pcall(ch.IsMouseMotionEnabled, ch)
                    if okM and motion then
                        pcall(ch.SetMouseMotionEnabled, ch, false)
                    end
                end
            end
        end
    end
    -- pingBlocker 는 EllesmereUIMinimap 의 PLAYER_LOGIN 핸들러에서 생성 →
    -- 1/3/8초 후 적용 시도 (생성 타이밍 변동 대비).
    if C_Timer and C_Timer.After then
        C_Timer.After(1, _FixMinimapBlocker)
        C_Timer.After(3, _FixMinimapBlocker)
        C_Timer.After(8, _FixMinimapBlocker)
    end
end
