CakePHP: i18n y Translate Behavior

Últimamente he tenido un montón de problemas con el Translate Behavior, que es el encargado de agregar internacionalización a una aplicación web desarrollada en CakePHP.

El problema no sé si era que yo no entendía el funcionamiento interno o que no se adaptaba a mis necesidades. Pero antes de entrar en los detalles del mismo veamos la forma de actuar de dicho behavior.

Supuesto

Se supone que tenemos una tabla -fictícea- llamada posts por ejemplo, cuya estructura es muy simple: posts(id, title, content, created, modified). Los campos susceptibles de traducción son title y content. Entonces la tabla que realmente tenemos que crear sería la siguiente: posts(id, created, modified), excluyendo los campos traducibles.

Una vez hemos hecho el análisis crearemos la tabla donde se van a guardar las traducciones, esta tabla se llama i18n y su esquema sql viene en app/config/sql/i18n.sql, es el siguiente:

CREATE TABLE i18n (
    id int(10) NOT NULL auto_increment,
    locale varchar(6) NOT NULL,
    model varchar(255) NOT NULL,
    foreign_key int(10) NOT NULL,
    field varchar(255) NOT NULL,
    content mediumtext,
    PRIMARY KEY (id),
#   UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
#   INDEX I18N_LOCALE_ROW(locale, model, foreign_key),
#   INDEX I18N_LOCALE_MODEL(locale, model),
#   INDEX I18N_FIELD(model, foreign_key, field),
#   INDEX I18N_ROW(model, foreign_key),
    INDEX locale (locale),
    INDEX model (model),
    INDEX row_id (foreign_key),
    INDEX field (field)
);

Hasta aquí la configuración de la base de datos, ya tenemos las tablas preparadas para aceptar información.

Modelo

Ahora vamos a configurar el modelo Posts para indicarle a CakePHP cuales son los campos susceptibles de traducción y ate cabos sueltos. Editamos post.php agregando lo siguiente en $actsAs:

class Post extends AppModel
{
    var $name = 'Post';
    var $actsAs = array('Translate' => array('title', 'content'));
}

Se supone que a partir de ahora cuando guardemos datos a través de un formulario se guardará más o menos algo así:

INSERT INTO `posts` (`modified`,`created`) VALUES ('2008-02-15 13:40:21','2008-02-15 13:40:21')
INSERT INTO `i18n` (`locale`,`model`,`foreign_key`,`field`,`content`) VALUES ('eng','Post',1,'MyTitle','MyContent')
INSERT INTO `i18n` (`locale`,`model`,`foreign_key`,`field`,`content`) VALUES ('spa','Post',1,'MiTitulo','MiContenido')

Suponiendo que MyTitle, MyContent y MiTitulo, MiContenido sean los valores rellenados en los inputs del formulario :D. Bien, pues aqui es donde radica el principal problema. Según he probado -y por pruebas no ha sido- falla la foreign_key puesto que la cambia cada vez que intentamos guardar un idioma distinto, con lo que no se refiere al mismo Post y nada de lo anterior funciona.

Hack

La solución la he encontrado aquí, un pequeño hack al behavior y automágicamente podremos insertar varios idiomas de golpe. Una vez editado el archivo cake/libs/model/behaviors/translate.php y agregado el anterior hack todo será más sencillo, fijaos en las diferencias:

if (is_array($value)) {
    foreach ($value as $loc=>$val) {
        $tmploc = array('locale'=>$loc);
        $RuntimeModel->create(array_unique(array_merge($conditions,$tmploc, array($RuntimeModel->displayField => $field, 'content' => $val))));
        $RuntimeModel->save();
    }
}
else {
    $RuntimeModel->create(array_merge($conditions, array($RuntimeModel->displayField => $field, 'content' => $value)));
    $RuntimeModel->save();
}

Ahora solo falta preparar los datos -osea, el formulario-.

Vista

Podemos hacerlo de varias formas, primero la guarra que no servirá de mucho si queremos agregar un idioma nuevo, puesto que tendremos que tocar todos los formularios de agregado/edición:

echo $form->create('Post');
# Spa
echo $form->input('Post.title.spa');
echo $form->input('Post.content.spa');
# Eng
echo $form->input('Post.title.eng');
echo $form->input('Post.content.eng');
# Por
echo $form->input('Post.title.por');
echo $form->input('Post.content.por');

echo $form->end('Submit');

Y la forma más lógica sería tener un array donde almacenamos todos los lenguajes que va a soportar nuestra aplicación, por ejemplo en config/config.php algo así:

$config['Settings'] = Configure::read('Settings');
$config['Settings'] = Set::merge(ife(empty($config['Settings']), array(), $config['Settings']), array
(
    'default_language' => 'spa',
    'languages' => array('eng','spa', 'por', 'gal'),
));

Ojo: Para cargar esta configuración debemos agregar una linea en config/bootstrap.php:

Configure::load('config');

Con lo que a la hora de crear el formulario de agregado/edición de datos todo sería más sencillo:

$languages=Configure::read('Settings.languages');
echo $form->create('Post');
foreach($languages as $lang)
{
    echo $form->input('Post.title.'.$lang);
    echo $form->input('Post.content.'.$lang);
}
echo $form->end('Submit');

Controlador

El punto final sería en el controlador, la funcion admin_add() o admin_edit() que se encargan de insertar-modificar los datos introducidos. No tiene mucho truco:

function admin_add()
{
    if (!empty($this->data))
    {
        $this->Post->create();
        $this->Post->save($this->data);
    }
}

Conclusión

Así de simple. Creo que no se me olvida nada, aunque el descubrimiento ha sido reciente y he decidido escribirlo ahora que está fresco. Imagino que la integración con l10n será mucho más sencilla siempre que mantengamos la misma convención a la hora de llamar a los idiomas (spa, eng...).

About the author

Óscar
has doubledaddy super powers, father of Hugo and Nico, husband of Marta, *nix user, Djangonaut and open source passionate.