让应用程序覆盖更广泛的受众群体的最佳方式之一是适应多种语言。这通常是一项艰巨的任务,但 CakePHP 中的国际化和本地化功能让它变得更加容易。
首先,了解一些术语很重要。国际化是指应用程序具有本地化的能力。本地化是指适应应用程序以满足特定语言(或文化)要求(即“语言环境”)。国际化和本地化通常分别缩写为 i18n 和 l10n;18 和 10 是第一个和最后一个字符之间的字符数。
要将单语言应用程序转换为多语言应用程序,只需执行几个步骤,第一步是使用 __()
函数在代码中。以下是一些单语言应用程序代码的示例
<h2>Popular Articles</h2>
要使代码国际化,您需要做的就是将字符串用 __()
包裹,如下所示
<h2><?= __('Popular Articles') ?></h2>
不做其他操作,这两个代码示例在功能上是相同的 - 它们都将相同的内容发送到浏览器。如果存在翻译,则 __()
函数将翻译传递的字符串,否则将返回未修改的字符串。
可以使用存储在应用程序中的语言文件来提供翻译。CakePHP 翻译文件的默认格式是 Gettext 格式。文件需要放置在 **resources/locales/** 下,在这个目录中,应该为应用程序需要支持的每种语言创建一个子文件夹
resources/
locales/
en_US/
default.po
en_GB/
default.po
validation.po
es/
default.po
默认域是“default”,因此语言环境文件夹至少应包含如上所示的 **default.po** 文件。域是指任何任意分组的翻译消息。当没有使用组时,将选择默认组。
从 CakePHP 库中提取的核心字符串消息可以单独存储在 **resources/locales/** 中名为 **cake.po** 的文件中。 CakePHP localized 库 包含核心(cake 域)中面向客户端的翻译字符串的翻译。要使用这些文件,请将它们链接或复制到预期位置:**resources/locales/<locale>/cake.po**。如果您的语言环境不完整或不正确,请在此存储库中提交 PR 来修复它。
插件也可以包含翻译文件,惯例是使用插件名称的 under_scored
版本作为翻译消息的域
MyPlugin/
resources/
locales/
fr/
my_plugin.po
additional.po
de/
my_plugin.po
翻译文件夹可以是语言的两位或三位字母 ISO 代码,也可以是完整的 ICU 语言环境名称,例如 fr_FR
、es_AR
、da_DK
,其中包含语言和使用该语言的国家/地区。
有关语言环境的完整列表,请参见 https://www.localeplanet.com/icu/。
Changed in version 4.5.0: 从 4.5.0 开始,插件可以包含多个翻译域。使用 MyPlugin.additional
来引用插件域。
一个示例翻译文件可能如下所示
msgid "My name is {0}"
msgstr "Je m'appelle {0}"
msgid "I'm {0,number} years old"
msgstr "J'ai {0,number} ans"
注意
翻译被缓存 - 确保在更改翻译后始终清除缓存!您可以使用 缓存工具 并运行例如 bin/cake cache clear _cake_core_
,或者手动清除 tmp/cache/persistent
文件夹(如果使用基于文件的缓存)。
要从 __() 及其他国际化类型的消息中创建 pot 文件,这些消息可以在应用程序代码中找到,您可以使用 i18n 命令。请阅读 以下章节 以了解更多信息。
可以在 **config/app.php** 文件中设置 App.defaultLocale
来设置默认语言环境
'App' => [
...
'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
...
]
这将控制应用程序的多个方面,包括默认翻译语言、日期格式、数字格式和货币,无论何时使用 CakePHP 提供的本地化库显示其中任何一种。
要更改翻译字符串的语言,可以调用此方法
use Cake\I18n\I18n;
I18n::setLocale('de_DE');
这也会更改使用本地化工具之一时数字和日期的格式。
CakePHP 提供了几个有助于您国际化应用程序的函数。最常用的一个是 __()
。此函数用于检索单个翻译消息,或者如果未找到翻译,则返回相同的字符串
echo __('Popular Articles');
如果需要对消息进行分组,例如插件中的翻译,可以使用 __d()
函数从另一个域中获取消息
echo __d('my_plugin', 'Trending right now');
注意
如果要翻译供应商命名空间的插件,则必须使用域字符串 vendor/plugin_name
。但相关的语言文件将变为 plugins/<Vendor>/<PluginName>/resources/locales/<locale>/plugin_name.po
,位于插件文件夹中。
有时,翻译字符串对于翻译人员来说可能模棱两可。如果两个字符串相同但指的是不同的东西,就会发生这种情况。例如,“letter”在英语中有几种含义。为了解决这个问题,可以使用 __x()
函数
echo __x('written communication', 'He read the first letter');
echo __x('alphabet learning', 'He read the first letter');
第一个参数是消息的上下文,第二个是待翻译的消息。
msgctxt "written communication"
msgid "He read the first letter"
msgstr "Er las den ersten Brief"
翻译函数允许您使用在消息本身或翻译字符串中定义的特殊标记将变量插入消息中
echo __("Hello, my name is {0}, I'm {1} years old", ['Sara', 12]);
标记是数字,与传递的数组中的键相对应。您也可以将变量作为独立的参数传递给函数
echo __("Small step for {0}, Big leap for {1}", 'Man', 'Humanity');
所有翻译函数都支持占位符替换
__d('validation', 'The field {0} cannot be left empty', 'Name');
__x('alphabet', 'He read the letter {0}', 'Z');
'
(单引号)字符在翻译消息中充当转义代码。单引号之间的任何变量都不会被替换,而是被视为文字文本。例如
__("This variable '{0}' be replaced.", 'will not');
通过使用两个相邻的引号,您的变量将被正确替换
__("This variable ''{0}'' be replaced.", 'will');
这些函数利用了 ICU MessageFormatter,因此您可以翻译消息并同时本地化日期、数字和货币
echo __(
'Hi {0}, your balance on the {1,date} is {2,number,currency}',
['Charles', new DateTime('2014-01-13 11:12:00'), 1354.37]
);
// Returns
Hi Charles, your balance on the Jan 13, 2014, 11:12 AM is $ 1,354.37
占位符中的数字也可以使用精细控制的输出进行格式化
echo __(
'You have traveled {0,number} kilometers in {1,number,integer} weeks',
[5423.344, 5.1]
);
// Returns
You have traveled 5,423.34 kilometers in 5 weeks
echo __('There are {0,number,#,###} people on earth', 6.1 * pow(10, 8));
// Returns
There are 6,100,000,000 people on earth
以下是在 number
之后可以放置的格式化程序规范列表
integer
:删除小数部分
currency
:放置语言环境货币符号并四舍五入小数
percent
:将数字格式化为百分比
日期也可以使用占位符数字后面的 date
单词进行格式化。以下是额外的选项列表
简短
中等
长
完整
占位符数字后面的 time
单词也被接受,它理解与 date
相同的选项。
您也可以在消息字符串中使用命名占位符,例如 {name}
。使用命名占位符时,请使用键值对在数组中传递占位符和替换内容,例如
// echos: Hi. My name is Sara. I'm 12 years old.
echo __("Hi. My name is {name}. I'm {age} years old.", ['name' => 'Sara', 'age' => 12]);
国际化应用程序的一个重要部分是根据显示消息的语言正确地对消息进行复数化。CakePHP 提供了几种方法来在消息中正确选择复数。
第一个是利用翻译函数中默认提供的 ICU
消息格式。在翻译文件中,您可能有以下字符串
msgid "{0,plural,=0{No records found} =1{Found 1 record} other{Found # records}}"
msgstr "{0,plural,=0{Ningún resultado} =1{1 resultado} other{# resultados}}"
msgid "{placeholder,plural,=0{No records found} =1{Found 1 record} other{Found {1} records}}"
msgstr "{placeholder,plural,=0{Ningún resultado} =1{1 resultado} other{{1} resultados}}"
在应用程序中,使用以下代码输出该字符串的任何一个翻译
__('{0,plural,=0{No records found }=1{Found 1 record} other{Found # records}}', [0]);
// Returns "Ningún resultado" as the argument {0} is 0
__('{0,plural,=0{No records found} =1{Found 1 record} other{Found # records}}', [1]);
// Returns "1 resultado" because the argument {0} is 1
__('{placeholder,plural,=0{No records found} =1{Found 1 record} other{Found {1} records}}', [0, 'many', 'placeholder' => 2])
// Returns "many resultados" because the argument {placeholder} is 2 and
// argument {1} is 'many'
仔细观察我们刚刚使用的格式,就会很清楚消息是如何构建的
{ [count placeholder],plural, case1{message} case2{message} case3{...} ... }
[count placeholder]
可以是传递给翻译函数的任何变量的数组键号。它将用于选择正确的复数形式。
请注意,要引用 [count placeholder]
在 {message}
中,您必须使用 #
。
当然,如果您不想在代码中键入完整的复数选择序列,可以使用更简单的消息 ID
msgid "search.results"
msgstr "{0,plural,=0{Ningún resultado} =1{1 resultado} other{{1} resultados}}"
然后在您的代码中使用新的字符串
__('search.results', [2, 2]);
// Returns: "2 resultados"
后一种版本有一个缺点,即即使对于默认语言,也需要有一个翻译消息文件,但它有一个优点,即它使代码更易读,并将复杂的复数选择字符串保留在翻译文件中。
有时,在复数中使用直接数字匹配是不切实际的。例如,像阿拉伯语这样的语言在您指的是几件事物时需要一个不同的复数,而在指的是许多事物时需要另一个复数。在这些情况下,您可以使用 ICU 匹配别名。而不是写
=0{No results} =1{...} other{...}
你可以做
zero{No Results} one{One result} few{...} many{...} other{...}
请务必阅读 语言复数规则指南 ,以全面了解您可以为每种语言使用的别名。
接受的第二种复数选择格式是使用 Gettext 的内置功能。在这种情况下,复数将存储在 .po
文件中,为每个复数形式创建单独的消息翻译行
# One message identifier for singular
msgid "One file removed"
# Another one for plural
msgid_plural "{0} files removed"
# Translation in singular
msgstr[0] "Un fichero eliminado"
# Translation in plural
msgstr[1] "{0} ficheros eliminados"
使用此其他格式时,您需要使用另一个翻译函数
// Returns: "10 ficheros eliminados"
$count = 10;
__n('One file removed', '{0} files removed', $count, $count);
// It is also possible to use it inside a domain
__dn('my_plugin', 'One file removed', '{0} files removed', $count, $count);
msgstr[]
中的数字是 Gettext 为语言的复数形式分配的数字。一些语言有不止两种复数形式,例如克罗地亚语
msgid "One file removed"
msgid_plural "{0} files removed"
msgstr[0] "{0} datoteka je uklonjena"
msgstr[1] "{0} datoteke su uklonjene"
msgstr[2] "{0} datoteka je uklonjeno"
请访问 Launchpad 语言页面 ,详细了解每种语言的复数形式数字。
如果您需要偏离 CakePHP 关于翻译消息存储位置和方式的约定,您可以创建自己的翻译消息加载器。创建您自己的翻译器最简单的方法是为单个域和语言环境定义一个加载器
use Cake\I18n\Package;
// Prior to 4.2 you need to use Aura\Intl\Package
I18n::setTranslator('animals', function () {
$package = new Package(
'default', // The formatting strategy (ICU)
'default' // The fallback domain
);
$package->setMessages([
'Dog' => 'Chien',
'Cat' => 'Chat',
'Bird' => 'Oiseau'
...
]);
return $package;
}, 'fr_FR');
上面的代码可以添加到您的 **config/bootstrap.php** 中,以便在使用任何翻译函数之前找到翻译。创建翻译器所需的绝对最小要求是,加载器函数应该返回一个 Cake\I18n\Package
对象(在 4.2 之前,它应该是一个 Aura\Intl\Package
对象)。代码到位后,您可以像往常一样使用翻译函数
I18n::setLocale('fr_FR');
__d('animals', 'Dog'); // Returns "Chien"
如您所见,Package
对象将翻译消息作为数组接收。您可以通过任何您喜欢的方式传递 setMessages()
方法:使用内联代码、包含另一个文件、调用另一个函数等。CakePHP 提供了一些加载器函数,如果您只需要更改消息的加载位置,就可以重复使用这些函数。例如,您仍然可以使用 **.po** 文件,但从另一个位置加载
use Cake\I18n\MessagesFileLoader as Loader;
// Load messages from resources/locales/folder/sub_folder/filename.po
I18n::setTranslator(
'animals',
new Loader('filename', 'folder/sub_folder', 'po'),
'fr_FR'
);
可以使用 CakePHP 使用的相同约定,但使用 PoFileParser
以外的消息解析器。例如,如果您想使用 YAML
加载翻译消息,首先需要创建解析器类
namespace App\I18n\Parser;
class YamlFileParser
{
public function parse($file)
{
return yaml_parse_file($file);
}
}
该文件应在应用程序的 **src/I18n/Parser** 目录中创建。接下来,在 **resources/locales/fr_FR/animals.yaml** 下创建翻译文件
Dog: Chien
Cat: Chat
Bird: Oiseau
最后,为域和语言环境配置翻译加载器
use Cake\I18n\MessagesFileLoader as Loader;
I18n::setTranslator(
'animals',
new Loader('animals', 'fr_FR', 'yaml'),
'fr_FR'
);
通过为每个需要支持的域和语言环境调用 I18n::setTranslator()
来配置翻译器可能很乏味,尤其是如果您需要支持不止几个不同的语言环境时。为了避免这个问题,CakePHP 允许您为每个域定义通用翻译器加载器。
假设您想要从外部服务加载默认域和任何语言的所有翻译
use Cake\I18n\Package;
// Prior to 4.2 you need to use Aura\Intl\Package
I18n::config('default', function ($domain, $locale) {
$locale = Locale::parseLocale($locale);
$lang = $locale['language'];
$messages = file_get_contents("http://example.com/translations/$lang.json");
return new Package(
'default', // Formatter
null, // Fallback (none for default domain)
json_decode($messages, true)
)
});
上面的示例调用示例外部服务来加载包含翻译的 JSON 文件,然后仅为应用程序中请求的任何语言环境构建 Package
对象。
如果您想更改所有没有设置特定加载器的包的包的加载方式,可以使用 _fallback
包来替换回退包加载器
I18n::config('_fallback', function ($domain, $locale) {
// Custom code that yields a package here.
});
用于 setMessages()
的数组可以精心设计,以指示翻译器在不同的域下存储消息或触发 Gettext 风格的复数选择。以下是在不同上下文中存储相同键的翻译的示例
[
'He reads the letter {0}' => [
'alphabet' => 'Él lee la letra {0}',
'written communication' => 'Él lee la carta {0}',
],
]
类似地,您可以通过在每个复数形式下有一个嵌套的数组键来使用消息数组来表达 Gettext 风格的复数
[
'I have read one book' => 'He leído un libro',
'I have read {0} books' => [
'He leído un libro',
'He leído {0} libros',
],
]
在前面的示例中,我们已经看到,包是使用 default
作为第一个参数构建的,并且它用注释表示对应于要使用的格式化程序。格式化程序是负责插值翻译消息中的变量和选择正确的复数形式的类。
如果您正在处理遗留应用程序,或者您不需要 ICU 消息格式提供的功能,CakePHP 还提供 sprintf
格式化程序
return Package('sprintf', 'fallback_domain', $messages);
要翻译的消息将被传递给 sprintf()
函数以插值变量
__('Hello, my name is %s and I am %d years old', 'José', 29);
可以在 CakePHP 创建所有翻译器之前设置所有翻译器的默认格式化程序,以便首次使用它们之前设置。这并不包括使用 setTranslator()
和 config()
方法手动创建的翻译器
I18n::defaultFormatter('sprintf');
在应用程序中输出日期和数字时,您通常需要它们按照您希望页面显示的国家或地区的首选格式进行格式化。
为了更改日期和数字的显示方式,您只需要更改当前语言环境设置并使用正确的类
use Cake\I18n\I18n;
use Cake\I18n\DateTime;
use Cake\I18n\Number;
I18n::setLocale('fr-FR');
$date = new DateTime('2015-04-05 23:00:00');
echo $date; // Displays 05/04/2015 23:00
echo Number::format(524.23); // Displays 524,23
请务必阅读 Date & Time 和 Number 部分,以了解有关格式化选项的更多信息。
默认情况下,ORM 结果返回的日期使用 Cake\I18n\DateTime
类,因此直接在应用程序中显示它们将受到更改当前语言环境的影响。
在从请求中接受本地化数据时,最好以用户的本地化格式接受日期时间信息。在控制器或 Middleware 中,您可以配置日期、时间和日期时间类型以解析本地化格式
use Cake\Database\TypeFactory;
// Enable default locale format parsing.
TypeFactory::build('datetime')->useLocaleParser();
// Configure a custom datetime format parser format.
TypeFactory::build('datetime')->useLocaleParser()->setLocaleFormat('dd-M-y');
// You can also use IntlDateFormatter constants.
TypeFactory::build('datetime')->useLocaleParser()
->setLocaleFormat([IntlDateFormatter::SHORT, -1]);
默认解析格式与默认字符串格式相同。
在处理来自不同时区用户的數據時,您需要將請求數據中的日期時間轉換為應用程序的時區。您可以使用控制器或 Middleware 中的 setUserTimezone()
來簡化此過程
// Set the user's timezone
TypeFactory::build('datetime')->setUserTimezone($user->timezone);
设置后,当应用程序从请求数据中创建或更新实体时,ORM 将自动将用户时区的日期时间值转换为应用程序的时区。这确保您的应用程序始终在 App.defaultTimezone
中定义的时区中工作。
如果您的应用程序在许多操作中处理日期时间信息,您可以使用中间件来定义时区转换和语言环境解析
namespace App\Middleware;
use Cake\Database\TypeFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class DatetimeMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Get the user from the request.
// This example assumes your user entity has a timezone attribute.
$user = $request->getAttribute('identity');
if ($user) {
TypeFactory::build('datetime')
->useLocaleParser()
->setUserTimezone($user->timezone);
}
return $handler->handle($request);
}
}
在应用程序中使用 LocaleSelectorMiddleware
,CakePHP 将根据当前用户自动设置语言环境
// in src/Application.php
use Cake\I18n\Middleware\LocaleSelectorMiddleware;
// Update the middleware function, adding the new middleware
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
// Add middleware and set the valid locales
$middlewareQueue->add(new LocaleSelectorMiddleware(['en_US', 'fr_FR']));
// To accept any locale header value
$middlewareQueue->add(new LocaleSelectorMiddleware(['*']));
}
LocaleSelectorMiddleware
将使用 Accept-Language
标头自动设置用户的首选语言环境。您可以使用语言环境列表选项来限制哪些语言环境将被自动使用。
如果您要翻译内容/实体,则应查看翻译行为。