Skip to content

Internationalization (i18n)

TokUI splits visible text into two layers — keeping i18n responsibilities clean:

LayerContentOwnerHow to switch
L1 Component chromearia-labels, placeholders, empty states, default button text, pagination totals, date weekdays — the "skeleton" textTokUIsetLocale()
L2 Business textDSL attribute values like tt: / l: / tx: / opt: (card titles, form labels, option text)Your appBackend ships a different DSL per locale

Key point: TokUI does not translate DSL text. [btn tx:Submit] renders Submit verbatim. The framework only localizes the L1 skeleton — zh-CN + en-US built in, more injectable.

Switching language

1. At construction (recommended)

js
import { TokUI } from '@jboltai/tokui';
import '@jboltai/tokui/css';

const ui = new TokUI({
  container: '#app',
  locale: 'en-US',          // 'zh-CN' | 'en-US' | 'en' | 'zh' | ...
});

2. At runtime

js
import { setLocale, getLocale } from '@jboltai/tokui';

setLocale('en-US');         // aliases accepted: 'en' / 'en-GB' → 'en-US', 'zh' / 'zh-TW' → 'zh-CN'
getLocale();                // → 'en-US'

3. Auto-detection (default)

Without locale, TokUI probes document.documentElement.langnavigator.languagezh-CN. Setting <html lang="en"> is enough to flip all chrome to English.

Already-rendered DOM does not auto-refreshsetLocale only affects subsequent renders. For a live switch, call ui.rerender() (next section) — the library re-renders in place.

Wiring into an app (live switch)

To make already-rendered DOM follow a language switch, use the instance method rerender(). A TokUI instance auto-caches the DSL of its last render() / feed(), and rerender() clears the container and re-renders one-shot with the current locale/theme — no need for the app to cache DSL itself.

js
import { TokUI, setLocale } from '@jboltai/tokui';
import '@jboltai/tokui/css';

const ui = new TokUI({ container: '#app', locale: 'zh-CN' });

ui.render('[pagination total:5 count:42 show-total clk:noop]');

// Switch language + refresh in place (no network, no surrounding DOM touched)
setLocale('en-US');
ui.rerender();   // → 分页/共42条 becomes Pagination/42 items

Streaming works too — the instance accumulates fed chunks and rerender() replays the full content:

js
ui.startStream();
controller.on('chunk', c => ui.feed(c));   // _lastDsl accumulates
controller.on('end',  () => ui.endStream());

// After the stream ends, switch language
setLocale('en-US');
ui.rerender();

Persist across reloads with localStorage (restore on first visit):

js
const LANG_KEY = 'app-lang';
const ui = new TokUI({
  container: '#app',
  locale: localStorage.getItem(LANG_KEY) || 'zh-CN',   // apply last choice at startup
});

function switchLang(locale) {
  setLocale(locale);
  localStorage.setItem(LANG_KEY, locale);
  ui.rerender();
}

Multiple containers (e.g. chat, one instance per message): rerender() is per-instance — keep a reference to each and iterate:

js
const instances = [];   // push one ui per message
function switchLang(locale) {
  setLocale(locale);
  instances.forEach(ui => ui.rerender());
}

rerender() returns false when there is no cached content (never rendered) or no container. Calling it mid-stream re-renders from the DSL arrived so far — usually you call it after endStream().

Business text (L2, DSL values like tt: / l:) is not translated by rerender() — those are DSL literals. To switch business text, have the backend emit a different DSL per locale, or translate before feeding (next section).

Registering a new locale

Only zh-CN + en-US ship built-in. Inject others on demand; you may pass only the keys you need (uncovered keys fall back to zh-CN):

js
import { registerLocale, setLocale } from '@jboltai/tokui';

registerLocale('ja-JP', {
  'common.close': '閉じる',
  'common.ok': 'OK',
  'pagination.aria': 'ページネーション',
  'pagination.totalCount': '全{count}件',
  'chart.empty': 'データなし',
  // ... keys not listed fall back to zh-CN
});

setLocale('ja-JP');

registerLocale merges incrementally: repeated calls for the same locale accumulate and never overwrite keys you didn't pass.

Built-in catalog (L1)

About 80 keys, named by component.position semantics. Full list in src/core/i18n.js (STRINGS). Common groups:

GroupExample key中文English
common.*common.close / common.ok / common.loading关闭 / 确定 / 加载中Close / OK / Loading
pagination.*pagination.totalCount共{count}条{count} items
lightbox.*lightbox.zoomIn / lightbox.rotateLeft放大 / 左旋90°Zoom in / Rotate 90° left
chart.*chart.empty / chart.seriesDefault暂无数据 / 系列No data / Series
datepicker.*datepicker.title / datepicker.weekday.1{y}年{m}月 / 一{m}/{y} / Mon
status.*status.running / status.done运行中 / 完成Running / Done
command.*command.placeholder / command.noResult输入关键词搜索... / 没有找到匹配结果Type to search... / No results found
bubble.*bubble.you / bubble.ai / bubble.system / bubble.assistant你 / AI / 系统 / 助手You / AI / System / Assistant

Interpolation uses {name} placeholders, e.g. t('pagination.totalCount', { count: 42 })共42条 / 42 items.

Localizing business text (L2)

Business text lives in the DSL and is generated by your backend. Three common patterns:

1. Backend dispatches per locale (recommended)

js
// Backend picks DSL by Accept-Language or user setting
const dsl = locale === 'en'
  ? '[card tt:"Order Detail"][p Total: ¥128][/card]'
  : '[card tt:"订单详情"][p 合计: ¥128][/card]';
ui.render(dsl);

2. Frontend pre-translation

Keep a business dictionary and replace keys before feeding:

js
const BIZ = {
  'en': { '订单详情': 'Order Detail', '合计': 'Total' },
  'ja': { '订单详情': '注文詳細', '合计': '合計' },
};
function translate(dsl, locale) {
  const dict = BIZ[locale] || {};
  return Object.keys(dict).reduce((s, k) => s.split(k).join(dict[k]), dsl);
}
ui.feed(translate(chunk, getLocale()));

3. Locale-neutral keys from backend + frontend translation (advanced; needs a key convention)

TokUI imposes none of these — the DSL is data, and your architecture decides the translation strategy.

Performance

t() is a single object property lookup plus an optional {name} replace; setLocale only swaps the internal dict reference (O(1)), and the monomorphic lookup is engine-inlinable. Versus hardcoded literals, the render-time cost is unmeasurable.

Next