PHP 的国际化与本地化

v1.1.0 2020-08-27 21:05 UTC

This package is auto-updated.

Last update: 2024-09-16 17:39:40 UTC


README

PHP 的国际化与本地化

为您的应用程序提供多语言支持,向不同国家和地区的用户提供不同格式和惯例。

要求

  • PHP 5.6.0+
    • GNU gettext 扩展 (gettext)
    • 国际化扩展 (intl)

注意:在 Windows 上,您可能需要使用非线程安全 (NTS) 版本的 PHP。

安装

  1. 通过 Composer 包含库 [?]

    $ composer require delight-im/i18n
  2. 包含 Composer 自动加载器

    require __DIR__ . '/vendor/autoload.php';

用法

什么是区域设置?

简单来说,区域设置是一组用户偏好和期望,在全球更大的社区中共享,并因地理区域而异。值得注意的是,这包括用户的语言以及他们期望数字、日期和时间的格式。

确定支持的初始区域设置集

无论您最初决定支持哪种语言、脚本和区域,您都可以在任何后续时间添加或删除区域设置。因此,您可能希望从仅1-3个区域设置开始,以更快地入门。

您可以在 Codes 类中找到各种区域设置代码列表,并使用相应的常量来引用区域设置,这是推荐解决方案。或者,您也可以复制它们的字符串值,这些值使用 IETF BCP 47 (RFC 5646) 或 Unicode CLDR 标识符的子集。

在使用初始语言集之前,请确保它们已安装在任何您想要开发或部署应用程序的机器上,并确保操作系统已识别它们

$ locale -a

请确保注意操作系统使用的区域设置名称的确切语法,特别是连字符、下划线和后缀,例如 en-USen_US

如果尚未安装某些区域设置,您可以像以下示例中的 es-AR 区域设置一样添加它

$ sudo locale-gen es_AR
$ sudo locale-gen es_AR.UTF-8
$ sudo update-locale
$ sudo service apache2 restart

注意:在类Unix操作系统中,安装期间使用的区域设置代码必须使用下划线。

创建新实例

为了创建 I18n 类的实例,只需提供您支持的区域设置集。唯一特殊的是第一个区域设置,如果没有更好的匹配项,它还充当默认区域设置。

$i18n = new \Delight\I18n\I18n([
    \Delight\I18n\Codes::EN_US,
    \Delight\I18n\Codes::DA_DK,
    \Delight\I18n\Codes::ES,
    \Delight\I18n\Codes::ES_AR,
    \Delight\I18n\Codes::KO,
    \Delight\I18n\Codes::KO_KR,
    \Delight\I18n\Codes::RU_RU,
    \Delight\I18n\Codes::SW
]);

翻译文件的目录和文件名

您的翻译文件将稍后存储在以下位置

locale/<LOCALE_CODE>/LC_MESSAGES/messages.po

例如,可以使用 es-ES 区域设置

locale/es_ES/LC_MESSAGES/messages.po

如果您需要更改 locale 目录的路径或希望使用该目录的不同名称,只需明确指定其路径即可

$i18n->setDirectory(__DIR__ . '/../translations');

LC_MESSAGES 目录中的文件名,即 messages.po,是应用程序模块的名称,带有 PO(可移植对象)文件的扩展名。通常不需要更改它,但如果您仍然想这样做,只需调用以下方法

$i18n->setModule('messages');

注意:在类Unix操作系统中,目录名中使用的区域设置代码必须使用下划线,而在Windows中,代码必须使用连字符。

为用户激活正确的区域设置

自动

选择最适合用户的区域设置的最简单方法是让此库根据各种信号和选项自动决定

$i18n->setLocaleAutomatically();

它将根据以下因素(按此顺序)进行检查和决定

  1. 子域(带区域代码,例如 da-DK.example.com

    注意:子域中的区域代码(最左侧的子域)不区分大小写,即 da-dk 也有效,并且您可以省略区域或脚本名称,即仅 da 就足够了。

  2. 路径前缀(带区域代码,例如 http://www.example.com/pt-BR/welcome.html

    注意:路径前缀中的区域代码不区分大小写,即 pt-br 也有效,并且您可以省略区域或脚本名称,即仅 pt 就足够了。

  3. 查询字符串(带区域代码)

    1. locale 参数
    2. language 参数
    3. lang 参数
    4. lc 参数
  4. 通过 I18n#setSessionField 定义的 会话字段(例如 $i18n->setSessionField('locale');

  5. Cookie(通过 I18n#setCookieName 定义,例如 $i18n->setCookieName('lc');),可选的生存期通过 I18n#setCookieLifetime 定义(例如 $i18n->setCookieLifetime(60 * 60 * 24);),其中 null 的值表示 Cookie 在当前浏览器会话结束时过期

  6. HTTP 请求头 Accept-Language(例如 en-US,en;q=0.5

您通常会选择其中一个选项来存储和传输区域代码,其他因素(特别是最后一个)作为后备选项。前三个选项(以及最后一个)在搜索引擎优化(SEO)和缓存方面可能提供优势。

手动

当然,您也可以手动指定用户的区域设置

try {
    $i18n->setLocaleManually('es-AR');
}
catch (\Delight\I18n\Throwable\LocaleNotSupportedException $e) {
    die('The locale requested by the user is not supported');
}

启用翻译别名

在您的应用程序代码中设置以下别名,以简化您与此库的工作,使您的代码更易于阅读,并支持包含的工具和其他 GNU gettext 工具

function _f($text, ...$replacements) { global $i18n; return $i18n->translateFormatted($text, ...$replacements); }

function _fe($text, ...$replacements) { global $i18n; return $i18n->translateFormattedExtended($text, ...$replacements); }

function _p($text, $alternative, $count) { global $i18n; return $i18n->translatePlural($text, $alternative, $count); }

function _pf($text, $alternative, $count, ...$replacements) { global $i18n; return $i18n->translatePluralFormatted($text, $alternative, $count, ...$replacements); }

function _pfe($text, $alternative, $count, ...$replacements) { global $i18n; return $i18n->translatePluralFormattedExtended($text, $alternative, $count, ...$replacements); }

function _c($text, $context) { global $i18n; return $i18n->translateWithContext($text, $context); }

function _m($text) { global $i18n; return $i18n->markForTranslation($text); }

如果包含全局 I18n 实例的变量不是命名为 $i18n,则当然必须相应地调整上述代码片段中每个 $i18n 的出现。

识别、标记和格式化可翻译字符串

为了国际化您的代码库,您必须识别和标记可翻译的字符串,并使用更复杂的字符串进行格式化。之后,这些标记的字符串可以自动提取,在代码外进行翻译,并在运行时由此库再次插入。

通常,您应该在标记字符串进行翻译时遵循以下简单规则

  • 尽可能使用尽可能大的文本单元。这可能是一个单词(例如按钮上的“保存”),几个单词(例如标题中的“创建新账户”),或者完整的句子(例如“您的账户已创建。”)。
  • 尽可能将整个句子视为原子单元,除非绝对必要,否则不要从多个翻译过的单词或部分组成句子。
  • 使用专用函数和方法进行字符串格式化,而不是求助于字符串连接或字符串插值。
  • 使用专用函数和方法处理单数和复数形式,这对于具有复杂复数规则的语言也很有效,这些规则并不总是像英语的二进制规则那样简单。

基本字符串

将用户界面的句子、短语和标签包装在_函数内部

_('Welcome to our online store!');
// Welcome to our online store!
_('Create account');
// Create account
_('You have been successfully logged out.');
// You have been successfully logged out.

带有格式的字符串

将用户界面的句子、短语和标签包装在_f函数内部

_f('This is %1$s.', 'Bob');
// This is Bob.
_f('This is %1$d.', 3);
// This is 3.
_f('This is %1$05d.', 3);
// This is 00003.
_f('This is %1$ 5d.', 3);
// This is     3.
// This is ␣␣␣␣3.
_f('This is %1$+d.', 3);
// This is +3.
_f('This is %1$+06d.', 3);
// This is +00003.
_f('This is %1$+ 6d.', 3);
// This is     +3.
// This is ␣␣␣␣+3.
_f('This is %1$f.', 3.14);
// This is 3.140000.
_f('This is %1$012f.', 3.14);
// This is 00003.140000.
_f('This is %1$010.4f.', 3.14);
// This is 00003.1400.
_f('This is %1$ 12f.', 3.14);
// This is     3.140000.
// This is ␣␣␣␣3.140000.
_f('This is %1$ 10.4f.', 3.14);
// This is     3.1400.
// This is ␣␣␣␣3.1400.
_f('This is %1$+f.', 3.14);
// This is +3.140000.
_f('This is %1$+013f.', 3.14);
// This is +00003.140000.
_f('This is %1$+011.4f.', 3.14);
// This is +00003.1400.
_f('This is %1$+ 13f.', 3.14);
// This is     +3.140000.
// This is ␣␣␣␣+3.140000.
_f('This is %1$+ 11.4f.', 3.14);
// This is     +3.1400.
// This is ␣␣␣␣+3.1400.
_f('Hello %s!', 'Jane');
// Hello Jane!
_f('%1$s is %2$d years old.', 'John', 30);
// John is 30 years old.

注意:这使用了来自C语言(以及PHP)的“printf”格式字符串语法。为了将百分号(用作字面量)转义,只需将其加倍,例如50 %%

注意:当您的格式字符串具有多个占位符和替换时,始终对占位符进行编号,以避免歧义并允许在翻译过程中具有灵活性。例如,使用%1$s is from %2$s而不是%s is from %s

具有扩展格式的字符串

将用户界面的句子、短语和标签包装在_fe函数内部

_fe('This is {0}.', 'Bob');
// This is Bob.
_fe('This is {0, number}.', 1003.14);
// This is 1,003.14.
_fe('This is {0, number, percent}.', 0.42);
// This is 42%.
_fe('This is {0, date}.', -14182916);
// This is Jul 20, 1969.
_fe('This is {0, date, short}.', -14182916);
// This is 7/20/69.
_fe('This is {0, date, medium}.', -14182916);
// This is Jul 20, 1969.
_fe('This is {0, date, long}.', -14182916);
// This is July 20, 1969.
_fe('This is {0, date, full}.', -14182916);
// This is Sunday, July 20, 1969.
_fe('This is {0, time}.', -14182916);
// This is 1:18:04 PM.
_fe('This is {0, time, short}.', -14182916);
// This is 1:18 PM.
_fe('This is {0, time, medium}.', -14182916);
// This is 1:18:04 PM.
_fe('This is {0, time, long}.', -14182916);
// This is 1:18:04 PM GMT-7.
_fe('This is {0, time, full}.', -14182916);
// This is 1:18:04 PM GMT-07:00.
_fe('This is {0, spellout}.', 314159);
// This is three hundred fourteen thousand one hundred fifty-nine.
_fe('This is {0, ordinal}.', 314159);
// This is 314,159th.
_fe('Hello {0}!', 'Jane');
// Hello Jane!
_fe('{0} is {1, number} years old.', 'John', 30);
// John is 30 years old.

注意:这使用了ICU的“MessageFormat”语法。为了将大括号(用作字面量)转义,请将其用单引号括起来,例如'{''}'。为了将单引号(用作字面量)转义,只需将其加倍,例如it''s。如果您在PHP中使用单引号作为字符串字面量,您还必须使用反斜杠转义插入的单引号,例如\'{\'\'}\'it\'\'s

单数和复数形式

将用户界面的句子、短语和标签包装在_p函数内部

_p('cat', 'cats', 1);
// cat
_p('cat', 'cats', 2);
// cats
_p('cat', 'cats', 3);
// cats
_p('The file has been saved.', 'The files have been saved.', 1);
// The file has been saved.
_p('The file has been saved.', 'The files have been saved.', 2);
// The files have been saved.
_p('The file has been saved.', 'The files have been saved.', 3);
// The files have been saved.

带有格式的单数和复数形式

将用户界面的句子、短语和标签包装在_pf函数内部

_pf('There is %d monkey.', 'There are %d monkeys.', 0);
// There are 0 monkeys.
_pf('There is %d monkey.', 'There are %d monkeys.', 1);
// There is 1 monkey.
_pf('There is %d monkey.', 'There are %d monkeys.', 2);
// There are 2 monkeys.
_pf('There is %1$d monkey in %2$s.', 'There are %1$d monkeys in %2$s.', 3, 'Anytown');
// There are 3 monkeys in Anytown.
_pf('You have %d new message', 'You have %d new messages', 0);
// You have 0 new messages
_pf('You have %d new message', 'You have %d new messages', 1);
// You have 1 new message
_pf('You have %d new message', 'You have %d new messages', 32);
// You have 32 new messages

注意:这使用了来自C语言(以及PHP)的“printf”格式字符串语法。为了将百分号(用作字面量)转义,只需将其加倍,例如50 %%

具有扩展格式的单数和复数形式

将用户界面的句子、短语和标签包装在_pfe函数内部

_pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 0);
// There are 0 monkeys.
_pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 1);
// There is 1 monkey.
_pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 2);
// There are 2 monkeys.
_pfe('There is {0, number} monkey in {1}.', 'There are {0, number} monkeys in {1}.', 3, 'Anytown');
// There are 3 monkeys in Anytown.
_pfe('You have {0, number} new message', 'You have {0, number} new messages', 0);
// You have 0 new messages
_pfe('You have {0, number} new message', 'You have {0, number} new messages', 1);
// You have 1 new message
_pfe('You have {0, number} new message', 'You have {0, number} new messages', 32);
// You have 32 new messages

注意:这使用了ICU的“MessageFormat”语法。为了将大括号(用作字面量)转义,请将其用单引号括起来,例如'{''}'。为了将单引号(用作字面量)转义,只需将其加倍,例如it''s。如果您在PHP中使用单引号作为字符串字面量,您还必须使用反斜杠转义插入的单引号,例如\'{\'\'}\'it\'\'s

带有上下文的字符串

将用户界面的句子、短语和标签包装在_c函数内部

_c('Order', 'sorting');
// or
_c('Order', 'purchase');
// or
_c('Order', 'mathematics');
// or
_c('Order', 'classification');
_c('Address:', 'location');
// or
_c('Address:', 'www');
// or
_c('Address:', 'email');
// or
_c('Address:', 'letter');
// or
_c('Address:', 'speech');

标记为稍后翻译的字符串

将用户界面的句子、短语和标签包装在_m函数内部。这是一个无操作指令,即(乍一看),它不起作用。但它为稍后翻译的包装文本标记。如果文本不应立即翻译,而将在稍后翻译,通常在尽可能晚的时间从变量中翻译,则这很有用

_m('User');
// User

此返回值可以插入到您的数据库中,例如,它将始终使用源代码中的原始字符串。稍后,您可以使用以下调用从变量翻译该字符串

$text = 'User';
_($text);
// User

提取和更新可翻译字符串

为了从您的PHP文件中提取所有可翻译字符串,您可以使用内置工具来完成此任务。同样,请确保注意您的操作系统使用的区域名称的确切语法,特别是关于连字符、下划线和后缀,例如en-USen_US。如果您不确定,请在CLI上再次检查locale -a命令的输出。

# For the `mr-IN` locale, with the default directory, with the default domain, and with fuzzy matching
$ bash ./i18n.sh mr-IN
# For the `sq-MK` locale, with the directory 'translations', with the default domain, and with fuzzy matching
$ bash ./i18n.sh sq-MK translations
# For the `yo-NG` locale, with the default directory, with the domain 'plugin', and with fuzzy matching
$ bash ./i18n.sh yo-NG "" plugin
# For the `fr-FR` locale, with the default directory, with the default domain, and without fuzzy matching
$ bash ./i18n.sh fr-FR "" "" nofuzzy

这将为指定的语言创建或更新一个PO(可移植对象)文件,然后您可以将其翻译、与您的翻译团队共享或发送给外部翻译人员。

如果您只需要一个通用的POT(可移植对象模板)文件,其中包含所有提取的字符串,而不是特定于任何语言,则只需省略带有区域代码的参数(或将它设置为空字符串)

# With the default directory, with the default domain, and with fuzzy matching
$ bash ./i18n.sh
# With the directory 'translations', with the default domain, and with fuzzy matching
$ bash ./i18n.sh "" translations
# With the default directory, with the domain 'plugin', and with fuzzy matching
$ bash ./i18n.sh "" "" plugin
# With the default directory, with the default domain, and without fuzzy matching
$ bash ./i18n.sh "" "" "" nofuzzy

翻译提取的字符串

无论处理提取字符串实际任务的是谁,无论是你、你的翻译团队还是外部翻译人员,负责人员都需要他们的语言版本的PO(便携对象)文件,或者在某些情况下,通用的POT(便携对象模板)文件。

只需打开相关的文件,搜索下有msgstr ""的字符串。这些是需要继续工作的空翻译字符串。除此之外,任何上面有#, fuzzy的字符串之前都已有翻译,但源代码中的原始字符串已更改,因此翻译必须进行审查(并移除“模糊”标志或注释)。

将翻译导出为二进制格式

在你完成翻译并保存了语言的PO(便携对象)文件后,你需要再次运行“提取和更新可翻译字符串”中的命令,以便将这些翻译导出为二进制格式。

它们将被存储在你的PO(便携对象)文件旁边的MO(机器对象)文件中,准备自动替换原始字符串。

获取活动区域设置

$i18n->getLocale();
// en-US
$i18n->getSystemLocale();
// en_US.utf8

关于区域设置的信息

当前语言中的区域设置名称

$i18n->getLocaleName();
// English (United States)
$i18n->getLocaleName('fr-BE');
// French (Belgium)
\Delight\I18n\Locale::toName('nb-NO');
// Norwegian Bokmål (Norway)

区域设置的本地名称

$i18n->getNativeLocaleName();
// English (United States)
$i18n->getNativeLocaleName('fr-BE');
// français (Belgique)
\Delight\I18n\Locale::toNativeName('nb-NO');
// norsk bokmål (Norge)

区域设置的英文名称

\Delight\I18n\Locale::toEnglishName('nb-NO');
// Norwegian Bokmål (Norway)

当前语言中的语言名称

$i18n->getLanguageName();
// English
$i18n->getLanguageName('fr-BE');
// French
\Delight\I18n\Locale::toLanguageName('nb-NO');
// Norwegian Bokmål

语言的本地名称

$i18n->getNativeLanguageName();
// English
$i18n->getNativeLanguageName('fr-BE');
// français
\Delight\I18n\Locale::toNativeLanguageName('nb-NO');
// norsk bokmål

语言的英文名称

\Delight\I18n\Locale::toEnglishLanguageName('nb-NO');
// Norwegian Bokmål

当前语言中的脚本名称

\Delight\I18n\Locale::toScriptName('nb-Latn-NO');
// Latin

脚本的本地名称

\Delight\I18n\Locale::toNativeScriptName('nb-Latn-NO');
// latinsk

脚本的英文名称

\Delight\I18n\Locale::toEnglishScriptName('nb-Latn-NO');
// Latin

当前语言中的区域名称

\Delight\I18n\Locale::toRegionName('nb-NO');
// Norway

区域的本地名称

\Delight\I18n\Locale::toNativeRegionName('nb-NO');
// Norge

区域的英文名称

\Delight\I18n\Locale::toEnglishRegionName('nb-NO');
// Norway

语言代码

\Delight\I18n\Locale::toLanguageCode('nb-Latn-NO');
// nb

脚本代码

\Delight\I18n\Locale::toScriptCode('nb-Latn-NO');
// Latn

区域代码

\Delight\I18n\Locale::toRegionCode('nb-Latn-NO');
// NO

文本的方向性

\Delight\I18n\Locale::isRtl('ur-PK');
// true
\Delight\I18n\Locale::isLtr('ln-CD');
// true

控制区域设置的查找和比较的宽松度

当使用I18n#setLocaleAutomatically自动确定并激活用户的正确区域设置时,你可以控制要考虑的类似或相关的区域设置。因此,你可以控制区域设置查找和比较的方式。

如果默认行为不适合你,只需向I18n#setLocaleAutomatically提供可选的第一个参数,即$leniency。以下表格列出了匹配两个区域代码所需的最低容错值。

故障排除

  • 翻译通常会被缓存,因此可能需要重新启动Web服务器才能使任何更改生效。

贡献

所有贡献都受欢迎!如果你愿意贡献,请首先创建一个问题,以便你的功能、问题或疑问可以讨论。

许可

本项目受MIT许可的条款约束。