CakePHP: i18n y Translate Behavior - Userlinux.net

Ú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…

CakePHP: i18n y Translate Behavior

r0sk 15.Feb.2008 12 Comentarios 5272 Lecturas
Ú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...).

Comentarios


gravatar
17.Feb.2008
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?

gravatar
18.Feb.2008
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.

gravatar
18.Feb.2008
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

gravatar
Pablo
11.May.2008
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:


$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

gravatar
24.May.2008
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!

gravatar
Pablo
28.May.2008
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.

gravatar
david
23.Sep.2008
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.

gravatar
Feli
11.Mar.2009
Hola!! muy interesante el post y muy útil.

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 "". $idioma['Idioma']['idioma'] ."";
//recorro los menus y asigno el value manualmente
foreach ($menus as $menu):
//voy recuperando los valores y los pongo como value
echo $form->input('Horcormenu.titulo.'.$idioma['Idioma']['ISO'], array('label'=>'Título de menú
', 'value'=>$menu['I18n__titulo']['content']));
echo $form->input('Horcormenu.contenido.'.$idioma['Idioma']['ISO'], array('label'=>'Descripción
', 'value'=>$menu['I18n__contenido']['content']));
endforeach;
echo "
";
echo " ";
//si detectara un idioma pero no tiene traduccion lo muestro
} else {
echo "
";
echo "". $idioma['Idioma']['idioma'] ."";
echo $form->input('Horcormenu.titulo.'.$idioma['Idioma']['ISO'], array('label'=>'Título de menú
', 'value'=>''));
echo $form->input('Horcormenu.contenido.'.$idioma['Idioma']['ISO'], array('label'=>'Descripción
', 'value'=>''));
echo "
";
echo " ";
}
}

Todo ésto funciona perfectamente, pero no me guarda bien los datos desde el EDIT

gravatar
26.Jun.2009
Hola Feli: A mi me pasaba lo mismo. Me funcionaba todo, pero me guardaba mal los datos en el edit. El problema era que al armar el arreglo de datos desde la función edit del controller, perdía el ID. Con lo cual, al no tener el id, el edit funcionaba como un add. Lo arreglé agregando el armado de datos en el controller de la siguiente manera:

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!!

gravatar
Robert Rodriguez
03.Jul.2009
¿Como logro eliminar o darle mantenimiento a la tabla i18n lo que he eliminado de la tabla original?

gravatar
12.Jul.2009
Hola Robert, no entiendo cuál es tu problema. Las tablas quedan relacionadas, y si agregas cosas en la tabla original se agregan en i18n y lo mismo pasa si borrás. Si modificás, se modifica el contenido en la tabla i18n. Pero todo esto lo hace cake "mágicamente", si no te funciona es porque estás armando algo mal o tenés alguna complejidad en el problema y que yo no estoy considerando en la respuesta.-
Espero puedas solicionar tu problema.-

gravatar
03.Feb.2010
Hola,

El translate parece que no traduce las tablas de las subconsultas como comenta el usuario David.

¿Alquien sabe como hacer para que esto funcione?

Escribe un comentario

Nick
Email
URL

Buscar

Cargando...

Categorías

Últimos comentarios

  • BartlettLilly20
  • r0sk
  • coder
  • argordmel
  • uveic
  • MarcosBL
  • quemada
  • alexander
  • Hakky111
  • tramel
  • hoyadas
  • hoyadas
  • hoyadas
  • Anubys
  • Arturo

Tagcloud

lugo freebsd seguridad futbol iphone mysql champions userlinux alemania_2006 copa bsd cakephp rfilms deportes meme blogs conciertos bake debian macosx apple humor programación música games 2008 beers tip lucux cake sysadmin ssh ibook films cumpleaños bash league barça soccer mundial frases opinion felicidades hack php juegos cms personal 2007 ubuntu ds mac sidenotes openbsd linux blogsfera rsidenotes blog nintendo san_froilan

Archivo

Social

Twitter

Enlaces

Enlaces de interés