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
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...).Hola, he encontrado muy interesantes tus artículos y muy bonito tu blog.
Estoy de acuerdo contigo en que algunas cosas pueden llegar a hacerse complicadas usando un framework como CakePHP ,es por eso que me decidí a hacer zenphp y he intentado hacer lo más fácil posible todo el tema de traducciones hasta simplificarlo bastante, ésta era la asignatura que me faltaba, el automatizarlo todo, pero usando UTF-8 en lugar de i18n.
¿Crees que es útil darle tantas vueltas a un proceso automático cuando estás perdiendo más tiempo de desarrollo en poner a punto todos los requirimientos de la plataforma para que funcione como es debido?
Bueno esta interesante lo del i18n, yo he trabajdo con este behavior y es algo que cuesta un poco en un principio pero que al final es logico, tambien lo manejo con ln10 lo cual me parece lógico. tengo una pequeña página http://clubvegetariano.com que usa ln10 y i18n. Lo que tu no haces aqui es bueno pero no veo el lugar donde uses la tabla posts, todo lo alamcenas en la tabla i18n. Lo que hago yo es hacer funcionar el behavior cuando se usa un lenguaje distinto a el default. de la siguiente manera.
Nota para que les funciones deben bajar la version de aqui
En el Modelo en este caso Post he definido una funcion que les haga el cambio.
class Post extends AppModel
{
var $displayField = \'name\';
var $actsAs = array(\'Translate\' => array(\'title\', \'content\') );
function setLanguage($lang=null)
{
if($lang==null)
$lang = Configure::read(\'Config.language\');
else
Configure::write(\'Config.language\',$lang);
if(DEFAULT_LANGUAGE==$lang)
$this->detach(\'Translate\');
}
}
En caso de que tengan la versión : 1.2.0.631, deben hacer lo siguiente:
class Post extends AppModel
{
var $displayField = \'name\';
var $actsAs = array();
function setLanguage($lang=null)
{
if($lang==null)
$lang = Configure::read(\'Config.language\');
else
Configure::write(\'Config.language\',$lang);
if(DEFAULT_LANGUAGE!=$lang)
$this->actsAs = array(\'Translate\' => array(\'title\', \'content\') );
$this->__construct();
}
}
De esa maner guardaran en la tabla posts todo lo que este en el DEFAULT_LANGUAGE que es una constante que pueden definirla en el archivo /config/bootstrap.php asi:
define(DEFAULT_LANGUAGE, \'spa\');
Despues tienen saber como usar esto en su controlador posts_controller.php
public function index($id=null)
{
$lang = Config::read(\'Config.language\');
#definir lenguaje
$this->Page->setLanguage($lang);
if($page==null){
$text = $this->Page->findById(1);
}else{
$id = Sanitize::escape($id);
$text = $this->Page->findById($id);
}
$this->pageTitle = $text[\'Page\'][\'title\'];
$this->set(\'texto\', $text);
}
Para que todo esto les funcione es importante que el app_controller.php definan en la funcion beforeFilter lo siguiente.
uses(\'L10n\');
class AppCotroller extends Controller
{
var $languages = array(\'eng\', \'spa\', \'por\'); //Lenguajes que se usaran
function beforeFilter()
{
$this->L10n = new L10n();
if(!in_array($this->Cookie->read(\'lang\'),$this->languages)){
$this->Cookie->write(\'lang\', DEFAULT_LANGUAGE,false, 2592000);
$lang = DEFAULT_LANGUAGE;
}
//Aqui pueden cambiar su lenguaje solo deben hacer en alguna vista un vinculo
//que tenga el parametro $html->link(\'Español\', \'/Controller/action/lang:esp\');
if(isset($this->params[\'named\'][\'lang\']) && in_array($this->params[\'named\'][\'lang\'],$this->languages) ){
#$this->Cookie->del(\'lang\');
$lang = $this->params[\'named\'][\'lang\'];
$this->Cookie->write(\'lang\',$lang, false, 2592000);
}
$lang = $this->Cookie->read(\'lang\');
$this->L10n->get($lang);
Configure::write(\'Config.language\', $lang);
}
}
En este caso he usado ln10 que pueden encontrar inforación aqui
Saludos gracias por el tutor.
Bueno solo quiero decirte que veas la conexión de la base de datos con que codificación está, por que tu sitio está manejando mal las codificaciones.
Saludos
Hola, tras buscar y buscar información sobre aplicaciones multi-idioma en cake y solución a sus ya conocidos problemas encontré este post que me salvó la vida. Gracias :)
Lo he implementado todo pero tengo un pequeño gran problema que no se si os ha pasado o sabeis como solucinar:
Necesito poder editar los registros de todos los idiomas en un mismo formulario. Al cargar los datos en el formulario de edición este se completa con los datos repetidos tan solo en el idioma actual y no en todos los idiomas y no se me ocurre nada. :(
Me gustaría saber si os ha pasado si se os ocurre algo.
El controlador:
function edit($id = null)
{
if (empty($this->data))
{
$this->PropertyType->id = $id;
$this->data = $this->PropertyType->read();
}
else
{
if ($this->PropertyType->save($this->data['PropertyType']))
{
$this->flash('El tipo de propiedad ha sido actualizado', '/property_types');
}
}
}
La vista:
<?php
$languages=Configure::read('Settings.languages');
echo $form->create('PropertyTypes', array('action' => 'edit'));
echo $form->input('PropertyType.id', array('type' => 'hidden'));
foreach($languages as $lang)
{
echo $form->input('PropertyType.name.'.$lang);
}
echo $form->end('Actualizar');
?>
Mil gracias
Hola Pablo, yo tenia la misma necesidad... y tambien este hack me salvo la vida. Bueno cuestion que pensando un poquito... le agregue otro hack muy similar al beforeSave del traslate y realmente logre hacer update los campos multi-idioma al mismo tiempo de maravillas. Paso como quedaria el hack completo...
function afterSave(&$model, $created) {
if (!isset($this->runtime[$model->alias]['beforeSave'])) {
return true;
}
$locale = $this->_getLocale($model);
$tempData = $this->runtime[$model->alias]['beforeSave'];
unset($this->runtime[$model->alias]['beforeSave']);
$conditions = array('locale' => $locale, 'model' => $model->alias, 'foreign_key' => $model->id);
$RuntimeModel =& $this->translateModel($model);
if (empty($created)) {
$translations = $RuntimeModel->find('list', array('conditions' => array_merge($conditions, array($RuntimeModel->displayField => array_keys($tempData)))));
if ($translations) {
foreach ($translations as $id => $field) {
if (is_array($tempData[$field])) // ESTE IF SERIA EL NUEVO HACK AGREGADO POR MI
{
foreach ($tempData[$field] as $loc=>$val)
{
$RuntimeModel->create();
$RuntimeModel->save(array($RuntimeModel->alias => array('id' => $id, 'content' => $tempData[$field][$loc])));
$id++; // SE SUPONE QUE EL PROXIMO ID ES EL MISMO CAMPO DEL SIGUIENTE IDIOMA
}
}
else
{
$RuntimeModel->create();
$RuntimeModel->save(array($RuntimeModel->alias => array('id' => $id, 'content' => $tempData[$field])));
}
unset($tempData[$field]);
}
}
}
if (!empty($tempData)) {
foreach ($tempData as $field => $value) {
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();
}
}
}
}
Espero les sirva!
Muchisimas gracias, todavía no lo he probado pero lo haré en estos días.
Sabéis si esto es un bug o simplemente es el funcionamiento que va a llevar en la versión 1.2.
Saludos, y muchas gracias otra vez.
Hola!
Que suerte haber dado con tu web, de los poquitos sitios con documentación decente para cakephp...
Tengo una duda, que no se si podrás resolverme, pero si lo haces te lo agradeceré mucho :D
Estamos haciendo un proyecto con cakephp 1.2 e internacionalización, mediante una tabla de traducciones i18n que tiene los campos id, locale, model, foreign_key, field y content.
Todo funciona correctamente hasta que intentamos hacer una consulta de una tabla y su relacionada. Por ejemplo, Category y Subcategory.
Pido las categorías y me devuelve una matriz con el array de Category y array con las Subcategory asociadas a la categoría. Hasta quí bien. El problema es que las traducciones de Subcategory no me las devuelve, y si pongo recursive = 2 me da una estructura de array sin sentido (o al menos yo no se lo encuentro).
Con recursive = 2:
$this->Category->recursive = 2;
$data = $this->Category->find('all');
Resultado: http://pastebin.com/f7b634a05
Con recursive por defecto:
$data = $this->Category->find('all');
Resultado: http://pastebin.com/f574fc755
Resultado ESPERADO (y no obtenido):
$data = $this->Category->find('all');
Resultado: http://pastebin.com/f52f8af27
Como se puede ver, en el resultado esperado está la traducción de las subcategorías, pero no doy con la manera de sacarlo.
¿Alguna idea u orientación por dónde coger esto?
Muchas gracias,
David.


