Symfony and i18n - site interface translation
February 20th, 2008 Posted in geeky stuffUPDATE: 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=80×5, 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=80×5, 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']);
}
}
}

17 Responses to “Symfony and i18n - site interface translation”
By develop7 on Feb 25, 2008
Great job. Plugin, eh?
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
By gareth on Apr 23, 2008
Thanks for your comment - you’re right, I have the following in lib/model/TransUnitPeer.php
class TransUnitPeer extends BaseTransUnitPeer
{
static public function getTargetLangArray()
{
$c = new Criteria();
$catalogues = CataloguePeer::doSelect($c);
foreach ($catalogues as $catalogue) {
$cat_id = $catalogue->getCatId();
$cat[$cat_id] = $catalogue->getTargetLang();
}
return $cat;
}
}
By Eddie on Jun 10, 2008
Hi Gareth,
I’m really having difficulties here :-/ For example symfony is missing the createSuccess.php template. Any chance you could post it here?
Eddie
By gareth on Jun 14, 2008
Hi Eddie,
You shouldn’t need createSuccess.php, as all this is handled by the generator.yml file. Do you see an error message ? What is the URL ?
thks
Gareth
By gareth on Jun 14, 2008
for those who asked, I’ve just packaged up the files into a pear package, so you can install the plugin as follows :
symfony plugin-install http://www.bemused.org/symfony/sfI18nDbTranslationPlugin/sfI18nDbTranslationPlugin-0.0.1.tgz
I haven’t tested it thouroughly, so if it doesn’t work for you, let me know and I’ll have a look.
By gareth on Jun 14, 2008
make that :
symfony plugin-install http://www.bemused.org/symfony/sfI18nDbTranslationPlugin/sfI18nDbTranslationPlugin-0.0.2.tgz
I’ve added some missing files, let me know if it works for you.
By robert_speer on Aug 26, 2008
If you use created_at and updated_at, OR created_on and updated_on, instead of date_added and date_modified, symfony will automatically update those for you
totally minor, but an interesting nugget
By 4levels on Dec 31, 2008
Hi there,
thanx for the hands-on how to implement this nice feature!
I’m having currently the following issues:
* The items are correctly stored into the database and the admin is almost working, but the translation is not, strings in my templates remain untranslated.
* The archive with the plugin files is offline since a while now, can you please upload it again? Especially the filter and other partials would be very nice
I managed to get the backend working but the edit action still gives me troubles
Thanks a lot again!
By sebastian on Jan 7, 2009
thanks alot! you saved my day!
By gareth on Jan 8, 2009
Hi Robert,
I’m using the schema from the file symfony/i18n/sfMessageSource_MySQL.class.php which is where I got the field names from. Actually I think that the created_at/updated_at fields only work for tables that are part of your model (/lib/model/*)
By gareth on Jan 8, 2009
Hi,
If the strings are getting correctly stored in the DB, but not showing up in frontend pages, check your i18n settings in
apps/frontend/config/factories.ymlYou should have something like this :
all:i18n:
class: sfI18N
param:
source: MySQL
database: mysql://user:passwd@host/table
debug: off
untranslated_prefix: "[T]”
untranslated_suffix: “[/T]”
cache:
class: sfFileCache
param:
automatic_cleaning_factor: 0
cache_dir: %SF_I18N_CACHE_DIR%
lifetime: 86400
prefix: %SF_APP_DIR%
Sorry about the missing plugin files, I moved servers recently and forgot to put the files in the right place. Should be here now.
By kevin on Mar 18, 2009
Great, but only worth it if you have a lot a string to translate or you delegate translation to peoplewith zero computin skill.
But if not, use “php symfony i18n:extract app_name to_culture –auto-save” to extract all the untranslated string surounded by __() from the application and store it in the proper XLIFF file.
E.G : php symfony i18n:extract frontend fr –auto-save
It will store all the english string that need french translation in app/frontend/i18n/fr/messages.xml
Then, using easyeclipse for lamp dev (http://www.easyeclipse.org/site/distributions/lamp.html), you have an XML editor that is far sufficent to edit quicly the XML file.
By gareth on Mar 19, 2009
Hi Kevin,
You correctly understood why I developed this extension - to enable people to easily edit translations, without the need to edit xml files.
The other advantage is that you can have several people editing translations without having to manage merges in a single translation file. Also I don’t need to push the translation file to the live servers every time a translation is made, the latest translations are always instantly available on the production machines.
Gareth