can you get more random?

Symfony and i18n – site interface translation

February 20th, 2008 Posted in geeky stuff

UPDATE: I’ve packaged up the files into a plugin, which can be found here.

I’ve been playing around with Symfony recently, with some interesting results.
One frustrations for me was the lack of documentation explaining how to store translations in a database. While symfony provides a way to localise records in the DB, the default method of using XLIFF files for site translation seemed more complicated than necessary.
As I couldnt find any information about implementing a admin interface to manage site translations, I’ve implemented this myself, what follows is a description of the steps to do this. If there are better ways to do this, I’d love to know, I’m new to symfony, so still learning.

Here are the steps to add a admin interface to manage site interface translations.

1. Add DB schema details in config/translation_schema.yml:


propel:
catalogue:
cat_id: { type: integer, size: 11, required: true, autoincrement: true, primaryKey: true }
name: { type: varchar, size: 100, required: true, default: '' }
source_lang: { type: varchar, size: 100, required: true, default: '' }
target_lang: { type: varchar, size: 100, required: true, default: '' }
date_created: { type: integer, size: 11, required: true, default: 0 }
date_modified: { type: integer, size: 11, required: true, default: 0 }
author: { type: varchar, size: 255, required: true, default: '' }
trans_unit:
msg_id: { type: integer, size: 11, required: true, autoincrement: true, primaryKey: true }
cat_id: { type: integer, size: 11, required: true, default: 1, foreignTable: catalogue, foreignRef
erence: cat_id }
source: { type: longvarchar, required: true }
target: { type: longvarchar, required: true }
comments: { type: longvarchar }
date_added: { type: integer, size: 11, required: true, default: now }
date_modified: { type: integer, size: 11, required: true, default: now }
author: { type: varchar, size: 255, required: true, default: ''}
translated: { type: boolean, required: true, default: false }

2. rebuild model: symfony propel-build-model and add the tables to your database.

CREATE TABLE `trans_unit` (
`msg_id` int(11) NOT NULL auto_increment,
`cat_id` int(11) NOT NULL default '1',
`id` varchar(255) NOT NULL default '',
`source` text NOT NULL,
`target` text NOT NULL,
`comments` text NOT NULL,
`date_added` int(11) NOT NULL default '0',
`date_modified` int(11) NOT NULL default '0',
`author` varchar(255) NOT NULL default '',
`translated` tinyint(1) NOT NULL default '0',
PRIMARY KEY (`msg_id`)
)
CREATE TABLE `catalogue` (
`cat_id` int(11) NOT NULL auto_increment,
`name` varchar(100) NOT NULL default '',
`source_lang` varchar(100) NOT NULL default '',
`target_lang` varchar(100) NOT NULL default '',
`date_created` int(11) NOT NULL default '0',
`date_modified` int(11) NOT NULL default '0',
`author` varchar(255) NOT NULL default '',
PRIMARY KEY (`cat_id`)
)

3. enable dictionary translation of interface in apps/frontend/config/i18n.yml:

prod:
default_culture: en_GB
# source: XLIFF
source: MySQL
database: mysql://user:password@host/database
debug: off
cache: on
untranslated_prefix: "[T]"
untranslated_suffix: "[/T]"
dev:
default_culture: en_GB
source: MySQL
database: mysql://user:password@host/database
debug: on
cache: off
untranslated_prefix: "[T]"
untranslated_suffix: "[/T]"

At this stage, if you’ve been using __(‘string to translate’) in your templates, you should be able to see [T]untranslated strings[/T] tagged in your development environment.

4. To check if everything is working correctly, you can manually add some data into the DB :

to define the dictionaries, you need an record in the catalogue table for each culture:

INSERT INTO `catalogue` VALUES (1,'messages.fr_FR','en_GB','fr_FR',0,1197401102,'username'),(2,'messages.de_DE','en_GB','de_DE',0,1197401129,'username'),(3,'messages.en_GB','en_GB','en_GB',0,1197397744,'username');

The translations are stores in the trans_unit table :

INSERT INTO `trans_unit` VALUES (1,3,'1','Hello %1%','Hello %1%','',1197393042,0,'',1), (79,1,'2','Hello %1%','Bonjour %1%','',1197401102,0,'',1), (154,2,'2','Hello %1%','Hallo %1%','',1197401129,0,'',1);

Using something similar to echo __('Hello %1%', array('%1%' => $username) in your templates should give you the correct translations.

Next I wanted to add an admin interface so an admin user can edit these translations without having to upload an XLIFF file. Here’s the trans_unit generator.yml file I used:

generator:
class: sfPropelAdminGenerator
param:
model_class: TransUnit
theme: default
list:
title: "Translation list"
max_per_page: 50
display: [_lang, =source, target, comments, translated]
filters: [source, _translated, _catfilter]
object_actions:
_edit: ~
_delete: ~
sort: source
fields:
source: { params: disabled=false }
translated: { params: disabled=false type: boolean }
_lang { params: lang }
catfilter { name: language }
create:
title: "create Translation"
display: [source, comments]
fields:
source: { params: disabled=false size=80, type: input_tag }
comments: { params: disabled=false size=80x5, type: textarea_tag }
actions:
_list: ~
_save: ~
_delete: ~
edit:
title: "Edit Translation"
display: [_cat, source, target, comments]
fields:
source: { params: disabled=false size=80, type: input_tag }
target: { params: disabled=false size=80, type: input_tag }
comments: { params: disabled=false size=80x5, type: textarea_tag }
actions:
_list: ~
_save: ~
_delete: ~

the aim was to add the source string for all cultures simultaneously, but only mark the string as translated once the translation has been added. This allows the admin to search for untranslated strings. This is what I added in trans_unit/actions.class.php:

class trans_unitActions extends autotrans_unitActions
{

// need to add to all languages by default
public function executeCreate()
{
$this->trans_unit = new TransUnit();
if ($this->getRequest()->getMethod() == sfRequest::POST)
{
$trans_unit = $this->getRequestParameter('trans_unit');
foreach (CataloguePeer::getCatalogues() as $catalogue) {
$c = new Criteria();
$c->add(TransUnitPeer::SOURCE, $trans_unit['source']);
$c->add(TransUnitPeer::CAT_ID, $catalogue->getCatId());
if (! TransUnitPeer::doSelectOne($c)) {
$this->trans_unit = new TransUnit();
$this->trans_unit->setSource($trans_unit['source']);
$this->trans_unit->setTarget('');
$this->trans_unit->setCatalogue($catalogue);
$this->trans_unit->save();
}
}
parent::executeList();
$this->setTemplate('list');
}
$this->labels = $this->getLabels();
//$this->setFlash('notice', $string);
//$this->setTemplate('create');
}

public function executeEdit()
{
$c = new Criteria();
if ($this->getRequestParameter('cat_id')) {
$c->add(TransUnitPeer::CAT_ID,$this->getRequestParameter('cat_id'));
}
$this->trans_unit = TransUnitPeer::doSelectOne($c);
if ($this->trans_unit) {
$this->lang = $this->trans_unit->getTargetLang();
}
$this->labels = $this->getLabels();
parent::executeEdit();
}

protected function updateTransUnitFromRequest()
{
$trans_unit = $this->getRequestParameter('trans_unit');
$this->trans_unit->setTranslated(1);
parent::updateTransUnitFromRequest();
}
protected function addFiltersCriteria($c)
{
if (isset($this->filters['source_is_empty']))
{
$criterion = $c->getNewCriterion(TransUnitPeer::SOURCE, '');
$criterion->addOr($c->getNewCriterion(TransUnitPeer::SOURCE, null, Criteria::ISNULL));
$c->add($criterion);
}
else if (isset($this->filters['source']) && $this->filters['source'] !== '')
{
$c->add(TransUnitPeer::SOURCE, '%' . $this->filters['source']. '%', Criteria::LIKE);
}
if (isset($this->filters['translated_is_empty']))
{
$criterion = $c->getNewCriterion(TransUnitPeer::TRANSLATED, '');
$criterion->addOr($c->getNewCriterion(TransUnitPeer::TRANSLATED, null, Criteria::ISNULL));
$c->add($criterion);
}
else if (isset($this->filters['translated']) && $this->filters['translated'] !== '')
{
$c->add(TransUnitPeer::TRANSLATED, $this->filters['translated']);
}
if (isset($this->filters['cat_is_empty']))
{
$criterion = $c->getNewCriterion(TransUnitPeer::CAT_ID, '');
$criterion->addOr($c->getNewCriterion(TransUnitPeer::CAT_ID, null, Criteria::ISNULL));
$c->add($criterion);
}
else if (isset($this->filters['cat_id']) && $this->filters['cat_id'] !== '')
{
$c->add(TransUnitPeer::CAT_ID, $this->filters['cat_id']);
}
}
}

This gives the interface shown in the following screenshot.Screenshot of translation interface

Share
  1. 5 Responses to “Symfony and i18n – site interface translation”

  2. By develop7 on Feb 25, 2008

    Great job. Plugin, eh?

  3. By staelche on Apr 19, 2008

    Hi,

    I want to use your great plugin, but do you have written any custom db methods in TransUnitPeer? because there are errors, which say that I don’t have a TransUnitPeer::getTargetLangArray!

    Greetz

    staelche

  1. 3 Trackback(s)

  2. Feb 25, 2008: rpsblog.com » A week of symfony #60 (18->24 february 2008)
  3. Feb 26, 2008: fluffigt.com » Blog Archive » links for 2008-02-26
  4. Mar 4, 2008: Symfony.es » Blog Archive » Una semana con Symfony #33 (18-24 febrero 2008)

Post a Comment