Translating strings with dynamic content

In this recipe, we will learn how to allow strings consisting of parts that are not static, such as variable values, to be translatable.

Getting ready

To go through this recipe, we need a basic application skeleton to work with. Go through the entire recipe Internationalizing controller and view texts.

How to do it...

  1. Edit the file 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'),
    }
    }
    }
    
  2. Edit the view file 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)); ?>
    &nbsp;-&nbsp;
    <?php echo $this->Paginator->prev(__('<< Previous', true)); ?>
    &nbsp;
    <?php echo $this->Paginator->numbers(); ?>
    &nbsp;
    <?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>
    

How it works...

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.

Reordering and reusing interpolation arguments

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);

See also

  • Extracting and translating text

Extracting and translating text

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.

Getting ready

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.

How to do it...

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:

How to do it...

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:

How to do it...

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);

Note

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:

How to do it...

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.

How it works...

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().

Note

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.

Translating database records with the Translate behavior

In this recipe, we will learn how to allow translation of database records by means of CakePHP's Translate behavior.

Getting ready

To go through this recipe, we need a basic application skeleton to work with. Go through the entire recipe Internationalizing controller and view texts.

How to do it...

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:

How to do it...

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).

How it works...

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

locale

The locale (language) this particular record field is being translated to.

model

The model where the record being translated belongs.

foreign_key

The ID (primary key) in model that identifies the record being translated.

field

The field being translated.

content

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.

Using separate translation tables

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';
}
?>

See also

  • Setting and remembering the language
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.142.144