Now.js Framework Documentation

Now.js Framework Documentation

I18nManager

TH 09 Nov 2026 03:28

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.json

en.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 ระดับนอกสุดเท่านั้น

หลักการทำงาน

  1. พบครั้งแรก: บันทึก innerHTML ดั้งเดิมเป็น template ไว้ใน data-i18n-tpl
  2. แปลภาษา: interpolate เฉพาะ direct text nodes ({LNG_...} → ค่าที่แปลแล้ว) child elements ไม่ถูกกระทบ
  3. เปลี่ยนภาษา: ใช้ 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/AppConfigManager
  • placeholder ที่มี {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');  // undefined

I18nManager.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)

เอกสารที่เกี่ยวข้อง