多国语言 i18n 实现
Eagle 插件内置了 i18next 模块,所以多语言不需要引入任何第三方库。这让国际化变成了一件配置性的工作:建目录、写 JSON、改 manifest、换调用方式。
五步实现
第一步:建立 _locales 文件夹
在插件根目录建一个 _locales 文件夹,文件夹名必须是下划线开头的 _locales,不能写成 locales 或 i18n。
第二步:为每种语言建一个 JSON 文件
文件名就是语言代码。官方列出的支持语系是:en、ja_JP、es_ES、de_DE、zh_TW、zh_CN、ko_KR、ru_RU。
{
"manifest": {
"app": {
"name": "i18n example"
}
},
"contextMenu": {
"copy": "Copy",
"paste": "Paste"
}
}
{
"manifest": {
"app": {
"name": "多国语言范例"
}
},
"contextMenu": {
"copy": "复制",
"paste": "粘贴"
}
}
繁体中文文件把「复制」写成「複製」、「粘贴」写成「貼上」——繁体不能只是简体文件的复制,审核标准里明确说了「混杂语言到无法正常阅读」会让文案看起来尚未完成。
目录结构:
my-plugin
├─ manifest.json
├─ index.html
├─ js
│ └─ plugin.js
└─ _locales
├─ en.json
├─ zh_CN.json
└─ zh_TW.json
第三步:调整 manifest.json
两处改动:把 name 换成插值表达式,再声明支持的语言和默认语言。
{
"id": "LE564883T24ZR",
"version": "1.0.0",
"name": "`manifest.app.name`",
"logo": "/logo.png",
"keywords": [],
"fallbackLanguage": "zh_CN",
"languages": ["en", "zh_TW", "zh_CN", "ja_JP"],
"main": {
"url": "index.html",
"width": 640,
"height": 480
}
}
name写成manifest.app.name,Eagle 会按当前语系去_locales里取对应值。fallbackLanguage指定某个语言缺翻译时回退到哪份。languages声明支持哪些,用户在 Eagle 里切换语言时,只有列在这里的才会被使用。
⚠️ 一个容易忽略的点:manifest 里的 name 也要翻译。商店列表显示的名称会随 Eagle 的界面语言变化,如果只有中文用户看到中文名、英文用户看到拼音,那就失去了做 i18n 的意义。
第四步:在代码里换成 i18next.t()
eagle.onPluginCreate((plugin) => {
// 取得多国语言字段
let copyText = i18next.t('contextMenu.copy');
let pasteText = i18next.t('contextMenu.paste');
document.querySelector('#message').innerHTML = `
<ul>
<li>Language: ${eagle.app.locale}</li>
<li>Copy: ${copyText}</li>
<li>Paste: ${pasteText}</li>
</ul>
`;
});
i18next 是全局对象,不用 require。键名用点号分层,和 JSON 里的嵌套结构一一对应。
第五步:切换语言验证
在 Eagle 里:点「Eagle」按钮 → 「偏好配置」→ 「常用」→ 修改「语言」设置,然后重新打开插件,确认文案跟着变。
覆盖到界面和通知
不只是 manifest.name 和右键菜单,所有用户可见的文案都应该走 i18next:
eagle.onPluginCreate(async () => {
const t = (key) => i18next.t(key);
document.querySelector('#btn-run').textContent = t('actions.run');
document.querySelector('#btn-cancel').textContent = t('actions.cancel');
document.querySelector('#status').textContent = t('status.ready');
// 系统对话框也要翻译
const result = await eagle.dialog.showMessageBox({
title: t('confirm.title'),
message: t('confirm.message'),
buttons: [t('confirm.yes'), t('confirm.no')],
type: 'question'
});
// 原生右键菜单
eagle.contextMenu.open([
{ id: 'rename', label: t('menu.rename'), click: () => { /* ... */ } },
{ id: 'export', label: t('menu.export'), click: () => { /* ... */ } }
]);
});
// 通知同样
await eagle.notification.show({
title: i18next.t('notify.done.title'),
body: i18next.t('notify.done.body')
});
对应的语言文件:
{
"actions": {
"run": "开始处理",
"cancel": "取消"
},
"status": {
"ready": "就绪"
},
"confirm": {
"title": "确认",
"message": "即将处理选中的素材",
"yes": "确定",
"no": "取消"
},
"menu": {
"rename": "重命名",
"export": "导出"
},
"notify": {
"done": {
"title": "处理完成",
"body": "所有素材已处理完毕"
}
}
}
读取当前语系
console.log(eagle.app.locale); // en / zh_CN / zh_TW / ja_JP / ko_KR / es_ES / de_DE / ru_RU
需要做「按语言显示不同排版」这类事时(比如日文韩文需要不同的字体栈),可以读 locale 来分支。
const CJK_FONTS = '"PingFang SC", "Microsoft YaHei", "Hiragino Sans", "Malgun Gothic"';
const LATIN_FONTS = '-apple-system, "Segoe UI", Roboto, sans-serif';
document.body.style.fontFamily = ['zh_CN', 'zh_TW', 'ja_JP', 'ko_KR']
.includes(eagle.app.locale) ? CJK_FONTS : LATIN_FONTS;
建议的落地策略
- 先设计键名,再写代码。 用
模块.动作的分层命名,比如export.dialog.title。事后补翻译最痛苦。 - 默认语言用最全的那份。
fallbackLanguage指向你写得最完整的语言,其他语言缺的键会回退到这里。 - 只发布你真的翻完的语言。
languages里列了但文件里没内容的语言,用户看到的是contextMenu.copy这样的原始键名——这在审核里会被当成「保留翻译键」。 - 日期、数字也要本地化。
toLocaleString()会按语系输出,模板字符串拼接不会。 - 商店文案也逐语言检查。 审核会分别检查每个投稿语言版本,一个语言写得完整不能弥补另一个语言的问题。
常见坑
文件夹名写成
locales或i18n。 必须是_locales,带前导下划线。写错了 Eagle 找不到翻译,manifest.app.name会原样显示成一串花括号。语言文件不完整但仍在
languages里声明了。 缺失的键会显示成键名本身。审核标准把「保留翻译键」列为看起来尚未完成的典型表现。延伸注意: * 文件夹名写成
locales或i18n。 必须是_locales,带前导下划线。写错了 Eagle 找不到翻译,manifest.app.name会原样显示成一串花括号。 * 语言文件不完整但仍在languages里声明了。 缺失的键会显示成键名本身。审核标准把「保留翻译键」列为看起来尚未完成的典型表现。整理人:WorkBuddy
