Internationalization & Localization
One of the best ways for an application to reach a larger audience is to caterto multiple languages. This can often prove to be a daunting task, but theinternationalization and localization features in CakePHP make it much easier.
First, it’s important to understand some terminology. _Internationalization_refers to the ability of an application to be localized. The term _localization_refers to the adaptation of an application to meet specific language (orculture) requirements (i.e. a “locale”). Internationalization and localizationare often abbreviated as i18n and l10n respectively; 18 and 10 are the numberof characters between the first and last character.
Setting Up Translations
There are only a few steps to go from a single-language application to amulti-lingual application, the first of which is to make use of the__()
function in your code. Below is an example of some code for asingle-language application:
- <h2>Popular Articles</h2>
To internationalize your code, all you need to do is to wrap strings in__()
like so:
- <h2><?= __('Popular Articles') ?></h2>
Doing nothing else, these two code examples are functionally identical - theywill both send the same content to the browser. The __()
functionwill translate the passed string if a translation is available, or return itunmodified.
Language Files
Translations can be made available by using language files stored in theapplication. The default format for CakePHP translation files is theGettext format. Files need to beplaced under src/Locale/ and within this directory, there should be asubfolder for each language the application needs to support:
- /src
- /Locale
- /en_US
- default.po
- /en_GB
- default.po
- validation.po
- /es
- default.po
The default domain is ‘default’, therefore the locale folder should at leastcontain the default.po file as shown above. A domain refers to any arbitrarygrouping of translation messages. When no group is used, then the default groupis selected.
The core strings messages extracted from the CakePHP library can be storedseparately in a file named cake.po in src/Locale/.The CakePHP localized library housestranslations for the client-facing translated strings in the core (the cakedomain). To use these files, link or copy them into their expected location:src/Locale/<locale>/cake.po. If your locale is incomplete or incorrect,please submit a PR in this repository to fix it.
Plugins can also contain translation files, the convention is to use theunder_scored
version of the plugin name as the domain for the translationmessages:
- MyPlugin
- /src
- /Locale
- /fr
- my_plugin.po
- /de
- my_plugin.po
Translation folders can be the two or three letter ISO code of the language orthe full locale name such as fr_FR
, es_AR
, da_DK
which containsboth the language and the country where it is spoken.
An example translation file could look like this:
- msgid "My name is {0}"
- msgstr "Je m'appelle {0}"
- msgid "I'm {0,number} years old"
- msgstr "J'ai {0,number} ans"
Extract Pot Files with I18n Shell
To create the pot files from __() and other internationalized types ofmessages that can be found in the application code, you can use the i18n shell.Please read the following chapter tolearn more.
Setting the Default Locale
The default locale can be set in your config/app.php file by settingApp.defaultLocale
:
- 'App' => [
- ...
- 'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
- ...
- ]
This will control several aspects of the application, including the defaulttranslations language, the date format, number format and currency whenever anyof those is displayed using the localization libraries that CakePHP provides.
Changing the Locale at Runtime
To change the language for translated strings you can call this method:
- use Cake\I18n\I18n;
- // Prior to 3.5 use I18n::locale()
- I18n::setLocale('de_DE');
This will also change how numbers and dates are formatted when using one of thelocalization tools.
Using Translation Functions
CakePHP provides several functions that will help you internationalize yourapplication. The most frequently used one is __()
. This functionis used to retrieve a single translation message or return the same string if notranslation was found:
- echo __('Popular Articles');
If you need to group your messages, for example, translations inside a plugin,you can use the __d()
function to fetch messages from anotherdomain:
- echo __d('my_plugin', 'Trending right now');
Note
If you want to translate your plugins and they’re namespaced, you must nameyour domain string Namespace/PluginName
. But the related language filewill become plugins/Namespace/PluginName/src/Locale/plugin_name.po
inside your plugin folder.
Sometimes translations strings can be ambiguous for people translating them.This can happen if two strings are identical but refer to different things. Forexample, ‘letter’ has multiple meanings in English. To solve that problem, youcan use the __x()
function:
- echo __x('written communication', 'He read the first letter');
- echo __x('alphabet learning', 'He read the first letter');
The first argument is the context of the message and the second is the messageto be translated.
- msgctxt "written communication"
- msgid "He read the first letter"
- msgstr "Er las den ersten Brief"
Using Variables in Translation Messages
Translation functions allow you to interpolate variables into the messages usingspecial markers defined in the message itself or in the translated string:
- echo __("Hello, my name is {0}, I'm {1} years old", ['Sara', 12]);
Markers are numeric, and correspond to the keys in the passed array. You canalso pass variables as independent arguments to the function:
- echo __("Small step for {0}, Big leap for {1}", 'Man', 'Humanity');
All translation functions support placeholder replacements:
- __d('validation', 'The field {0} cannot be left empty', 'Name');
- __x('alphabet', 'He read the letter {0}', 'Z');
The '
(single quote) character acts as an escape code in translationmessages. Any variables between single quotes will not be replaced and istreated as literal text. For example:
- __("This variable '{0}' be replaced.", 'will not');
By using two adjacent quotes your variables will be replaced properly:
- __("This variable ''{0}'' be replaced.", 'will');
These functions take advantage of theICU MessageFormatterso you can translate messages and localize dates, numbers and currency at thesame time:
- echo __(
- 'Hi {0}, your balance on the {1,date} is {2,number,currency}',
- ['Charles', new FrozenTime('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
Numbers in placeholders can be formatted as well with fine grain control of theoutput:
- 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
This is the list of formatter specifiers you can put after the word number
:
integer
: Removes the decimal partcurrency
: Puts the locale currency symbol and rounds decimalspercent
: Formats the number as a percentage
Dates can also be formatted by using the worddate
after the placeholdernumber. A list of extra options follows:short
medium
long
full
The wordtime
after the placeholder number is also accepted and itunderstands the same options asdate
.
Note
Named placeholders are supported in PHP 5.5+ and are formatted as{name}
. When using named placeholders pass the variables in an arrayusing key/value pairs, for example ['name' => 'Sara', 'age' => 12]
.
It is recommended to use PHP 5.5 or higher when making use ofinternationalization features in CakePHP. The php5-intl
extension mustbe installed and the ICU version should be above 48.x.y (to check the ICUversion Intl::getIcuVersion()
).
Plurals
One crucial part of internationalizing your application is getting your messagespluralized correctly depending on the language they are shown. CakePHP providesa couple ways to correctly select plurals in your messages.
Using ICU Plural Selection
The first one is taking advantage of the ICU
message format that comes bydefault in the translation functions. In the translations file you could havethe following strings
- 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}}"
And in the application use the following code to output either of thetranslations for such string:
- __('{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'
A closer look to the format we just used will make it evident how messages arebuilt:
- { [count placeholder],plural, case1{message} case2{message} case3{...} ... }
The [count placeholder]
can be the array key number of any of the variablesyou pass to the translation function. It will be used for selecting the correctplural form.
Note that to reference [count placeholder]
within {message}
you have touse #
.
You can of course use simpler message ids if you don’t want to type the fullplural selection sequence in your code
- msgid "search.results"
- msgstr "{0,plural,=0{Ningún resultado} =1{1 resultado} other{{1} resultados}}"
Then use the new string in your code:
- __('search.results', [2, 2]);
- // Returns: "2 resultados"
The latter version has the downside that there is a need to have a translationmessages file even for the default language, but has the advantage that it makesthe code more readable and leaves the complicated plural selection strings inthe translation files.
Sometimes using direct number matching in plurals is impractical. For example,languages like Arabic require a different plural when you referto few things and other plural form for many things. In those cases you canuse the ICU matching aliases. Instead of writing:
- =0{No results} =1{...} other{...}
You can do:
- zero{No Results} one{One result} few{...} many{...} other{...}
Make sure you read theLanguage Plural Rules Guideto get a complete overview of the aliases you can use for each language.
Using Gettext Plural Selection
The second plural selection format accepted is using the built-in capabilitiesof Gettext. In this case, plurals will be stored in the .po
file by creating a separate message translation line per plural form:
- # 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"
When using this other format, you are required to use another translationfunction:
- // 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);
The number inside msgstr[]
is the number assigned by Gettext for the pluralform of the language. Some languages have more than two plural forms, forexample Croatian:
- 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"
Please visit the Launchpad languages pagefor a detailed explanation of the plural form numbers for each language.
Creating Your Own Translators
If you need to diverge from CakePHP conventions regarding where and howtranslation messages are stored, you can create your own translation messageloader. The easiest way to create your own translator is by defining a loaderfor a single domain and locale:
- 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');
The above code can be added to your config/bootstrap.php so thattranslations can be found before any translation function is used. The absoluteminimum that is required for creating a translator is that the loader functionshould return a Aura\Intl\Package
object. Once the code is in place you canuse the translation functions as usual:
- // Prior to 3.5 use I18n::locale()
- I18n::setLocale('fr_FR');
- __d('animals', 'Dog'); // Returns "Chien"
As you see, Package
objects take translation messages as an array. You canpass the setMessages()
method however you like: with inline code, includinganother file, calling another function, etc. CakePHP provides a few loaderfunctions you can reuse if you just need to change where messages are loaded.For example, you can still use .po files, but loaded from another location:
- use Cake\I18n\MessagesFileLoader as Loader;
- // Load messages from src/Locale/folder/sub_folder/filename.po
- // Prior to 3.5 use translator()
- I18n::setTranslator(
- 'animals',
- new Loader('filename', 'folder/sub_folder', 'po'),
- 'fr_FR'
- );
Creating Message Parsers
It is possible to continue using the same conventions CakePHP uses, but usea message parser other than PoFileParser
. For example, if you wanted to loadtranslation messages using YAML
, you will first need to created the parserclass:
- namespace App\I18n\Parser;
- class YamlFileParser
- {
- public function parse($file)
- {
- return yaml_parse_file($file);
- }
- }
The file should be created in the src/I18n/Parser directory of yourapplication. Next, create the translations file undersrc/Locale/fr_FR/animals.yaml
- Dog: Chien
- Cat: Chat
- Bird: Oiseau
And finally, configure the translation loader for the domain and locale:
- use Cake\I18n\MessagesFileLoader as Loader;
- // Prior to 3.5 use translator()
- I18n::setTranslator(
- 'animals',
- new Loader('animals', 'fr_FR', 'yaml'),
- 'fr_FR'
- );
Creating Generic Translators
Configuring translators by calling I18n::setTranslator()
for each domain andlocale you need to support can be tedious, specially if you need to support morethan a few different locales. To avoid this problem, CakePHP lets you definegeneric translator loaders for each domain.
Imagine that you wanted to load all translations for the default domain and forany language from an external service:
- use Aura\Intl\Package;
- I18n::config('default', function ($domain, $locale) {
- $locale = Locale::parseLocale($locale);
- $language = $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)
- )
- });
The above example calls an example external service to load a JSON file with thetranslations and then just build a Package
object for any locale that isrequested in the application.
If you’d like to change how packages are loaded for all packages, that don’thave specific loaders set you can replace the fallback package loader by usingthe _fallback
package:
- I18n::config('_fallback', function ($domain, $locale) {
- // Custom code that yields a package here.
- });
New in version 3.4.0: Replacing the _fallback
loader was added in 3.4.0
Plurals and Context in Custom Translators
The arrays used for setMessages()
can be crafted to instruct the translatorto store messages under different domains or to trigger Gettext-style pluralselection. The following is an example of storing translations for the same keyin different contexts:
- [
- 'He reads the letter {0}' => [
- 'alphabet' => 'Él lee la letra {0}',
- 'written communication' => 'Él lee la carta {0}'
- ]
- ]
Similarly, you can express Gettext-style plurals using the messages array byhaving a nested array key per plural form:
- [
- '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'
- ]
- ]
Using Different Formatters
In previous examples we have seen that Packages are built using default
asfirst argument, and it was indicated with a comment that it corresponded to theformatter to be used. Formatters are classes responsible for interpolatingvariables in translation messages and selecting the correct plural form.
If you’re dealing with a legacy application, or you don’t need the power offeredby the ICU message formatting, CakePHP also provides the sprintf
formatter:
- return Package('sprintf', 'fallback_domain', $messages);
The messages to be translated will be passed to the sprintf()
function forinterpolating the variables:
- __('Hello, my name is %s and I am %d years old', 'José', 29);
It is possible to set the default formatter for all translators created byCakePHP before they are used for the first time. This does not include manuallycreated translators using the setTranslator()
and config()
methods:
- I18n::defaultFormatter('sprintf');
Localizing Dates and Numbers
When outputting Dates and Numbers in your application, you will often need thatthey are formatted according to the preferred format for the country or regionthat you wish your page to be displayed.
In order to change how dates and numbers are displayed you just need to changethe current locale setting and use the right classes:
- use Cake\I18n\I18n;
- use Cake\I18n\Time;
- use Cake\I18n\Number;
- // Prior to 3.5 use I18n::locale()
- I18n::setLocale('fr-FR');
- $date = new Time('2015-04-05 23:00:00');
- echo $date; // Displays 05/04/2015 23:00
- echo Number::format(524.23); // Displays 524,23
Make sure you read the Date & Time and Numbersections to learn more about formatting options.
By default dates returned for the ORM results use the Cake\I18n\Time
class,so displaying them directly in you application will be affected by changing thecurrent locale.
Parsing Localized Datetime Data
When accepting localized data from the request, it is nice to accept datetimeinformation in a user’s localized format. In a controller, orDispatcher Filters you can configure the Date, Time, andDateTime types to parse localized formats:
- use Cake\Database\Type;
- // Enable default locale format parsing.
- Type::build('datetime')->useLocaleParser();
- // Configure a custom datetime format parser format.
- Type::build('datetime')->useLocaleParser()->setLocaleFormat('dd-M-y');
- // You can also use IntlDateFormatter constants.
- Type::build('datetime')->useLocaleParser()
- ->setLocaleFormat([IntlDateFormatter::SHORT, -1]);
The default parsing format is the same as the default string format.
Automatically Choosing the Locale Based on Request Data
By using the LocaleSelectorFilter
in your application, CakePHP willautomatically set the locale based on the current user:
- // in src/Application.php
- use Cake\I18n\Middleware\LocaleSelectorMiddleware;
- // Update the middleware function, adding the new middleware
- public function middleware($middleware)
- {
- // Add middleware and set the valid locales
- $middleware->add(new LocaleSelectorMiddleware(['en_US', 'fr_FR']));
- }
- // Prior to 3.3.0, use the DispatchFilter
- // in config/bootstrap.php
- DispatcherFactory::add('LocaleSelector');
- // Restrict the locales to only en_US, fr_FR
- DispatcherFactory::add('LocaleSelector', ['locales' => ['en_US', 'fr_FR']]);
The LocaleSelectorFilter
will use the Accept-Language
header toautomatically set the user’s preferred locale. You can use the locale listoption to restrict which locales will automatically be used.