Translating of something with gettext is well-discussed problem, so there’s no need to discuss it again. Everything works like a charm, when you dealing with string constants. But what to do if you need to translate a string retrieved from some variable, e.g. _($str)? Translation is not a problem at all, so if you have $str defined as ‘Text’, previous call will output ‘Text’, unless it will find a translated string with such msgid. Unfortunately you won’t be able to prepare a pot-file automatically with these messages. But even when you were eaten, you have at least two ways out. So let’s find some of them ;))
I first met this problem, while was dealing with Doctrine’s enums. So let’s define a sample model that we will use in examples, and which will show the bottle neck:
<?php
class Chick extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('address', 'text');
$this->hasColumn('boobs', 'enum', null, array(
'values' => array('cranberries', 'peaches', 'oranges', 'balloons')
);
}
}
As you can see every Chick has boobs of some type. So somewhere we’ll write them as:
/** @var $chick Chick */
printf(_('%s has boobs similar to %s'), $chick->name, _($chick->boobs));
Let’s do it easy
The most simple way to achieve our goal (pot file creation with xgettext or poedit) is to use a block with desired messages that will never be executed:
if (0) { // will never be executed
_('cranberries');
_('peaches');
_('oranges');
_('balloons');
}
This is quite easy way. But it has very big disadvantage — you have to keep that block always up-to date — every time you change something related to the variable string, you have to pay attention on that block. So we are ready for second way now…
Let’s do it smarter
Another option we can deal with variable strings is to mark such strings with something. And then gather such strings into separate dummy file that will be used as one of sources of strings but will never be used in real world. Let’s modify enum definition in the way it will have some marker, like this:
// ... skipped ...
$this->hasColumn('boobs', 'enum', null, array(
'values' => array(
'cranberries', //_
'peaches', //_
'oranges', //_
'balloons' //_
)
);
// ... skipped ...
Now all we need is a some dummy parser that will be able to deal with such strings, for example, let’s write it in Ruby (so it can be included as one of Rakefile tasks):
require 'find'
files_pattern = /\.(?:php|phtml|inc)$/
single_quoted = /'(?>(?:\\.|[^'])+)'/
double_quoted = /"(?>(?:\\.|[^"])+)"/
quoted_string = /#{single_quoted}|#{double_quoted}/
match_pattern = /(#{quoted_string}).+\/\/_/
result = "<?php\n"
Find.find('.') do |f|
next unless File.file? f and f.match('')
File.open(f).each_with_index do |s, i|
m = s.match(match_pattern)
result << "_(#{m[1]}); // #{f}:#{i}\n" unless m.nil?
end
end
puts result
The output of it’s execution will produce something like this:
<?php
_('cranberries'); // ./models/Chick.php:9
_('peaches'); // ./models/Chick.php:10
_('oranges'); // ./models/Chick.php:11
_('balloons'); // ./models/Chick.php:12
Now we can use both of described ways, and they are good enough for variable strings. But for Doctrine_Record’s enums there is much more beautiful way to achieve this :)) So let’s find it…
Let’s do it our way
Doctrine ORM allows you to define a mutator for each property your model has. So according to our Chick model we can define a special getter getBoobs() and setter setBoobs which will auto translate our boobs’ types :)) Basically this can become an idea of how to achieve this with some of your objects. Here’s dummy version of such getter/setter:
// ... skipped ...
public function getBoobs()
{
switch ($this->_get('boobs')) {
case 'cranberries': return _('little cranberries');
case 'peaches': return _('smooth peaches');
case 'oranges': return _('tasty oranges');
case 'balloons': return _('mighty balloons');
default: throw new Doctrine_Exception("I don't have a clue how this chick boobs looks like.");
}
}
public function setBoobs($boobs)
{
switch (_($boobs)) {
case 'little cranberries': return $this->_set('boobs', 'cranberries');
case 'smooth peaches': return $this->_set('boobs', 'peaches');
case 'tasty oranges': return $this->_set('boobs', 'oranges');
case 'mighty balloons': return $this->_set('boobs', 'balloons');
default: throw new Doctrine_Exception("I don't have any idea about specified boobs type.");
}
}
// ... skipped ...
Looks awful! Too many similar code. So let’s define a map of values like this:
// ... skipped ...
private static $_boobs = null;
private function _getBoobs()
{
if (null === self::$_boobs) {
self::$_boobs = array(
'cranberries' => _('little cranberries'),
'peaches' => _('smooth peaches'),
'oranges' => _('tasty oranges'),
'balloons' => _('mighty balloons')
);
}
return self::$_boobs;
}
// ... skipped ...
Now we have values kept in one place, so let’s extend model with mutators keeping in mind this map, so here’s final version of model:
<?php
class Chick extends Doctrine_Record
{
private static $_boobs = null;
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('address', 'text');
$this->hasColumn('boobs', 'enum', null, array(
'values' => array_keys(self::_getBoobs())
);
}
private static function _getBoobs()
{
if (null === self::$_boobs) {
self::$_boobs = array(
'cranberries' => _('little cranberries'),
'peaches' => _('smooth peaches'),
'oranges' => _('tasty oranges'),
'balloons' => _('mighty baloons')
);
}
return self::$_boobs;
}
public function getBoobs()
{
$boobs = self::_getBoobs();
return $boobs[$this->_get('boobs')];
}
public function setBoobs($value)
{
if (false !== ($boobs = array_search($value, self::_getBoobs()))) {
$value = $boobs;
}
return $this->_set('boobs', $value);
}
}
So now, previous printf call can become:
/** @var $chick Chick */
printf(_('%s has boobs as good as %s'), $chick->name, $chick->boobs);
And you might noticed that setBoobs() allows both translated and untranslated strings as input. So both of following lines will work as expected:
/** @var $chicks Array of Chick $chicks[0]->boobs = 'oranges'; $chicks[1]->boobs = $chicks[0]->boobs;
Now that’s all! :))