pinga/locale

PHP 的国际化与本地化

v0.1 2023-03-10 08:22 UTC

This package is auto-updated.

Last update: 2024-09-10 11:44:59 UTC


README

PHP 的国际化与本地化

为您的应用程序提供多语言支持,以适应不同国家的用户,满足不同的格式和约定。基于优秀的 delight-im/PHP-I18N

需求

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

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

macOS:使用打包的 bash 脚本生成 po 和 mo 文件,请安装 gnu-sed。

$ brew install gnu-sed
$ PATH="$(brew --prefix)/opt/gnu-sed/libexec/gnubin:$PATH"

安装

  1. 通过 Composer 包含库 [?]

    $ composer require pinga/locale
  2. 将 i18n bash 脚本复制到项目根目录

    $ cp vendor/pinga/locale/i18n.sh .
  3. 包含 Composer 自动加载器

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

使用方法

什么是区域设置?

简单来说,区域设置是一组用户偏好和期望,在世界范围内的较大社区中共享,并根据地理区域而变化。值得注意的是,这包括用户的语言以及他们期望数字、日期和时间如何格式化。

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

无论您最初决定支持哪些语言、脚本和区域,您都可以在任何以后的时间添加或删除区域设置。所以您可能希望从1-3个区域设置开始,以便更快地启动。

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

在使用您的初始语言集之前,您应确保它们安装在任何您希望开发或部署应用程序的机器上,并确保操作系统知道这些语言。

$ locale -a

如果某个区域设置尚未安装,您可以根据以下示例添加它,例如 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::UK_UA,
    \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. the locale 参数
    2. the language 参数
    3. the lang 参数
    4. the 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 %%

注意:当您的格式字符串具有多个占位符和替换项时,始终对占位符进行编号以避免歧义,并允许在翻译过程中具有灵活性。例如,不要使用%s is from %s,而应使用%1$s is from %2$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文件中提取所有可翻译字符串,您可以使用内置工具执行此任务

# 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 的字符串之前都有过翻译,但由于源代码中的原始字符串已更改,因此需要审查翻译(并移除“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。以下表格列出了匹配两个区域设置代码所需的最小容差值

故障排除

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

贡献

所有贡献都受到欢迎!如果您想做出贡献,请首先创建一个问题,以便您的功能、问题或问题可以讨论。

许可

本项目的许可条款适用于 MIT 许可证