Now.js Framework Documentation
I18nManager
I18nManager
ภาพรวม
I18nManager คือระบบ internationalization (i18n) ใน Now.js Framework รองรับ multi-language translations และ locale management
ใช้เมื่อ:
- ต้องการ multi-language support
- ต้องการเปลี่ยนภาษาแบบ dynamic
- ต้องการ translate UI elements
- ต้องการ locale-aware formatting
ทำไมต้องใช้:
- ✅ File-based translations (JSON)
- ✅ Dynamic locale switching
- ✅ HTML attribute support (
data-i18n) - ✅ Parameter interpolation
- ✅ Fallback locale
- ✅ Browser locale detection
- ✅ LocalStorage persistence
การใช้งานพื้นฐาน
Initialization
await I18nManager.init({
enabled: true,
defaultLocale: 'en',
availableLocales: ['en', 'th', 'ja'],
useBrowserLocale: true
});HTML Declarative
<!-- Simple translation -->
<h1 data-i18n="welcome">Welcome</h1>
<!-- Attribute translation -->
<input data-i18n-placeholder="searchPlaceholder" placeholder="Search...">
<button data-i18n-title="saveTooltip" title="Save">💾</button>JavaScript API
// Translate
const text = I18nManager.translate('welcome');
// => 'ยินดีต้อนรับ' (if locale is 'th')
// With parameters
const greeting = I18nManager.translate('greeting', { name: 'John' });
// => 'สวัสดี John!' (if locale is 'th')
// Change locale
await I18nManager.setLocale('th');Translation Files
สร้างไฟล์ JSON ใน /translations/:
/translations/
en.json
th.json
ja.jsonen.json
{
"welcome": "Welcome",
"greeting": "Hello, {name}!",
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"buttons": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
}
}th.json
{
"welcome": "ยินดีต้อนรับ",
"greeting": "สวัสดี {name}!",
"nav": {
"home": "หน้าแรก",
"about": "เกี่ยวกับ",
"contact": "ติดต่อ"
},
"buttons": {
"save": "บันทึก",
"cancel": "ยกเลิก",
"delete": "ลบ"
}
}Data Attributes
| Attribute | คำอธิบาย |
|---|---|
data-i18n |
Translation key สำหรับ text content |
data-i18n-tpl |
(สร้างอัตโนมัติ) เก็บ innerHTML template สำหรับ smart mode |
data-i18n-placeholder data-i18n-title data-i18n-alt data-i18n-aria-label data-i18n-aria-placeholder data-i18n-label |
ต้นฉบับของแอตทริบิวต์นั้น: translation key หรือข้อความที่มี {LNG_...} แปลใหม่ทุกครั้งที่เปลี่ยนภาษา |
<input type="text"
data-i18n-placeholder="search.placeholder"
data-i18n-aria-label="search.ariaLabel">
<img data-i18n-alt="images.logo.alt" src="/logo.png">แอตทริบิวต์ที่เขียนด้วย marker ไม่ต้องใส่ data-i18n-* เอง: ครั้งแรกที่แปล
title="{LNG_Toggle theme}" ระบบเก็บ marker ไว้ใน data-i18n-title ให้ tooltip
เปลี่ยนตามภาษาทุกครั้งหลังจากนั้น
- สคริปต์ที่ตั้งแอตทริบิวต์เป็นข้อความของตัวเองภายหลังเป็นเจ้าของแอตทริบิวต์นั้นต่อไป:
data-i18n-<attribute>ถูกลบและข้อความไม่ถูกแตะอีก - เขียน marker
{LNG_...}ใหม่ลงแอตทริบิวต์ = ใช้แทนต้นฉบับเดิม - ภายใน
translate="no"ไม่มีอะไรถูกแปลหรือถูกเก็บ
Smart Translation (รักษา Child Elements)
เมื่อ element ที่มี data-i18n มี child HTML elements อยู่ภายใน (เช่น <em>, <a>, <span>, <input>) ระบบแปลภาษาจะใช้ smart mode — รักษา child elements ทั้งหมดไว้ และแปลเฉพาะ text nodes ระดับนอกสุดเท่านั้น
หลักการทำงาน
- พบครั้งแรก: บันทึก
innerHTMLดั้งเดิมเป็น template ไว้ในdata-i18n-tpl - แปลภาษา: interpolate เฉพาะ direct text nodes (
{LNG_...}→ ค่าที่แปลแล้ว) child elements ไม่ถูกกระทบ - เปลี่ยนภาษา: ใช้ template ที่บันทึกไว้ซ้ำ ทำให้แปลใหม่ได้สะอาดทุกครั้ง
ตัวอย่างพื้นฐาน
<!-- Smart mode: tag <em> ถูกรักษาไว้ -->
<div class="comment" data-i18n>
{LNG_Facebook profile username eg} https://www.facebook.com/<em>username</em>
</div>
<!-- หลังแปลภาษา (locale: th) -->
<div class="comment" data-i18n data-i18n-tpl="...original...">
ชื่อผู้ใช้ Facebook เช่น https://www.facebook.com/<em>username</em>
</div>แปล Child Elements แยกแต่ละตัว
โดยปกติ child elements จะ ถูกข้าม ถ้าต้องการแปล child element ให้เพิ่ม data-i18n บน child นั้น:
<div class="info" data-i18n>
{LNG_Select file type}: <strong data-i18n>{LNG_PDF Document}</strong>
</div>ผลลัพธ์: ทั้ง text ด้านนอกและเนื้อหาใน <strong> ถูกแปลแยกอิสระจากกัน
ทำงานร่วมกับ data-text, data-attr
Child elements ที่ใช้ directive อย่าง data-text หรือ data-attr จะถูกรักษาไว้ใน template และทำงานแยกอิสระ:
<label data-i18n>
{LNG_Allowed IPs}:
<textarea name="api_ips" data-text="api_ips" placeholder="{LNG_All IP addresses}"></textarea>
</label>data-i18nบน<label>แปล{LNG_Allowed IPs}:data-textบน<textarea>จัดการโดย FormManager/AppConfigManagerplaceholderที่มี{LNG_...}จัดการโดยtranslateAttributesIn()- ไม่มีความขัดแย้งระหว่าง managers
Simple Mode (เข้ากันได้ย้อนหลัง)
เมื่อ element ไม่มี child elements จะใช้วิธี textContent แบบเดิม — เข้ากันได้ย้อนหลังทั้งหมด:
<!-- Simple mode: ไม่มี child elements -->
<h1 data-i18n>{LNG_Welcome}</h1>
<button data-i18n="buttons.save">Save</button>การตั้งค่า
await I18nManager.init({
// Enable i18n
enabled: true,
// Default locale
defaultLocale: 'en',
// Available locales
availableLocales: ['en', 'th', 'ja', 'zh'],
// Storage key for persisting locale
storageKey: 'app_lang',
// คุกกี้ที่เก็บภาษาด้วย อ่านก่อน localStorage (null = เก็บใน localStorage อย่างเดียว)
cookieName: 'my_lang',
// Use browser's locale if available
useBrowserLocale: true,
// Skip English lookup (use key as fallback)
noTranslateEnglish: true,
// แปลเนื้อหาใหม่ก่อนเบราว์เซอร์วาด (ค่าเริ่มต้น true)
translateBeforePaint: true
});คุกกี้ภาษา (cookieName)
setLocale() เขียนภาษาที่เลือกลงคุกกี้ด้วย นอกจาก localStorage และตอนเริ่มอ่านคุกกี้ก่อน ค่าเริ่มต้น my_lang คือคุกกี้ที่ Language ของ Kotchasan ใช้เลือกแฟ้มภาษา ทุกอย่างที่เซิร์ฟเวอร์สร้าง — ข้อความจาก API, เนื้อหา modal ที่ประกอบฝั่งเซิร์ฟเวอร์, อีเมลที่ส่งระหว่าง request นั้น — จึงเป็นภาษาเดียวกับที่เลือกบนหน้าเว็บ รวมถึงหลังรีเฟรช
- ลำดับตอนเริ่ม: คุกกี้ → localStorage →
<html lang>→defaultLocaleคุกกี้ที่เซิร์ฟเวอร์ตั้ง (เช่นจาก?lang=) จึงชนะค่าเก่าใน localStorage - เขียนคุกกี้ด้วย
path=/,max-ageหนึ่งปี และSameSite=Laxและเขียนซ้ำทุกครั้งที่เริ่ม เพื่อให้ตรงกับหน้าเว็บเสมอ cookieName: nullเก็บภาษาใน localStorage อย่างเดียว เซิร์ฟเวอร์จะใช้ภาษาเริ่มต้นของตัวเองต่อไป
await Now.init({
i18n: {enabled: true, availableLocales: ['en', 'th'], cookieName: 'my_lang'}
});แปลก่อนแสดงผล (translateBeforePaint)
เมื่อเปิด (ค่าเริ่มต้น) เนื้อหาที่ถูกเพิ่มลงหน้า — เทมเพลตของ route, modal, แถวที่ data-for สร้าง — ถูกแปลก่อนเบราว์เซอร์วาดเฟรมถัดไป ผู้ใช้จึงไม่เห็น {LNG_...} หรือคีย์ภาษาอังกฤษแวบขึ้นมาก่อนกลายเป็นคำแปล
- DOM observer แปลโหนดที่เพิ่มเข้ามาทันทีใน callback ของ MutationObserver (ทำงานเป็น microtask ก่อนการวาดเสมอ) โหนดทั้งหมดที่เพิ่มในรอบเดียวกันยังถูกแปลรวมเป็นชุดเดียว
setLocale()แปลทั้งหน้าก่อน resolve (ไม่รอ 100 ms)TemplateManager.scheduleI18nUpdate()(ใช้หลังdata-for/data-html) รวบงานเป็น microtask แทนการรอ 50 ms
ปิดได้ทั้งตอนเริ่มและระหว่างทำงาน — เมื่อปิดจะกลับไปแบบเดิม คือรอให้หน้านิ่ง 50 ms แล้วค่อยแปล และ setLocale() แปลทั้งหน้าหลัง 100 ms
// ตอนเริ่ม
await Now.init({
i18n: {enabled: true, availableLocales: ['en', 'th'], translateBeforePaint: false}
});
// ระหว่างทำงาน
I18nManager.config.translateBeforePaint = false;การเปลี่ยนแปลง DOM ที่เกิดจากการแปลเอง (ข้อความ และ innerHTML ของ Smart mode) ถูกทิ้งไม่ส่งกลับเข้า observer —
data-i18nแบบ Smart mode ที่ซ้อนกันจึงแปลครั้งเดียวแล้วจบ ไม่เขียนลูกของตัวเองใหม่วนไปเรื่อย ๆ
API อ้างอิง
I18nManager.setLocale(locale, force?)
เปลี่ยนภาษา
| Parameter | Type | Description |
|---|---|---|
locale |
string | Locale code (e.g., 'th', 'en') |
force |
boolean | Force reload translations |
Returns: Promise<void>
await I18nManager.setLocale('th');I18nManager.getCurrentLocale()
รับ locale ปัจจุบัน
Returns: string
const locale = I18nManager.getCurrentLocale();
// 'th'I18nManager.translate(key, params?, locale?)
แปลข้อความ
| Parameter | Type | Description |
|---|---|---|
key |
string | Translation key (dot notation supported) |
params |
object | Interpolation parameters |
locale |
string | Specific locale (optional) |
Returns: string
// Simple
I18nManager.translate('welcome');
// Nested key
I18nManager.translate('nav.home');
// With params
I18nManager.translate('greeting', { name: 'John' });
// Specific locale
I18nManager.translate('welcome', {}, 'ja');กฎของปีกกา (ใช้กับ translate(), interpolate(), DOM observer และ data-i18n เหมือนกัน):
{LNG_xxx}คือ marker ของระบบแปล — ตัวเดียวที่ถูกค้นหาในข้อความ key ที่ไม่มีใน catalog แสดงชื่อตัวเอง{name}ถูกแทนก็ต่อเมื่อ caller ส่งparams.nameมา ค่าที่ส่งถูกใส่ตามตัวอักษร ไม่ถูกแปลซ้ำ{...}อื่นทั้งหมดคงไว้ตามที่เขียน —{"a":1},{{ expr }},{Guest}ผ่านออกมาเหมือนเดิม
(เดิม pass นี้ค้น catalog ด้วยและตัดปีกกาทิ้งเมื่อไม่พบ ข้อมูลที่มีปีกกาจึงเพี้ยน)
I18nManager.translate('Selected {count} rows', {count: 3}); // 'เลือก 3 รายการ'
I18nManager.translate('Selected {count} rows'); // 'เลือก {count} รายการ' — ลืมส่ง param เห็นทันที
I18nManager.interpolate('{LNG_Guest}: {"a":1}'); // 'ผู้มาเยือน: {"a":1}'I18nManager.updateElements(container?)
อัพเดท translations ใน DOM
| Parameter | Type | Description |
|---|---|---|
container |
HTMLElement | Container to update (default: document) |
// Update all elements
I18nManager.updateElements();
// Update specific container
I18nManager.updateElements(document.getElementById('modal'));I18nManager.hasTranslation(key, locale?)
ตรวจสอบว่ามี translation หรือไม่
| Parameter | Type | Description |
|---|---|---|
key |
string | Translation key |
locale |
string | Specific locale (optional) |
Returns: boolean
I18nManager.lookupEntry(translations, key, literal?)
อ่านค่าหนึ่งค่าจาก translation map โดยดูเฉพาะค่าที่ map มีเอง ชื่อที่ทุก object สืบทอดมา (constructor, toString, __proto__) ไม่นับเป็น key จึงได้ undefined ทั้ง translate(), interpolate() และ hasTranslation() ค้น key ผ่านเมธอดนี้
| Parameter | Type | Description |
|---|---|---|
translations |
object | map ที่จะค้น เช่น getTranslations() |
key |
string | key ถ้าไม่มีช่องว่างจะถือเป็น path แบบจุด (section.key) เว้นแต่มี key นั้นตรงตัวอยู่แล้ว (No. · Loading...) · ต้องตรงทุกตัวอักษร (save หรือ SAVE ไม่ใช่ Save — แบบเดียวกับ Kotchasan\Language ฝั่งเซิร์ฟเวอร์) |
literal |
boolean | ค้น key ตามตัว ไม่แยกตามจุด (ค่าเริ่มต้น false) |
Returns: ค่าที่พบ (ข้อความ ตัวเลข list หรือ section) หรือ undefined
const map = I18nManager.getTranslations();
I18nManager.lookupEntry(map, 'Save'); // 'บันทึก'
I18nManager.lookupEntry(map, 'save'); // undefined — ตัวพิมพ์ต่างคือคนละ key
I18nManager.lookupEntry(map, 'MONTH_SHORT'); // {1: 'ม.ค.', ...}
I18nManager.lookupEntry(map, 'constructor'); // undefinedI18nManager.getTranslations(locale?)
รับ translations ทั้งหมด
Returns: object
เหตุการณ์
| Event | เมื่อเกิด | Detail |
|---|---|---|
i18n:initialized |
init() เสร็จ | ไม่มี |
locale:changed |
ภาษาเปลี่ยน | {locale, forced} |
i18n:loaded |
โหลดไฟล์แปลเสร็จ | {locale, success, reason} |
i18n:updated |
แปลข้อความในหน้าใหม่ทั้งหมด | {locale, elementsUpdated} |
i18n:elements:updated |
แปลข้อความเฉพาะบาง container | {container, elementsUpdated, locale} |
EventManager.on('locale:changed', (data) => {
console.log(`Language changed to: ${data.locale}`);
});ตัวอย่างการใช้งานจริง
Language Switcher
<select id="language-select">
<option value="en">English</option>
<option value="th">ไทย</option>
<option value="ja">日本語</option>
</select>
<script>
const select = document.getElementById('language-select');
// Set current value
select.value = I18nManager.getCurrentLocale();
// Handle change
select.addEventListener('change', async (e) => {
await I18nManager.setLocale(e.target.value);
});
</script>Dynamic Content Translation
// After loading dynamic content
async function loadProducts() {
const products = await ApiService.get('/api/products');
const container = document.getElementById('products');
container.innerHTML = products.map(p => `
<div class="product">
<h3>${p.name}</h3>
<button data-i18n="buttons.addToCart">Add to Cart</button>
</div>
`).join('');
// Update translations for new elements
I18nManager.updateElements(container);
}Pluralization
// th.json
{
"items": {
"zero": "ไม่มีรายการ",
"one": "1 รายการ",
"other": "{count} รายการ"
}
}function translatePlural(key, count) {
const pluralKey = count === 0 ? 'zero' : count === 1 ? 'one' : 'other';
return I18nManager.translate(`${key}.${pluralKey}`, { count });
}
translatePlural('items', 0); // 'ไม่มีรายการ'
translatePlural('items', 1); // '1 รายการ'
translatePlural('items', 5); // '5 รายการ'ข้อควรระวัง
⚠️ 1. รอ Translations โหลดก่อน
// ❌ ใช้ก่อน init
I18nManager.translate('key');
// ✅ รอ init
await I18nManager.init({ enabled: true });
I18nManager.translate('key');⚠️ 2. อัพเดท Dynamic Content
เนื้อหาที่ เพิ่ม ลงหน้า (innerHTML, appendChild) ถูก DOM observer แปลให้เอง ไม่ต้องเรียกอะไรเพิ่ม
แต่ถ้า เปลี่ยนข้อความของ element ที่อยู่ในหน้าแล้ว ให้เป็นคีย์ใหม่ จะไม่มีโหนดใหม่ให้ observer เห็น ต้องเรียก updateElements เอง
// ✅ เพิ่มเนื้อหาใหม่ — แปลอัตโนมัติ
container.innerHTML = '<span data-i18n>Save</span>';
// ❌ เปลี่ยนคีย์ของ element เดิม — observer ไม่เห็น
label.setAttribute('data-i18n', 'Cancel');
// ✅ เรียก updateElements
label.setAttribute('data-i18n', 'Cancel');
I18nManager.updateElements(label.parentElement);⚠️ 3. Fallback Keys
// หาก key ไม่มี จะใช้ key เป็น fallback
I18nManager.translate('missing.key');
// => 'missing.key'
// เพิ่ม default locale fallback
// en.json ควรมีครบทุก key⚠️ 4. Smart Mode ใช้ innerHTML Template
<!-- ✅ ใช้ pattern {LNG_...} สำหรับข้อความที่ต้องการแปล -->
<div data-i18n>
{LNG_Description}: <em>example</em>
</div>
<!-- ❌ อย่าแก้ไข innerHTML หลังจากแปลครั้งแรก
(template ที่ cache ไว้จะไม่อัปเดต) -->
<script>
// การทำแบบนี้จะทำให้ template cache เสีย
element.innerHTML = 'new content <em>here</em>';
// ✅ ถ้าต้องการอัปเดต ให้ trigger การแปลใหม่:
I18nManager.updateElements(element.parentElement);
</script>⚠️ 5. ข้อมูลของผู้ใช้ที่มี {LNG_...} — ใช้ translate="no"
DOM observer ของ I18nManager แปลทุก text node (และ attribute placeholder/title/alt/aria-label)
ที่มีรูปแบบ {LNG_...} รวมถึงค่าที่มาจากข้อมูล เช่น ชื่อเอกสารที่ผู้ใช้ตั้งว่า {LNG_Documents}
จะถูกแสดงเป็น "เอกสาร" — ปลอมข้อความบนหน้าจอได้
ใส่ attribute มาตรฐาน HTML translate="no" ที่ element ใดก็ได้ แล้วทุกอย่างข้างในจะไม่ถูกแปล
(text node, attribute, data-i18n และการแปล {LNG_...} ของ data-text / data-attr ใน TemplateManager)
<!-- ค่าจากผู้ใช้: แสดงตามที่เก็บ -->
<dd translate="no" data-text="title"></dd>
<ul translate="no" data-for="item in comments"><template><li data-text="item.body"></li></template></ul>
<!-- ป้ายของหน้าจอ: แปลตามปกติ -->
<label data-i18n>{LNG_Title}</label>TableManager ใส่ translate="no" ให้เซลล์ที่แสดงค่าจากแถวโดยตรงและเซลล์ data-template
(หลังแปล token ของ template แล้ว) โดยอัตโนมัติ — formatter ที่เขียนเองต้องใส่เองเมื่อแสดงข้อมูลผู้ใช้
ตรวจสอบได้ด้วย I18nManager.isExcluded(node)
เอกสารที่เกี่ยวข้อง
- Utils.date - Date formatting with locale
- TemplateManager - Template rendering