issem danny
issem danny

Reputation: 128

CakePhp TranslateBehavior, validate and save multiple locale

Context: I Want to create a web application using CakePhp which should be translatable. I want to save multiple translations for the same field in one form.

Problem: I've tried a dozen ways to get this to work and I did. But I ended up using two custom SQL queries which really doesn't feel like a cakePhp solution.

Question: Does anybody know a better way to achieve the same result?

What I tried:

I'll add my view and model below: (if u want to see more code or need more info just ask)

/App/View/Category/add.ctp

<?php echo $this->Form->create(); ?>
<?php echo $this->Form->input('title|dut'); ?>
<?php echo $this->Form->input('title|eng'); ?>
<?php echo $this->Form->input('title|fre'); ?>
<?php echo $this->Form->input('description|dut', array('type'=>'textarea')); ?>
<?php echo $this->Form->input('description|eng', array('type'=>'textarea')); ?>
<?php echo $this->Form->input('description|fre', array('type'=>'textarea')); ?>
<?php echo $this->Form->end('add'); ?>

/App/Model/AppModel.php

<?php
App::uses('Model', 'Model');
class AppModel extends Model {

  /**
   * Check Unique
   *
   * Searches the i18n table to determine wetter a field is unique or not.
   * Expects field name to be as following: "fieldname|locale".
   * 
   * @param array $data     The data of the field, automatically passed trough by cakePhp.
   * @param string $field   The name of the field, which should match the one in the view.
   * @returns boolean
   */
  public function checkUnique($data, $field) {
    // Seperate the field key and locale which are seperated by "|".
    $a = preg_split('/[|]/', $field, 2);
    // If field key and locale are found...
    if (is_array($a) || count($a) === 2) {
      $q = sprintf("SELECT * FROM i18n WHERE i18n.locale = '%s' AND i18n.model = '%s' AND i18n.field = '%s' AND i18n.content = '%s' LIMIT 1",
        Sanitize::escape($a[1]),
        Sanitize::escape(strtolower($this->name)),
        Sanitize::escape($a[0]),
        Sanitize::escape($data[$field])
      );
      if ($this->query($q)) {
        return false;
      }
      return true;
    }
  }

  /**
   *  Setup Translation
   *
   *  Loops trough the fields. If a field is translatable
   *  (which it will know by it's structure [fieldname]|[locale])
   *  and has the default locale. Then it's value will be stored
   *  in the array where cake expects it 
   *  (data[Model][fieldname] instead of data[Model][fieldname|defaultLocale])
   *  so that cake will save it to the database.
   * 
   *  In the afterSave method the translations will be saved, for then we know
   *  the lastInsertId which is also the foreign_key of the i18n table.
   */
  public function _setupTranslations() {
    foreach($this->data[$this->name] as $key => $value) {
      $a = preg_split('/[|]/', $key, 2);
      if (is_array($a) && count($a) === 2) {
        $languages = Configure::read('Config.languages');
        if ($a[1] === $languages[Configure::read('Config.defaultLanguage')]['locale']) {
          $this->data[$this->name][$a[0]] = $value;
        }
      }
    }
  }

  /**
   *  Save Translations
   *  
   *  Saves the translations to the i18n database.
   *  Expects form fields with translations to have
   *  following structure: [fieldname]|[locale] (ex. title|eng, title|fre, ...).
   */
  public function _saveTranslations() {
    foreach($this->data[$this->name] as $key => $value) {
      $a = preg_split('/[|]/', $key, 2);
      if (is_array($a) && count($a) === 2) {
        $q = sprintf("INSERT INTO i18n (locale, model, foreign_key, field, content) VALUES ('%s', '%s', '%s', '%s', '%s')",
          Sanitize::escape($a[1]),
          Sanitize::escape(strtolower($this->name)),
          Sanitize::escape($this->id),
          Sanitize::escape($a[0]),
          Sanitize::escape($value)
        );
        $this->query($q);
      }
    }
  }

  /**
   * Before Save
   */
  public function beforeSave() { 
    $this->_setupTranslations();
    return true;
  }

  /**
   * After Save
   */
  public function afterSave() {
    $this->_saveTranslations();
    return true;
  }
}

/App/Model/Category.php

<?php
class Category extends AppModel {
  public $name = 'Category';
  public $hasMany = array(
    'Item'=>array(
      'className'=>'Item',
      'foreignKey'=>'category_id',
      'order'=>'Item.title ASC'
    )
  );
  var $actsAs = array(
    'Translate'=>array(
      'title',
      'description'
    )
  );
  public $validate = array(
    'title|dut'=>array(
      'required'=>array(
        'rule'=>'notEmpty',
        'message'=>'Veld verplicht'
      ),
      'unique'=>array(
        'rule'=>array('checkUnique', 'title|dut'),
        'message'=>'Titel reeds in gebruik'
      ),
    ),
    'title|eng'=>array(
      'required'=>array(
        'rule'=>'notEmpty',
        'message'=>'Veld verplicht'
      ),
      'unique'=>array(
        'rule'=>array('checkUnique', 'title|eng'),
        'message'=>'Titel reeds in gebruik'
      ),
    ),
    'title|fre'=>array(
      'required'=>array(
        'rule'=>'notEmpty',
        'message'=>'Veld verplicht'
      ),
      'unique'=>array(
        'rule'=>array('checkUnique', 'title|fre'),
        'message'=>'Titel reeds in gebruik'
      ),
    ),
  );
}
?>

NOTE: There isn't that much information out there on this subject... I have a lot more questions about the translation behavior like getting the recursive results also in the correct locale, ... Anybody know a good tut or source of info (cookbook is quite limited)

Thanks for reading!!

Upvotes: 1

Views: 1055

Answers (1)

Chuck Burgess
Chuck Burgess

Reputation: 11574

It appears you may be building a CRM of sorts that allows the users to establish content that is read into the site based on the language they have set. I would use the built in i18n and l10n. It makes it really simple, but this is probably not a solution for dynamic content.

Having said that, the only other way I can think of doing this is very tedious. I would build a single screen with a language identifier drop down. So instead of trying to cram ALL languages in the same screen with a test box for each language, I would create one form and then use a drop down for the language.

Your model is using a column to define with language the row belongs to. The form you have created is expressing all languages in a single row. So if you were to view the Index page showing the records, of course you would see:

title 1 eng
title 1 dut
title 1 fre
title 2 eng
title 2 dut
title 2 fre
...

Further more, if you were ever to add a new language, you will have to modify the validation in the model and the form.

However, if you are set on doing it this way, change the | to _ and off you go. But then you will need to store all of the data in a single record. So when you look at the Index for the records, you will see:

title 1 end dut fre
title 2 end dut fre
...

My Advice:

1) Use the built in i18n / l10n using .po / .pot files.

2) If the content will be changing frequently and required to be stored in the database so it can be easily changed / updated frequently on the fly, then use a drop down.

Language: dropdown
Title: text_field

Upvotes: 1

Related Questions