地區(region)/語系(locale)判斷規則全圖

建立 2026-09-02 ・ 對照 at-inwin-plugin v0.60.0 + atelement-widgets v3.15.4 逐行核對 ・ 前後端共用參考

這份文件是做什麼的

接下來要動三件事:megamenu 的地區切換商品/分類的地區語系隱藏商品/分類的地區語系資料讀取。三件都同時牽涉前後端,所以先把兩邊的現況畫成同一張圖, 避免各自照著自己那半邊的假設往下做。

兩邊的程式碼都是逐行讀過才寫的,每條結論都附 檔案:行號,可以直接查證; 站上的部分是實測,附了指令與數字。如果有哪裡跟你的理解對不上, 很可能是我讀錯了——請直接指出來。

隱藏規則有兩種完全不同的失效成因,混在一起談會導向錯的解法:

① raw SQL —— 站台 Header 的 megamenu 在前端 atelement-widgets, 它的分類與商品查詢刻意繞過整個 WordPress query API ⇒ 沒有任何 hook 掛得上去, 需要前端配合改 widget。

② 次要查詢 —— Elementor Pro 的 loop widget 跑在我們自己的 iLayout/樣板裡, 它是正常的 WP_Querypre_get_posts 也有跑, 只是我們自己要求 is_main_query() 而主動放行了

站上實測顯示,客戶看得到的漏出大宗是 ②,不是 ①—— 25 個 Series 頁與 3 個產品線 Main 頁都屬於這一類,而它們完全在我們自己的控制範圍內。
目錄
  1. 兩個真值來源:region 與 locale 怎麼決定
  2. 資料讀取:兩套互不相同的座標系統
  3. 隱藏規則:一個判定函式、八個攔截點
  4. 失效面全表:誰攔得到、誰攔不到、為什麼
  5. 三件待辦各卡在哪一段

這份圖的證據邊界

一、兩個真值來源

region 與 locale 是兩條完全獨立的解析鏈,各有自己的快取、白名單與 fallback, 彼此不交互作用。兩者都由後端 iTemplateRenderer 提供唯一實作。

flowchart TD
    subgraph R["resolve_current_region() iTemplateRenderer.php:181-207"]
        R0{"static cache
已有值?"} -->|是| RH["直接回傳"] R0 -->|否| R1{"$_GET['region']
在白名單?"} R1 -->|是| RW["write_region_cookie()
種 30 天 cookie"] --> RC["回傳"] R1 -->|否 / 不存在| R2{"$_COOKIE
inwin_region
在白名單?"} R2 -->|是| RC R2 -->|否| R3["fallback 'main'"] --> RC end subgraph L["resolve_current_locale() iTemplateRenderer.php:310-324"] L0{"function static
cache 已有值?"} -->|是| LH["直接回傳"] L0 -->|否| L1["apply_filters
('wpml_current_language')"] L1 -->|null| L2["'en'"] --> LC["回傳"] L1 -->|有值| L3{"在 locale_mapping
對照表?"} L3 -->|是| L4["內部 4 字母碼
zh-hant → hant"] --> LC L3 -->|否| L5["⚠️ 原值穿透
pt-pt → 'pt-pt'"] --> LC end style RW fill:#fff3cd,stroke:#9a6700 style L5 fill:#fdf0f0,stroke:#8b1d1d style R3 fill:#eef,stroke:#556 style L2 fill:#eef,stroke:#556

圖 1 — 兩條解析鏈。唯一會產生副作用(種 cookie)的是 region 的第一步。

白名單與不對稱之處

regionlocale
解析白名單 americas / apac / emea / other / main
iTemplateRenderer.php:144
對照表 9 種 + en
iTemplateRenderer.php:249-260
可存進隱藏設定的值 4 個(不含 main
RegionRegistry.php:32
10 個(含 en
RegionRegistry.php:112-120
快取形式 class property $cached_region
reset_cached_region()
函式內 static,無重置點
未知值的行為 忽略,往下一步走 原值穿透?? $wpml_locale
寫入 cookie 只有 ?region= 命中白名單時
iTemplateRenderer.php:188
不寫

🔴 locale 的快取沒有重置點,會造成整個 request 鎖死在英文

resolve_current_locale() 用的是函式內 static $cached:311-312), 沒有對應的 reset。若任何程式在 WPML 就緒前呼叫它一次apply_filters('wpml_current_language', null) 會回 null, 該 request 的 locale 就永久固定成 'en':317), 之後所有翻譯、locale 隱藏判定、樣板 locale patch 全部退回英文。

目前最早的呼叫點是 pre_get_postsHiddenVisibilityGate.php:435), 實務上晚於 WPML init——但這是時序耦合,不是結構保證。任何新增的早期 hook 都可能引爆它。

cookie 的三個性質

write_region_cookie()iTemplateRenderer.php:217-227): 名稱 inwin_region、30 天、Path=/SecureHttpOnly=false(刻意讓 JS 可讀寫)、SameSite=Lax

誰在消費這兩個值

模組regionlocale備註
iTemplate(標題 filter)iTemplate.php:144,167
HiddenVisibilityGate三處 :396,434,551
iValue/Adapter/Product/Text五處
ProductCompareTableMerger:39
ProductCategoryTranslation分類側完全沒有 region
rg region modules/ProductCategory/ 零命中)
iValue/Adapter/ProductCategory/Text
iValue/Adapter/ProductCategory/Gallery

二、資料讀取:兩套互不相同的座標系統

這是最容易誤解的一段。「商品基本資訊」與「iTemplate 樣板資料」不是同一套 key、也不是同一種疊加規則

flowchart LR
    subgraph A["(A) 商品基本欄位 ProductFieldResolver"]
        direction TB
        A1["地區軸
_product_{field}_{region}"] -->|非空即回| AR(["回傳"]) A2["語系軸
_product_{field}_{locale}"] -->|非空即回| AR A3["基準
原生欄位 post_title 等"] --> AR A1 -.->|空| A2 -.->|空| A3 AX["❌ 沒有交叉座標
region+locale 不存在"] end subgraph B["(B) iTemplate 樣板 iTemplateVariantKeys"] direction TB B0["基準
itemplate_*_main_en"] --> BM["array_merge
後套者覆蓋"] B1["語系軸 main_{locale}"] --> BM B2["地區軸 {region}_en"] --> BM B3["交叉 {region}_{locale}
key space 保留
目前無 UI 寫入"] --> BM BM --> BR(["合併結果"]) end style AX fill:#fdf0f0,stroke:#8b1d1d style B3 fill:#f2f3f5,stroke:#999,stroke-dasharray: 4 3

圖 2 — 兩套座標系統。左邊「先返回者贏」,右邊「後套者覆蓋」,但兩者的淨效果都是地區蓋語系

(A) 商品基本欄位(B) iTemplate 樣板
實作ProductFieldResolver::resolve()
:85-101
iTemplateVariantKeys::candidate_meta_keys()
:62-67
機制三段 if,先返回者贏陣列順序即套用順序,後套者覆蓋
疊加結果地區蓋語系(裁定 D8)——反轉成本=對調兩段程式碼
交叉座標不存在key space 保留、無 UI 寫入、
寫入 API 明確拒絕
分類側✗ 沒有地區軸(plm#19)✓ 有效(走 get_term_meta

🔴 商品基本欄位沒有交叉座標 ⇒ 地區會吃掉語系

訪客在 (apac, hant) 下,若該商品有 _product_title_apac, 就會拿到 APAC 版(多半是英文),而 _product_title_hant 這個繁中翻譯 永遠讀不到。這是 D8 裁定的直接後果、不是 bug,但它意味著 「設了地區內容的商品,在該地區失去所有語系翻譯」。樣板側因為有交叉 key space 而沒有這個問題。

後台頁簽 ↔ 座標對照

頁簽商品(6 個)分類(4 個)座標儲存通道
基本資訊原生欄位WP 原生
基本資訊 地區✗ plm#19_product_{f}_{region}admin-ajax
基本資訊 翻譯✗ plm#19_product_{f}_{locale}admin-ajax
樣板設定main_enREST
樣板設定 地區{region}_enREST
樣板設定 翻譯main_{locale}REST

後台切換座標用的是 ?edit_region=,不是 ?region=

兩支 JS 都明文註記原因:?region=前台 resolve_current_region() 的正式輸入, 帶了會種下 30 天 cookie(product-fields-manager.js:193-195product-region-editing.js:14)。 後台頁簽是純 JS 切換 + history.replaceState,不換頁。

三、隱藏規則:一個判定函式、八個攔截點

v0.59.0 起是兩組一維(region 一組、locale 一組),OR 判定——命中任一即隱藏。 meta key _inwin_product_hidden_visibility只掛在 product 上,形狀是扁平字串陣列 如 ['locale:hant', 'region:apac']

flowchart TD
    Q["前台請求"] --> BP{"should_bypass()?
cron / WP-CLI / 後台非 AJAX
/ REST / Elementor 預覽
/ VisualEditor"} BP -->|是| SKIP["整組跳過"] BP -->|否| SC{"場景"} SC -->|單一商品頁| H1["template_redirect (pri 1)
get_post_meta → 判定"] SC -->|archive / tax / search| H2["pre_get_posts (pri 10)
需 is_main_query()"] SC -->|related / upsell
cross-sell / compare| H3["四個 WC filter
已有 id 清單"] H1 --> J H3 --> J H2 --> CAND["get_hidden_product_ids()
meta_query 三條 LIKE
=候選過濾"] CAND --> FETCH["fetch_hidden_meta_for_ids()
先問 get_post_metadata filter
回 null 才查 DB"] FETCH --> J{"is_hidden_by_meta()
唯一判定"} J -->|命中 region 或 locale
或舊格式降維| HIDE["隱藏"] J -->|否| SHOW["顯示"] HIDE -.->|單頁| R404["set_404()"] HIDE -.->|清單| RNOT["併入 post__not_in"] style BP fill:#fff3cd,stroke:#9a6700 style J fill:#eaf6ed,stroke:#1a7f37 style CAND fill:#f2f3f5,stroke:#999

圖 3 — 三條讀取路徑結構性共用同一個判定函式;SQL 只負責縮小候選集,不做判定。

這個設計的核心不可退讓

HiddenVisibilityGate.php:313-316 是「SQL 只當候選過濾、判定一律回 is_hidden_by_meta()」的體現。曾有 review 提議「在 SQL 裡把 locale 白名單再寫一遍」以省掉後過濾—— 已被否決。理由是那會讓判定邏輯出現第二份實作,而兩份必然漂移。

八個攔截點

#hookpri攔什麼手法
1template_redirect1商品單頁set_404(),不 wp_die()
2pre_get_posts10archive/product_cat/product_tag/搜尋併入 post__not_in
3–4woocommerce_product_get_related10相關商品移除 id
5..._get_upsell_ids10Upsell移除 id
6..._get_cross_sell_ids10Cross-sell移除 id
7inwin_product_compare_visible_ids10產品比較結構上永遠 no-op(見 §4)
8save_post_product99遞增 cache version

四、失效面全表

以下每一條都是結構性的——不是還沒寫,是現在的架構下寫不到。 依「客戶看得見的程度」排序。

🔴 先分清楚:「攔不到」有兩種完全不同的成因,責任歸屬也不同

類型 ① raw SQL類型 ② 次要查詢
atelement-widgets 的 megamenuElementor Pro 的 loop widget(跑在我們自己的 iLayout/樣板裡)
機制直接 $wpdb->get_results()
根本不經過 WP query 層
正常的 WP_Query
pre_get_posts 有跑
為什麼沒生效沒有任何 hook 可掛我們自己要求 is_main_query() 而主動放行
影響面站台 Header megamenuSeries 頁、三條產品線的 Main 頁
誰能修需要前端配合改 widget我們自己這邊就能修

⚠️ 這兩類很容易被混為一談。實測顯示客戶看得到的漏出,大宗是類型 ②,不是 ①—— 而類型 ② 完全在我們自己的控制範圍內。

flowchart LR
    subgraph BE["at-inwin-plugin(後端)"]
        HOOKS["hook 層
pre_get_posts / template_redirect
get_terms_args / WC filters"] end subgraph FE["atelement-widgets(前端 · Lilith)"] RAW1["query_child_terms_en_raw()
raw SQL"] RAW2["get_en_term_raw()
raw SQL"] RAW3["get_ipc_products_by_subcat()
raw SQL"] TM["get_term_meta()
✅ 仍走 WP API"] end HOOKS -.->|❌ 攔不到| RAW1 HOOKS -.->|❌ 攔不到| RAW2 HOOKS -.->|❌ 攔不到| RAW3 HOOKS ==>|✅ 唯一接觸點| TM RAW1 --> MENU["站台 Header megamenu
全站每頁"] RAW2 --> MENU RAW3 --> MENU TM --> MENU style RAW1 fill:#fdf0f0,stroke:#8b1d1d style RAW2 fill:#fdf0f0,stroke:#8b1d1d style RAW3 fill:#fdf0f0,stroke:#8b1d1d style TM fill:#eaf6ed,stroke:#1a7f37

圖 4 — 前端的四個資料接觸點,只有 get_term_meta() 那一條攔得到。

A. 客戶會直接看到的

A1 · megamenu 會列出「點了會 404」的商品 已上線

IPC 第 3 欄的商品清單走 raw SQL(at-main-menu.php:906-960), 篩選條件只有 post_type='product' AND post_status='publish'。 後端的兩個執行點(template_redirect 單頁 404、pre_get_posts archive 排除) 都攔不到這條查詢

⇒ 當前地區/語系下應被隱藏的商品,仍會出現在選單裡並帶著可點的連結,點進去才被 404。 前端的程式碼註解已知悉此事(at-main-menu.php:901),但沒有任何補償措施。 順帶:WooCommerce 自己的 exclude-from-catalog 也一併失效。

A2 · 產品比較的隱藏過濾結構上永遠不會執行

這是一條環環相扣的死路:

inwin_product_compare_visible_ids 這個攔截點在實際使用情境下是 no-op。 這是後端要修的,列在這裡是因為它會影響「比較功能要不要跟著地區走」的討論。

A3 · 商品分類(product_cat)目前完全沒有隱藏機制 plm#11

HiddenVisibility 整組只掛在 product 上:metabox 註冊在 'product'、 儲存掛 save_post_product、meta key 是 post meta。分類 term 本身無法被隱藏

A4 · 分類的「基本資訊」在任何地區下都是同一份 plm#19

分類側只有 locale 軸、沒有 region 軸。分類名稱/副標題/簡介/圖庫在 americasapac 下完全相同。 但「樣板設定 地區」是有效的(走 iTemplateVariantKeysget_term_meta)—— 這個不對稱容易讓人誤判成 bug。

B. 查詢層的涵蓋缺口

缺口結構性原因位置
archive 的候選探索看不到
filter 改寫後的 meta
post__not_in 必須在查詢之前算出 ⇒ 得先有候選名單 ⇒ 只能反查 postmeta 表。 而 core 的 get_metadata_raw()先問 filter、非 null 就不看 DB(WPML 掛的就是它)。
方向是單向的:filter 把「命中」改成「不命中」時後過濾吃的是 filter 值 ⇒ 不會比單頁多藏東西。 已用測試正面釘住。
Gate.php:196-216
:275-302
🔴 只有 main query 被攔
(類型 ②,站上實測已確認)
maybe_filter_query 要求 is_main_query()。 ⇒ Elementor loop 只要用自建查詢post_query_post_typeproductrelated)就完全不過濾; 走 current_query 的才吃得到主查詢的排除。
related/upsell/cross-sell 是靠另外四個 WC filter 補的,不是靠這條。
sitemap 也在外:實測 Yoast 的 /product-sitemap.xml 帶不帶 ?region= 完全相同(246 個 URL、byte 數一致)。
Gate.php:419
搜尋只在 post_type 明確含 product 時才攔 query_targets_product()post_type 未設定時回 false(保守選擇) ⇒ 未帶 post_type 的站內搜尋不排除隱藏商品。 Gate.php:582-594
麵包屑完全在規則之外 站上的麵包屑是 Elementor Pro 的 breadcrumbs widget, 而它的 render() 只是 WPSEO_Breadcrumbs::breadcrumb() 的薄包裝 ⇒ 內容全由 Yoast 產生。全站只有一個這種 widget(post 9587), 被 Header 引用 ⇒ 每一頁都有。
⇒ 隱藏規則沒有掛 Yoast 的 filter,就管不到麵包屑
elementor-pro/.../breadcrumbs.php
只涵蓋 product_cat / product_tag product_brand、屬性 pa_* 的 archive 不在範圍。 Gate.php:422-424
cache 失效只綁 save_post_product REST 寫入、WP-CLI、update_post_meta() 直呼、遷移腳本都不會讓 archive 的 hidden id 清單失效。 另 flush_cache() 忽略 wp_cache_set() 回傳值, 且內部 get 非 forced ⇒ 讀到落後副本會把 version 寫小、讓舊快取復活。 Gate.php:100
:329-332
儲存只認 classic metabox 表單 沒有 nonce 就 return ⇒ block editor、REST、WC 批次匯入寫不到也刪不掉這個 meta。 檔內承認並視為對稱限制。 HiddenVisibility.php:47-52

C. 前端那側的斷點

缺口說明位置
三個 raw SQL 讓所有 term filter 落空 後端若把分類隱藏實作成 get_terms_args / terms_clauses / pre_get_terms / get_term 上的 filter(這是最自然的 WP 作法),前端一條都收不到
只有「用 term meta 表達的隱藏規則」擋得住這個選單。
at-main-menu.php
:1200-1211
:1249-1278
地區切換不重新載入 地區欄的 <a href="#"> 沒有 preventDefault(),點擊只寫 cookie + 跳錨點。 ⇒ 當前頁的後端渲染內容仍是舊地區,要到下一次導覽才對齊。 at-main-menu.js:217-220
megamenu 的地區其實不影響內容 get_bm_categories_for_region()$region_slug函式體完全忽略它, 同一份 JSON 原樣複製到四個地區。註解自陳「Phase 4 靜態版…Phase 7 時替換」。 ⇒ 地區在前端目前只是一個寫 cookie 的 UI at-main-menu.php:827-832
手機版永遠不會設定地區 mobile accordion 底下沒有地區層_currentRegionSlug 保持 null ⇒ 不寫 cookie ⇒ 後端維持 main=不做地區隱藏。這是刻意設計,但後果是手機流量全部落在 main。 at-main-menu.php:577-636
BM 的連結永遠是 EN URL $localize_url = false ⇒ BM 所有連結不帶語系前綴。 ⇒ 從 BM 選單進入的流量,locale 維度被固定成 en at-main-menu.php:831
show_in_menu 現在可讀不可寫 前台仍在讀並據以隱藏分類,但後台 UI 被 SHOW_IN_MENU_UI = false 關掉。 ⇒ DB 裡殘留的 '0'永久隱藏該分類整棵子樹且無法從後台修復。 關閉原因寫的是「後台 metabox 的職責範圍尚未與後端確認」——那個後端就是我們。 class-menu-category-metabox.php:34

D. 站上實際分佈 實測

以上都是「機制上會怎樣」。這一節是站上實際有多少—— 資料來自 mlab 本機站(2026-07-29 正式站快照),登入狀態實測。

兩個會讓人量錯的環境陷阱

110 個商品分類的渲染路徑分群

分群分類數商品計數隱藏規則生效?
兩者皆無 ⇒ 原生 WooCommerce archive loop52334✅ 實測生效
只有 Elementor archive 樣板
loop-grid + current_query
23457推論生效
未實測(見下)
只有 itemplate_id(iLayout)⇒ Series 頁2594實測漏出
兩者都有(Elementor 那支是 draft ⇒ 實走 iLayout)12

另外 378/389/390 三條產品線的 Main 頁雖然有 Elementor archive 樣板, 但主體是 loop-carousel 且用自建查詢post_query_post_type: product) ⇒ 同樣漏出。

C583 案例的完整矩陣

C583(post 36746,["locale:hant","region:emea"])與 EA065(["region:apac"]) 是全站僅有的兩筆隱藏設定。數字=該商品連結在 HTML 中出現的次數:

分類頁渲染路徑plain?region=emea?region=apac
379 b-case原生 Woo loopc583=20 ✅2
380 b-c-atx原生 Woo loopc583=2 / ea065=2c583=0 ✅ea065=0 ✅
381 b-c-atx-c(C Series)iLayoutc583=1c583=1 ❌1
378 models-businessloop-carousel 自建查詢c583=1 / ea065=1c583=1 ❌ea065=1 ❌
商品單頁single200404 ✅200

漏出來的是貨真價實的商品卡片e-loop-item-36746、帶可點連結),不是 schema 殘留。 渲染它的是 BM_CASE_series_list(post 18072)的 loop-carousel, 查詢為 post_query_post_type: related

三個「量不到」的邊界,不要當成沒問題

資料量:分類層目前是一片空白

項目筆數意義
_inwin_product_hidden_visibility(post meta)2全站只有 C583、EA065
wp_termmeta%inwin%%hidden%%region%0分類層完全沒有隱藏資料(plm#11 是從零開始)
itemplate_data_* 的非 main_en 座標post meta 1
term meta 1
地區/語系座標幾乎沒被用過
商品語系翻譯 meta_product_title_hant 2
_product_title_japn 1
翻譯資料極少
megamenu 四個地區區塊的分類樹四份 JSON 完全相同(各 44 節點、md5 一致)⇒ 實證了「地區不影響選單內容」

E. 值得記一筆的其他發現

五、三件待辦各卡在哪一段

1 · mega menu 切換地區的處理

現況:地區切換只寫 cookie、不重載,而且前端的 megamenu 內容根本不隨地區變化get_bm_categories_for_region() 忽略參數)。後端則沒有任何前台切換 UI。

要決定的

2 · 商品/分類的地區、語系隱藏

商品側已完成(v0.59.0 兩組一維),但有三個已上線的破口, 其中兩個在我們自己這邊

分類側(plm#11)尚未開始,而這份圖給出一個硬約束

分類隱藏必須用 term meta 表達,不能只靠 get_terms_args 之類的 filter。 因為前端 megamenu 走 raw SQL,唯一還會經過 WP API 的接觸點是 get_term_meta()。 選錯實作方式的話,後台設了隱藏、選單照樣列出來。

這也重新框定了 C2(全域 get_terms_args 過濾)的取捨:它涵蓋不到 megamenu 這件事已確認, 而且它每頁都會被 Elementor 的面板查詢觸發一次——正好落在它卡了四輪的 re-entrancy 問題上。

3 · 商品/分類的地區、語系資料讀取

三個已知的不對稱,動之前要先確認哪些是刻意的:

另外前端有兩個對後端 key 的硬編碼假設_product_title_{locale}(商品名) 與 \inwin_template_plugin\Module\iTemplate\iTemplateRenderer::resolve_current_locale()(完整命名空間寫死)。 後端改了任一個都不會報錯,只會靜默退回英文。


需要兩邊一起確認的三件事

  1. 資料模型已經改過了。 docs/main_menu/後端-商品區域隱藏-整合參考.md 建立於 2026-07-03、基於 v0.38.1, 記的是「region:locale 二維」模型;v0.59.0 起已改成兩組一維 (region 一組、locale 一組,OR 判定)。照舊文件做整合會對不上,這份圖的 §3 是現行版本。
  2. 分類隱藏預計用 term meta 實作。 理由是 §4-C 第一條:filter 收不到 raw SQL,而 get_term_meta() 是唯一還走 WP API 的接觸點。 你在整合參考裡問的三個問題(meta key/格式/掛在哪一層),我們會在規格裡正式定義後給你。 如果 term meta 的形狀有你這邊偏好的樣子,這時候提最省事。
  3. SHOW_IN_MENU_UI 的職責範圍。 你註記的「等後端確認 metabox 職責範圍」——這題該我們回答。 在回答之前不會動 show_in_menu,避免做出語義重疊的第二套開關。

資料來源:at-inwin-plugin v0.60.0 與 atelement-widgets v3.15.4 的原始碼逐行核對(2026-09-02)。 站上資料引用自 mlab 本機站,該站是 2026-07-29 正式站快照,之後的異動無從得知。 本頁供 InWin 專案前後端共同參考。推論與實測已分別標示;有出入之處請直接指正。