做过多语言网站的开发者大概都遇到过这样的问题:界面文字翻译好了,但德语用户看到的日期还是美式格式,法语用户看到的数字千分位符号不对,阿拉伯语的排序完全是乱序的。这类问题靠gettext这种纯文本翻译方案是解决不了的,因为它们涉及的不是“翻译”而是“本地化格式”。PHP的intl扩展正是为解决这类问题而生的,它基于ICU(International Components for Unicode)库,提供了数字、货币、日期、字符串排序、复数消息等一整套本地化处理能力。这篇文章就来详细讲讲intl扩展的使用方法。

什么是intl扩展,为什么它比gettext更适合格式本地化
intl是PHP的一个官方扩展,底层封装了IBM开发的ICU库。ICU是目前业界最完善的Unicode和全球化支持库之一,Java的国际化能力、Chrome浏览器的日期格式化,背后都是同一套ICU。这意味着intl扩展处理本地化的规则数据和操作系统、浏览器保持高度一致,不会出现服务器和前端显示不一致的尴尬情况。
很多初学者会把gettext和intl混为一谈,其实两者定位完全不同。gettext解决的是“文案翻译”问题,即把一句英文界面对应成一句中文,它是纯字符串替换。而intl解决的是“格式差异”问题,比如同样是数字1234567.89,德语要显示为1.234.567,89,法语要显示为1 234 567,89,这种规则无法靠翻译实现,只能靠格式化器来处理。实际项目中,两者往往是配合使用的:gettext负责文案,intl负责格式。当然,如果只需要轻量的文案翻译,也可以不用gettext,直接用PHP数组定义语言包,然后专心用intl处理格式问题。
需要特别说明的是,PHP 8之后intl扩展默认支持的ICU版本更高,对相对时间格式化(如“3天前”)、日期区间格式化等新特性支持更好,建议尽量使用较新的PHP版本。
安装启用intl扩展与基础环境检查
intl扩展不是默认启用的,需要手动安装。Linux下如果用apt,可以执行安装命令,包名一般是php-intl或php版本对应的包,例如php8.1-intl。如果用yum,则可能是php-intl。Windows环境下的PHP发行包已经自带了php_intl.dll,只需要在php.ini中找到extension=intl这一行,去掉前面的分号注释即可。修改后重启Web服务器或php-fpm进程。
安装完成后,可以通过两种方式验证。第一种是查看phpinfo输出中是否有intl段落;第二种更直接,在命令行执行一段检查代码:
<?php
// 检查intl扩展是否加载
if (!extension_loaded('intl')) {
die('intl扩展未安装,请检查php.ini配置');
}
// 查看ICU版本,版本越高支持的CLDR数据越新
echo 'ICU版本: ' . INTL_ICU_VERSION . PHP_EOL;
echo 'ICU数据版本: ' . INTL_ICU_DATA_VERSION . PHP_EOL;
// 输出当前默认区域设置
echo '默认locale: ' . ini_get('intl.default_locale') . PHP_EOL;</code>
<?php
if (!extension_loaded('intl')) {
die('intl扩展未安装');
}
echo 'ICU版本: ' . INTL_ICU_VERSION . PHP_EOL;
echo '默认locale: ' . ini_get('intl.default_locale') . PHP_EOL;这段代码会输出ICU库的版本号和默认locale。ICU版本很重要,因为不同版本的CLDR数据(公共区域数据仓库)对各国格式的定义会有细微更新。如果发现格式化结果和预期不符,可以先排查是不是ICU版本太旧导致的。
除了php.ini中的intl.default_locale配置,代码里也可以用locale_set_default()动态设置默认区域,或者在使用各个Formatter类时显式传入locale字符串,比如zh_CN、en_US、de_DE。建议的做法是根据用户的语言偏好动态传入,而不是依赖全局默认值,这样多语言切换时更灵活。
用NumberFormatter格式化数字和货币
数字格式是本地化中最容易被忽视的一环。很多人以为用number_format函数加上千分位就够了,但这个函数只能按固定规则格式化,无法适应各国差异。NumberFormatter则可以根据locale自动选择小数点符号、千分位分隔符甚至数字书写系统。
<?php
// 同一个数字在不同locale下的显示效果
$number = 1234567.891;
$fmtDe = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
echo $fmtDe->format($number); // 输出: 1.234.567,891
$fmtFr = new NumberFormatter('fr_FR', NumberFormatter::DECIMAL);
echo $fmtFr->format($number); // 输出: 1 234 567,891(使用窄空格分隔)
$fmtZh = new NumberFormatter('zh_CN', NumberFormatter::DECIMAL);
echo $fmtZh->format($number); // 输出: 1,234,567.891
// 百分比格式化
$fmtPct = new NumberFormatter('en_US', NumberFormatter::PERCENT);
echo $fmtPct->format(0.256); // 输出: 25.6%货币格式化是电商和支付场景的硬需求。注意货币格式化时不要自己拼接货币符号,因为不同语言中符号的位置、空格都有讲究,比如美元符号在英语中放在数字前面,而某些语言中货币代码会放在后面。正确做法是传入ISO 4217货币代码,让Formatter决定怎么展示:
<?php
// 货币格式化,第二参数直接给数值
$fmtUsd = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
echo $fmtUsd->formatCurrency(99.5, 'USD'); // 输出: $99.50
$fmtEur = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $fmtEur->formatCurrency(99.5, 'EUR'); // 输出: 99,50 €
$fmtCny = new NumberFormatter('zh_CN', NumberFormatter::CURRENCY);
echo $fmtCny->formatCurrency(99.5, 'CNY'); // 输出: ¥99.50
// 需要会计格式或去除小数位时,可以设置属性
$fmtCny->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 0);
$fmtCny->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
echo $fmtCny->formatCurrency(99.5, 'CNY'); // 输出: ¥100(四舍五入)NumberFormatter还支持解析功能,即把本地化格式的字符串反向解析成数值,这在处理用户输入时很有用。比如德国用户输入"1.234,50",用parse()方法配合de_DE的formatter就能正确得到1234.5这个浮点数。
用IntlDateFormatter处理日期时间本地化
日期格式的本地化差异比数字更复杂。美式习惯是月在前,中文习惯年月日,波斯历、佛历等非公历的需求虽然少见但真实存在。IntlDateFormatter可以根据locale自动输出符合当地习惯的日期格式:
<?php
$ts = strtotime('2024-06-15 14:30:00');
// 使用预定义格式,相对完整格式
$fmtEn = new IntlDateFormatter('en_US', IntlDateFormatter::LONG, IntlDateFormatter::SHORT);
echo $fmtEn->format($ts); // 输出: June 15, 2024 at 2:30 PM
$fmtZh = new IntlDateFormatter('zh_CN', IntlDateFormatter::LONG, IntlDateFormatter::SHORT);
echo $fmtZh->format($ts); // 输出: 2024年6月15日 下午2:30
$fmtDe = new IntlDateFormatter('de_DE', IntlDateFormatter::LONG, IntlDateFormatter::SHORT);
echo $fmtDe->format($ts); // 输出: 15. Juni 2024 um 14:30
// 使用自定义模式
$fmtCustom = new IntlDateFormatter('zh_CN', IntlDateFormatter::FULL, IntlDateFormatter::NONE, 'Asia/Shanghai', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd EEEE');
echo $fmtCustom->format($ts); // 输出: 2024-06-15 星期六构造函数的第5个参数可以指定时区,第6个参数是自定义模式串。这里要注意,intl的模式语法和date函数的格式符不一样,比如intl中yyyy表示年份、EEEE表示完整的星期名,而date函数里是大写Y和小写w,写混了很容易出错,建议对照ICU的日期模式文档来写。
另一个实用技巧是IntlDateFormatter支持对象格式化,可以直接传入DateTime对象,也可以传入时间戳。如果项目里已经有DateTime实例,直接传给format方法即可,不需要先转成时间戳。解析同样支持,parse()方法可以把本地化的日期字符串转成时间戳,配合localtime()相关的错误码可以处理用户提交的本地化日期输入。
用Collator实现符合语言习惯的字符串排序
排序是纯PHP函数完全搞不定的领域。用sort函数对包含重音字符的字符串排序时,结果是按字节码排的,法语中的é会被排到z后面,这在法语用户看来完全是乱序。Collator按各语言的排序规则来比较字符串:
<?php
$words = ['éclair', 'banana', 'École', 'apple', 'zèbre'];
// 普通sort按字节排序,重音字符会排到最后
sort($words);
print_r($words); // apple, banana 排前面,éclair等排后面,且大小写分离
// 用Collator按法语规则排序
$coll = new Collator('fr_FR');
$coll->asort($words);
print_r($words); // apple, banana, éclair, École, zèbre 符合法语习惯
// 也可以作为回调用于usort
$data = [['name' => 'Müller'], ['name' => 'Maier'], ['name' => 'Meyer']];
$collDe = new Collator('de_DE');
usort($data, function ($a, $b) use ($collDe) {
return $collDe->compare($a['name'], $b['name']);
});Collator还提供了一些强度设置。默认强度下比较时会忽略大小写差异,如果需要区分大小写,可以调用setStrength()并传入Collator::PRIMARY、SECONDARY、TERTIARY等常量,级别越高比较越严格。此外setAttribute(Collator::NUMERIC_COLLATION, Collator::ON)可以开启数字感知排序,让file2排在file10前面,而不是按字符串顺序把file10排在file2前面,这对文件列表排序很实用。
用MessageFormatter处理复数和性别语法
翻译句子时最麻烦的是语法变化。中文说“1条消息”“5条消息”,量词不变,但英文是1 message和5 messages,俄语和阿拉伯语的变化规则更复杂。如果靠if判断数字来拼字符串,每种语言都要写一套逻辑,根本无法维护。MessageFormatter采用ICU消息格式语法,把复数规则交给CLDR数据自动处理:
<?php
// 复数处理
$pattern = '您有 {num, plural, =0 {没有新消息} =1 {一条新消息} other {# 条新消息}}';
echo MessageFormatter::formatMessage('zh_CN', $pattern, ['num' => 0]); // 您有 没有新消息
echo MessageFormatter::formatMessage('zh_CN', $pattern, ['num' => 5]); // 您有 5 条新消息
$patternEn = 'You have {num, plural, =0 {no messages} =1 {one message} other {# messages}}';
echo MessageFormatter::formatMessage('en_US', $patternEn, ['num' => 5]); // You have 5 messages
// select处理性别语法
$gender = 'You invited {name} to {gender, select, male {his} female {her} other {their}} party.';
echo MessageFormatter::formatMessage('en_US', $gender, ['name' => 'Anna', 'gender' => 'female']);
// 输出: You invited Anna to her party.plural语法中的=0、=1是精确匹配,other是兜底分支,井号#代表数字本身。不同语言的复数类别不同,俄语有one、few、many、other四种类别,ICU会根据数字自动选择对应分支,开发者只需要为每种类别提供译文即可,判断逻辑完全不用自己写。
MessageFormatter还可以嵌套数字和日期格式化,比如{count, number, integer}会自动调用数字格式化规则,一个消息模板就能覆盖变量翻译和格式化两件事。需要注意的是,MessageFormatter的静态方法formatMessage在语法错误时会返回false,生产环境建议用实例化方式配合getErrorMessage()做错误排查。
总结与方案选型建议
综合来看,一套完整的PHP多语言方案应该是三层结构:文案翻译用gettext或PHP数组语言包,格式处理用intl扩展的各个Formatter类,语言协商则通过解析Accept-Language请求头结合locale_accept_from_http()函数实现。intl扩展中的NumberFormatter、IntlDateFormatter、Collator、MessageFormatter各司其职,覆盖了数字、货币、日期、排序、语法变化这几类最常见的本地化需求。
实践中还有几点经验值得注意。首先是Formatter实例的创建有一定开销,高频调用场景下应该把实例缓存起来复用,而不是每次请求都new。其次是locale字符串要规范化,zh-CN和zh_CN在多数场景可以互相识别,但混用容易埋坑,建议统一用下划线格式。最后,格式化结果依赖ICU版本,如果有多台服务器,务必保证各台的ICU版本一致,否则不同机器渲染出来的格式可能有细微差异,排查起来非常痛苦。把这些细节处理好,你的PHP应用就能真正做到面向全球用户了。
PHP intl扩展PHP多语言gettext本地化修改时间:2026-09-04 21:03:01