PHP项目多语言与国际化

wen PHP项目 4

本文目录导读:

PHP项目多语言与国际化

  1. 国际化(i18n)与本地化(L10n)的区别
  2. 方案选型(从简单到复杂)
  3. 进阶技巧与最佳实践
  4. 总结推荐

在PHP项目中实现多语言与国际化的方式有很多,我会从最基础的方案企业级方案,以及高性能实践的常见坑,给你一个完整的思维导图和代码示例。


国际化(i18n)与本地化(L10n)的区别

  • i18n(国际化):代码层面的准备,把写死的文字抽离出来,不依赖特定语言。
  • L10n(本地化):为特定地区/语言提供翻译内容、日期格式、货币符号等。

方案选型(从简单到复杂)

方案 1:原生数组 / 常量(最简单的 PHP 方式)

适合小型项目、不需要额外依赖的场景。

<?php
// lang.php
return [
    'en' => [
        'welcome' => 'Welcome to our website!',
        'hello' => 'Hello, :name!',
        'date_format' => 'm/d/Y',
        'currency' => '$',
    ],
    'zh_CN' => [
        'welcome' => '欢迎访问我们的网站!',
        'hello' => '你好,:name!',
        'date_format' => 'Y-m-d',
        'currency' => '¥',
    ],
];

核心调用逻辑:

<?php
class Translator {
    private array $messages = [];
    private string $locale = 'en';
    public function __construct(string $locale = 'en') {
        $this->setLocale($locale);
    }
    public function setLocale(string $locale): void {
        $langFile = __DIR__ . "/lang/{$locale}.php";
        $this->messages = file_exists($langFile) ? require $langFile : [];
        $this->locale = $locale;
    }
    public function trans(string $key, array $params = []): string {
        $message = $this->messages[$key] ?? $key;
        // 处理占位符 :name
        foreach ($params as $k => $v) {
            $message = str_replace(":{$k}", $v, $message);
        }
        return $message;
    }
}
// 使用
$t = new Translator('zh_CN');
echo $t->trans('welcome'); // 欢迎访问我们的网站!
echo $t->trans('hello', ['name' => '张三']); // 你好,张三!

优点:零依赖,性能极佳。 缺点:多语言文件夹需要人工管理,无法自动检测浏览器语言(需自行封装)。


方案 2:Gettext(传统国际标准)

PHP 原生支持 gettext 扩展,使用 .po / .mo 文件编译词条,适合大型系统或需要专业翻译团队。

安装与使用步骤:

  1. 确认扩展启用(php.ini):

    extension=gettext
  2. 创建语言目录:

    /var/www/locale/zh_CN/LC_MESSAGES/messages.po
    /var/www/locale/en_US/LC_MESSAGES/messages.po
  3. .po 文件格式:

    msgid "Welcome"
    msgstr "欢迎光临"
    msgid "Hello %s"
    msgstr "你好 %s"
  4. 编译为 .mo

    msgfmt messages.po -o messages.mo
  5. PHP 调用:

    <?php
    $locale = 'zh_CN';
    putenv("LANG={$locale}"); 
    setlocale(LC_ALL, $locale);
    // 指定语言目录(必须是绝对路径)
    bindtextdomain('messages', '/var/www/locale');
    textdomain('messages');
    echo gettext('Welcome'); // 欢迎光临
    echo _('Hello %s', '张三'); // 你好 张三

注意:Windows 下 PHP 通常没有 gettext 扩展,生产环境多为 Linux 可用。


方案 3:组件库思想(Symfony Translator 风格)

如果你在用框架(Laravel、Symfony、ThinkPHP)等,它们都有内置翻译器,这里以 Laravel 为模仿对象展示逻辑,因为这种设计可以脱离框架使用:

<?php
class Translator {
    private array $fallback = [];
    private array $current = [];
    private string $locale;
    private string $fallbackLocale;
    public function __construct(string $locale, string $fallbackLocale = 'en') {
        $this->locale = $locale;
        $this->fallbackLocale = $fallbackLocale;
        $this->loadMessages($locale);
        $this->loadMessages($fallbackLocale);
    }
    private function loadMessages(string $locale): void {
        $file = __DIR__ . "/lang/{$locale}.php";
        if (!file_exists($file)) return;
        $data = require $file;
        if ($locale === $this->locale) {
            $this->current = $data;
        } else {
            $this->fallback = $data;
        }
    }
    public function trans(string $key, array $replace = []): string {
        $segments = explode('.', $key);
        // 递归查找
        $message = $this->findRecursive($segments, $this->current);
        if (!$message) {
            $message = $this->findRecursive($segments, $this->fallback) ?? $key;
        }
        // 替换参数
        foreach ($replace as $k => $v) {
            $message = str_replace(':' . $k, $v, $message);
            $message = str_replace('%' . $k . '%', $v, $message);
        }
        return $message;
    }
    private function findRecursive(array $segments, array $data): ?string {
        $temp = $data;
        foreach ($segments as $seg) {
            if (!is_array($temp) || !array_key_exists($seg, $temp)) {
                return null;
            }
            $temp = $temp[$seg];
        }
        return is_string($temp) ? $temp : null;
    }
}
// 语言文件结构 lang/zh_CN.php
return [
    'auth' => [
        'login_success' => '登录成功',
        'welcome' => '欢迎,:name',
    ],
    'errors' => [
        'not_found' => '页面不存在',
    ],
];

支持多级数组键名,并且有备选语言回退机制。


方案 4:高性能方案(编译为 PHP 常量)

当翻译文件巨大时,require 数组性能最优,还可以进一步优化为预编译为 PHP 类常量

<?php
// lang/compiled/zh_CN.php
class zh_CN {
    const WELCOME = '欢迎';
    const LOGIN = '登录';
}
// 调用
echo zh_CN::WELCOME; // 更快的常量查找

借助工具(如 po2php)可以自动将 .po 文件编译成 PHP 类常量。



进阶技巧与最佳实践

复数规则处理

不同语言复数规则不同(如英文 0/1/2,中文/日文无区分):

<?php
public function transChoice(string $singular, string $plural, int $number, array $params = []): string {
    $key = $number == 1 ? $singular : $plural;
    $params['count'] = $number;
    return $this->trans($key, $params);
}
// 用法
echo transChoice('1 item', ':count items', 5);

数字与货币格式化

使用 Intl 扩展(推荐,比原生 number_format 更专业):

<?php
$fmt = new \NumberFormatter('zh_CN', \NumberFormatter::CURRENCY);
echo $fmt->formatCurrency(1234.56, 'CNY'); // 输出 "¥1,234.56"
$dateFmt = new \IntlDateFormatter('zh_CN', \IntlDateFormatter::LONG, \IntlDateFormatter::NONE);
echo $dateFmt->format(new DateTime('2024-01-01')); // "2024年1月1日"

自动检测浏览器语言

<?php
function detectLocale(array $supportedLocales): string {
    $acceptLang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
    preg_match_all('/([a-z]{2}(?:-[A-Z]{2})?)/', $acceptLang, $matches);
    foreach ($matches[1] as $lang) {
        if (in_array($lang, $supportedLocales)) {
            return $lang;
        }
    }
    return $supportedLocales[0] ?? 'en';
}

使用 Composer 库(省事)

  • symfony/translation —— 官方重量级,功能全面。
  • illuminate/translation —— Laravel 的翻译组件,可独立使用。
  • gettext/gettext —— 集成 Gettext 的现代封装。

总结推荐

项目规模 推荐方案 原因
小型工具/插件 原生数组 简单快速,零依赖
中型应用 自制翻译器(数组+回退) 可控性高,支持占位符
大型系统/多语言团队 Gettext 或 Symfony Translation 标准化,翻译管理工具完善
高并发/性能敏感 编译成 PHP 数组/常量 无 IO 文件解析开销

最后建议:语言文本永远不要拼接写死在代码里,一定要走变量替换,并且保证时区/字符编码统一(UTF-8),如果需要长期维护,建议把翻译文件接入第三方翻译平台(如 Lokalise、Crowdin),通过 CI/CD 自动拉取语言包。

抱歉,评论功能暂时关闭!