Skip to content

Commit

Permalink
Add context support to the I18n class which resolves #2063
Browse files Browse the repository at this point in the history
This change adds Gettext context support to the I18n class. This
allows custom translations for verbs and nouns and more.
  • Loading branch information
Marlinc committed Jul 17, 2014
1 parent 7ea6626 commit b47f91c
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 19 deletions.
20 changes: 17 additions & 3 deletions lib/Cake/Console/Command/Task/ExtractTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,17 @@ public function execute() {
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;
Expand Down Expand Up @@ -374,6 +378,8 @@ protected function _extractTokens() {
$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'));
}
}

Expand Down Expand Up @@ -427,6 +433,9 @@ protected function _parse($functionName, $map) {
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);
Expand Down Expand Up @@ -551,6 +560,7 @@ protected function _buildFiles() {
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) {
Expand All @@ -560,11 +570,15 @@ protected function _buildFiles() {
$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";
Expand Down
36 changes: 26 additions & 10 deletions lib/Cake/I18n/I18n.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,13 @@ public static function getInstance() {
* @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) {
Expand Down Expand Up @@ -254,8 +257,10 @@ public static function translate($singular, $plural = null, $domain = null, $cat
}
}

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];
Expand Down Expand Up @@ -469,6 +474,7 @@ public static function loadMo($filename) {
// 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);
Expand All @@ -488,6 +494,10 @@ public static function loadMo($filename) {
if (strpos($msgstr, "\000")) {
$msgstr = explode("\000", $msgstr);
}

if ($msgid != '') {
$msgstr = array($context => $msgstr);
}
$translations[$msgid] = $msgstr;

if (isset($msgid_plural)) {
Expand Down Expand Up @@ -515,12 +525,15 @@ public static function loadPo($filename) {
$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)) {
Expand All @@ -529,31 +542,33 @@ public static function loadPo($filename) {
} 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;
Expand All @@ -563,9 +578,10 @@ public static function loadPo($filename) {
} 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));
Expand Down
4 changes: 4 additions & 0 deletions lib/Cake/Test/Case/Console/Command/Task/ExtractTaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ public function testExecute() {
$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');

Expand Down
28 changes: 22 additions & 6 deletions lib/Cake/Test/Case/I18n/I18nTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ public function testTranslationCaching() {
$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_'));

Expand All @@ -92,11 +92,11 @@ public function testTranslationCaching() {

// 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'));
}
Expand Down Expand Up @@ -1879,6 +1879,22 @@ public function testLoadLocaleDefinition() {
$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
*
Expand Down
29 changes: 29 additions & 0 deletions lib/Cake/Test/test_app/Locale/nld/LC_MESSAGES/default.po
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions lib/Cake/Test/test_app/View/Pages/extract.ctp
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ __('Hot features!'

// Category
echo __c('You have a new message (category: LC_TIME).', 5);

echo __x('mail', 'letter');
24 changes: 24 additions & 0 deletions lib/Cake/basics.php
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,30 @@ function __c($msg, $category, $args = null) {

}

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')) {

/**
Expand Down

0 comments on commit b47f91c

Please sign in to comment.