Now.js Framework Documentation
I18nManager
I18nManager
Overview
I18nManager is the internationalization (i18n) system in Now.js Framework. It supports multi-language translations and locale management.
When to use:
- Need multi-language support
- Need dynamic language switching
- Need to translate UI elements
- Need locale-aware formatting
Why use it:
- ✅ File-based translations (JSON)
- ✅ Dynamic locale switching
- ✅ HTML attribute support (
data-i18n) - ✅ Parameter interpolation
- ✅ Fallback locale
- ✅ Browser locale detection
- ✅ LocalStorage persistence
Basic Usage
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');
// => 'Welcome' (if locale is 'en')
// With parameters
const greeting = I18nManager.translate('greeting', { name: 'John' });
// => 'Hello, John!' (if locale is 'en')
// Change locale
await I18nManager.setLocale('th');Translation Files
Create JSON files in /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 | Description |
|---|---|
data-i18n |
Translation key for text content |
data-i18n-tpl |
(Auto-generated) Saved innerHTML template for smart mode |
data-i18n-placeholder data-i18n-title data-i18n-alt data-i18n-aria-label data-i18n-aria-placeholder data-i18n-label |
Source of that attribute: a translation key, or text with {LNG_...} markers. Translated again on every locale change |
<input type="text"
data-i18n-placeholder="search.placeholder"
data-i18n-aria-label="search.ariaLabel">
<img data-i18n-alt="images.logo.alt" src="/logo.png">An attribute written with a marker needs no data-i18n-* of its own: the first
time title="{LNG_Toggle theme}" is translated, the marker is stored in
data-i18n-title, so the tooltip follows every later locale change.
- A script that later sets the attribute to text of its own owns it from then on:
data-i18n-<attribute>is removed and the text is left alone - Writing a new
{LNG_...}marker into the attribute replaces the stored source - Inside
translate="no"nothing is translated or stored
Smart Translation (Preserving Child Elements)
When a data-i18n element contains child HTML elements (e.g. <em>, <a>, <span>, <input>), the translation system uses smart mode — it preserves all child elements and only translates the outermost text nodes.
How It Works
- First encounter: The original
innerHTMLis saved as a template indata-i18n-tpl - Translation: Only the direct text nodes are interpolated (
{LNG_...}→ translated value). Child elements remain intact - Locale switch: The saved template is re-used, ensuring clean re-translation every time
Basic Example
<!-- Smart mode: <em> tag is preserved -->
<div class="comment" data-i18n>
{LNG_Facebook profile username eg} https://www.facebook.com/<em>username</em>
</div>
<!-- After translation (locale: th) -->
<div class="comment" data-i18n data-i18n-tpl="...original...">
ชื่อผู้ใช้ Facebook เช่น https://www.facebook.com/<em>username</em>
</div>Translating Child Elements Individually
By default, child elements are skipped. To translate a child element, add data-i18n to it:
<div class="info" data-i18n>
{LNG_Select file type}: <strong data-i18n>{LNG_PDF Document}</strong>
</div>Result: Both the outer text and the <strong> content are translated independently.
Coexistence with data-text, data-attr
Child elements using directives like data-text or data-attr are preserved in the template and work independently:
<label data-i18n>
{LNG_Allowed IPs}:
<textarea name="api_ips" data-text="api_ips" placeholder="{LNG_All IP addresses}"></textarea>
</label>data-i18non the<label>translates{LNG_Allowed IPs}:data-texton the<textarea>is handled by FormManager/AppConfigManagerplaceholderwith{LNG_...}is handled bytranslateAttributesIn()- No conflicts between managers
Simple Mode (Backward Compatible)
When an element has no child elements, the original textContent-based approach is used — fully backward compatible:
<!-- Simple mode: no child elements -->
<h1 data-i18n>{LNG_Welcome}</h1>
<button data-i18n="buttons.save">Save</button>Configuration
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',
// Cookie that also holds the locale, read first (null = localStorage only)
cookieName: 'my_lang',
// Use browser's locale if available
useBrowserLocale: true,
// Skip English lookup (use key as fallback)
noTranslateEnglish: true,
// Translate new content before the browser paints it (default true)
translateBeforePaint: true
});Locale cookie (cookieName)
setLocale() writes the chosen locale to a cookie as well as to localStorage, and start-up reads the cookie first. The default my_lang is the cookie Kotchasan's Language picks its language file from, so everything the server renders — API responses, server-built modal content, email sent during the request — follows the language picked on the page, including after a reload.
- Start-up order: cookie → localStorage →
<html lang>→defaultLocale. A cookie the server set (for example from?lang=) therefore wins over an older choice in localStorage. - The cookie is written with
path=/,max-ageof one year andSameSite=Lax, and is rewritten on every start-up so it stays in step with the page. cookieName: nullkeeps the choice in localStorage only; the server then keeps its own default language.
await Now.init({
i18n: {enabled: true, availableLocales: ['en', 'th'], cookieName: 'my_lang'}
});Translate before paint (translateBeforePaint)
When on (the default), content added to the page — a route template, a modal, rows rendered by data-for — is translated before the browser paints the next frame, so {LNG_...} markers and English keys never flash up before the translation.
- The DOM observer translates added nodes inside its MutationObserver callback, which runs as a microtask and therefore always before the paint; every node added during one task is still translated as one batch.
setLocale()re-translates the page before it resolves (instead of 100 ms later).TemplateManager.scheduleI18nUpdate()(used afterdata-for/data-html) batches into a microtask instead of waiting 50 ms.
It can be turned off at start-up or at run time. Off restores the earlier behaviour: new content is translated 50 ms after the page goes quiet, and setLocale() re-translates the page 100 ms later.
// At start-up
await Now.init({
i18n: {enabled: true, availableLocales: ['en', 'th'], translateBeforePaint: false}
});
// At run time
I18nManager.config.translateBeforePaint = false;The DOM changes made by the translation itself (text, and the innerHTML of Smart mode) are not fed back into the observer, so a Smart-mode
data-i18nnested in another one is translated once and settles instead of rewriting its children over and over.
API Reference
I18nManager.setLocale(locale, force?)
Change language
| 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()
Get current locale
Returns: string
const locale = I18nManager.getCurrentLocale();
// 'en'I18nManager.translate(key, params?, locale?)
Translate text
| 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');Brace rules (the same for translate(), interpolate(), the DOM observer and data-i18n):
{LNG_xxx}is the translation marker — the only thing searched for in a string. A key missing from the catalog shows its own name.{name}is replaced only when the caller passedparams.name. The value is inserted verbatim and not translated again.- Every other
{...}is left as written —{"a":1},{{ expr }},{Guest}come out unchanged.
(This pass used to look the catalog up as well and drop the braces when nothing matched, which mangled any data carrying braces.)
I18nManager.translate('Selected {count} rows', {count: 3}); // 'Selected 3 rows'
I18nManager.translate('Selected {count} rows'); // 'Selected {count} rows' — a missing param is visible
I18nManager.interpolate('{LNG_Guest}: {"a":1}'); // 'Guest: {"a":1}' (or the catalog value)I18nManager.updateElements(container?)
Update translations in 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?)
Check if translation exists
| Parameter | Type | Description |
|---|---|---|
key |
string | Translation key |
locale |
string | Specific locale (optional) |
Returns: boolean
I18nManager.lookupEntry(translations, key, literal?)
Read one entry of a translation map, using the map's own entries only. Keys match exactly, character for character (save and SAVE are not Save), the same way Kotchasan\Language reads them on the server. Names that every object inherits (constructor, toString, __proto__) are not keys, so they come back undefined. translate(), interpolate() and hasTranslation() all look keys up through this method.
| Parameter | Type | Description |
|---|---|---|
translations |
object | Map to look in, e.g. getTranslations() |
key |
string | Key; without a space it is a dotted path (section.key) unless the map has the key as-is (No., Loading...) |
literal |
boolean | Look the key up as-is, never as a dotted path (default false) |
Returns: the entry (text, number, list or section), or undefined
const map = I18nManager.getTranslations();
I18nManager.lookupEntry(map, 'Save'); // 'บันทึก'
I18nManager.lookupEntry(map, 'save'); // undefined — another case is another key
I18nManager.lookupEntry(map, 'MONTH_SHORT'); // {1: 'ม.ค.', ...}
I18nManager.lookupEntry(map, 'constructor'); // undefinedI18nManager.getTranslations(locale?)
Get all translations
Returns: object
Events
| Event | When Triggered | Detail |
|---|---|---|
i18n:initialized |
init() completed | none |
locale:changed |
The locale changed | {locale, forced} |
i18n:loaded |
A translation file finished loading | {locale, success, reason} |
i18n:updated |
The whole page was retranslated | {locale, elementsUpdated} |
i18n:elements:updated |
One container was retranslated | {container, elementsUpdated, locale} |
EventManager.on('locale:changed', (data) => {
console.log(`Language changed to: ${data.locale}`);
});Real-World Examples
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
// en.json
{
"items": {
"zero": "No items",
"one": "1 item",
"other": "{count} items"
}
}function translatePlural(key, count) {
const pluralKey = count === 0 ? 'zero' : count === 1 ? 'one' : 'other';
return I18nManager.translate(`${key}.${pluralKey}`, { count });
}
translatePlural('items', 0); // 'No items'
translatePlural('items', 1); // '1 item'
translatePlural('items', 5); // '5 items'Common Pitfalls
⚠️ 1. Wait for Translations to Load
// ❌ Using before init
I18nManager.translate('key');
// ✅ Wait for init
await I18nManager.init({ enabled: true });
I18nManager.translate('key');⚠️ 2. Update Dynamic Content
Content added to the page (innerHTML, appendChild) is translated by the DOM observer — nothing to call.
Changing the text of an element that is already on the page to a new key adds no node for the observer to see, so call updateElements yourself.
// ✅ New content — translated automatically
container.innerHTML = '<span data-i18n>Save</span>';
// ❌ New key on an existing element — the observer does not see it
label.setAttribute('data-i18n', 'Cancel');
// ✅ Call updateElements
label.setAttribute('data-i18n', 'Cancel');
I18nManager.updateElements(label.parentElement);⚠️ 3. Fallback Keys
// If key not found, key is used as fallback
I18nManager.translate('missing.key');
// => 'missing.key'
// Add default locale fallback
// en.json should have all keys⚠️ 4. Smart Mode Uses innerHTML Template
<!-- ✅ Use {LNG_...} patterns for text that needs translation -->
<div data-i18n>
{LNG_Description}: <em>example</em>
</div>
<!-- ❌ Don't modify innerHTML after first translation
(the cached template won't update) -->
<script>
// This will break the template cache
element.innerHTML = 'new content <em>here</em>';
// ✅ If you need to update, re-trigger translation:
I18nManager.updateElements(element.parentElement);
</script>⚠️ 5. User data containing {LNG_...} — use translate="no"
I18nManager's DOM observer translates every text node (and the placeholder/title/alt/aria-label
attributes) that contains a {LNG_...} pattern — including values that arrived as data. A document
whose user-given title is {LNG_Documents} would be shown as "Documents": anyone could put words on the
screen the author never wrote.
Put the standard HTML attribute translate="no" on any element and nothing beneath it is translated
(text nodes, attributes, data-i18n, and the {LNG_...} pass of data-text / data-attr in TemplateManager).
<!-- user data: shown exactly as stored -->
<dd translate="no" data-text="title"></dd>
<ul translate="no" data-for="item in comments"><template><li data-text="item.body"></li></template></ul>
<!-- UI labels: translated as usual -->
<label data-i18n>{LNG_Title}</label>TableManager marks plain data cells and data-template cells (once the template's own tokens are
resolved) automatically; a custom formatter must set it itself when it renders user data.
I18nManager.isExcluded(node) tells whether a node is inside such an element.
Related Documentation
- Utils.date - Date formatting with locale
- TemplateManager - Template rendering