Internationalization
Plugin localization happens in two independent layers, each with its own scope:
- Manifest strings (
name,description, AI-tooldescription) are translated by the host from an optionali18nblock inmanifest.json. These power the plugin marketplace cards, the tool grid, the detail drawer, and the Agent tool palette — every surface the host renders. See Manifest string localization below. - Plugin UI strings (everything inside the iframe) are the plugin's responsibility. The host pushes the active locale via the SDK; the plugin reads it and translates with its own bundled message table. See Plugin UI localization below.
The host owns the locale either way; a plugin never ships its own language switcher.
Reading the locale
FengYuClient.ready() resolves with an Environment whose locale field holds the active locale tag (e.g. en, zh-CN):
import { fengyu } from './sdk.js'
const env = await fengyu.ready()
console.log(env.locale) // → 'zh-CN'ready() also applies the locale to the document — document.documentElement.lang is set to env.locale — so any locale-aware CSS or attribute selector works without extra wiring.
Responding to changes
If the user switches language while the plugin is open, the host emits an environment event. Subscribe with client.on('environment', handler):
const off = fengyu.on('environment', (env) => {
applyLocale(env.locale)
})
// ...later, on teardown
off()The handler receives the full updated Environment, so re-read env.locale (and env.theme) and re-render.
Do not subscribe after the handshake
For Vue plugins, prefer mountFengYuApp; it establishes the environment subscription before it waits for the ready handshake. If you write a custom bootstrap, preserve that ordering:
const off = client.on('environment', applyEnvironment)
const initial = await client.ready()
applyEnvironment(initial)The host can publish its first environment event as soon as the iframe loads. Registering the listener only after await client.ready() introduces a race: the initial event can be lost and the plugin remains on its fallback dark/English UI even though the host is light/Chinese. Environment events may be partial, so merge them with the last known state before updating Vuetify and the plugin message table.
Manifest string localization
The marketplace cards, the tool grid, the detail drawer, and the Agent tool palette all render strings the host reads from manifest.json. To localize them, add an optional i18n block keyed by short locale tag (en, zh). The top-level fields stay in English as the default; each locale override is independently optional — omit what you don't translate:
{
"id": "fan.summer.excel",
"name": "Excel Splitter",
"description": "Split Excel workbooks by sheet, column value, or complex rules",
"aiTools": [
{ "name": "excel_analyze", "method": "excel_analyze", "effect": "read", "description": "Analyze the granted Excel workbook…" }
],
"i18n": {
"zh": {
"name": "Excel 拆分器",
"description": "按工作表、列值或复杂规则拆分 Excel 工作簿",
"aiTools": {
"excel_analyze": { "description": "分析已授权的 Excel 工作簿……" }
}
}
}
}Resolution order for a request whose locale is zh-CN:
i18n["zh-CN"]— exact tagi18n["zh"]— language family- the top-level default (English)
Any level may be missing — the host falls through to the next, so a plugin can translate only the strings it cares about and never surfaces a blank. The host picks the locale from the request's Accept-Language header (the frontend sends the active UI language automatically).
What is not localized here
author— a brand identifier, never translated.inputSchema/outputSchema— their nested JSON Schematitle/descriptionstay in English (they describe tool parameters to the LLM).- AI-tool
descriptionoverrides are frontend display only. The string sent to the LLM is always the top-level English original, so tool-selection quality is unaffected by translation.
Plugin UI localization
The host does not translate strings that live inside a plugin's iframe UI. Bundle message tables and use the shared reactive runtime so fallback, interpolation, and locale normalization stay consistent across plugins:
import { createFengYuI18n, mountFengYuApp } from '@infinia/plugin-ui'
const messages = createFengYuI18n({
en: { title: 'Split complete', pick: 'Choose a file' },
zh: { title: '拆分完成', pick: '选择文件' },
})
await mountFengYuApp({ root: App, client, onEnvironment: messages.applyEnvironment })
messages.t('title')Fall back to a default locale (typically en) when the active locale has no translation. Because the host sets document.documentElement.lang, you can also read the locale from the DOM at any time as a fallback:
const locale = document.documentElement.lang // 'en' | 'zh-CN' | ...WARNING
Do not add a language switcher to your plugin UI. The host is the single source of truth for locale; a plugin-level switcher would desync from the rest of the app.
Next steps
- UI Micro-frontend — the full
Environmentshape and theon('environment')contract. - SDK & CLI — the
FengYuClientreference.