Add context support to the I18n class which resolves cakephp/cakephp#2063

This change adds Gettext context support to the I18n class. This
allows custom translations for verbs and nouns and more.
This commit is contained in:
Marlin Cremers 2014-06-08 11:54:28 +02:00
parent 7ea6626a15
commit b47f91c47c
7 changed files with 124 additions and 19 deletions

View file

@ -251,13 +251,17 @@ class ExtractTask extends AppShell {
protected function _addTranslation($category, $domain, $msgid, $details = array()) {
if (empty($this->_translations[$category][$domain][$msgid])) {
$this->_translations[$category][$domain][$msgid] = array(
'msgid_plural' => false
'msgid_plural' => false,
'msgctxt' => ''
);
}
if (isset($details['msgid_plural'])) {
$this->_translations[$category][$domain][$msgid]['msgid_plural'] = $details['msgid_plural'];
}
if (isset($details['msgctxt'])) {
$this->_translations[$category][$domain][$msgid]['msgctxt'] = $details['msgctxt'];
}
if (isset($details['file'])) {
$line = 0;
@ -374,6 +378,8 @@ class ExtractTask extends AppShell {
$this->_parse('__dc', array('domain', 'singular', 'category'));
$this->_parse('__dn', array('domain', 'singular', 'plural'));
$this->_parse('__dcn', array('domain', 'singular', 'plural', 'count', 'category'));
$this->_parse('__x', array('context', 'singular'));
}
}
@ -427,6 +433,9 @@ class ExtractTask extends AppShell {
if (isset($plural)) {
$details['msgid_plural'] = $plural;
}
if (isset($context)) {
$details['msgctxt'] = $context;
}
$this->_addTranslation($categoryName, $domain, $singular, $details);
} else {
$this->_markerError($this->_file, $line, $functionName, $count);
@ -551,6 +560,7 @@ class ExtractTask extends AppShell {
foreach ($domains as $domain => $translations) {
foreach ($translations as $msgid => $details) {
$plural = $details['msgid_plural'];
$context = $details['msgctxt'];
$files = $details['references'];
$occurrences = array();
foreach ($files as $file => $lines) {
@ -560,11 +570,15 @@ class ExtractTask extends AppShell {
$occurrences = implode("\n#: ", $occurrences);
$header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n";
$sentence = '';
if ($context) {
$sentence .= "msgctxt \"{$context}\"\n";
}
if ($plural === false) {
$sentence = "msgid \"{$msgid}\"\n";
$sentence .= "msgid \"{$msgid}\"\n";
$sentence .= "msgstr \"\"\n\n";
} else {
$sentence = "msgid \"{$msgid}\"\n";
$sentence .= "msgid \"{$msgid}\"\n";
$sentence .= "msgid_plural \"{$plural}\"\n";
$sentence .= "msgstr[0] \"\"\n";
$sentence .= "msgstr[1] \"\"\n\n";

View file

@ -188,10 +188,13 @@ class I18n {
* @param integer $count Count Count is used with $plural to choose the correct plural form.
* @param string $language Language to translate string to.
* If null it checks for language in session followed by Config.language configuration variable.
* @param string $context Context The context of the translation, e.g a verb or a noun.
* @return string translated string.
* @throws CakeException When '' is provided as a domain.
*/
public static function translate($singular, $plural = null, $domain = null, $category = self::LC_MESSAGES, $count = null, $language = null) {
public static function translate($singular, $plural = null, $domain = null, $category = self::LC_MESSAGES,
$count = null, $language = null, $context = null
) {
$_this = I18n::getInstance();
if (strpos($singular, "\r\n") !== false) {
@ -254,8 +257,10 @@ class I18n {
}
}
if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category][$singular])) {
if (($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$singular]) || ($plurals) && ($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$plural])) {
if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context])) {
if (($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context]) ||
($plurals) && ($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$plural][$context])
) {
if (is_array($trans)) {
if (isset($trans[$plurals])) {
$trans = $trans[$plurals];
@ -469,6 +474,7 @@ class I18n {
// Binary files extracted makes non-standard local variables
if ($data = file_get_contents($filename)) {
$translations = array();
$context = null;
$header = substr($data, 0, 20);
$header = unpack('L1magic/L1version/L1count/L1o_msg/L1o_trn', $header);
extract($header);
@ -488,6 +494,10 @@ class I18n {
if (strpos($msgstr, "\000")) {
$msgstr = explode("\000", $msgstr);
}
if ($msgid != '') {
$msgstr = array($context => $msgstr);
}
$translations[$msgid] = $msgstr;
if (isset($msgid_plural)) {
@ -515,12 +525,15 @@ class I18n {
$type = 0;
$translations = array();
$translationKey = '';
$translationContext = null;
$plural = 0;
$header = '';
do {
$line = trim(fgets($file));
if ($line === '' || $line[0] === '#') {
$translationContext = null;
continue;
}
if (preg_match("/msgid[[:space:]]+\"(.+)\"$/i", $line, $regs)) {
@ -529,31 +542,33 @@ class I18n {
} elseif (preg_match("/msgid[[:space:]]+\"\"$/i", $line, $regs)) {
$type = 2;
$translationKey = '';
} elseif (preg_match("/msgctxt[[:space:]]+\"(.+)\"$/i", $line, $regs)) {
$translationContext = $regs[1];
} elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && ($type == 1 || $type == 2 || $type == 3)) {
$type = 3;
$translationKey .= stripcslashes($regs[1]);
} elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
$translations[$translationKey] = stripcslashes($regs[1]);
$translations[$translationKey][$translationContext] = stripcslashes($regs[1]);
$type = 4;
} elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
$type = 4;
$translations[$translationKey] = '';
$translations[$translationKey][$translationContext] = '';
} elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 4 && $translationKey) {
$translations[$translationKey] .= stripcslashes($regs[1]);
$translations[$translationKey][$translationContext] .= stripcslashes($regs[1]);
} elseif (preg_match("/msgid_plural[[:space:]]+\".*\"$/i", $line, $regs)) {
$type = 6;
} elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 6 && $translationKey) {
$type = 6;
} elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
$plural = $regs[1];
$translations[$translationKey][$plural] = stripcslashes($regs[2]);
$translations[$translationKey][$translationContext][$plural] = stripcslashes($regs[2]);
$type = 7;
} elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
$plural = $regs[1];
$translations[$translationKey][$plural] = '';
$translations[$translationKey][$translationContext][$plural] = '';
$type = 7;
} elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 7 && $translationKey) {
$translations[$translationKey][$plural] .= stripcslashes($regs[1]);
$translations[$translationKey][$translationContext][$plural] .= stripcslashes($regs[1]);
} elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && $type == 2 && !$translationKey) {
$header .= stripcslashes($regs[1]);
$type = 5;
@ -563,9 +578,10 @@ class I18n {
} elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 5) {
$header .= stripcslashes($regs[1]);
} else {
unset($translations[$translationKey]);
unset($translations[$translationKey][$translationContext]);
$type = 0;
$translationKey = '';
$translationContext = null;
$plural = 0;
}
} while (!feof($file));

View file

@ -162,6 +162,10 @@ class ExtractTaskTest extends CakeTestCase {
$this->assertContains('msgid "double \\"quoted\\""', $result, 'Strings with quotes not handled correctly');
$this->assertContains("msgid \"single 'quoted'\"", $result, 'Strings with quotes not handled correctly');
$pattern = '/\#: (\\\\|\/)extract\.ctp:33\n';
$pattern .= 'msgctxt "mail"/';
$this->assertRegExp($pattern, $result);
// extract.ctp - reading the domain.pot
$result = file_get_contents($this->path . DS . 'domain.pot');

View file

@ -75,15 +75,15 @@ class I18nTest extends CakeTestCase {
$this->assertEquals('Dom 1 Foo', I18n::translate('dom1.foo', false, 'dom1'));
$this->assertEquals('Dom 1 Bar', I18n::translate('dom1.bar', false, 'dom1'));
$domains = I18n::domains();
$this->assertEquals('Dom 1 Foo', $domains['dom1']['cache_test_po']['LC_MESSAGES']['dom1.foo']);
$this->assertEquals('Dom 1 Foo', $domains['dom1']['cache_test_po']['LC_MESSAGES']['dom1.foo']['']);
// reset internally stored entries
I18n::clear();
// now only dom1 should be in cache
$cachedDom1 = Cache::read('dom1_' . $lang, '_cake_core_');
$this->assertEquals('Dom 1 Foo', $cachedDom1['LC_MESSAGES']['dom1.foo']);
$this->assertEquals('Dom 1 Bar', $cachedDom1['LC_MESSAGES']['dom1.bar']);
$this->assertEquals('Dom 1 Foo', $cachedDom1['LC_MESSAGES']['dom1.foo']['']);
$this->assertEquals('Dom 1 Bar', $cachedDom1['LC_MESSAGES']['dom1.bar']['']);
// dom2 not in cache
$this->assertFalse(Cache::read('dom2_' . $lang, '_cake_core_'));
@ -92,11 +92,11 @@ class I18nTest extends CakeTestCase {
// verify dom2 was cached through manual read from cache
$cachedDom2 = Cache::read('dom2_' . $lang, '_cake_core_');
$this->assertEquals('Dom 2 Foo', $cachedDom2['LC_MESSAGES']['dom2.foo']);
$this->assertEquals('Dom 2 Bar', $cachedDom2['LC_MESSAGES']['dom2.bar']);
$this->assertEquals('Dom 2 Foo', $cachedDom2['LC_MESSAGES']['dom2.foo']['']);
$this->assertEquals('Dom 2 Bar', $cachedDom2['LC_MESSAGES']['dom2.bar']['']);
// modify cache entry manually to verify that dom1 entries now will be read from cache
$cachedDom1['LC_MESSAGES']['dom1.foo'] = 'FOO';
$cachedDom1['LC_MESSAGES']['dom1.foo'][''] = 'FOO';
Cache::write('dom1_' . $lang, $cachedDom1, '_cake_core_');
$this->assertEquals('FOO', I18n::translate('dom1.foo', false, 'dom1'));
}
@ -1879,6 +1879,22 @@ class I18nTest extends CakeTestCase {
$this->assertSame($expected, $result['day']);
}
/**
* Test basic context support
*
* @return void
*/
public function testContext() {
Configure::write('Config.language', 'nld');
$this->assertSame("brief", __x('mail', 'letter'));
$this->assertSame("letter", __x('character', 'letter'));
$this->assertSame("bal", __x('spherical object', 'ball'));
$this->assertSame("danspartij", __x('social gathering', 'ball'));
$this->assertSame("balans", __('balance'));
$this->assertSame("saldo", __x('money', 'balance'));
}
/**
* Singular method
*

View file

@ -0,0 +1,29 @@
msgctxt "character"
msgid "letter"
msgid_plural "letters"
msgstr[0] "letter"
msgstr[1] "letters"
msgctxt "mail"
msgid "letter"
msgid_plural "letters"
msgstr[0] "brief"
msgstr[1] "brieven"
msgctxt "spherical object"
msgid "ball"
msgstr "bal"
msgctxt "social gathering"
msgid "ball"
msgstr "danspartij"
msgid "ball"
msgstr "bal"
msgid "balance"
msgstr "balans"
msgctxt "money"
msgid "balance"
msgstr "saldo"

View file

@ -29,3 +29,5 @@ __('Hot features!'
// Category
echo __c('You have a new message (category: LC_TIME).', 5);
echo __x('mail', 'letter');

View file

@ -748,6 +748,30 @@ if (!function_exists('__c')) {
}
if (!function_exists('__x')) {
/**
* Returns a translated string if one is found; Otherwise, the submitted message.
*
* @param string $context Context of the text
* @param string $singular Text to translate
* @param mixed $args Array with arguments or multiple arguments in function
* @return mixed translated string
* @link http://book.cakephp.org/2.0/en/core-libraries/global-constants-and-functions.html#__
*/
function __x($context, $singular, $args = null) {
if (!$singular) {
return;
}
App::uses('I18n', 'I18n');
$translated = I18n::translate($singular, null, null, null, null, null, $context);
$arguments = func_get_args();
return I18n::insertArgs($translated, array_slice($arguments, 1));
}
}
if (!function_exists('LogError')) {
/**