CakePHP: i18n y Translate Behavior
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. Editamosclass 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); } }
Comentarios
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?
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.
En caso de que tengan la versión : 1.2.0.631, deben hacer lo siguiente:
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:
Despues tienen saber como usar esto en su controlador posts_controller.php
Para que todo esto les funcione es importante que el app_controller.php definan en la funcion beforeFilter lo siguiente.
En este caso he usado ln10 que pueden encontrar inforación aqui
Saludos gracias por el tutor.
Saludos
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:
La vista:
Mil gracias
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!
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.
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.
Me quedo en la misma situación, todo funciona perfectamente, yo en vez de cargar los idiomas ($languages=Configure::read('Settings.languages')) los cargo desde una base de datos.
El problema es en el formulario de edición, creo que la explicación está en que controlador sólamente manda los datos correspondientes al idioma actual, y en el $this->data no hay información acerca del resto de idiomas, con lo cual no rellena los campos.
Aún así, he modificado la vista del edit de forma que llamo a una función y relleno los input con los valores ('values'=>'') que le corresponden a huevo, y lo hace bien, pero al guardar me guarda los campos como le da la gana.
De la siguiente forma:
//recupero los lenguajes de la base de datos
$idiomas=$this->requestaction("idiomas/index");
//recorro los lenguajes para los campos edit
foreach($idiomas as $idioma) {
$menus=$this->requestaction("horcormenus/campos/".$this->data['Horcormenu']['id']."/Horcormenu/" . strtolower($idioma['Idioma']['ISO']));
if (!empty($menus)) {
echo "";
echo " ";
//si detectara un idioma pero no tiene traduccion lo muestro
} else {
echo "";
echo " ";
}
}
Todo ésto funciona perfectamente, pero no me guarda bien los datos desde el EDIT
function edit($id = null) {
if (!$id && empty($this->data)) {
$this->Session->setFlash(__('Invalid Estado', true));
$this->redirect(array('action'=>'index'));
}
if (!empty($this->data)) {
if ($this->Estado->save($this->data)) {
$this->redirect(array('action'=>'index'));
}
else {
$this->Session->setFlash(__('The Estado could not be saved. Please, try again.', true));
}
}
if (empty($this->data)) {
$languages=Configure::read('Settings.languages');
foreach($languages as $lang){
$this->Estado->locale= $lang;
$this->data[$lang] = $this->Estado->read(null, $id);
}
}
$data = $this->data;
$acciones = $this->Estado->Accione->find('list');
$this->set(compact('acciones', 'data'));
}
Lo que está en negritas es lo que cambié para que funcionara.
Luego en la vista, lo levanto desde $data, el siguiente código estaría incluído en edit.ctp:
....
/*Donde dice spa, se puede levantar el default de la misma manera que el array de lenguajes*/
echo $form->input('id', array('value'=>$data['spa']['Estado']['id']));
//Las siguiente líneas se agregaron para i18n
foreach($languages as $lang)
{
echo $form->input('Estado.ds_estado.'.$lang, array('label'=>'Nombre en '.$lang, 'value'=>$data[$lang]['Estado']['nombre_estado']));
}
echo $form->end('Guardar');
?>
Pasó mucho tiempo del último POST, pero igual publiqué mi solución quizás a alguien le sirve.-
Saludos y gracias por los post anteriores!!
Espero puedas solicionar tu problema.-
El translate parece que no traduce las tablas de las subconsultas como comenta el usuario David.
¿Alquien sabe como hacer para que esto funcione?