14. 国际化与本地化

14.国际化与本地化

在现代 Web 应用开发中,支持多语言和本地化已经成为一项基本需求。特别是当应用面向全球用户时,良好的国际化设计能够显著提升用户体验。Phalcon 框架虽然没有重复造轮子实现完整的国际化组件,但它提供了与 PHP 国际扩展的无缝集成能力,让我们能够轻松构建多语言应用。

多语言支持

国际化(通常简称 i18n,因为"国际化"一词英文有 18 个字母)是指设计软件时,使它能够适应各种语言和地区的需求,而不需要对软件内部结构进行大规模修改。在 PHP 生态中,这主要通过 intl 扩展来实现,Phalcon 框架充分利用了这一扩展提供的功能。

检测用户语言偏好

实现国际化的第一步是确定用户的语言偏好。最常见的方式是通过 HTTP 请求头中的 Accept-Language 字段来获取:

<?php

use Locale;

// 从HTTP头获取用户首选语言
$locale = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);

// 输出可能是 'zh-CN'、'en-US' 或 'fr_FR' 等
echo $locale;

这段代码使用了 PHP intl 扩展中的 Locale 类来解析 HTTP 请求头,返回一个语言标识符。语言标识符通常由语言代码和地区代码组成,如 zh-CN 表示中国大陆的中文,en-US 表示美国英语。

在 Phalcon 应用中,我们还可以使用请求对象的 getBestLanguage() 方法来获取最佳语言:

<?php

// 在控制器中获取最佳语言
$language = $this->request->getBestLanguage();

这个方法会自动分析请求头,并返回与应用支持语言最匹配的语言代码。

Phalcon 翻译组件

Phalcon 提供了 Phalcon\Translate 组件来管理应用中的翻译字符串。这个组件支持多种适配器,包括数组、数据库等,让我们可以灵活地存储和检索翻译内容。

最常用的是数组适配器,它将翻译字符串存储在 PHP 数组中。下面是一个基本实现:

<?php

use Phalcon\Translate\Adapter\NativeArray;
use Phalcon\Translate\InterpolatorFactory;
use Phalcon\Translate\TranslateFactory;

// 创建翻译服务
$interpolator = new InterpolatorFactory();
$factory = new TranslateFactory($interpolator);

$translator = $factory->newInstance(
    'array',
    [
        'content' => [
            'welcome' => '欢迎来到我们的网站',
            'hello' => '你好,%name%',
            'current_time' => '当前时间是 %time%',
        ],
    ]
);

// 使用翻译服务
echo $translator->_('welcome'); // 输出: 欢迎来到我们的网站

echo $translator->_('hello', ['name' => '张三']); // 输出: 你好,张三

上面的代码首先创建了一个翻译工厂,然后使用数组适配器初始化了一个翻译服务。数组中的键是翻译字符串的标识符,值是对应的翻译文本。_() 方法用于获取翻译后的文本,第二个参数可以传递一个数组来替换文本中的占位符。

翻译文件管理

在实际应用中,我们不会将所有翻译字符串都硬编码在代码中,而是会将它们组织到单独的文件中。一种常见的做法是为每种语言创建一个翻译文件,然后根据用户的语言偏好加载相应的文件。

组织翻译文件

建议按照以下目录结构组织翻译文件:

app/
└── messages/
    ├── en.php
    ├── zh.php
    ├── fr.php
    └── es.php

每个文件返回一个包含翻译字符串的数组。例如,中文翻译文件 zh.php 可能如下:

<?php

// app/messages/zh.php
return [
    'welcome' => '欢迎来到我们的网站',
    'hello' => '你好,%name%',
    'current_time' => '当前时间是 %time%',
    'login' => '登录',
    'register' => '注册',
    'logout' => '退出',
    // 更多翻译...
];

对应的英文翻译文件 en.php:

<?php

// app/messages/en.php
return [
    'welcome' => 'Welcome to our website',
    'hello' => 'Hello, %name%',
    'current_time' => 'Current time is %time%',
    'login' => 'Login',
    'register' => 'Register',
    'logout' => 'Logout',
    // More translations...
];

创建翻译服务

为了在应用中方便地使用翻译功能,我们可以创建一个翻译服务,并将其注册到依赖注入容器中。这样,在控制器、视图等任何地方都可以轻松访问翻译服务。

<?php

namespace App\Services;

use Phalcon\Di\Injectable;
use Phalcon\Translate\Adapter\NativeArray;
use Phalcon\Translate\InterpolatorFactory;
use Phalcon\Translate\TranslateFactory;

class TranslateService extends Injectable
{
    /**
     * 获取翻译服务
     *
     * @param string $language
     * @return NativeArray
     */
    public function getTranslator(string $language = null): NativeArray
    {
        // 如果没有指定语言,从请求中获取最佳语言
        if (null === $language) {
            $language = $this->request->getBestLanguage();
        }

        // 简化语言代码,例如将 'zh-CN' 简化为 'zh'
        $shortLanguage = substr($language, 0, 2);

        // 翻译文件路径
        $translationFile = APP_PATH . '/messages/' . $shortLanguage . '.php';

        // 如果翻译文件不存在,使用默认语言(英语)
        if (!file_exists($translationFile)) {
            $translationFile = APP_PATH . '/messages/en.php';
        }

        // 加载翻译内容
        $messages = require $translationFile;

        // 创建翻译服务
        $interpolator = new InterpolatorFactory();
        $factory = new TranslateFactory($interpolator);

        return $factory->newInstance(
            'array',
            [
                'content' => $messages,
            ]
        );
    }
}

然后在应用的依赖注入容器中注册这个服务:

<?php

// 在di.php或bootstrap文件中
$di->setShared(
    'translate',
    function () {
        $translate = new App\Services\TranslateService();
        return $translate->getTranslator();
    }
);

现在,在控制器中可以这样使用翻译服务:

<?php

use Phalcon\Mvc\Controller;

class IndexController extends Controller
{
    public function indexAction()
    {
        // 获取翻译服务
        $t = $this->translate;

        // 使用翻译
        $this->view->welcomeMessage = $t->_('welcome');
        $this->view->helloMessage = $t->_('hello', ['name' => '张三']);
    }
}

在 Volt 模板中使用:

<h1>{{ t._('welcome') }}</h1>
<p>{{ t._('hello', ['name' => '张三']) }}</p>

日期时间本地化

处理不同语言和地区的日期和时间格式是本地化的重要部分。PHP 的 intl 扩展提供了 IntlDateFormatter 类来处理日期时间的本地化。

基本用法

<?php

use IntlDateFormatter;

// 创建日期格式化器
$formatter = new IntlDateFormatter(
    'zh_CN', // 地区
    IntlDateFormatter::FULL, // 日期格式
    IntlDateFormatter::FULL, // 时间格式
    'Asia/Shanghai', // 时区
    IntlDateFormatter::GREGORIAN // 日历类型
);

// 格式化当前时间
$now = new DateTime();
echo $formatter->format($now);
// 输出:2023年10月5日星期四 中国标准时间 15:30:45

上面的代码创建了一个中文(中国)的日期时间格式化器,使用完整的日期和时间格式,时区设置为亚洲/上海。

自定义日期时间格式

除了使用预定义的格式常量,我们还可以使用自定义格式模式:

<?php

use IntlDateFormatter;

$formatter = new IntlDateFormatter(
    'zh_CN',
    IntlDateFormatter::NONE, // 不显示日期
    IntlDateFormatter::NONE, // 不显示时间
    'Asia/Shanghai',
    IntlDateFormatter::GREGORIAN,
    'yyyy年MM月dd日 HH:mm:ss' // 自定义格式
);

$now = new DateTime();
echo $formatter->format($now); // 输出:2023年10月05日 15:30:45

在 Phalcon 视图中使用

为了方便在视图中使用本地化的日期时间格式,我们可以创建一个自定义的 Volt 函数:

<?php

// 在应用引导文件中
$view->registerEngines(
    [
        '.volt' => function ($view) use ($di) {
            $volt = new Phalcon\Mvc\View\Engine\Volt($view, $di);

            // 注册本地化日期格式化函数
            $volt->getCompiler()->addFunction(
                'localized_date',
                function ($resolvedArgs, $exprArgs) {
                    return 'App\\Helpers\\DateHelper::format(' . $resolvedArgs . ')';
                }
            );

            return $volt;
        },
    ]
);

然后创建一个日期辅助类:

<?php

namespace App\Helpers;

use IntlDateFormatter;
use Phalcon\Di\Injectable;

class DateHelper extends Injectable
{
    public static function format($timestamp, $pattern = null)
    {
        // 获取当前语言
        $language = $this->di->get('translate')->getLocale();

        $formatter = new IntlDateFormatter(
            $language,
            IntlDateFormatter::NONE,
            IntlDateFormatter::NONE,
            'Asia/Shanghai',
            IntlDateFormatter::GREGORIAN,
            $pattern ?? 'yyyy年MM月dd日 HH:mm'
        );

        return $formatter->format($timestamp);
    }
}

现在可以在 Volt 模板中直接使用这个函数:

<p>发布时间:{{ localized_date(article.created_at) }}</p>
<p>注册日期:{{ localized_date(user.registered_at, 'yyyy-MM-dd') }}</p>

货币与数字格式化

不同国家和地区有不同的数字和货币格式。例如,美国使用 1,234.56 的数字格式和 $1,234.56 的货币格式,而德国使用 1.234,56 的数字格式和 1.234,56 € 的货币格式。

数字格式化

使用 NumberFormatter 类可以轻松实现数字的本地化:

<?php

use NumberFormatter;

// 创建数字格式化器(中文/中国)
$formatter = new NumberFormatter('zh_CN', NumberFormatter::DECIMAL);
echo $formatter->format(1234567.89); // 输出:1,234,567.89

// 创建数字格式化器(德语/德国)
$formatter = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
echo $formatter->format(1234567.89); // 输出:1.234.567,89

// 创建数字格式化器(法语/法国)
$formatter = new NumberFormatter('fr_FR', NumberFormatter::DECIMAL);
echo $formatter->format(1234567.89); // 输出:1 234 567,89

货币格式化

同样,NumberFormatter 也可以用于货币格式化:

<?php

use NumberFormatter;

// 人民币格式化
$formatter = new NumberFormatter('zh_CN', NumberFormatter::CURRENCY);
echo $formatter->formatCurrency(1234.56, 'CNY'); // 输出:¥1,234.56

// 美元格式化
$formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
echo $formatter->formatCurrency(1234.56, 'USD'); // 输出:$1,234.56

// 欧元格式化
$formatter = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $formatter->formatCurrency(1234.56, 'EUR'); // 输出:1.234,56 €

百分比格式化

NumberFormatter 还支持百分比格式化:

<?php

use NumberFormatter;

// 百分比格式化
$formatter = new NumberFormatter('zh_CN', NumberFormatter::PERCENT);
echo $formatter->format(0.75); // 输出:75%

// 带两位小数的百分比
$formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 2);
echo $formatter->format(0.75); // 输出:75.00%

在 Phalcon 中集成数字格式化

为了在应用中方便使用数字格式化功能,我们可以创建一个格式化服务:

<?php

namespace App\Services;

use NumberFormatter;
use Phalcon\Di\Injectable;

class FormatService extends Injectable
{
    /**
     * 格式化数字
     *
     * @param float $number
     * @param int $style
     * @param int $precision
     * @return string
     */
    public function number($number, $style = NumberFormatter::DECIMAL, $precision = 2)
    {
        $language = $this->di->get('translate')->getLocale();
        $formatter = new NumberFormatter($language, $style);
        $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $precision);
        $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $precision);
        return $formatter->format($number);
    }

    /**
     * 格式化货币
     *
     * @param float $amount
     * @param string $currencyCode
     * @return string
     */
    public function currency($amount, $currencyCode = 'CNY')
    {
        $language = $this->di->get('translate')->getLocale();
        $formatter = new NumberFormatter($language, NumberFormatter::CURRENCY);
        return $formatter->formatCurrency($amount, $currencyCode);
    }
}

注册到依赖注入容器:

<?php

$di->setShared('format', function () {
    return new App\Services\FormatService();
});

现在可以在控制器中使用:

<?php

$this->view->price = $this->format->currency(99.99);
$this->view->discount = $this->format->number(0.2, NumberFormatter::PERCENT);

语言切换实现

允许用户手动切换语言是国际化应用的基本功能。常见的实现方式有两种:使用 URL 参数或使用 Cookie 存储用户的语言偏好。

基于 URL 的语言切换

基于 URL 的语言切换是最常见的方式,也有利于 SEO。例如:

  • https://example.com/en/about - 英文版本
  • https://example.com/zh/about - 中文版本

要实现这种方式,我们需要配置路由来识别语言参数:

<?php

use Phalcon\Mvc\Router;

$router = new Router();

// 定义带语言参数的路由组
$router->add('/{language:[a-z]{2}}/:controller/:action/:params', [
    'language' => 1,
    'controller' => 2,
    'action' => 3,
    'params' => 4
]);

// 默认路由(不带语言参数)
$router->add('/:controller/:action/:params', [
    'language' => 'zh', // 默认语言
    'controller' => 1,
    'action' => 2,
    'params' => 3
]);

return $router;

然后在基础控制器中处理语言参数:

<?php

use Phalcon\Mvc\Controller;

class ControllerBase extends Controller
{
    public function initialize()
    {
        // 从路由获取语言参数
        $language = $this->dispatcher->getParam('language');

        // 验证语言是否受支持
        $supportedLanguages = ['zh', 'en', 'fr', 'es'];
        if (!in_array($language, $supportedLanguages)) {
            $language = 'zh'; // 默认语言
        }

        // 设置翻译服务的语言
        $this->di->setShared(
            'translate',
            function () use ($language) {
                $translate = new App\Services\TranslateService();
                return $translate->getTranslator($language);
            }
        );

        // 将当前语言传递给视图
        $this->view->language = $language;
    }
}

基于 Cookie 的语言切换

另一种方式是使用 Cookie 存储用户的语言偏好。用户选择语言后,我们将语言代码存储在 Cookie 中,下次用户访问时读取 Cookie 并应用相应的语言。

<?php

// 在语言切换控制器中
class LanguageController extends ControllerBase
{
    public function switchAction()
    {
        // 获取请求的语言
        $language = $this->request->getPost('language');

        // 验证语言是否受支持
        $supportedLanguages = ['zh', 'en', 'fr', 'es'];
        if (in_array($language, $supportedLanguages)) {
            // 设置Cookie,有效期30天
            $this->cookies->set('language', $language, time() + 86400 * 30);
        }

        // 重定向回之前的页面
        $this->response->redirect($this->request->getHTTPReferer());
    }
}

然后在基础控制器中读取 Cookie:

<?php

public function initialize()
{
    // 从Cookie获取语言,默认为中文
    $language = $this->cookies->get('language', 'zh')->getValue();

    // 验证语言是否受支持
    $supportedLanguages = ['zh', 'en', 'fr', 'es'];
    if (!in_array($language, $supportedLanguages)) {
        $language = 'zh';
    }

    // 设置翻译服务...
}

语言切换组件

为了方便用户切换语言,我们可以创建一个语言切换组件,在页面上显示语言选择器:

<?php

// app/views/components/language_switcher.volt
<form action="{{ url('language/switch') }}" method="post">
    <select name="language" onchange="this.form.submit()">
        <option value="zh" {% if language == 'zh' %}selected{% endif %}>中文</option>
        <option value="en" {% if language == 'en' %}selected{% endif %}>English</option>
        <option value="fr" {% if language == 'fr' %}selected{% endif %}>Français</option>
        <option value="es" {% if language == 'es' %}selected{% endif %}>Español</option>
    </select>
</form>

然后在布局文件中包含这个组件:

// app/views/index.volt
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{{ t._('site_title') }}</title>
</head>
<body>
    <div class="header">
        {% include 'components/language_switcher' %}
        <!-- 其他头部内容 -->
    </div>
    <div class="content">
        {{ content() }}
    </div>
</body>
</html>

高级国际化技巧

复数规则

不同语言有不同的复数规则。例如,英语有单数和复数两种形式,而阿拉伯语有六种不同的复数形式。使用 MessageFormatter 可以处理复杂的复数规则:

<?php

use MessageFormatter;

// 英语复数规则
$formatter = new MessageFormatter('en_US', '{0, plural, =0{No items} =1{One item} other{# items}}');
echo $formatter->format([0]); // 输出:No items
echo $formatter->format([1]); // 输出:One item
echo $formatter->format([5]); // 输出:5 items

// 阿拉伯语复数规则
$formatter = new MessageFormatter('ar_SA', '{0, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{# عناصر} many{# عنصر} other{# عنصر}}');
echo $formatter->format([0]); // 输出:لا عناصر
echo $formatter->format([1]); // 输出:عنصر واحد
echo $formatter->format([2]); // 输出:عنصران
echo $formatter->format([5]); // 输出:5 عناصر
echo $formatter->format([11]); // 输出:11 عنصر

字符串排序

不同语言有不同的字符排序规则。使用 Collator 类可以实现本地化的字符串排序:

<?php

use Collator;

// 创建中文排序器
$collator = new Collator('zh_CN');
$strings = ['苹果', '香蕉', '橙子', '西瓜', '草莓'];
$collator->sort($strings);
print_r($strings);
// 输出:Array ( [0] => 苹果 [1] => 草莓 [2] => 西瓜 [3] => 橙子 [4] => 香蕉 )

// 创建德语排序器
$collator = new Collator('de_DE');
$strings = ['Apfel', 'Banane', 'Orange', 'Erdbeere', 'Wassermelone'];
$collator->sort($strings);
print_r($strings);
// 输出:Array ( [0] => Apfel [1] => Banane [2] => Erdbeere [3] => Orange [4] => Wassermelone )

文本转写

Transliterator 类可以将文本从一种脚本转写到另一种脚本,例如将中文转换为拼音,或将日文转换为罗马字:

<?php

use Transliterator;

// 创建转写器:将中文转换为拼音
$transliterator = Transliterator::create('Han-Latin; Latin-ASCII; Lower()');
$chinese = '我爱编程';
echo $transliterator->transliterate($chinese); // 输出:wo ai bian cheng

// 将日文转换为罗马字
$transliterator = Transliterator::create('Hiragana-Latin; Katakana-Latin; Latin-ASCII; Lower()');
$japanese = '私はプログラミングが好きです';
echo $transliterator->transliterate($japanese); // 输出:watashi ha puroguramingu ga suki desu

国际化最佳实践

1. 规划国际化架构

在项目初期就应该规划国际化架构,而不是后期再添加。这包括设计翻译文件结构、选择合适的翻译存储方式(数组、数据库等)以及确定语言切换机制。

2. 使用统一的翻译键

使用有意义且一致的翻译键命名约定,例如:

// 好的做法
'button.login' => '登录',
'button.register' => '注册',
'message.welcome' => '欢迎来到我们的网站',

// 不好的做法
'login_btn' => '登录',
'register' => '注册',
'welcomemsg' => '欢迎来到我们的网站',

3. 避免硬编码文本

确保应用中所有用户可见的文本都通过翻译服务输出,避免在代码中硬编码文本。

4. 考虑 RTL 语言

有些语言(如阿拉伯语、希伯来语)是从右到左书写的。在设计 UI 时要考虑这一点,确保应用能够正确处理 RTL 布局。

5. 测试多种语言

不要假设翻译后的文本长度与原文本相同。有些语言比其他语言更冗长,可能会导致布局问题。测试所有支持的语言,确保 UI 在各种语言下都能正常显示。

6. 使用专业翻译

机器翻译通常不够准确,特别是对于技术术语和复杂句子。考虑使用专业翻译服务来翻译应用内容。

7. 缓存翻译数据

翻译数据通常不会频繁变化,因此可以将翻译数据缓存起来,提高应用性能:

<?php

public function getTranslator(string $language = null): NativeArray
{
    $language = $language ?? 'zh';
    $cacheKey = 'translate_' . $language;

    // 尝试从缓存获取
    if ($this->cache->has($cacheKey)) {
        return $this->cache->get($cacheKey);
    }

    // 加载翻译文件...

    // 存入缓存,有效期1小时
    $this->cache->set($cacheKey, $translator, 3600);

    return $translator;
}

总结

国际化和本地化是构建全球应用的关键部分。Phalcon 框架虽然没有提供完整的国际化组件,但通过与 PHP intl 扩展的集成,我们可以轻松实现强大的国际化功能。

本章介绍了如何使用 Phalcon 的翻译组件来管理多语言内容,如何格式化日期、时间、数字和货币,以及如何实现语言切换功能。我们还探讨了一些高级国际化技巧和最佳实践。

通过合理规划和实施国际化策略,我们可以构建一个能够无缝适应不同语言和文化的应用,为全球用户提供出色的体验。