Software localization
PHP i18n with Zend Framework
PHP seems to resist time and is still quite a popular choice for building web apps. We've already discussed how to translate PHP apps. Our focus today is on Zend Framework, an open source, object-oriented PHP framework comprised of more than 60 packages, each one serving a particular purpose. It is the zend-i18n package that makes PHP i18n of web apps possible. There is another one called zend-mvc-i18n which is needed for the integration of the zend-i18n package into the MVC framework. As most of the web applications that we create follow an MVC design pattern, we'll use zend-mvc-i18n in this tutorial.
You can install the Zend package using the following command:
composer require zendframework/zendframework
You can also install only mvc-i18n using the following command:
composer require zendframework/zend-mvc-i18n
Composer is a dependency management tool that will install all the required dependencies of Zend Framework for you. If you haven't installed Composer on your machine yet, you can get it here.
For this tutorial, I've created a demo app. Here's how the folder structure of a simple project looks like:
Three sections are important for us: languages, public, and src. We'll discuss them as we move forward with the tutorial.
Translator object
zend-i18n comes with a complete translation suite supporting all major formats. It also includes popular features that come with a translator object. Our next task is to define the Translator object. To do that, three aspects need to be configured:
- Translation format
- Translation resource location
- Translation resource file
There are different formats you can use to represent translations:
- PHP arrays (store translated texts in PHP arrays),
- gettext (translated texts as normal texts in a file)
- Tmx (an XML standard called Translation Memory Exchange)
- Xliff (XML-based file format)
Using PHP arrays is not the most optimal practice. You should always separate translation strings from source code; gettext is the easiest and probably the most common method – that's exactly what I'll be using in this application. Nevertheless, I'll show you how to store translated texts in the xliff and Tmx formats as well.
Once you've created files with translated texts, you need to place them somewhere easy to find. Quite common locations are:
- Application/language
- data/language
As you can see, I use the application/language path to store my translation files.
The third configuration is feeding translation files to Translator object. You can either add each file individually or add all files at once using a pattern. Assume you've got 10 translation source files for 10 different languages. It could be quite cumbersome to add each file separately. Therefore, it's a good practice to use a file pattern to add all files to the Translator.
Let's create now Translator object:
<?php use Zend\I18n\Translator\Translator; return Translator::factory([ 'translation_file_patterns' => [ [ 'type' => 'gettext', 'base_dir' => __DIR__ . '/../languages', 'pattern' => '%s.mo', ], ], ]);
Note how I've set a pattern – something like a sprintf pattern. %s.mo tells the Translator to accept any string with MO extension. There are four files in my language es.po, es.mo, fr.po, and fr.mo (ES standing for Spanish and FR for French); .PO files contain the actual translation, while .MO files contain the machine object or a binary data file. The Translator needs binary files. Therefore, %s.mo feeds es.mo and fr.mo to the Translator.
Translating messages
After creating the Translator object, we can now move on to translating messages. This can be done using the translate method of the Translator object. Translating text is done in the view. In our application, the view is the index.php file in the Public folder. Before using Translator object, you need to include translator.php in the index.php file.
The format of the translate method is:
$translator->translate($message, $textDomain, $locale);
Its parameters represent the following:
- $message – the message to be translated
- $textDomain – the text domain where translation resides. It is an optional parameter. The default value is "default"
- $locale – Locale should be used. It is an optional parameter. If unset, default locale will be used
An example of the translate method would be:
$translator->translate("Translated text from a custom text domain.", "customDomain",”de_DE“);
Anyway, in our application, we'll use the translate method a bit differently. It doesn't make much sense to set a locale in each translated message. It's hardly ever you use several locals in the same view. You can set the locale using setLocale() method.
$lang = isset($_GET['lang']) ? $_GET['lang'] : 'en'; $translator->setLocale($lang);
Finally, our index.php file will look like this:
<?php include __DIR__ . '/../vendor/autoload.php'; $lang = isset($_GET['lang']) ? $_GET['lang'] : 'en'; /** @var Zend\I18n\Translator\Translator $translator */ $translator = include __DIR__ . '/../src/translator.php'; $translator->setLocale($lang); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <p><?php echo $translator->translate('Hello my friend'); ?></p> <p><?php echo $translator->translate('How are you?'); ?></p> <p><?php echo $translator->translate('My name is Adam Crik'); ?></p> </body> </html>
Translation formats
As noted at the outset, I'd also like to show you how to store translated texts in different formats. In our application, there are three strings. In gettext, translated texts are stored in .PO files.
Our fr.po file looks like this:
#: ../public/index.php:1 msgid "Hello my friend" msgstr "Bonjour, mon ami" #: ../public/index.php:2 msgid "How are you?" msgstr "Comment allez-vous?" #: ../public/index.php:3 msgid "My name is Adam Crik" msgstr "Je m'appelle Adam Crik"
If we use the XLIFF format, it will more or less look like this:
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en-US" trgLang="fr"> <file id="f1"> <unit id="1"> <segment> <source>Hello my friend</source> <target>Bonjour, mon ami</target> </segment> </unit> <unit id="2"> <segment> <source>How are you?</source> <target>Comment allez-vous?</target> </segment> </unit> <unit id="3"> <segment> <source>My name is Adam Crik</source> <target>Je m'appelle Adam Crik</target> </segment> </unit> </file> </xliff>
If we use the TMX format, it would be something like this:
<tmx version="1.4"> <body> <tu> <tuv xml:lang="en"> <seg>Hello my friend</seg> </tuv> <tuv xml:lang="fr"> <seg>Bonjour, mon ami</seg> </tuv> <tuv xml:lang="en"> <seg>How are you?</seg> </tuv> <tuv xml:lang="fr"> <seg>Comment allez-vous?</seg> </tuv> <tuv xml:lang="en"> <seg>My name is John Doe</seg> </tuv> <tuv xml:lang="fr"> <seg>Je m'appelle John Doe</seg> </tuv> </tu> </body> </tmx>
Zend I18n Helper Classes
If you read our blog, you already know that there are some concepts for which locality is very important – certain things simply can't exist without locality. We can give currency, data format, number formats as examples. If you're going to provide internationalization support for your application, it's essential that you translate these objects accordingly. Zend helper classes make the process easier by introducing those respective classes with locality support:
- NumberFormat helper
- CurrencyFormat helper
- DateFormt helper
Let’s get to know them better by taking a look at some examples.
NumberFormat helper
One of the most evident differences in different locales is how numbers are formatted. NumberFormat class handles these variances for you. Look at the following two examples:
echo $this->numberFormat( 2453678.26, NumberFormatter::DECIMAL, NumberFormatter::TYPE_DEFAULT, "de_DE" );
What it will display is 2.453.678,26.
echo $this->numberFormat( 2453678.26, NumberFormatter::DECIMAL, NumberFormatter::TYPE_DEFAULT, "en_US" );
2,453,678.26 gets displayed.
CurrencyFormat Helper
The globe is flooded by different currencies. As the custom is, people in a specific country should be able to pay for a product or service in their own currency. Currency becomes, thus, one of the main factors to be considered for adjustment in an internationalization project. It is the currencyFormat method that does the internationalization for you here. Check out the following code:
echo $this->currencyFormat(2543.91, "USD", "en_US");
What it will display is $2,543.91.
echo $this->currencyFormat(2543.91, "EUR", "de_DE");
It will display 2.543,91 €.
When you use en_US, the local currency is the US dollar. When you use de_DE, which stands for Germany, the currency turns into euro. However, that's not it. If you take a better look, the number format of the price has changed as well. The reason for that is that the CurrencyFormat method is a wrapper for the NumberFormat class.
DateFormat Helper
The last important helper class we're looking at is the DateFormat helper. Different countries use different date formats. The following example compares date formats created by the en_US and de_DE locales.
echo $this->dateFormat( new DateTime(), IntlDateFormatter::MEDIUM, // date "en_US" );
May 1, 2019 is the result displayed.
echo $this->dateFormat( new DateTime(), IntlDateFormatter::MEDIUM, // date "de_DE" );
It results in 01.05.2019 being displayed.
Zend Framework provides a whole lot more helper classes. You can find the complete list in their documentation.
Wrapping Up
This tutorial guided you through PHP i18n with Zend Framework. I truly hope you enjoyed it and learned a lot along the way. If you're still struggling to find a solid localization service, give Phrase a try. Phrase is a widely trusted all-in-one platform for localizing projects. Besides PHP, Phrase also supports other programming languages including Java, Python, Ruby or JavaScript. Sign up for a free 14-day trial and try it for yourself.