在PHP项目中实现国际化通常意味着要让同一套代码根据访问者的语言偏好输出不同语言的文本。最经典的做法是依赖gettext扩展,它原本是GNU的文本国际化工具,PHP通过函数封装让其可以直接读取编译后的mo文件。除此之外,也有大量轻量项目选择自建语言数组,用键值对的方式管理翻译。两种方式各有适用场景,理解其原理才能避免后期维护成本飙升。

一、基于gettext扩展的标准国际化方案
gettext的工作流程分为三步:在代码中使用_()或gettext()包裹原文,用xgettext工具扫描源码生成pot模板,翻译人员基于模板写出不同语言的po文件并最终编译为mo二进制文件。PHP运行时通过putenv和setlocale指定区域,再用bindtextdomain与textdomain绑定目录和域名,即可自动返回对应语言。这种方案的优势在于单复数、上下文等复杂语言规则由gettext底层处理,非常适合中大型系统。
下面是一段典型的初始化代码,注意路径中的反斜杠在Windows下也要原样保留,例如语言包目录可写成 C:wwwapplocale。代码中我们设置了中文简体环境,并绑定到messages域:
<?php
// 设置语言环境为中文简体
$locale = 'zh_CN.UTF-8';
putenv('LC_ALL=' . $locale);
setlocale(LC_ALL, $locale);
// 绑定语言包路径,注意Windows下反斜杠原样保留
$bindPath = 'C:wwwapplocale';
bindtextdomain('messages', $bindPath);
textdomain('messages');
// 输出翻译后的字符串
echo gettext('Welcome to our website');
echo _('Login');
?>
使用gettext时常见的坑是服务器未安装对应语言包,或者setlocale返回false却未察觉,导致一直显示英文原文。另外mo文件必须放在类似 locale/zh_CN/LC_MESSAGES/messages.mo 的规范目录中,否则bindtextdomain找不到文件。开发阶段可借助Poedit工具可视化编辑po文件,它还能自动提取源码中的待翻译字段,显著降低协作成本。
二、数组或JSON语言包的自建切换方案
如果运行环境无法启用gettext扩展,或者项目规模很小,用关联数组管理多语言是最直接的替代方案。我们通常为每种语言建立一个php文件,内部返回以原文或键名为索引的数组,切换时按需include并赋值给全局变量。这种写法不依赖系统区域,部署简单,但在处理俄语、阿拉伯语等拥有复杂单复数变化的语言时会显得力不从心,需要自己在代码中写判断逻辑。
以下示例展示了一个最简语言包与切换逻辑,通过会话保存用户选择,未选择时回退到浏览器Accept-Language头:
<?php
session_start();
// 语言包目录使用相对路径,反斜杠在Linux下应改为斜杠
$langFiles = [
'en' => '/var/www/lang/en.php',
'zh' => '/var/www/lang/zh.php'
];
if (isset($_GET['lang']) && array_key_exists($_GET['lang'], $langFiles)) {
$_SESSION['lang'] = $_GET['lang'];
} elseif (!isset($_SESSION['lang'])) {
$browserLang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
$_SESSION['lang'] = isset($langFiles[$browserLang]) ? $browserLang : 'en';
}
$translations = include $langFiles[$_SESSION['lang']];
function t($key) {
global $translations;
return isset($translations[$key]) ? $translations[$key] : $key;
}
echo t('site_title');
?>
这种方案的优点是所有翻译都在代码仓库里,版本管理清晰,也方便非技术翻译人员通过修改数组参与。但缺点同样明显:当文案上千条后,数组文件会变得臃肿,且缺乏gettext那样的上下文标注功能。实践中建议将语言包拆分成多个模块文件,按业务懒加载,并用JSON配合OPcache减少解析开销。
三、多语言切换的交互与持久化设计
无论采用哪种底层方案,面向用户的切换入口都需考虑体验一致性。最常见的是在导航栏放置语言下拉菜单,点击后通过URL参数或Ajax请求告知后端,后端写入会话或Cookie。要注意如果用了会话,必须确保会话在输出前已开启,否则语言设置不生效。对于SEO友好的站点,还应将语言放入URL路径或子域名,例如 /zh/about 与 /en/about,便于搜索引擎分别收录。
下面示例展示一个结合Cookie与会话的稳妥写法,优先读Cookie,这样即使用户未登录也能记住偏好,同时用filter_input做基本校验防止注入:
<?php
session_start();
$cookieLang = filter_input(INPUT_COOKIE, 'lang', FILTER_SANITIZE_STRING);
$getLang = filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_STRING);
$allowed = ['en', 'zh', 'ja'];
if ($getLang && in_array($getLang, $allowed, true)) {
$lang = $getLang;
setcookie('lang', $lang, time() + 86400 * 30, '/');
$_SESSION['lang'] = $lang;
} elseif ($cookieLang && in_array($cookieLang, $allowed, true)) {
$lang = $cookieLang;
$_SESSION['lang'] = $lang;
} elseif (isset($_SESSION['lang'])) {
$lang = $_SESSION['lang'];
} else {
$lang = 'en';
}
echo 'current language: ' . $lang;
?>
在架构层面,建议把语言解析封装成独立中间件或服务类,而不是在业务脚本里散落setlocale调用。这样在CLI命令、队列任务中也能统一注入语言环境。另外要注意数据库里存储的用户生成内容一般不该随界面语言翻译,应明确区分界面文案国际化与业务数据多语存储,后者往往要用额外字段或翻译表来实现。