Internacionalization
Building multi-language applications with
                  CakePHP
Who am I?
●   The argentinean in the Cake team
●   A C++ developer that turned to Java,
    then PHP
●   Working exclusively with CakePHP for 3
    years
Oh, BTW



Tomorrow is my birthday
Why do we need i18n?
●   Expand your audience
●   CakePHP makes it simple
●   Translators don't have to know our
    application
●   Works with files (views, models,
    controllers), and database records
●   Think about your future needs
This could get you killed
●   The switch lover
     switch ($language) {
        case 'en': echo 'My message'; break;
        case 'es': echo 'Mi mensaje'; break;
     }


●   The table lover
     $terms = $this->Term->find('all');
     $terms = Set::combine($terms, '/Term/code', '/Term/text');
     $this->set(compact('terms'));

     // ...

     echo $term['my_message'];
The CakePHP way
●   Methods

    ●   __() -> __('My message')
    ●   __n() -> __n('An element', 'Several
        elements', $count)
    ●   Configure::write('Config.language', 'en')

●   Translate behavior

●   i18n extractor
The CakePHP way
●   Multibyte
    ●   1 letter != 1 byte
         ●   8 bits -> 256
         ●   wchar_t (L'w')
    ●   mb_strlen(), mb_strpos(), mb_substr(), ...
    ●   Multibyte::checkMultibyte() -> ord($char)
        > 128
    ●   Multibyte::utf8($string) -> array (values
        higher than 128 allowed)
    ●   Multibyte::ascii($array) -> string
The CakePHP way

    <p><?php __('Welcome to my page'); ?></p>
    <?php echo $html->link(__('Home', true), '/'); ?>
    <p><?php echo sprintf(__('Your name is %s', true),
   $name); ?></p>




    $ cake i18n extract -output app/locale



                                             locale/eng/LC_MESSAGES
  locale/                                            default.po
default.pot            POEDIT
                                                     default.mo
Let's go to work
●   Our own example
    ●   Modify the view
    ●   Run the extractor
    ●   Look at the generated template file
         ●   Run POEDIT → nplurals=2; plural=(n != 1);
    ●   Look at the translated files
●   Some tips when using POEDIT
Translate Behavior
●   Internationalization for our database
    records

●   All translations in the same table

●   Automatically filters the records to
    fetch them in the current language
Translate Behavior
          class Post extends AppModel {
             public $actsAs = array('Translate' => array(
                 'title', 'body'
             ));
             public $belongsTo = array('User');
          }




CREATE TABLE `posts`(                      CREATE TABLE `posts`(
    `id` INT NOT NULL AUTO_INCREMENT,          `id` INT NOT NULL AUTO_INCREMENT,
    `user_id` INT NOT NULL,                    `user_id` INT NOT NULL,
    `title` VARCHAR(255) NOT NULL,             `title` VARCHAR(255) NOT NULL,
    `body` TEXT,                               `body` TEXT,
    `created` DATETIME,                        `created` DATETIME,
    `modified` DATETIME,                       `modified` DATETIME,
    PRIMARY KEY(`id`)                          PRIMARY KEY(`id`)
);                                         );
Translate Behavior
        mysql> desc i18n;
        +-------------+--------------+------+-----+---------+----------------+
        | Field       | Type         | Null | Key | Default | Extra          |
        +-------------+--------------+------+-----+---------+----------------+
        | id          | int(10)      | NO   | PRI | NULL    | auto_increment |
        | locale      | varchar(6)   | NO   | MUL | NULL    |                |
        | model       | varchar(255) | NO   | MUL | NULL    |                |
        | foreign_key | int(10)      | NO   | MUL | NULL    |                |
        | field       | varchar(255) | NO   | MUL | NULL    |                |
        | content     | text         | YES |      | NULL    |                |
        +-------------+--------------+------+-----+---------+----------------+



mysql> select * from i18n;
+----+--------+-------+-------------+-------+------------------------------------------+
| id | locale | model | foreign_key | field | content                                  |
+----+--------+-------+-------------+-------+------------------------------------------+
| 1 | eng     | Post |            1 | title | Pre-registration opened                  |
| 2 | spa     | Post |            1 | title | Pre-inscripciones abiertas               |
| 3 | eng     | Post |            1 | body | Body for Pre-registration opened          |
| 4 | spa     | Post |            1 | body | Cuerpo para Pre-inscripciones abiertas    |
+----+--------+-------+-------------+-------+------------------------------------------+
4 rows in set (0.00 sec)
Translate Behavior
          $posts = $this->Post->find('all', array(
              'recursive' => -1, 'fields' => array('title', 'body')
          ));
          $posts = Set::combine($posts, '/Post/title', '/Post/body');




 SELECT `I18n__title`.`content`, `I18n__body`.`content`
 FROM `posts` AS `Post`
 LEFT JOIN `i18n` AS `I18n__title` ON (`Post`.`id` = `I18n__title`.`foreign_key` AND
`I18n__title`.`model` = 'Post' AND `I18n__title`.`field` = 'title')
 LEFT JOIN `i18n` AS `I18n__body` ON (`Post`.`id` = `I18n__body`.`foreign_key` AND
`I18n__body`.`model` = 'Post' AND `I18n__body`.`field` = 'body')
 WHERE `I18n__title`.`locale` = 'eng' AND `I18n__body`.`locale` = 'eng'




           array(
              [Pre-registration opened] => Body for Pre-registration opened
              [Site Updates] => Body for Site Updates
           )
Translate Behavior
$this->Post->create();
$this->Post->save(array('Post' => array(
     'user_id' => 1,
     'title' => array('eng' => 'ENG 1', 'spa' => 'spa1'),
     'body' => array('eng' => 'Body for ENG 1', 'spa' => 'Cuerpo para spa1')
)));



          $this->Post->create();
          $this->Post->save(array('Post' => array(
               'user_id' => 1,
               'title' => array('eng' => 'ENG 1'),
               'body' => array('eng' => 'Body for ENG 1')
          )));
          $this->Post->save(array('Post' => array(
               'id' => $this->Post->id,
               'title' => array('spa' => 'spa1'),
               'body' => array('spa' => 'Cuerpo para spa1')
          )));
Changing the language
class AppController extends Controller {
   public $components = array('Cookie');
   public function beforeFilter() {
       $lang = null;
       if (!empty($this->params['url']['lang'])) {
            $lang = $this->params['url']['lang'];
            $this->Cookie->write('CakeFestLanguage', $lang, false, '+365 days');
       } else {
            $lang = $this->Cookie->read('CakeFestLanguage');
       }
       if (empty($lang)) {
            $lang = Configure::read('CakeFest.defaultLanguage');
       }
       Configure::write('Config.language', $lang);
   }

    function beforeRender() {
        $this->set('currentLanguage', Configure::read('Config.language'));
    }
}
Changing the language
 class AppHelper extends Helper {
    Public function url($url = null, $full = false) {
        if (!empty($url) && !is_array($url) && $url[0] == '/') {
              $urlRoute = Router::parse($url);
              if (!empty($urlRoute['controller'])) {
                    $url = array_merge(array_intersect_key($urlRoute,
array_flip(array('admin', 'controller', 'action', 'plugin'))), $urlRoute['pass'],
$urlRoute['named']);
              }
        }
        if (is_array($url)) {
              if (!isset($url['lang']) && Configure::read('Config.language') !=
Configure::read('CakeFest.defaultLanguage')) {
                    $url['lang'] = Configure::read('Config.language');
              }
        }
        return parent::url($url, $full);
    }
}
Caching i18n elements

$this->element('news', array('cache' => array(
   'time' => '+1 day',
   'key' => $currentLanguage
)));




                tmp/cache/views
               element_eng_news
               element_spa_news
And we are done



Questions?

Internationalizing CakePHP Applications

  • 1.
  • 2.
    Who am I? ● The argentinean in the Cake team ● A C++ developer that turned to Java, then PHP ● Working exclusively with CakePHP for 3 years
  • 3.
  • 4.
    Why do weneed i18n? ● Expand your audience ● CakePHP makes it simple ● Translators don't have to know our application ● Works with files (views, models, controllers), and database records ● Think about your future needs
  • 5.
    This could getyou killed ● The switch lover switch ($language) { case 'en': echo 'My message'; break; case 'es': echo 'Mi mensaje'; break; } ● The table lover $terms = $this->Term->find('all'); $terms = Set::combine($terms, '/Term/code', '/Term/text'); $this->set(compact('terms')); // ... echo $term['my_message'];
  • 6.
    The CakePHP way ● Methods ● __() -> __('My message') ● __n() -> __n('An element', 'Several elements', $count) ● Configure::write('Config.language', 'en') ● Translate behavior ● i18n extractor
  • 7.
    The CakePHP way ● Multibyte ● 1 letter != 1 byte ● 8 bits -> 256 ● wchar_t (L'w') ● mb_strlen(), mb_strpos(), mb_substr(), ... ● Multibyte::checkMultibyte() -> ord($char) > 128 ● Multibyte::utf8($string) -> array (values higher than 128 allowed) ● Multibyte::ascii($array) -> string
  • 8.
    The CakePHP way <p><?php __('Welcome to my page'); ?></p> <?php echo $html->link(__('Home', true), '/'); ?> <p><?php echo sprintf(__('Your name is %s', true), $name); ?></p> $ cake i18n extract -output app/locale locale/eng/LC_MESSAGES locale/ default.po default.pot POEDIT default.mo
  • 9.
    Let's go towork ● Our own example ● Modify the view ● Run the extractor ● Look at the generated template file ● Run POEDIT → nplurals=2; plural=(n != 1); ● Look at the translated files ● Some tips when using POEDIT
  • 10.
    Translate Behavior ● Internationalization for our database records ● All translations in the same table ● Automatically filters the records to fetch them in the current language
  • 11.
    Translate Behavior class Post extends AppModel { public $actsAs = array('Translate' => array( 'title', 'body' )); public $belongsTo = array('User'); } CREATE TABLE `posts`( CREATE TABLE `posts`( `id` INT NOT NULL AUTO_INCREMENT, `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `user_id` INT NOT NULL, `title` VARCHAR(255) NOT NULL, `title` VARCHAR(255) NOT NULL, `body` TEXT, `body` TEXT, `created` DATETIME, `created` DATETIME, `modified` DATETIME, `modified` DATETIME, PRIMARY KEY(`id`) PRIMARY KEY(`id`) ); );
  • 12.
    Translate Behavior mysql> desc i18n; +-------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+---------+----------------+ | id | int(10) | NO | PRI | NULL | auto_increment | | locale | varchar(6) | NO | MUL | NULL | | | model | varchar(255) | NO | MUL | NULL | | | foreign_key | int(10) | NO | MUL | NULL | | | field | varchar(255) | NO | MUL | NULL | | | content | text | YES | | NULL | | +-------------+--------------+------+-----+---------+----------------+ mysql> select * from i18n; +----+--------+-------+-------------+-------+------------------------------------------+ | id | locale | model | foreign_key | field | content | +----+--------+-------+-------------+-------+------------------------------------------+ | 1 | eng | Post | 1 | title | Pre-registration opened | | 2 | spa | Post | 1 | title | Pre-inscripciones abiertas | | 3 | eng | Post | 1 | body | Body for Pre-registration opened | | 4 | spa | Post | 1 | body | Cuerpo para Pre-inscripciones abiertas | +----+--------+-------+-------------+-------+------------------------------------------+ 4 rows in set (0.00 sec)
  • 13.
    Translate Behavior $posts = $this->Post->find('all', array( 'recursive' => -1, 'fields' => array('title', 'body') )); $posts = Set::combine($posts, '/Post/title', '/Post/body'); SELECT `I18n__title`.`content`, `I18n__body`.`content` FROM `posts` AS `Post` LEFT JOIN `i18n` AS `I18n__title` ON (`Post`.`id` = `I18n__title`.`foreign_key` AND `I18n__title`.`model` = 'Post' AND `I18n__title`.`field` = 'title') LEFT JOIN `i18n` AS `I18n__body` ON (`Post`.`id` = `I18n__body`.`foreign_key` AND `I18n__body`.`model` = 'Post' AND `I18n__body`.`field` = 'body') WHERE `I18n__title`.`locale` = 'eng' AND `I18n__body`.`locale` = 'eng' array( [Pre-registration opened] => Body for Pre-registration opened [Site Updates] => Body for Site Updates )
  • 14.
    Translate Behavior $this->Post->create(); $this->Post->save(array('Post' =>array( 'user_id' => 1, 'title' => array('eng' => 'ENG 1', 'spa' => 'spa1'), 'body' => array('eng' => 'Body for ENG 1', 'spa' => 'Cuerpo para spa1') ))); $this->Post->create(); $this->Post->save(array('Post' => array( 'user_id' => 1, 'title' => array('eng' => 'ENG 1'), 'body' => array('eng' => 'Body for ENG 1') ))); $this->Post->save(array('Post' => array( 'id' => $this->Post->id, 'title' => array('spa' => 'spa1'), 'body' => array('spa' => 'Cuerpo para spa1') )));
  • 15.
    Changing the language classAppController extends Controller { public $components = array('Cookie'); public function beforeFilter() { $lang = null; if (!empty($this->params['url']['lang'])) { $lang = $this->params['url']['lang']; $this->Cookie->write('CakeFestLanguage', $lang, false, '+365 days'); } else { $lang = $this->Cookie->read('CakeFestLanguage'); } if (empty($lang)) { $lang = Configure::read('CakeFest.defaultLanguage'); } Configure::write('Config.language', $lang); } function beforeRender() { $this->set('currentLanguage', Configure::read('Config.language')); } }
  • 16.
    Changing the language class AppHelper extends Helper { Public function url($url = null, $full = false) { if (!empty($url) && !is_array($url) && $url[0] == '/') { $urlRoute = Router::parse($url); if (!empty($urlRoute['controller'])) { $url = array_merge(array_intersect_key($urlRoute, array_flip(array('admin', 'controller', 'action', 'plugin'))), $urlRoute['pass'], $urlRoute['named']); } } if (is_array($url)) { if (!isset($url['lang']) && Configure::read('Config.language') != Configure::read('CakeFest.defaultLanguage')) { $url['lang'] = Configure::read('Config.language'); } } return parent::url($url, $full); } }
  • 17.
    Caching i18n elements $this->element('news',array('cache' => array( 'time' => '+1 day', 'key' => $currentLanguage ))); tmp/cache/views element_eng_news element_spa_news
  • 18.
    And we aredone Questions?