Xiuno BBS 开发实践教程 - 国际化
# Xiuno BBS 开发实践教程 - 国际化
> **前置要求**:本文要求你已经完成《Xiuno BBS 开发实践教程 - 网站大事记》一文,并且理解了其中所有内容。
***
你的插件可能只在你的小圈子里用——但万一呢?万一有个泰国的朋友看到了你的网站大事记,兴致勃勃地点进来,结果满屏都是中文,他可能只能默默关掉页面了。
又或者,你自己就想做一个英文论坛。那些硬编码的中文字符串,就像钉子一样钉在代码里,让你无处下手。
别担心,Xiuno BBS 其实早就准备好了多语言的基础设施。你只需要学会用一个函数——`lang()`——就能让你的插件跨越语言的边界。
听起来还不错吧?那我们开始吧。
## 一、为什么需要国际化
国际化(Internationalization,因为首尾之间有18个字母,所以常简写为 i18n)听起来是个挺吓人的词,但实际上它的核心思想很简单:**不要把人类语言的文字直接写死在代码里,而是用"键"来代替,运行时再根据当前语言查找对应的翻译。**
这么做的好处显而易见:
- **可维护性**:改翻译不用改代码,只需要改语言文件
- **可扩展性**:想要支持新语言?加一份语言文件就够了
- **可分享性**:你的插件发出去之后,别人可以只翻译语言文件就能适配自己的语言环境
Xiuno BBS 自身就已经内置了多语言支持。你可以在网站后台的"设置"里切换界面语言——简体中文、繁体中文、英语、俄语、泰语。这些翻译全部存储在 `lang/` 目录下的语言文件中。
那你的插件要怎么接入这个体系呢?答案就是——Hook 机制。
## 二、lang()函数的工作原理
在深入实操之前,我们需要先搞清楚 `lang()` 是怎么运作的。理解了原理,后面的一切都会顺理成章。
### 全局语言数组 `$lang`
Xiuno BBS 在启动时,会根据当前选择的语言(比如 `zh-cn` 或 `en-us`),加载对应的语言文件。语言文件位于 `lang/` 目录下:
```
lang/
zh-cn/
bbs.php ← 简体中文
en-us/
bbs.php ← 英文
zh-tw/
bbs.php ← 繁体中文
ru-ru/
bbs.php ← 俄语
th-th/
bbs.php ← 泰语
```
每个语言文件的结构都是一样的——返回一个关联数组:
```php
'是',
'no'=>'否',
'submit'=>'提交',
// ... 很多个键值对 ...
);
```
Xiuno BBS 会把这个数组赋值给全局变量 `$lang`(与 `$_SERVER['lang']`),然后你就可以用 `lang()` 函数从中取值了。
### lang() 函数的定义
`lang()` 函数定义在 xiunophp 的 `misc.func.php` 中,代码非常简短:
```php
function lang($key, $arr = array()) {
$lang = $_SERVER['lang'];
if(!isset($lang[$key])) return 'lang['.$key.']';
$s = $lang[$key];
if(!empty($arr)) {
foreach($arr as $k=>$v) {
$s = str_replace('{'.$k.'}', $v, $s);
}
}
return $s;
}
```
看,就这么几行。它的逻辑是:
1. 在全局 `$lang` 数组中查找 `$key`
2. 如果找不到,就返回 `lang[$key]` 这样的字符串(这是降级策略,至少不会白屏)
3. 如果找到了,就把翻译字符串中 `{变量名}` 形式的占位符替换成你传入的值
4. 返回替换后的字符串
所以,最简单的用法就是:
```php
echo lang('submit'); // 当前语言是zh-cn时输出"提交",是en-us时输出"Submit"
```
### Hook注入机制:插件如何添加自己的翻译
关键问题来了:语言文件是 Xiuno BBS 核心的文件,我们不能直接修改它(改了下次更新就没了)。那插件怎么注册自己的翻译呢?
答案是——你已经猜到了——**Hook 机制**。
如果你打开 `lang/zh-cn/bbs.php` 的末尾,会看到这么一行:
```php
// hook lang_zh_cn_bbs.php
);
?>
```
看到了吗?`// hook lang_zh_cn_bbs.php` 这个注释就是一个 Hook 点!它位于 `return array(` 的花括号内部,也就是在这个数组的末尾。
这意味着:你可以在插件的 `hook/` 文件夹里放一个名为 `lang_zh_cn_bbs.php` 的文件,Xiuno BBS 的编译器就会把这个文件的内容注入到 `lang/zh-cn/bbs.php` 的那个 Hook 点位置——也就是注入到那个数组的末尾。
所以,如果你的插件 `hook/lang_zh_cn_bbs.php` 长这样:
```php
'my_events_log_title' => '网站大事记',
```
经过编译后,缓存的语言文件就相当于在原数组末尾加上了这一行。
**非常重要**:这个 Hook 文件里**不需要**写 `'强制',
'is_default'=>'默认',
```
就这么两行。没有 ` 1` 时,Xiuno BBS 每次请求都会重新编译所有插件 Hook,合并到 `tmp/` 目录下的缓存文件中。语言文件也不例外——最终合并后的结果会出现在 `tmp/lang_zh-cn_bbs.php` 中。
在正式环境(`DEBUG=0`)中,缓存是持久的,不会每次都重新编译。如果你添加了新的语言 Hook 文件但没看到翻译生效,可以:
- 清除 `tmp/` 目录下的缓存文件
- 或者在后台把插件禁用再启用一次(这会触发重新编译)
## 三、两种使用方式:花括号 vs sprintf
`lang()` 函数本身支持一种占位符语法——用花括号 `{变量名}` 来标记可替换的部分。但 Xiuno BBS 社区中还有另一种常见的用法:用 `sprintf()` 配合 `%1$s`、`%2$s` 这样的占位符。
两种方式都可以,但我更推荐 sprintf 方式。下面我们分别看看。
### 方式一:花括号语法(Xiuno原生风格)
这是 `lang()` 函数原生支持的方式。在语言文件中用 `{变量名}` 作为占位符:
```php
// 语言文件
'my_events_log_created_by' => '{username} 创建了这条记录',
```
使用时,通过 `lang()` 的第二个参数传入替换值:
```php
echo lang('my_events_log_created_by', array('username' => '张三'));
// 输出:张三 创建了这条记录
```
`lang()` 内部会遍历数组,把 `{username}` 替换成 `张三`。
### 方式二:sprintf语法(推荐)
在语言文件中使用 `sprintf` 的占位符格式:
```php
// 语言文件
'my_events_log_created_by' => '%1$s 创建了这条记录',
```
使用时,用 `sprintf()` 来格式化:
```php
echo sprintf(lang('my_events_log_created_by'), '张三');
// 输出:张三 创建了这条记录
```
### 为什么推荐 sprintf 方式?
1. **支持参数重排**。不同语言的语序可能不同。比如中文是"张三创建了这条记录",但英语可能是"This record was created by John"。用花括号语法,你只能按固定的变量名来替换;而用 sprintf 的 `%1$s`、`%2$s`,你可以在翻译中自由调整顺序:
```php
// 中文语言文件
'my_events_log_action_by' => '%1$s 执行了 %2$s',
// 英文语言文件
'my_events_log_action_by' => '%2$s was performed by %1$s',
```
这样,同样的代码 `sprintf(lang('my_events_log_action_by'), '张三', '删除')`,在中文下输出"张三 执行了 删除",在英文下输出"Delete was performed by 张三"。
2. **更健壮**。花括号语法依赖变量名匹配——如果语言文件里写了 `{username}` 但你传的数组键名是 `user_name`,替换就不会发生,用户会看到一串奇怪的花括号文字。而 sprintf 的 `%1$s`、`%2$s` 是按位置匹配的,只要参数数量对了就不会出问题。
3. **语言包只需要纯占位符**。翻译者不需要理解变量名的含义,只需要知道"第1个参数放在这里"就行了。
当然,对于**没有变量**的纯文本翻译(比如"提交"、"删除"这种),两种方式完全一样——直接 `lang('key')` 就行,不需要任何占位符。
## 四、为my_events_log插件添加国际化
好了,理论讲完了,让我们回到你的 `my_events_log` 插件,把那些硬编码的中文全部换成 `lang()` 调用。
回顾一下,需要国际化的字符串散落在这些文件中:
- **路由文件** `route/events_log.php`:`message()` 里的中文提示
- **视图文件** `view/htm/add.htm`:页面标题、按钮文字等
- **视图文件** `view/htm/edit.htm`:页面标题、按钮文字等
- **视图文件** `view/htm/list.htm`:页面标题、按钮文字、确认对话框等
- **视图文件** `view/htm/view.htm`:发布时间标签、按钮文字等
- **导航Hook** `hook/header_nav_forum_end.htm`:菜单文字
让我们一步步来。
### 第1步:创建中文语言Hook文件
在 `my_events_log/hook/` 文件夹里创建 `lang_zh_cn_bbs.php`,内容如下:
```php
// 路由文件中的提示信息
'my_events_log_title_required' => '请输入标题!',
'my_events_log_content_required' => '请输入内容!',
'my_events_log_add_success' => '添加成功!',
'my_events_log_add_failed' => '添加失败,请重试!',
'my_events_log_update_success' => '更新成功!',
'my_events_log_update_failed' => '更新失败,请重试!',
'my_events_log_not_exist' => '事件记录不存在',
'my_events_log_delete_success' => '删除成功!',
'my_events_log_delete_failed' => '删除失败,可能已经删掉了',
'my_events_log_unknown_action' => '未知的操作',
// 视图文件中的界面文字
'my_events_log_nav' => '大事记',
'my_events_log_list_title' => '网站大事记列表',
'my_events_log_add_event' => '添加事件',
'my_events_log_add_new_event' => '添加新事件',
'my_events_log_edit_event' => '编辑事件',
'my_events_log_view_detail' => '查看详情',
'my_events_log_edit' => '编辑',
'my_events_log_delete' => '删除',
'my_events_log_confirm_delete' => '确定删除?',
'my_events_log_back_to_list' => '返回列表',
'my_events_log_title' => '标题',
'my_events_log_content' => '内容',
'my_events_log_submit' => '提交',
'my_events_log_update' => '更新',
'my_events_log_publish_time' => '发布时间:',
'my_events_log_cancel' => '取消',
```
注意:
- 每个键名都以 `my_events_log_` 开头,这是插件的名称。这样做可以避免和其他插件的翻译键名冲突。想象一下,如果大家都用 `add_success` 这个键名,后加载的插件就会覆盖前面的——那场面可不太好看。
- 文件没有 ` 'Please enter a title!',
'my_events_log_content_required' => 'Please enter the content!',
'my_events_log_add_success' => 'Added successfully!',
'my_events_log_add_failed' => 'Failed to add, please try again!',
'my_events_log_update_success' => 'Updated successfully!',
'my_events_log_update_failed' => 'Failed to update, please try again!',
'my_events_log_not_exist' => 'Event record does not exist',
'my_events_log_delete_success' => 'Deleted successfully!',
'my_events_log_delete_failed' => 'Failed to delete, it may have already been removed',
'my_events_log_unknown_action' => 'Unknown action',
// View texts
'my_events_log_nav' => 'Events',
'my_events_log_list_title' => 'Events Log',
'my_events_log_add_event' => 'Add Event',
'my_events_log_add_new_event' => 'Add New Event',
'my_events_log_edit_event' => 'Edit Event',
'my_events_log_view_detail' => 'View Details',
'my_events_log_edit' => 'Edit',
'my_events_log_delete' => 'Delete',
'my_events_log_confirm_delete' => 'Are you sure to delete?',
'my_events_log_back_to_list' => 'Back to List',
'my_events_log_title' => 'Title',
'my_events_log_content' => 'Content',
'my_events_log_submit' => 'Submit',
'my_events_log_update' => 'Update',
'my_events_log_publish_time' => 'Published: ',
'my_events_log_cancel' => 'Cancel',
```
键名必须和中文文件完全一致,只是值换成对应的英文翻译。Xiuno BBS 会根据当前语言设置,自动选择加载哪个语言文件。
### 第3步:修改路由文件
打开 `my_events_log/route/events_log.php`,我们需要把所有硬编码的中文字符串替换成 `lang()` 调用。
**修改前**:
```php
if (empty($data['title'])) {
message(2, "请输入标题!");
}
if (empty($data['content'])) {
message(2, "请输入内容!");
}
$r = events_log_create($data["title"], $data['content']);
if ($r) {
message(0, "添加成功!");
} else {
message(1, "添加失败,请重试!");
}
```
**修改后**:
```php
if (empty($data['title'])) {
message(2, lang('my_events_log_title_required'));
}
if (empty($data['content'])) {
message(2, lang('my_events_log_content_required'));
}
$r = events_log_create($data["title"], $data['content']);
if ($r) {
message(0, lang('my_events_log_add_success'));
} else {
message(1, lang('my_events_log_add_failed'));
}
```
很简单吧?就是用 `lang('键名')` 替换掉原来的中文字符串。其他地方的 `message()` 调用也一样改法。下面是完整的路由文件修改后的样子:
```php
strip_tags(param('title', '')),
'content' => param('content', ''),
];
if (empty($data['title'])) {
message(2, lang('my_events_log_title_required'));
}
if (empty($data['content'])) {
message(2, lang('my_events_log_content_required'));
}
$r = events_log_create($data["title"], $data['content']);
if ($r) {
message(0, lang('my_events_log_add_success'));
} else {
message(1, lang('my_events_log_add_failed'));
}
} else {
include APP_PATH . 'plugin/my_events_log/view/htm/add.htm';
}
break;
case 'edit':
$id = param('id', 0);
$events_log = events_log_read($id);
if ($events_log) {
if (isset($_POST['submit'])) {
$data = [
'title' => strip_tags(param('title', '')),
'content' => param('content', ''),
];
if (empty($data['title'])) {
message(2, lang('my_events_log_title_required'));
}
if (empty($data['content'])) {
message(2, lang('my_events_log_content_required'));
}
$r = events_log_update($id, $data["title"], $data['content']);
if ($r) {
message(0, lang('my_events_log_update_success'));
} else {
message(1, lang('my_events_log_update_failed'));
}
} else {
$id = param('id', 0);
$event = events_log_read($id);
include APP_PATH . 'plugin/my_events_log/view/htm/edit.htm';
}
} else {
message(1, lang('my_events_log_not_exist'));
}
break;
case 'delete':
$r = events_log_delete(param('id', 0));
if ($r) {
message(0, lang('my_events_log_delete_success'));
} else {
message(1, lang('my_events_log_delete_failed'));
}
break;
case 'list':
$page = param('page', 1);
$pagesize = 10;
$tmp_events_result = events_log_find([], ['create_time' => 0], 1, 1000);
if (is_array($tmp_events_result)) {
$total = count($tmp_events_result);
} else {
$total = 0;
}
$events = events_log_find([], ['create_time' => 0], $page, $pagesize);
$pagination = pagination(url("events_log-list-{page}"), $total, $page, $pagesize);
include APP_PATH . 'plugin/my_events_log/view/htm/list.htm';
break;
case 'view':
$id = param('id', 0);
$events_log = events_log_read($id);
if ($events_log) {
include APP_PATH . 'plugin/my_events_log/view/htm/view.htm';
} else {
message(1, lang('my_events_log_not_exist'));
}
break;
default:
message(1, lang('my_events_log_unknown_action'));
break;
}
```
### 第4步:修改视图文件
视图文件里的中文字符串是直接写在 HTML 中的,我们需要用 `= lang('键名') ?>` 来替换它们。
#### add.htm
**修改前**:
```php
返回列表
...
...
...
```
**修改后**:
```php
= lang('my_events_log_back_to_list') ?>
...
...
...
```
#### edit.htm
**修改前**:
```php
返回列表
...
...
...
```
**修改后**:
```php
= lang('my_events_log_back_to_list') ?>
...
...
...
```
#### list.htm
这个文件改动稍多一点,因为它包含模态框和 Ajax 相关的内容。
**修改前**:
```php
...
+ 添加事件
...
添加新事件
...
...
...
...
查看详情
...
编辑
...
onclick="return confirm('确定删除?')"
```
**修改后**:
```php
...
+ = lang('my_events_log_add_event') ?>
...
= lang('my_events_log_add_new_event') ?>
...
...
...
...
= lang('my_events_log_view_detail') ?>
...
= lang('my_events_log_edit') ?>
...
onclick="return confirm('= lang('my_events_log_confirm_delete') ?>')"
```
注意 `confirm()` 里的那行——在 `onclick` 属性中用 PHP 输出翻译内容:
```php
onclick="return confirm('= lang('my_events_log_confirm_delete') ?>')"
```
这是安全的,因为我们的翻译文本中不包含会破坏 JavaScript 的特殊字符。如果你担心翻译文本中可能包含单引号,可以改用 `htmlspecialchars()` 做一次转义:
```php
onclick="return confirm('= htmlspecialchars(lang('my_events_log_confirm_delete')) ?>')"
```
#### view.htm
**修改前**:
```php
发布时间:= date('Y-m-d H:i', $events_log['create_time']) ?>
...
返回列表
```
**修改后**:
```php
= lang('my_events_log_publish_time') ?>= date('Y-m-d H:i', $events_log['create_time']) ?>
...
= lang('my_events_log_back_to_list') ?>
```
### 第5步:修改导航Hook文件
**修改前** (`hook/header_nav_forum_end.htm`):
```php
大事记
```
**修改后**:
```php
= lang('my_events_log_nav') ?>
```
### 一个稍复杂的 sprintf 示例
到目前为止,我们替换的都是纯文本字符串——没有变量,不需要占位符。但在实际开发中,你可能会遇到需要拼接变量的情况。
比如,假设你希望在删除事件后显示"事件「网站上线」已被删除"这样的消息。如果硬编码,可能是:
```php
message(0, "事件「{$event['title']}」已被删除");
```
要国际化它,首先在语言文件中用 `sprintf` 的占位符:
```php
// zh-cn 语言文件
'my_events_log_deleted' => '事件「%1$s」已被删除',
// en-us 语言文件
'my_events_log_deleted' => 'Event "%1$s" has been deleted',
```
然后在代码中用 `sprintf()` 来组合:
```php
message(0, sprintf(lang('my_events_log_deleted'), $event['title']));
```
这样,中文下输出"事件「网站上线」已被删除",英文下输出"Event "Website Launched" has been deleted"。
再强调一次 sprintf 的优势:如果你有多个变量,英文翻译可以自由调整它们的顺序,而中文不需要改任何代码:
```php
// 语言文件
'my_events_log_user_action' => '%1$s 对 %2$s 执行了操作', // zh-cn
'my_events_log_user_action' => 'Operation performed on %2$s by %1$s', // en-us
// 代码
echo sprintf(lang('my_events_log_user_action'), $username, $event_title);
```
## 五、命名规范与最佳实践
走完上面的步骤,你的插件已经具备国际化的能力了。但在继续之前,让我们总结一些关键的最佳实践——它们能帮你少踩坑。
### 1. 键名一定要加插件前缀
这是最重要的规则。始终用你的插件名作为键名的前缀,用下划线连接:
| 写法 | 评价 |
|------|------|
| `'my_events_log_add_success' => '添加成功'` | 正确,有插件前缀 |
| `'add_success' => '添加成功'` | 危险,可能和其他插件冲突 |
为什么?因为所有插件的语言 Hook 文件都会被合并到同一个 `$lang` 数组里。如果你用了 `add_success` 这个键名,而另一个插件也用了同样的键名,后加载的插件就会覆盖你的翻译——而且没有任何报错,你只会看到莫名其妙的翻译结果。
用插件名做前缀,就相当于给你的翻译加上了命名空间,保证不会撞车。
### 2. 值里含有单引号时,用双引号包裹
这是 PHP 的语法问题,和 Xiuno BBS 本身无关,但特别容易踩坑。
如果你的翻译文本中包含英文单引号(撇号),比如 "Author's Recent Threads",你**必须**用双引号来包裹这个值:
```php
'my_key' => "Author's Recent Threads", // 正确
```
如果你用单引号:
```php
'my_key' => 'Author's Recent Threads', // 语法错误!
```
PHP 会把 `Author` 后面的单引号当成字符串的结束,然后面对 `s Recent Threads',` 这堆东西一脸茫然,直接报语法错误。
更糟糕的是,这个错误会导致**整个语言文件**无法加载——不仅仅是这一行,而是当前语言的所有翻译都会失效。然后你的网站可能就白屏了。
所以:**如果翻译值里可能有单引号,就用双引号包裹。** 不确定的话,统一用双引号也完全没问题。
### 3. 键名用英文 snake_case
键名本身应该用英文,用下划线分隔:
```php
'my_events_log_add_success' => '添加成功', // 正确
'my_events_log_tianjia_chenggong' => '添加成功', // 别扭
'my_events_log_添加成功' => '添加成功', // 千万别
```
键名是给代码看的,不是给人看的。用英文键名可以保证代码的可读性和兼容性——不管当前是什么语言,键名始终不变。
### 4. 至少提供 zh-cn 和 en-us
Xiuno BBS 支持多种语言,但最基础的两个是简体中文(`zh-cn`)和英文(`en-us`)。如果可能的话,把这两种语言的翻译都写上。
如果你只提供了 `zh-cn`,那当用户切换到英文时,`lang()` 函数会找不到对应的英文翻译,于是降级返回 `lang[my_events_log_xxx]` 这样的字符串——虽然不会白屏,但看起来肯定不太友好。
如果你实在不想翻译,至少也把英文文件建好,值可以和中文文件一样先用英文占位——总比显示 `lang[...]` 好。
### 5. 不要覆盖核心或其他插件的键名
理论上,你的 Hook 文件可以覆盖已有的语言键名——因为它后加载,会覆盖前面的值。但请**不要这么做**,除非你明确知道自己在干什么。
覆盖核心翻译会影响整个网站,覆盖其他插件的翻译会让那个插件功能异常。如果确实需要修改某个翻译,应该通过覆盖模板或者其他更安全的方式来实现。
### 6. 测试你的国际化
完成国际化之后,别忘了测试:
1. 在网站后台切换到英文,访问你的插件页面,确认所有文字都变成了英文
2. 切回中文,确认一切正常
3. 如果切换语言后某些翻译没有生效,检查 `tmp/` 目录是否需要清空
## 六、常见问题
### Q:添加语言Hook文件后,网站白屏了怎么办?
A:几乎可以确定是**单引号问题**。检查你的语言文件中是否有值包含了单引号(撇号),如果有,把外层的单引号改成双引号。也可以查看 PHP 的错误日志来定位具体的语法错误行号。
### Q:为什么我的翻译没有显示出来?
A:最常见的原因是缓存没有更新。试试以下方法:
- 把 `index.php` 中的 `DEBUG` 改成 `2`,Xiuno BBS 每次请求都会重新编译
- 或者清空 `tmp/` 目录下的所有文件
- 或者在后台把插件禁用再启用一次
如果清完缓存还是不行,检查你的 Hook 文件名是否正确——中文语言文件必须是 `lang_zh_cn_bbs.php`(注意是下划线,不是横线),英文语言文件必须是 `lang_en_us_bbs.php`。
### Q:如果当前语言中缺少某个键,会怎么样?
A:`lang()` 函数有降级策略——如果找不到对应的键,它会返回 `lang[键名]` 这样的字符串。比如 `lang('my_events_log_foo')` 在找不到翻译时,会返回 `lang[my_events_log_foo]`。虽然不会白屏,但用户会看到这个奇怪的字符串,所以尽量保证每种语言都有完整的翻译。
### Q:我可以覆盖核心的语言键名吗?
A:技术上可以——你的 Hook 文件会被合并到主语言数组中,如果你的键名和核心的一样,你的值会覆盖核心的值。但**非常不建议这么做**,因为:
- 这会影响整个网站,不仅仅是你的插件
- 其他插件可能依赖核心翻译的原有值
- 如果核心更新了翻译,你的覆盖可能会产生不一致
除非你有充分的理由,否则不要这么做。
### Q:如果我只想支持中文呢?
A:那也建议你用 `lang()` 来组织文字——哪怕你只建了 `lang_zh_cn_bbs.php`。这样做的好处是:将来某天你想支持其他语言时,只需要加一个语言文件就行了,不用再回头改代码。而且把文字集中到一个文件里管理,比散落在各个代码文件中要好维护得多。
### Q:sprintf 的占位符 `%s` 和 `%1$s` 有什么区别?
A:`%s` 是按顺序的占位符——第几个 `%s` 就对应 `sprintf()` 的第几个参数。而 `%1$s` 是按编号的占位符——`%1$s` 始终对应第一个参数,`%2$s` 始终对应第二个参数,无论它们在字符串中的位置如何。
推荐始终使用 `%1$s`、`%2$s` 这种编号形式,因为它们支持在翻译中调整参数顺序。
***
## 恭喜你!
你的 `my_events_log` 插件现在已经是多语言的了。切换语言试试——看着那些中文文字瞬间变成英文,是不是还挺有成就感的?
国际化这件事,说大不大说小不小。如果你从一开始就养成用 `lang()` 的习惯,以后就再也不用回头改了。但如果你一开始把文字硬编码了,等你想国际化的时候,就得把所有文件翻一遍——那可就不是什么愉快的体验了。
所以,下次写新插件的时候,记得:**第一步就把语言 Hook 文件建好**。这是给自己的未来省时间。
下一篇文章见!