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
return array(
'yes'=>'是',
'no'=>'否',
'submit'=>'提交',
// ... 很多个键值对 ...
);
Xiuno BBS 会把这个数组赋值给全局变量 $lang(与 $_SERVER['lang']),然后你就可以用 lang() 函数从中取值了。
lang() 函数的定义
lang() 函数定义在 xiunophp 的 misc.func.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;
}
看,就这么几行。它的逻辑是:
- 在全局
$lang数组中查找$key - 如果找不到,就返回
lang[$key]这样的字符串(这是降级策略,至少不会白屏) - 如果找到了,就把翻译字符串中
{变量名}形式的占位符替换成你传入的值 - 返回替换后的字符串
所以,最简单的用法就是:
echo lang('submit'); // 当前语言是zh-cn时输出"提交",是en-us时输出"Submit"
Hook注入机制:插件如何添加自己的翻译
关键问题来了:语言文件是 Xiuno BBS 核心的文件,我们不能直接修改它(改了下次更新就没了)。那插件怎么注册自己的翻译呢?
答案是——你已经猜到了——Hook 机制。
如果你打开 lang/zh-cn/bbs.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 长这样:
'my_events_log_title' => '网站大事记',
经过编译后,缓存的语言文件就相当于在原数组末尾加上了这一行。
非常重要:这个 Hook 文件里不需要写 <?php,不需要写 return array(,也不需要写 );。因为它会被原样插入到主语言文件的数组内部。它只需要包含键值对行就行了——就像上面的例子那样。
你可以看看现有的插件是怎么写的。比如 xn_tag 插件的 hook/lang_zh_cn_bbs.php:
'is_force'=>'强制',
'is_default'=>'默认',
就这么两行。没有 <?php,没有 return,干净利落。
同理,en-us 也有自己的 Hook 点:// hook lang_en_us_bbs.php。你在插件的 hook/ 文件夹里放 lang_en_us_bbs.php,就能注册英文翻译。其他语言以此类推。
缓存机制
当 DEBUG > 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() 函数原生支持的方式。在语言文件中用 {变量名} 作为占位符:
// 语言文件
'my_events_log_created_by' => '{username} 创建了这条记录',
使用时,通过 lang() 的第二个参数传入替换值:
echo lang('my_events_log_created_by', array('username' => '张三'));
// 输出:张三 创建了这条记录
lang() 内部会遍历数组,把 {username} 替换成 张三。
方式二:sprintf语法(推荐)
在语言文件中使用 sprintf 的占位符格式:
// 语言文件
'my_events_log_created_by' => '%1$s 创建了这条记录',
使用时,用 sprintf() 来格式化:
echo sprintf(lang('my_events_log_created_by'), '张三');
// 输出:张三 创建了这条记录
为什么推荐 sprintf 方式?
-
支持参数重排。不同语言的语序可能不同。比如中文是"张三创建了这条记录",但英语可能是"This record was created by John"。用花括号语法,你只能按固定的变量名来替换;而用 sprintf 的
%1$s、%2$s,你可以在翻译中自由调整顺序:// 中文语言文件 '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 张三"。 -
更健壮。花括号语法依赖变量名匹配——如果语言文件里写了
{username}但你传的数组键名是user_name,替换就不会发生,用户会看到一串奇怪的花括号文字。而 sprintf 的%1$s、%2$s是按位置匹配的,只要参数数量对了就不会出问题。 -
语言包只需要纯占位符。翻译者不需要理解变量名的含义,只需要知道"第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,内容如下:
// 路由文件中的提示信息
'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这个键名,后加载的插件就会覆盖前面的——那场面可不太好看。 - 文件没有
<?php,没有return array(。因为这些内容会被原样插入到主语言文件的数组内部。
第2步:创建英文语言Hook文件
在 my_events_log/hook/ 文件夹里创建 lang_en_us_bbs.php,内容如下:
// Route messages
'my_events_log_title_required' => '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() 调用。
修改前:
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, "添加失败,请重试!");
}
修改后:
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
$action = param(1, 'list');
switch ($action) {
case 'add':
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_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
修改前:
<h2 class="page-header">添加新事件</h2>
<a href="<?= url('events_log-list') ?>" class="btn btn-default">返回列表</a>
...
<label>标题</label>
...
<label>内容</label>
...
<button type="submit" name="submit" class="btn btn-primary">提交</button>
修改后:
<h2 class="page-header"><?= lang('my_events_log_add_new_event') ?></h2>
<a href="<?= url('events_log-list') ?>" class="btn btn-default"><?= lang('my_events_log_back_to_list') ?></a>
...
<label><?= lang('my_events_log_title') ?></label>
...
<label><?= lang('my_events_log_content') ?></label>
...
<button type="submit" name="submit" class="btn btn-primary"><?= lang('my_events_log_submit') ?></button>
edit.htm
修改前:
<h2 class="page-header">编辑事件 <?= $event['id'] ?></h2>
<a href="<?= url('events_log-list') ?>" class="btn btn-default">返回列表</a>
...
<label>标题</label>
...
<label>内容</label>
...
<button type="submit" name="submit" class="btn btn-primary">更新</button>
修改后:
<h2 class="page-header"><?= lang('my_events_log_edit_event') ?> <?= $event['id'] ?></h2>
<a href="<?= url('events_log-list') ?>" class="btn btn-default"><?= lang('my_events_log_back_to_list') ?></a>
...
<label><?= lang('my_events_log_title') ?></label>
...
<label><?= lang('my_events_log_content') ?></label>
...
<button type="submit" name="submit" class="btn btn-primary"><?= lang('my_events_log_update') ?></button>
list.htm
这个文件改动稍多一点,因为它包含模态框和 Ajax 相关的内容。
修改前:
<h2 class="page-header">网站大事记列表</h2>
...
<a href="<?= url('events_log-add') ?>" class="btn btn-success">+ 添加事件</a>
...
<h5 class="modal-title" id="addEventModalLabel">添加新事件</h5>
...
<label for="title">标题</label>
...
<label for="content">内容</label>
...
<button type="submit" class="btn btn-primary">提交</button>
<button type="reset" class="btn btn-secondary" data-dismiss="modal">取消</button>
...
<a href="<?= url('events_log-view', ['id' => $event['id']]) ?>" class="btn btn-link">查看详情</a>
...
<a href="<?= url('events_log-edit', ['id' => $event['id']]) ?>" class="btn btn-xs btn-warning">编辑</a>
<button type="button" class="btn btn-xs btn-danger" onclick="processDelete(<?= $event['id'] ?>)">删除</button>
...
onclick="return confirm('确定删除?')"
修改后:
<h2 class="page-header"><?= lang('my_events_log_list_title') ?></h2>
...
<a href="<?= url('events_log-add') ?>" class="btn btn-success">+ <?= lang('my_events_log_add_event') ?></a>
...
<h5 class="modal-title" id="addEventModalLabel"><?= lang('my_events_log_add_new_event') ?></h5>
...
<label for="title"><?= lang('my_events_log_title') ?></label>
...
<label for="content"><?= lang('my_events_log_content') ?></label>
...
<button type="submit" class="btn btn-primary"><?= lang('my_events_log_submit') ?></button>
<button type="reset" class="btn btn-secondary" data-dismiss="modal"><?= lang('my_events_log_cancel') ?></button>
...
<a href="<?= url('events_log-view', ['id' => $event['id']]) ?>" class="btn btn-link"><?= lang('my_events_log_view_detail') ?></a>
...
<a href="<?= url('events_log-edit', ['id' => $event['id']]) ?>" class="btn btn-xs btn-warning"><?= lang('my_events_log_edit') ?></a>
<button type="button" class="btn btn-xs btn-danger" onclick="processDelete(<?= $event['id'] ?>)"><?= lang('my_events_log_delete') ?></button>
...
onclick="return confirm('<?= lang('my_events_log_confirm_delete') ?>')"
view.htm
修改前:
<small class="text-muted">发布时间:<?= date('Y-m-d H:i', $events_log['create_time']) ?></small>
...
<a href="<?= url('events_log-list') ?>" class="btn btn-default">返回列表</a>
修改后:
<small class="text-muted"><?= lang('my_events_log_publish_time') ?><?= date('Y-m-d H:i', $events_log['create_time']) ?></small>
...
<a href="<?= url('events_log-list') ?>" class="btn btn-default"><?= lang('my_events_log_back_to_list') ?></a>
第5步:修改导航Hook文件
修改前 (hook/header_nav_forum_end.htm):
<li class="nav-item">
<a class="nav-link" href="<?= url('events_log-list') ?>">
<i class="icon-clock-o"></i> 大事记
</a>
</li>
修改后:
<li class="nav-item">
<a class="nav-link" href="<?= url('events_log-list') ?>">
<i class="icon-clock-o"></i> <?= lang('my_events_log_nav') ?>
</a>
</li>
一个稍复杂的 sprintf 示例
到目前为止,我们替换的都是纯文本字符串——没有变量,不需要占位符。但在实际开发中,你可能会遇到需要拼接变量的情况。
比如,假设你希望在删除事件后显示"事件「网站上线」已被删除"这样的消息。如果硬编码,可能是:
message(0, "事件「{$event['title']}」已被删除");
要国际化它,首先在语言文件中用 sprintf 的占位符:
// zh-cn 语言文件
'my_events_log_deleted' => '事件「%1$s」已被删除',
// en-us 语言文件
'my_events_log_deleted' => 'Event "%1$s" has been deleted',
然后在代码中用 sprintf() 来组合:
message(0, sprintf(lang('my_events_log_deleted'), $event['title']));
这样,中文下输出"事件「网站上线」已被删除",英文下输出"Event "Website Launched" has been deleted"。
再强调一次 sprintf 的优势:如果你有多个变量,英文翻译可以自由调整它们的顺序,而中文不需要改任何代码:
// 语言文件
'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",你必须用双引号来包裹这个值:
'my_key' => "Author's Recent Threads", // 正确
如果你用单引号:
'my_key' => 'Author's Recent Threads', // 语法错误!
PHP 会把 Author 后面的单引号当成字符串的结束,然后面对 s Recent Threads', 这堆东西一脸茫然,直接报语法错误。
更糟糕的是,这个错误会导致整个语言文件无法加载——不仅仅是这一行,而是当前语言的所有翻译都会失效。然后你的网站可能就白屏了。
所以:如果翻译值里可能有单引号,就用双引号包裹。 不确定的话,统一用双引号也完全没问题。
3. 键名用英文 snake_case
键名本身应该用英文,用下划线分隔:
'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. 测试你的国际化
完成国际化之后,别忘了测试:
- 在网站后台切换到英文,访问你的插件页面,确认所有文字都变成了英文
- 切回中文,确认一切正常
- 如果切换语言后某些翻译没有生效,检查
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 文件建好。这是给自己的未来省时间。
下一篇文章见!
- 发现新大陆了 2023-11-4
- XIUNO邮箱设置教程 2021-11-21
- 请遇到问题的站长先看过来!!! 2022-9-18
- 这个原本是不是不支持PHP7.4? 2021-8-21
- 问题:那些插件值得装… 2022-12-20
|
192 主题数 |
1493 帖子数 |
Xiuno BBS开源程序交流论坛