Xiuno BBS 开发实践教程 - 国际化
Tillreetree 15小时前

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;
}

看,就这么几行。它的逻辑是:

  1. 在全局 $lang 数组中查找 $key
  2. 如果找不到,就返回 lang[$key] 这样的字符串(这是降级策略,至少不会白屏)
  3. 如果找到了,就把翻译字符串中 {变量名} 形式的占位符替换成你传入的值
  4. 返回替换后的字符串

所以,最简单的用法就是:

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 方式?

  1. 支持参数重排。不同语言的语序可能不同。比如中文是"张三创建了这条记录",但英语可能是"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 张三"。

  2. 更健壮。花括号语法依赖变量名匹配——如果语言文件里写了 {username} 但你传的数组键名是 user_name,替换就不会发生,用户会看到一串奇怪的花括号文字。而 sprintf 的 %1$s%2$s 是按位置匹配的,只要参数数量对了就不会出问题。

  3. 语言包只需要纯占位符。翻译者不需要理解变量名的含义,只需要知道"第1个参数放在这里"就行了。

当然,对于没有变量的纯文本翻译(比如"提交"、"删除"这种),两种方式完全一样——直接 lang('key') 就行,不需要任何占位符。

四、为my_events_log插件添加国际化

好了,理论讲完了,让我们回到你的 my_events_log 插件,把那些硬编码的中文全部换成 lang() 调用。

回顾一下,需要国际化的字符串散落在这些文件中:

  • 路由文件 route/events_log.phpmessage() 里的中文提示
  • 视图文件 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. 测试你的国际化

完成国际化之后,别忘了测试:

  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 文件建好。这是给自己的未来省时间。

下一篇文章见!

最新回复 (0)
全部楼主
返回
192
主题数
1493
帖子数
扫码访问