In this recipe, we will learn how to allow strings consisting of parts that are not static, such as variable values, to be translatable.
To go through this recipe, we need a basic application skeleton to work with. Go through the entire recipe Internationalizing controller and view texts.
articles_controller.php
located in your app/controllers
folder and make the following changes to the add()
method:public function add() {
if (!empty($this->data)) {
$this->Article->create();
if ($this->Article->save($this->data)) {
$this->Session->setFlash(
sprintf(__('Article "%s" saved', true), $this->Article->field('title'))
);
$this->redirect(array('action'=>'index'));
} else {
$this->Session->setFlash('Please correct the errors'),
}
}
}
index.ctp
located in your app/views/articles
folder and make the following changes:<h1><?php __('Articles'), ?></h1>
<p>
<?php echo $this->Paginator->counter(__('Showing records %start%-%end% in page %page% out of %pages%', true)); ?>
-
<?php echo $this->Paginator->prev(__('<< Previous', true)); ?>
<?php echo $this->Paginator->numbers(); ?>
<?php echo $this->Paginator->next(__('Next >>', true)); ?>
</p>
<p>
<?php
$count = count($articles);
printf(__n('%d article', '%d articles', $count, true), $count);
?>
</p>
<ul>
<?php foreach($articles as $article) { ?>
<li><?php echo $this->Html->link(
$article['Article']['title'],
array('action'=>'view', $article['Article']['id'])
); ?></li>
<?php } ?>
</ul>
<p><?php echo $this->Html->link(__('Create article', true), array('action'=>'add')); ?></p>
When looking forward to including dynamic information, such as the value of a variable, or in this case, the value of a table field in the database, one can be tempted to simply append the variable to the string that is sent to the translator function:
$translated = __('Hello ' . $name, true); // This is wrong
This is not a valid expression, as CakePHP's extractor, shown in the recipe Extracting and translating text, expects only static strings as arguments to the translator functions, and other languages may need to re-order the sentence. Therefore, we need to use some way of string interpolation, so we chose to use the most common ones offered by PHP: the printf()
and sprintf()
functions.
Both functions take the same number and type of arguments. The first argument is mandatory and specifies the string to use for interpolation, while any subsequent argument is used to produce the final string. The only difference between printf()
and sprintf()
is that the former will output the resulting string, while the later simply returns it.
We start by changing the success message given by the ArticlesController
class whenever an article is created. We use sprintf()
as we need to send it through to the setFlash()
method of the Session
component. In this case, we use the expression %s
to interpolate the value of the title
field for the newly created article.
Similarly, our latest change uses %d
to interpolate the decimal value of the variable count
, and uses printf()
to output the result string.
When using expressions such as %s
or %d
to tell printf()
and sprintf()
where to place the value of an argument, we have no flexibility in terms of value positioning, and no practical way to reuse a value, as each of those expressions needs to match a specific argument. Let us assume the following expression:
printf('Your name is %s and your country is %s', $name, $country);
The first %s
expression gets replaced with the value of the name
variable, while the last %s
expression is replaced with the value of the country
variable. What if we wanted to change the order of these values in the string without altering the order of the arguments that are sent to printf()?
We can instead specify which argument is used by an interpolation expression by referring to an argument number (name being the argument number 1
, and country
argument number 2
):
printf('You are from %2$s and your name is %1$s', $name, $country);
This also allows us to reuse an argument without having to add it as an extra argument to printf()
:
printf('You are from %2$s and your name is %1$s . Welcome %1$s!', $name, $country);
In this recipe, we will learn how to extract all strings that need translation from our CakePHP applications and then perform the actual translations using free software.
To go through this recipe, we need a basic application skeleton to work with. Go through the entire recipe Internationalizing controller and view texts.
We also need to have Poedit installed in our system. Go to http://www.poedit.net/download.php and download the appropriate file for your operative system.
From the command line, and while in your app/
directory, issue the following command:
If you are on a GNU Linux / Mac / Unix system:
../cake/console/cake i18n extract
If you are on Microsoft Windows:
..cakeconsolecake.bat i18n extract
You should accept the default options, as shown in the following screenshot:
After answering the final question, the shell should go through your application files and generate a translation template in a file named default.pot
, placing it in your app/locale
folder.
Open Poedit, and then click on the menu File, and option New catalog from POT file. You should now see an open file dialog box. Browse to your app/locale
folder, select the default.pot
file, and click the Open button. A setting window should appear, as shown in the following screenshot:
In the Settings window, enter the desired project name and project information. In the Plural Forms field you should enter an expression that tells Poedit how to recognize plural translations. For most languages, such as English, Spanish, German and Portuguese, you should enter the following expression:
nplurals=2; plural=(n != 1);
More information about plural forms and which value should be given, depending on the language you are translating to, is available at http://drupal.org/node/17564.
Once you have entered all the desired details, click on the OK button. You will now be asked where to store the translated file. Create a folder named spa
and place it in your app/locale
folder. Inside the spa
folder, create a folder named LC_MESSAGES
. Then, while in Poedit's dialog box, select the folder app/locale/spa/LC_MESSAGES
and click the button Save without changing the file name, which should be default.po
.
Poedit will now show you all the original strings, and allow you to translate each by entering the desired translation in the bottom text area. After you enter your translations, Poedit may look like the following screenshot:
Click on the menu File, and then option Save to save the translated file. There should now be two files in your app/locale/spa/LC_MESSAGES
folder: default.po
and default.mo
.
CakePHP's extractor will first ask which paths to process. When all paths have been specified, it will browse recursively through its directories and look for any use of a translator function (any of __(), __n(), __d(), __dn(), __dc(), __dcn()
, and __c()
) in PHP and view files. For each found usage, it will extract the strings that need translation (first argument on calls to __()
and __c()
; the second argument on calls to __d()
and __dc()
; the first and second arguments on calls to __n()
; and the second and third arguments on calls to __dn()
and __dcn()
.
It is important to only use static strings, avoiding any PHP expressions, on the arguments the extractor looks for. If you want to learn how to interpolate variable values in the strings that need translation, see the recipe Translating strings with dynamic content.
Once CakePHP's extractor has obtained all strings that need translation, it will create the appropriate translation template files. If you used any translator function that specifies a domain (__d()
, __dn(), __dc()
, and __dcn()
), you can optionally merge all strings into one template file, or have each domain create a separate template file. Template files have the pot
extension, and use the domain name as its filename (default.pot being the default template file).
If you open default.pot
with a text editor, you will notice that it starts with a header that includes several settings, and then includes two lines for each string that needs translation: a line that defines a msgid
(the string to be translated), and a line that has an empty string for msgstr
(the translated string).
We then use Poedit to open this template file, translate the strings, and save it in the appropriate directory (app/locale/spa/LC_MESSAGES
), where Poedit will create two files: default.po
and default.pot
. If you open default.po
with a text editor, you will notice it almost looks exactly as the template file does, except that the header settings have changed to what we defined, and the msgid
lines are filled with our translations. The default.mo
file is a binary version of the default.po
file, also generated by Poedit, and is used by CakePHP to speed processing of the translation file.
In this recipe, we will learn how to allow translation of database records by means of CakePHP's Translate
behavior.
To go through this recipe, we need a basic application skeleton to work with. Go through the entire recipe Internationalizing controller and view texts.
From the command line, and while in your app/
directory, issue the following command:
If you are on a GNU Linux / Mac / Unix system:
../cake/console/cake i18n initdb
If you are on Microsoft Windows:
..cakeconsolecake.bat i18n initdb
Accept all the default answers. The shell should finish by creating a table named i18n
, as shown in the following screenshot:
Edit your app/models/article.php
file and add the following property:
<?php
class Article extends AppModel {
public $validate = array(
'title' => 'notEmpty',
'body' => 'notEmpty'
);
public $actsAs = array(
'Translate' => array('title', 'body')
);
}
?>
We now need to move the values for the title
and body
fields from the articles
table to the i18n
table, and then drop those fields from the articles
table. Issue the following SQL statements:
INSERT INTO `i18n`(`locale`, `model`, `foreign_key`, `field`, `content`) SELECT 'eng', 'Article', `articles`.`id`, 'title', `articles`.`title` FROM `articles`; INSERT INTO `i18n`(`locale`, `model`, `foreign_key`, `field`, `content`) SELECT 'eng', 'Article', `articles`.`id`, 'body', `articles`.`body` FROM `articles`; ALTER TABLE `articles` DROP COLUMN `title`, DROP COLUMN `body`;
Add the Spanish translations for our articles by Issuing the following SQL statements:
INSERT INTO `i18n`(`locale`, `model`, `foreign_key`, `field`, `content`) VALUES ('spa', 'Article', 1, 'title', 'Primer Artículo'), ('spa', 'Article', 1, 'body', 'Cuerpo para el primer Artículo'), ('spa', 'Article', 2, 'title', 'Segundo Artículo'), ('spa', 'Article', 2, 'body', 'Cuerpo para el segundo Artículo'), ('spa', 'Article', 3, 'title', 'Tercer Artículo'), ('spa', 'Article', 3, 'body', 'Cuerpo para el tercer Artículo'),
Finally, edit your app/config/bootstrap.php
file and add the following above the PHP closing tag:
Configure::write('Config.language', 'eng'),
If you now browse to http://localhost/articles
, you should see the same listing of articles, as shown in the first screenshot (recipe Internationalizing controller and view texts).
We start by using the shell to create the table required by the Translate
behavior. This table is by default named i18n
, and contains (besides its primary key) the following fields:
Field |
Purpose |
---|---|
|
The locale (language) this particular record field is being translated to. |
|
The model where the record being translated belongs. |
|
The ID (primary key) in |
|
The field being translated. |
|
The translated value for the record field. |
We then add the Translate
behavior to our Article
model, and set it to translate the title
and body
fields. This means that these fields will no longer be a part of the articles
table, but instead be stored in the i18n
table. Using the model
and foreign_key
values in the i18n
table, the Translate
behavior will fetch the appropriate values for these fields whenever an Article
record is obtained matching the application language.
We copy the values of the title
and body
fields into the i18n
table, and we then remove these fields from the articles
table. No change is needed in the find()
call that is used in our ArticlesController
class. Furthermore, the creation of articles will continue to work transparently, as the Translate
behavior will use the current language when saving records through the Article
model.
The final step is telling CakePHP which is the default application language, by setting the Config.language
configuration setting. If this step is omitted, CakePHP will obtain the current language by looking into the HTTP_ACCEPT_LANGUAGE
header sent by the client browser.
Any model that uses the Translate
behavior will by default use this i18n
table to store the different translations for each of its translated fields. This could be troublesome if we have a large number of records, or a large number of translated models. Fortunately, the Translate
behavior allows us to configure a different translation model.
As an example, let us assume that we want to store all article translations in a table called article_translations
. Create the table and then copy the translated records from the i18n
table by issuing the following SQL statements:
CREATE TABLE `article_translations`( `id` INT UNSIGNED AUTO_INCREMENT NOT NULL, `model` VARCHAR(255) NOT NULL, `foreign_key` INT UNSIGNED NOT NULL, `locale` VARCHAR(6) NOT NULL, `field` VARCHAR(255) NOT NULL, `content` TEXT default NULL, KEY `model__foreign_key`(`model`, `foreign_key`), KEY `model__foreign_key__locale`(`model`, `foreign_key`, `locale`), PRIMARY KEY(`id`) ); INSERT INTO `article_translations` SELECT `id`, `model`, `foreign_key`, `locale`, `field`, `content` FROM `i18n`;
Create a file named article_translation.php
and place it in your app/models
folder, with the following contents:
<?php class ArticleTranslation extends AppModel { public $displayField = 'field'; } ?>
The displayField
property in the translation model tells the Translate
behavior which field in the table holds the name of the field being translated.
Finally, edit your app/models/article.php
file and make the following changes:
<?php
class Article extends AppModel {
public $validate = array(
'title' => 'notEmpty',
'body' => 'notEmpty'
);
public $actsAs = array(
'Translate' => array('title', 'body')
);
public $translateModel = 'ArticleTranslation';
}
?>
18.117.142.144