# BÁO CÁO KHẢO SÁT KIẾN TRÚC ĐA NGÔN NGỮ (i18n) & ĐA NỀN TẢNG FREEEXILE 2026
**Tác giả**: `explorer_survey_i18n_1` (Khối Khảo Sát & Nghiên Cứu Kiến Trúc)  
**Thời gian**: 2026-10-02T01:50:00Z  
**Phạm vi**: `client/webapp/js/`, `client/src/`, `docs/standards/`, `tests/unit/`  
**Mục tiêu**: Khảo sát hiện trạng, khoanh vùng nguyên nhân gốc rễ (root cause) lỗi không nhận diện cài đặt ngôn ngữ trên Chat UI và các module UI khác, thiết kế kiến trúc Reactive i18n Event Bus và chuẩn hóa đa nền tảng (Web PWA, iOS Metal, Desktop).

---

## 1. TỔNG QUAN KHẢO SÁT (EXECUTIVE SUMMARY)

Dự án FreeExile định vị là game ARPG Cổ Võ Hoang Dã 2.5D chuẩn AAA (PoE2 spirit), hỗ trợ 9 ngôn ngữ toàn cầu (`vi`, `en`, `zh`, `ja`, `ko`, `th`, `de`, `ru`, `es`) và vận hành đa nền tảng (Web PWA 120 FPS, iOS Metal Native, Desktop Standalone).

Qua rà soát toàn diện codebase:
1. **Tồn tại hệ thống i18n cốt lõi nhưng bị phân mảnh**:
   - `client/webapp/js/data/i18n.js` và `client/webapp/js/data/i18n_catalog.js` quản lý từ điển 9 ngôn ngữ với 143 key chuẩn hóa và cơ chế phát sự kiện `freeexile:localeChanged`.
   - Tuy nhiên, phần lớn các module UI (đặc biệt là `chat_ui.js`, `character_equipment.js`, `inventory_stash.js`, `agent_orb.js`, `minimap_hud.js`) không đăng ký lắng nghe sự kiện từ bus này.
2. **Nguyên nhân gốc rễ khiến Chat UI không phản hồi cài đặt ngôn ngữ**:
   - `chat_ui.js` (dòng 310) chỉ gắn listener vào sự kiện DOM `change` trực tiếp của phần tử `#lang-select`: `document.getElementById('lang-select')?.addEventListener('change', ...)`.
   - Khi ngôn ngữ được thay đổi thông qua code (`FreeExileI18n.setLocale('en')`), qua Settings Modal hoặc qua phím tắt, trình duyệt không kích hoạt sự kiện DOM `change` trên thẻ `<select>`, dẫn đến `chat_ui.js` hoàn toàn điếc trước sự kiện.
   - Khi khởi động (`DOMContentLoaded`), `chat_ui.js` không đọc `FreeExileI18n.getLocale()` hoặc `localStorage` để khởi tạo nhãn ngôn ngữ ban đầu, khiến giao diện luôn ở tiếng Việt mặc định.
   - Hàm `updateLanguage(lang)` trong `chat_ui.js` chỉ dịch nhãn 8 tab kênh (`.chat-channel-tab`) và ticker channel, bỏ quên 100% placeholder input, thông báo quyền hạn (`validateChannelPermission`), nhãn nút gửi ('Gửi'), thông báo hệ thống ('Hệ Thống', '[Cảnh Báo]'), badge cooldown ('Khóa', 'Chờ {sec}s'), và toàn bộ nội dung tooltip vật phẩm rich item linking (phẩm cấp, hệ ngũ hành, HMAC verification).
   - `chat_ui.js` duy trì một từ điển độc lập `CHANNEL_I18N` cô lập, không liên kết với `I18N_CATALOG`.
3. **Thực trạng Settings Modal**:
   - Không tồn tại file độc lập `settings_modal.js`. Mã điều khiển modal nằm trong `client/webapp/js/ui/feedback.js` (`openSettingsModal`, `closeSettingsModal`) và template tại `client/webapp/templates/settings_modal.html` / `template_catalog_system.js`.
   - Modal Cài Đặt (`modal-settings`) hiện tại hoàn toàn **không có tùy chọn chọn ngôn ngữ**. Thẻ chọn ngôn ngữ `#lang-select` thực tế nằm ở header HUD chính (`index.html:85`) cạnh nút mở Cài Đặt.
4. **Kiến trúc đề xuất**:
   - Nâng cấp `FreeExileI18n` thành **Reactive i18n Event Bus** chuẩn Pub/Sub (hỗ trợ `subscribe()`, unregister callback, DOM `CustomEvent`, và đồng bộ `storage` event).
   - Tách mã preloader tài nguyên (`ASSETS`, `assetList`) ra khỏi `i18n.js` để tuân thủ giới hạn dòng GEMINI.md (hiện `i18n.js` 306 dòng, `i18n_catalog.js` 634 dòng).
   - Chuẩn hóa toàn bộ chuỗi Chat UI sang translation keys với fallback an toàn.
   - Xây dựng linter tĩnh kiểm tra chuỗi hardcoded và kịch bản kiểm thử E2E chống hồi quy.

---

## 2. HIỆN TRẠNG KHO TỪ ĐIỂN & MODULE i18n (CURRENT i18n INVENTORY)

### 2.1. Cấu trúc File & Phân Bổ Trách Nhiệm
| File | Vai trò | Dòng | Trạng thái & Vấn đề |
|---|---|---|---|
| `client/webapp/js/data/i18n.js` | Core i18n Engine & Singleton `FreeExileI18n` | 306 | Nhồi nhét 116 dòng preload ảnh (`ASSETS`, `assetList`); Singleton quản lý locale, localStorage, listener callback và phát `CustomEvent('freeexile:localeChanged')`. |
| `client/webapp/js/data/i18n_catalog.js` | Kho từ điển 9 ngôn ngữ (`I18N_CATALOG`) | 634 | Chứa 143 canonical keys chuẩn hóa qua 9 ngôn ngữ (`vi`, `en`, `zh`, `ja`, `ko`, `th`, `de`, `ru`, `es`). Tuân thủ quy chuẩn microcopy <= 2 từ. |
| `client/src/i18n/LocalizationManager.ts` | i18n Engine phía TypeScript / iOS Metal | 102 | Hỗ trợ 9 ngôn ngữ, chuỗi fallback (Target -> EN -> VI -> Key), quản lý `LocaleFontProfile` cho Apple Metal shaders. Chưa có cơ chế Event Observer. |
| `docs/standards/GLOBAL_LOCALIZATION_DICTIONARY.md` | Tài liệu thuật ngữ chuẩn hóa 9 ngôn ngữ | 178 | Bảng tra cứu thuật ngữ Cổ Võ Hoang Dã (lore, currency, skills, zones, bosses). |
| `tests/unit/test_webapp_localization_engine.py` | Unit test kiểm tra tính toàn vẹn i18n | 287 | 16 test cases kiểm tra 100% key parity (143 keys), microcopy <= 2 từ, cấm ngoặc đơn song ngữ, kiểm tra độ dài file code. Pass 100%. |

### 2.2. Kiểm Tra Từ Điển Hiện Có
- Kiểm tra bằng lệnh: `pytest tests/unit/test_webapp_localization_engine.py` -> **16/16 PASSED** trong 0.29s.
- `I18N_CATALOG` hiện chứa chính xác **143 keys** trên cả 9 ngôn ngữ:
  - `vi`, `en`, `zh`, `ja`, `ko`, `th`, `de`, `ru`, `es`.
  - Có sẵn các key liên quan đến HUD, Dialogue, Tooltip, Modal titles, Skills, Dodge, NPC names/titles/services, Notice.
  - **Chưa có các key cho Chat UI**: Chưa có key cho placeholder chat, permission error messages, cooldown badge, item tooltip labels (HMAC, phẩm cấp, ngũ hành), ticker prompt.

---

## 3. PHÂN TÍCH CÀI ĐẶT NGÔN NGỮ & SETTINGS MODAL

### 3.1. Cấu trúc Settings Modal
- **Vị trí code**: Không có file `settings_modal.js`.
  - Template: `client/webapp/templates/settings_modal.html` và `client/webapp/js/ui/templates/template_catalog_system.js` (dòng 99-287).
  - Controller: `client/webapp/js/ui/feedback.js` chứa các hàm:
    - `openSettingsModal()`: gỡ class `hidden` khỏi `#modal-settings`, gọi `window.setGamePaused(true)`.
    - `closeSettingsModal()`: thêm class `hidden`, gọi `window.setGamePaused(false)`.
    - `toggleSettingsModal()`: chuyển đổi trạng thái ẩn/hiện.
  - Phím tắt: `O` trong `main.js` kích hoạt `btn-open-settings.click()`; phím `Escape` đóng modal và unpause.
- **Nội dung bên trong Modal Settings**:
  - Cột 1: Đồ Họa & Tốc Độ Khung Hình (60 FPS / 120 FPS, ánh sáng, đổ bóng).
  - Cột 2: Âm Thanh & Rung Phản Hồi (Mute, Master, SFX, Ambient, Haptics, Test preview).
  - Cột 3: Thao Tác & Điều Khiển (Cần ảo linh hoạt/cố định, tự khóa mục tiêu, hiện phím gợi ý).
  - Banner Hòm Thư Góp Ý (`btn-open-feedback-form`).
  - Banner Tẩy Sạch Dữ Liệu Thử Nghiệm (`btn-settings-clean-slate`).
- **Phát hiện quan trọng**: Trong `modal-settings` **hoàn toàn không có dropdown hay radio button chọn ngôn ngữ**.

### 3.2. Vị Trí Thao Tác Đổi Ngôn Ngữ Thực Tế
- Thẻ chọn ngôn ngữ `#lang-select` được đặt tại thanh HUD Header phía trên bên phải:
  ```html
  <!-- client/webapp/index.html dòng 84-85 -->
  <button id="btn-open-settings" ... data-tooltip-title="Cài Đặt">...</button>
  <select id="lang-select" class="bg-stone-900 border border-stone-800 text-stone-300 text-[9px] px-1 py-0.5 rounded ..." data-tooltip-title-key="tooltip_lang_title" ...>
    <option value="vi" selected>VI</option>
    <option value="en">EN</option>
    <option value="zh">ZH</option>
    <option value="ja">JA</option>
    <option value="ko">KO</option>
    <option value="th">TH</option>
    <option value="de">DE</option>
    <option value="ru">RU</option>
    <option value="es">ES</option>
  </select>
  ```
- Người dùng tương tác trực tiếp trên thanh HUD hoặc gọi hàm JavaScript `applyLanguage(lang)`.

### 3.3. Cơ Chế Lưu Trữ (Persistence)
- Lưu trong `localStorage` với key: `'freeexile_locale'`:
  - Đọc: `localStorage.getItem('freeexile_locale')`
  - Ghi: `localStorage.setItem('freeexile_locale', locale)`
- Fallback khi `localStorage` rỗng: `this.currentLocale = 'vi'`.
- Không sử dụng cookie hay query params.

### 3.4. Chu Trình Phát Sự Kiện Khi Đổi Ngôn Ngữ
Khi gọi `FreeExileI18n.setLocale(locale)`:
```
[User change select / applyLanguage(lang)]
                │
                ▼
      FreeExileI18n.setLocale(locale)
                │
        ┌───────┴────────────────────────────────────────┐
        ▼                                                ▼
localStorage.setItem('freeexile_locale', locale)    Update #lang-select.value
        │
        ▼
FreeExileI18n.updateDOM(document)
  - Quét [data-i18n], [data-i18n-html], [data-i18n-placeholder]
  - Quét [data-tooltip-title-key], [data-tooltip-desc-key]
  - Cập nhật một số ID tĩnh: txt-char-name, txt-modal-meridian-title, txt-modal-vault-title, skill-dodge
        │
        ▼
this.listeners.forEach(fn => fn(locale))
        │
        ▼
window.dispatchEvent(new CustomEvent('freeexile:localeChanged', {
  detail: { locale, dict: this.catalog[locale] || {} }
}))
```

---

## 4. CHI TIẾT LỖI TẠI CHAT UI (`chat_ui.js`)

Qua phân tích chi tiết file `client/webapp/js/ui/chat_ui.js` (342 dòng):

### 4.1. Sự Cố Đăng Ký Lắng Nghe Sự Kiện (Event Listener Mismatch)
- **Dòng 310 trong `chat_ui.js`**:
  ```javascript
  document.getElementById('lang-select')?.addEventListener('change', (e) => updateLanguage(e.target.value));
  ```
- **Hậu quả**:
  1. Chỉ bắt được khi người dùng click chuột vật lý và thay đổi phần tử `<select id="lang-select">`.
  2. Bất kỳ lệnh đổi ngôn ngữ nào qua script (`FreeExileI18n.setLocale('en')`, `applyLanguage('en')`, phím tắt, hoặc đồng bộ từ native iOS / Settings Modal) đều thay đổi thuộc tính `select.value` bằng code mà **không kích hoạt sự kiện DOM `change`**.
  3. `chat_ui.js` **hoàn toàn không đăng ký** `window.addEventListener('freeexile:localeChanged', ...)` hoặc `FreeExileI18n.onLocaleChanged(...)`.
  4. Trong hàm khởi tạo `initChatUI()` (dòng 293-316), không hề có lệnh gọi `updateLanguage(FreeExileI18n.getLocale())`. Do đó, nếu người dùng đã lưu ngôn ngữ là `'en'` trong `localStorage`, khi mở game, Chat UI vẫn hiển thị nhãn tiếng Việt được hardcode trong HTML ban đầu!

### 4.2. Từ Điển Cô Lập `CHANNEL_I18N`
- Dòng 17-29 định nghĩa riêng `export const CHANNEL_I18N = { vi: {...}, en: {...}, ... }`.
- Đây là một "ốc đảo dữ liệu" (data silo), vi phạm nguyên tắc Single Source of Truth, tách biệt khỏi `i18n_catalog.js`.

### 4.3. Danh Sách Các Chuỗi Hardcoded Tiếng Việt Chưa Được Quốc Tế Hóa
| Thành phần | Dòng code | Nội dung Tiếng Việt Hardcoded | Tác động khi đổi sang EN / khác |
|---|---|---|---|
| **Tab kênh HTML** | `index.html:156-157` | `Thế Giới`, `Khu Vực`, `Bang Hội`, `Đội Ngũ`, `Mật Thư`, `Hệ Thống`, `Chiêu Mộ`, `Góp Ý` | Không tự dịch khi init nếu locale khác 'vi' |
| **Hằng số kênh** | `chat_ui.js:6-15` | `CHANNELS: { 1: { label: 'Thế Giới' }, 2: { label: 'Khu Vực' } ... }` | Log tin nhắn dùng `ch.label` luôn ra tiếng Việt |
| **Quyền kênh** | `chat_ui.js:124-129` | `'Kênh Hệ Thống chỉ dành cho thông báo từ máy chủ.'`, `'Chưa gia nhập Bang Hội.'`, `'Chưa gia nhập Tổ Đội.'`, `'Cần đạt cấp 20 để phát tán kênh Thế Giới.'`, `'Cần đạt cấp 10 để dùng kênh Chiêu Mộ.'` | Thông báo lỗi khi chat sai kênh luôn là tiếng Việt |
| **Placeholder Input** | `chat_ui.js:147` | `Nhập tin nhắn [${ch.label}]...` và `[${ch.label}] ${perm.reason}` | Placeholder ô nhập tin nhắn luôn là tiếng Việt |
| **Badge Cooldown** | `chat_ui.js:156, 163` | `Khóa`, `Chờ ${sec}s` | Badge thời gian hồi luôn là tiếng Việt |
| **Nút Gửi** | `index.html:162` | `<button id="chat-send-btn">Gửi</button>` | Nút bấm gửi luôn là tiếng Việt |
| **Ticker thu nhỏ** | `index.html:152` | `Chạm để mở kênh đàm đạo...` | Ticker thu gọn luôn là tiếng Việt |
| **Thông báo hệ thống** | `chat_ui.js:135, 137` | `senderName: 'Hệ Thống'`, `displayContent: '[Cảnh Báo] ${text}'` | Tên người gửi và tiền tố cảnh báo luôn là tiếng Việt |
| **Timestamp tin nhắn** | `chat_ui.js:265` | `toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' })` | Định dạng giờ bị cố định theo chuẩn Việt Nam |
| **Item Tooltip** | `chat_ui.js:38-42` | `RARITY_INFO`: `'Phàm Phẩm'`, `'Linh Phẩm'`, `'Cực Phẩm'`, `'Thần Phẩm'`, `'Thái Cổ'`; `ELEMENT_NAMES`: `'Kim'`, `'Mộc'`, `'Thủy'`, `'Hỏa'`, `'Thổ'` | Thuộc tính vật phẩm link trong chat luôn ra tiếng Việt |
| **Item Tooltip Badges**| `chat_ui.js:183-201`| `'Vật Phẩm'`, `'• Không có dữ liệu thuộc tính (Chưa xác thực)'`, `'• Không có thuộc tính bổ trợ'`, `'✓ HMAC Xác Thực'`, `'⚠ Chưa Xác Thực'`, `'Thiên Công'` | Toàn bộ trạng thái xác thực và tooltip item đều hardcoded |

---

## 5. ĐỐI SOÁT CÁC MODULE UI KHÁC TRONG CLIENT

Khảo sát 40 files trong `client/webapp/js/ui/`:

### 5.1. Nhóm Đã Đăng Ký Sự Kiện `freeexile:localeChanged` (Chỉ có 5 modules)
1. `dialogue_npc.js`: Gọi `renderDialogueModal()` khi có sự kiện đổi locale; sử dụng `window.I18N.getNpcName()` và `window.I18N.t()`. Đây là module tuân thủ i18n tốt nhất hiện tại.
2. `hideout_gates.js`: Lắng nghe sự kiện, gọi `FreeExileI18n.updateDOM(m)` và `updateHideoutUI()`.
3. `story_quest_board.js`: Lắng nghe sự kiện, gọi `FreeExileI18n.updateDOM(m)` và cập nhật danh sách nhiệm vụ.
4. `seasonal_reset.js`: Lắng nghe sự kiện, gọi `FreeExileI18n.updateDOM(m)`.
5. `shop_megashop.js`: Lắng nghe sự kiện, gọi `FreeExileI18n.updateDOM(m)` và re-render tabs.

### 5.2. Nhóm Chưa Đăng Ký Sự Kiện & Hardcoded Tiếng Việt (Cần khắc phục)
1. `character_equipment.js`: Toàn bộ 8 slot trang bị (`'Nón Bảo Hộ'`, `'Vũ Khí Chính'`, `'Huyết Giáp'`), tên vũ khí khởi đầu (`'Tân Thủ Cốt Kiếm'`), chỉ số thuộc tính (`'+25 Sát thương vật lý'`) đều hardcoded tiếng Việt.
2. `inventory_stash.js`: Tên vật phẩm khởi đầu (`'Bình Khí Huyết Nhỏ'`), tên tab rương (`'Kho Chung'`, `'Tiền Tệ'`, `'Trang Bị'`), phẩm cấp đều hardcoded tiếng Việt.
3. `agent_orb.js` & `agent_orb_types.js`: Tên cấp châu (`'Chiến Hồn Tế Cốt Sơ Cấp (2 Giờ)'`), tư thế chiến đấu (`'⚔️ Cương Mãnh'`, `'⚖️ Cân Bằng'`, `'💎 Tầm Bảo'`, `'🛡️ Ẩn Nhẫn'`), dòng suy nghĩ khởi tạo đều hardcoded tiếng Việt.
4. `minimap_hud.js`: Tên khu vực `#minimap-zone-label` ("Táng Kiếm Nhai") hardcoded trong DOM.
5. `feedback.js`: Tiêu đề form góp ý, placeholder, thông báo popup (`safeShowNotice`) hardcoded tiếng Việt.
6. `template_manager.js`: Khi template HTML được mount động vào `#modal-container`, `template_manager.js` phát ra sự kiện `freeexile:templateMounted` và `freeexile:templatesMounted`, nhưng `i18n.js` **không hề lắng nghe** các sự kiện này! Kết quả: modal mount động sau khi trang load sẽ không được `updateDOM()` tự động nếu người dùng mở bằng phím tắt.

---

## 6. YÊU CẦU ĐỒNG BỘ ĐA NỀN TẢNG (CROSS-PLATFORM ARCHITECTURE)

Hệ thống FreeExile vận hành trên 3 môi trường thực thi:

```
                      ┌────────────────────────────────────────┐
                      │      FreeExile Central i18n Core       │
                      │     (Master 9-Language Dictionary)     │
                      └──────────────────┬─────────────────────┘
                                         │
        ┌────────────────────────────────┼────────────────────────────────┐
        ▼                                ▼                                ▼
  ┌───────────┐                  ┌───────────────┐                 ┌─────────────┐
  │  Web PWA  │                  │   iOS Metal   │                 │   Desktop   │
  │ (120 FPS) │                  │ (Native Host) │                 │  (Electron/ │
  └─────┬─────┘                  └───────┬───────┘                 │    Tauri)   │
        │                                │                         └──────┬──────┘
        ▼                                ▼                                ▼
  DOM updateDOM()              WKWebView MessageHandler           storage event
  Canvas entity_renderer       Metal LocaleFontProfile            multi-window sync
  (Render 120Hz no reload)     (Rebuild Glyph Shaders)            (navigator.lang)
```

### 6.1. Web PWA (Mobile Safari, Chrome Mobile, Desktop Web)
- **Tiêu chí**: Chuyển đổi tức thì trong $0\text{ ms}$, **tuyệt đối không `location.reload()` hay F5**.
- **Cơ chế 2 tầng**:
  - *Tầng Canvas (60-120 FPS)*: `entity_renderer.js` vẽ tên người chơi và quái qua hàm `render()`. Vì mỗi frame đều gọi `I18N.getPlayerName()` và `I18N.getMonsterName()`, khi `currentLocale` thay đổi, text trên Canvas tự động chuyển sang tiếng mới ở ngay frame tiếp theo ($8.33\text{ ms}$).
  - *Tầng DOM UI*: Toàn bộ modal, chat dock, HUD header tự động cập nhật qua Reactive Event Bus.

### 6.2. iOS Metal / Native Mobile (`client/src/`)
- Quản lý qua `client/src/i18n/LocalizationManager.ts`.
- **Đặc thù Metal Text Rendering**:
  - Khi đổi ngôn ngữ (ví dụ sang Tiếng Thái `th` hoặc Tiếng Trung `zh`), engine đồ họa Metal cần cập nhật `LocaleFontProfile`:
    - `th`: Font `Thonburi`, `lineHeightMultiplier: 1.25`
    - `zh`: Font `PingFang SC`, `lineHeightMultiplier: 1.1`
    - `ja`: Font `Hiragino Sans`, `lineHeightMultiplier: 1.15`
  - Bridge giao tiếp: Qua `window.webkit.messageHandlers.localeChanged.postMessage(locale)` để host native đổi font texture atlas mà không ngắt frame loop của `MetalFramePacer.ts`.

### 6.3. Desktop (Standalone / Multi-window)
- Tự động nhận diện ngôn ngữ máy người dùng qua `navigator.language` khi chạy lần đầu nếu `localStorage` chưa có cấu hình.
- Đồng bộ đa cửa sổ (multi-instance / popout windows) thông qua sự kiện chuẩn của trình duyệt:
  ```javascript
  window.addEventListener('storage', (e) => {
    if (e.key === 'freeexile_locale' && e.newValue) {
      FreeExileI18n.setLocale(e.newValue);
    }
  });
  ```

---

## 7. THIẾT KẾ KIẾN TRÚC REACTIVE i18n EVENT BUS (RECOMMENDED ARCHITECTURE)

Nhằm giải quyết dứt điểm các tồn đọng kỹ thuật và đáp ứng chuẩn mực 2026, kiến trúc mới được thiết kế gồm 3 trụ cột:

### Trụ cột 1: Centralized Reactive i18n Manager & Event Bus
Nâng cấp `FreeExileI18nEngine` trong `client/webapp/js/data/i18n.js`:
1. **Pub/Sub Subscriber Pattern hoàn chỉnh**:
   - `subscribe(callback)`: Thêm callback `(newLocale, prevLocale, dict) => void` và trả về hàm hủy đăng ký `unsubscribe()`.
   - `setLocale(locale)`:
     - Cập nhật `this.currentLocale`.
     - Lưu `localStorage.setItem('freeexile_locale', locale)`.
     - Đồng bộ giá trị thẻ `#lang-select` (nếu có).
     - Thực thi `updateDOM(document)`.
     - Thực thi tất cả registered subscriber callbacks với try-catch an toàn.
     - Phát `CustomEvent('freeexile:localeChanged', { detail: { locale, prevLocale, dict } })`.
2. **Tự động bắt sự kiện Template Mounting**:
   - Lắng nghe `freeexile:templateMounted` -> tự động gọi `updateDOM(e.detail.element)`.
   - Lắng nghe `freeexile:templatesMounted` -> tự động gọi `updateDOM(document)`.
   - Loại bỏ đoạn code `click` hacky `setTimeout(..., 15)` hiện tại.
3. **Đồng bộ đa tab / cross-window**:
   - Lắng nghe `window.addEventListener('storage', ...)`.

### Trụ cột 2: Dynamic Subscriber Mechanism Cho Chat UI & UI Modules
Trong `client/webapp/js/ui/chat_ui.js`:
1. Đăng ký subscriber với Event Bus:
   ```javascript
   // Thay thế dòng 310 bằng event bus listener
   if (typeof window !== 'undefined') {
     window.addEventListener('freeexile:localeChanged', (e) => {
       updateLanguage(e.detail.locale);
     });
   }
   ```
2. Khởi tạo ngôn ngữ ngay trong `initChatUI()`:
   ```javascript
   const currentLocale = window.FreeExileI18n?.getLocale?.() || 'vi';
   updateLanguage(currentLocale);
   ```
3. Nâng cấp toàn diện hàm `updateLanguage(lang)`:
   - Dịch 8 tab kênh (`.chat-channel-tab`) lấy trực tiếp từ `I18N.t(`channel_${key}`)`.
   - Dịch ticker thu nhỏ (`#chat-ticker-channel`, `#chat-ticker-text`).
   - Cập nhật placeholder ô nhập (`#chat-input`) theo trạng thái quyền hạn được dịch động.
   - Cập nhật nút gửi (`#chat-send-btn`).
   - Cập nhật badge cooldown (`#chat-cooldown-badge`).
   - Re-render log tin nhắn hiện tại (`renderChatLog()`) để đồng bộ tag kênh và format thời gian theo locale.
   - Xóa bỏ từ điển thừa `CHANNEL_I18N` để đưa toàn bộ 8 channel keys vào `I18N_CATALOG`.

### Trụ cột 3: Chuẩn Hóa Key Từ Điển & Quy Chuẩn Đặt Tên (Naming Conventions)
Mọi key mới bổ sung vào `I18N_CATALOG` bắt buộc tuân thủ:
1. **Quy tắc tiền tố miền (Domain Prefixing)**:
   - `channel_[id]`: `channel_world`, `channel_zone`, `channel_guild`, `channel_party`, `channel_whisper`, `channel_system`, `channel_recruit`, `channel_feedback`.
   - `chat_[id]`: `chat_input_placeholder`, `chat_btn_send`, `chat_btn_collapse`, `chat_ticker_prompt`, `chat_badge_locked`, `chat_badge_wait`.
   - `chat_perm_[reason]`: `chat_perm_system_only`, `chat_perm_guild_required`, `chat_perm_party_required`, `chat_perm_level_world`, `chat_perm_level_recruit`.
   - `rarity_[id]`: `rarity_normal`, `rarity_magic`, `rarity_rare`, `rarity_unique`, `rarity_ancient`.
   - `element_[id]`: `element_metal`, `element_wood`, `element_water`, `element_fire`, `element_earth`.
   - `item_tooltip_[id]`: `item_tooltip_default_name`, `item_tooltip_hmac_verified`, `item_tooltip_hmac_unverified`, `item_tooltip_no_affixes`, `item_tooltip_crafter_unknown`.
2. **Quy chuẩn Microcopy**:
   - Nhãn nút bấm và tab: Tối đa 1-2 từ trên `vi`, `en`, `de`, `ru`, `es`, `ko`; tối đa 4 ký tự trên `zh`; 6 ký tự trên `ja`; 12 ký tự trên `th`.
   - Tuyệt đối không dùng dấu ngoặc đơn song ngữ dạng `Thế Giới (World)`.
3. **Tính Đầy Đủ 100% (9-Language Parity)**:
   - Bất kỳ key nào thêm vào `vi` bắt buộc phải có bản dịch tương ứng ở đủ 8 ngôn ngữ còn lại.

---

## 8. KẾ HOẠCH BẢO VỆ CHỐNG HỒI QUY (ANTI-REGRESSION & LINTING)

Để ngăn chặn lỗi tái phát trong các phiên phát triển tiếp theo:
1. **Linter phát hiện chuỗi Hardcoded (`tools/lint/check_i18n_hygiene.py`)**:
   - Quét mã nguồn trong `client/webapp/js/ui/`.
   - Phát hiện các chuỗi tiếng Việt có dấu hardcoded nằm ngoài phạm vi comment.
   - Chặn đứng commit nếu phát hiện chuỗi văn bản UI không thông qua `I18N.t()` hoặc `data-i18n`.
2. **Bộ Unit Test Kiểm Tra Đổi Ngôn Ngữ Tức Thì**:
   - `test_chat_ui_reactive_language_switch`: Mô phỏng gọi `FreeExileI18n.setLocale('en')` -> Kiểm tra các phần tử `#chat-send-btn`, `#chat-input`, tabs kênh, cooldown badge đổi sang English ngay trong cùng tick.
   - `test_i18n_catalog_nine_language_parity`: Kiểm tra toàn bộ key mới có mặt đủ trên 9 ngôn ngữ.
3. **Kịch bản E2E Test Runner**:
   - Tự động hóa qua Chrome DevTools MCP hoặc Playwright/Puppeteer: Click chọn ngôn ngữ trên HUD -> Chụp ảnh màn hình kiểm chứng Chat UI chuyển đổi mượt mà.

---

## 9. LỘ TRÌNH THỰC THI (IMPLEMENTATION ROADMAP)

| Giai đoạn | Nội dung công việc | Phụ trách đề xuất |
|---|---|---|
| **M1: Core Event Bus & Chat UI** | 1. Tách asset preloader khỏi `i18n.js` sang `asset_preloader.js`.<br>2. Bổ sung `subscribe()` và DOM template hooks vào `FreeExileI18n`.<br>3. Bổ sung các key Chat vào `i18n_catalog.js` đủ 9 ngôn ngữ.<br>4. Refactor `chat_ui.js` sang Reactive i18n, loại bỏ `CHANNEL_I18N` cô lập, xóa bỏ toàn bộ chuỗi hardcoded. | Lead Client Engineer |
| **M2: Anti-Regression Suite** | 1. Xây dựng công cụ kiểm tra tĩnh `tools/lint/check_i18n_hygiene.py`.<br>2. Bổ sung unit tests cho Reactive Chat UI vào `tests/unit/test_webapp_chat_ui.py`.<br>3. Kiểm chứng E2E language hot-swapping. | QA & Tooling Engineer |
| **M3: Chuẩn Hóa Tài Liệu** | 1. Cập nhật `AGENTS.md`, `GEMINI.md` với quy tắc bắt buộc dùng `I18N.t()` / `data-i18n` cho mọi UI mới.<br>2. Cập nhật `docs/standards/ENGINEERING_STANDARDS_2026.md` và `docs/standards/GLOBAL_LOCALIZATION_DICTIONARY.md`. | Lead Documentation Architect |
| **M4: Nghiệm Thu & Forensic Audit** | 1. Chạy toàn bộ test suite (894+ tests pass 100%).<br>2. Kiểm tra vệ sinh mã nguồn (hygiene check soft cap <= 350 dòng).<br>3. Forensic security & anti-cheat audit pass. | Studio Producer & CISO |
