diff --git a/lib/Cake/Console/Command/Task/ExtractTask.php b/lib/Cake/Console/Command/Task/ExtractTask.php index 4986960e6..366c2d871 100644 --- a/lib/Cake/Console/Command/Task/ExtractTask.php +++ b/lib/Cake/Console/Command/Task/ExtractTask.php @@ -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"; diff --git a/lib/Cake/I18n/I18n.php b/lib/Cake/I18n/I18n.php index 43cb4b1d5..a91828301 100644 --- a/lib/Cake/I18n/I18n.php +++ b/lib/Cake/I18n/I18n.php @@ -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)); diff --git a/lib/Cake/Test/Case/Console/Command/Task/ExtractTaskTest.php b/lib/Cake/Test/Case/Console/Command/Task/ExtractTaskTest.php index e93a31eb4..7013771fb 100644 --- a/lib/Cake/Test/Case/Console/Command/Task/ExtractTaskTest.php +++ b/lib/Cake/Test/Case/Console/Command/Task/ExtractTaskTest.php @@ -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'); diff --git a/lib/Cake/Test/Case/I18n/I18nTest.php b/lib/Cake/Test/Case/I18n/I18nTest.php index 35ff4b056..7e7af0aff 100644 --- a/lib/Cake/Test/Case/I18n/I18nTest.php +++ b/lib/Cake/Test/Case/I18n/I18nTest.php @@ -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 * diff --git a/lib/Cake/Test/test_app/Locale/nld/LC_MESSAGES/default.po b/lib/Cake/Test/test_app/Locale/nld/LC_MESSAGES/default.po new file mode 100644 index 000000000..dfee4f93e --- /dev/null +++ b/lib/Cake/Test/test_app/Locale/nld/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lib/Cake/Test/test_app/View/Pages/extract.ctp b/lib/Cake/Test/test_app/View/Pages/extract.ctp index e2180fe33..bee786c9c 100644 --- a/lib/Cake/Test/test_app/View/Pages/extract.ctp +++ b/lib/Cake/Test/test_app/View/Pages/extract.ctp @@ -29,3 +29,5 @@ __('Hot features!' // Category echo __c('You have a new message (category: LC_TIME).', 5); + +echo __x('mail', 'letter'); diff --git a/lib/Cake/basics.php b/lib/Cake/basics.php index f5984b2b7..d8b30be03 100644 --- a/lib/Cake/basics.php +++ b/lib/Cake/basics.php @@ -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')) { /**