一、先搞懂两个容易混淆的核心概念

很多刚接触多语言开发的朋友,会把“国际化”和“本地化”当成一回事,其实两者是完全不同的阶段,搞反了会直接踩坑。先给大家用大白话讲清楚: 国际化(简称i18n)是“通用化”的过程——比如把网站里的中文“登录”、“注册”,换成一个可以被替换的占位符,不管以后要加多少种语言,都不用改核心代码;本地化(简称l10n)是“适配化”的过程——比如给占位符配好中文的“登录”、英文的“Login”、日文的“ログイン”,还要处理日期、数字格式(比如中国用2024-10-01,美国用10/01/2024)这些地域相关的细节。

举个最常见的反例:如果一开始直接把“登录”写死在代码里,后来要加英文,就得把所有写死的地方找出来替换,改完还容易漏,改代码的风险也高;而用国际化的思路,只需要改语言配置文件,核心代码不用动,效率差好几倍。

二、Symfony的基础配置:先搭好多语言的架子

Symfony自带了专门的国际化组件,不用额外装第三方包,配置起来也简单,只要按步骤来就行。这里先给大家明确技术栈:所有示例都用Symfony 6.4(LTS版本,稳定)+ PHP 8.2,确保大家跑的时候不会有版本兼容问题。

2.1 先配置支持的语言和默认语言

首先要改Symfony的核心配置文件,告诉系统我们支持哪几种语言,默认用哪一种。这个文件在config/services.yaml里,找到parameters节点,加两行配置:

# config/services.yaml
parameters:
    # 支持的语言列表,按优先级排序,第一个是默认语言
    supported_locales: ['zh_CN', 'en_US', 'ja_JP']
    # 浏览器请求的语言匹配时,只匹配前两位(比如zh对应zh_CN,en对应en_US)
    locale_fallback: 'zh_CN'

这里的语言格式是“语言代码_地域代码”,比如zh_CN是中文(中国),en_US是英文(美国),ja_JP是日文(日本),别写错格式,不然会匹配失败。

然后还要改config/packages/translation.yaml,开启翻译组件的核心功能:

# config/packages/translation.yaml
framework:
    default_locale: '%locale_fallback%' # 引用上面配置的默认语言
    translator:
        fallbacks: ['%locale_fallback%'] # 翻译找不到时的回退语言
        paths:
            - '%kernel.project_dir%/translations' # 翻译文件的存放目录

2.2 准备翻译文件:把所有要替换的内容都放这里

翻译文件专门存在translations目录下,每个语言一个文件,命名规则是messages.语言代码.格式,比如中文的翻译文件是messages.zh_CN.yaml,英文的是messages.en_US.yaml,这里用YAML格式,比PHP数组更直观。

先给大家做一个完整的翻译文件示例,包含常用的页面内容、按钮文字、提示信息:

# translations/messages.zh_CN.yaml
# 翻译文件格式:键 = 值,键是我们在代码里用的占位符,值是对应语言的内容
home:
    title: 欢迎来到我的网站
    content: 这是网站的首页内容
    welcome: 你好,%username%! # 带变量的翻译,后面会讲怎么传变量
button:
    login: 登录
    register: 注册
    logout: 退出登录
error:
    404: 抱歉,你访问的页面不存在
    permission: 你没有权限访问这个页面
# translations/messages.en_US.yaml
home:
    title: Welcome to my website
    content: This is the content of the homepage
    welcome: Hello, %username%!
button:
    login: Login
    register: Register
    logout: Logout
error:
    404: Sorry, the page you visited does not exist
    permission: You do not have permission to access this page
# translations/messages.ja_JP.yaml
home:
    title: 私のウェブサイトへようこそ
    content: これはホームページの内容です
    welcome: こんにちは、%username%!
button:
    login: ログイン
    register: 登録
    logout: ログアウト
error:
    404: 申し訳ありません、アクセスしたページが存在しません
    permission: このページにアクセスする権限がありません

三、多语言内容渲染:代码里怎么用翻译

配置完翻译文件,接下来就是在代码里替换写死的内容,Symfony的翻译组件支持在控制器、Twig模板(Symfony的默认模板引擎)、甚至实体类里用,这里给大家做最常用的两种场景的示例。

3.1 在Twig模板里渲染翻译

Twig模板是Symfony开发网站最常用的模板,翻译的语法很简单,用{{ '翻译键'|trans }}就可以,带变量的话用{{ '翻译键'|trans({'变量名': '变量值'}) }}。

给大家做一个完整的首页模板示例,路径是templates/home/index.html.twig:

{# templates/home/index.html.twig #}
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    {# 页面标题用翻译,直接写翻译键 #}
    <title>{{ 'home.title'|trans }}</title>
</head>
<body>
    {# 欢迎语带变量,把用户名传进去 #}
    <h1>{{ 'home.welcome'|trans({'%username%': user.username}) }}</h1>
    <p>{{ 'home.content'|trans }}</p>
    <div class="button-group">
        {# 按钮文字用翻译 #}
        <button>{{ 'button.login'|trans }}</button>
        <button>{{ 'button.register'|trans }}</button>
    </div>
</body>
</html>

这里的user.username是从控制器传过来的变量,比如控制器里给模板传$user = ['username' => '张三'],那么中文环境下就会显示“你好,张三!”,英文环境下显示“Hello, 张三!”,变量是直接传递的,不会被翻译。

3.2 在控制器里渲染翻译

有时候需要在控制器里生成内容,比如返回JSON接口、或者给日志写内容,这时候可以用翻译服务,Symfony的翻译服务可以直接注入到控制器里。

给大家做一个控制器的示例,路径是src/Controller/HomeController.php:

<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Translation\TranslatorInterface; // 翻译服务的接口
use Symfony\Component\HttpFoundation\JsonResponse;

class HomeController extends AbstractController
{
    // 注入翻译服务,Symfony会自动实例化
    public function __construct(
        private TranslatorInterface $translator
    ) {}

    public function index()
    {
        // 控制器里获取当前语言,比如zh_CN、en_US
        $currentLocale = $this->translator->getLocale();

        // 控制器里翻译内容,第一个参数是翻译键,第二个是变量数组,第三个是翻译域名(默认是messages)
        $welcomeText = $this->translator->trans(
            'home.welcome',
            ['%username%' => '张三']
        );

        // 把变量传给模板
        return $this->render('home/index.html.twig', [
            'user' => ['username' => '张三'],
            'current_locale' => $currentLocale
        ]);
    }

    // 接口示例,返回JSON格式的翻译内容
    public function getErrorJson()
    {
        $errorText = $this->translator->trans('error.404');
        return new JsonResponse([
            'code' => 404,
            'message' => $errorText
        ]);
    }
}

这里要注意,控制器里的翻译会自动匹配当前请求的语言,不用额外传语言参数,Symfony会根据请求头的Accept-Language或者用户选择的语言自动判断。

四、常见问题处理:踩过的坑都给大家列出来

配置完翻译,很多人会遇到各种问题,比如翻译不生效、变量传错、语言切换失败,这里给大家讲最常见的几个问题的解决方法。

4.1 翻译不生效的排查步骤

翻译不生效是最常见的问题,很多人改了翻译文件,刷新页面还是显示原来的内容,这时候按下面的步骤排查:

  1. 检查翻译键有没有写错:比如翻译文件里的键是home.title,代码里写成home.titles(多了个s),肯定找不到;
  2. 检查翻译文件的命名格式:必须是messages.zh_CN.yaml,不能写成zh_CN.messages.yaml,顺序错了会识别不到;
  3. 清除Symfony的缓存:Symfony会把翻译文件缓存起来,改完翻译文件必须清缓存,命令是:
# 清除缓存,dev环境用这个,prod环境用prod参数
php bin/console cache:clear --env=dev
  1. 检查支持的语言列表:如果加了新的语言,比如fr_FR,必须加到config/services.yaml的supported_locales里,不然会被过滤掉。

4.2 带变量的翻译内容乱码或者不显示

有时候传了变量,结果显示的是“你好,%username%!”,而不是“你好,张三!”,这时候检查两个地方:

  1. 翻译文件里的变量格式是不是%变量名%,注意是百分号包裹,不是大括号或者其他符号;
  2. 代码里传的变量名和翻译文件里的是不是一致,比如翻译文件里是%username%,代码里传的是{'user_name': '张三'},变量名不一致,就会匹配失败。

4.3 日期、数字的本地化适配

除了文字翻译,还有日期、数字这些需要本地化的内容,比如中国的日期是“2024年10月1日”,美国的是“October 1, 2024”,数字的话中国用千分位“1,234.56”,有些国家用“1.234,56”。

Symfony的Intl组件专门处理这个,需要先安装:

# 安装Intl组件,处理日期、数字的本地化
composer require symfony/intl

然后在Twig模板里用专门的过滤器,给大家做一个示例:

{# 日期本地化,格式是long(长格式),自动匹配当前语言 #}
<p>日期:{{ '2024-10-01'|format_date('long') }}</p>
{# 数字本地化,格式是currency(货币),自动匹配当前语言 #}
<p>价格:{{ 1234.56|format_currency('CNY') }}</p>

中文环境下会显示: 日期:2024年10月1日 价格:¥1,234.56 英文环境下会显示: 日期:October 1, 2024 价格:$1,234.56 完全不用自己写格式,Symfony会自动处理。

五、应用场景、优缺点和注意事项

5.1 应用场景

Symfony的国际化配置适合所有需要多语言支持的项目,比如:

  • 面向全球用户的网站,比如跨境电商、海外社交平台;
  • 面向国内多地域的系统,比如给台湾、香港用户做繁体中文适配;
  • 企业内部系统,支持多语言切换,方便不同国家的员工使用;
  • 多语言接口,比如给海外APP提供中文、英文、日文的接口内容。

5.2 技术优缺点

优点

  1. 原生支持:Symfony自带翻译组件,不用额外装第三方包,稳定性高;
  2. 配置简单:只需要改几个配置文件,写翻译文件,就能实现多语言;
  3. 灵活:支持在控制器、模板、实体类、接口里用翻译,适配各种场景;
  4. 缓存机制:翻译文件会被缓存,不会影响网站性能;
  5. 本地化支持:自带Intl组件,处理日期、数字、货币这些内容,不用自己写逻辑。

缺点

  1. 翻译文件管理麻烦:如果项目很大,翻译文件会越来越多,容易出现翻译键重复、翻译内容不一致的问题;
  2. 动态内容翻译难:比如用户上传的内容(比如评论、文章),Symfony的翻译组件是静态翻译,不能实时翻译动态内容;
  3. 语言切换逻辑需要自己实现:Symfony只提供翻译功能,语言切换的逻辑(比如用户点击按钮切换语言,保存用户的语言偏好)需要自己写;
  4. 开发成本:需要把所有写死的内容都替换成翻译键,初期开发时间会增加。

5.3 注意事项

  1. 翻译键要统一命名:比如所有按钮的翻译键都用button.xxx,所有页面标题都用home.xxx,方便管理;
  2. 不要用特殊字符当翻译键:比如空格、中文,容易出现匹配问题;
  3. 翻译内容要准确:比如英文的“login”和“sign in”的区别,要根据场景准确翻译;
  4. 测试所有语言:改完翻译文件后,要切换到所有支持的语言测试,确保内容正确;
  5. 缓存问题:改完翻译文件一定要清缓存,不然不会生效;
  6. 权限问题:翻译文件的权限要和Symfony的项目权限一致,不然会出现读取失败的问题。

六、文章总结

Symfony的国际化与本地化配置,核心是先把内容通用化(国际化),再适配不同的语言和地域(本地化)。整个流程其实很简单:先配置支持的语言,再写翻译文件,然后在代码里用翻译键替换写死的内容,最后处理日期、数字这些本地化内容。

虽然过程中会遇到翻译不生效、变量传错、语言切换失败这些问题,但只要按步骤排查,都能解决。对于需要多语言支持的项目,用Symfony的原生翻译组件,比自己写逻辑要稳定很多,也能节省开发时间。

最后要提醒大家,多语言开发不是一次性的工作,后期还要维护翻译文件,比如新增内容、修改翻译内容,所以初期做好翻译文件的管理,会给后期维护节省很多时间。