diff --git a/src/Symfony/Component/Translation/Dumper/PoFileDumper.php b/src/Symfony/Component/Translation/Dumper/PoFileDumper.php index d957ab91a33b4..3c78ee967c444 100644 --- a/src/Symfony/Component/Translation/Dumper/PoFileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/PoFileDumper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Gettext; /** * PoFileDumper generates a gettext formatted string representation of a message catalogue. @@ -23,18 +24,43 @@ class PoFileDumper extends FileDumper /** * {@inheritDoc} */ - public function format(MessageCatalogue $messages, $domain = 'messages') + public function format(MessageCatalogue $catalogue, $domain = 'messages') { $output = ''; + $messages = $catalogue->all(); + $header = Gettext::getHeader($messages['messages']); $newLine = false; - foreach ($messages->all($domain) as $source => $target) { + if (!empty($header)) { + $output .= Gettext::headerToString($header); + $newLine = true; + } + Gettext::deleteHeader($messages['messages']); + $messages = $messages[$domain]; + // Make plural form translations arrays + $this->extractSingulars($messages); + foreach ($messages as $source => $target) { if ($newLine) { - $output .= "\n"; + $output .= "\n\n"; + } else { + $newLine = true; + } + // Gettext PO files only understand non indexed rules or 'standard' + if (!is_array($target)) { + $output .= sprintf("msgid \"%s\"\n", $this->escape($source)); + $output .= sprintf("msgstr \"%s\"", $this->escape($target)); } else { - $newLine = true; + // ExtractSingular return 3 items so extract these. + list( $singularKey, $plural_key, $targets) = $target; + $output .= sprintf("msgid \"%s\"\n", $this->escape($source)); + $output .= sprintf("msgid_plural \"%s\"\n", $this->escape($plural_key)); + $targets = explode("|", $targets); + foreach ($targets as $index => $target) { + if ($index>0) { + $output .= "\n"; + } + $output .= sprintf('msgstr[%d] "%s"', $index, $this->escape($target)); + } } - $output .= sprintf('msgid "%s"'."\n", $this->escape($source)); - $output .= sprintf('msgstr "%s"', $this->escape($target)); } return $output; @@ -52,4 +78,43 @@ private function escape($str) { return addcslashes($str, "\0..\37\42\134"); } + + /** + * Merges the singular and plurals back into 1 item. + * + * Gettext allows for a combination of messages being a singular and + * a plural form for the source. + * + * msgid "One sheep" + * msgid_plural "@count sheep" + * msgstr[0] "un mouton" + * msgstr[1] "@count moutons" + * + * By scanning $messages for "One sheep|@count sheep" we can recombine + * these string for Dumping. + * + * @param array $messages All plural forms are merged into the first occurence of a singular. + */ + private function extractSingulars(array &$messages) + { + $messageBundles = array(); + foreach ($messages as $key => $message) { + if (strpos($key, '|') !== FALSE) { + $messageBundles[] = $key; + } + } + foreach ($messageBundles as $bundle) { + list($singularKey, $pluralKey) = explode('|', $bundle, 2); + if (isset($messages[$singularKey])&&isset($messages[$pluralKey])) { + $messages[$singularKey] = array( + $singularKey, + $pluralKey, + $messages[$pluralKey], + ); + unset($messages[$pluralKey]); + unset($messages[$bundle]); + } + } + } + } diff --git a/src/Symfony/Component/Translation/Gettext.php b/src/Symfony/Component/Translation/Gettext.php new file mode 100644 index 0000000000000..7393a9a37cdd1 --- /dev/null +++ b/src/Symfony/Component/Translation/Gettext.php @@ -0,0 +1,205 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +/** + * Provide for specific Gettext related helper functionality. + * + * @see http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files + * @author Clemens Tolboom + * @copyright Clemens Tolboom clemens@build2be.com + */ +class Gettext +{ + /** + * Defines a key for managing a PO Header in our messages. + * + * Gettext files (.po .mo) can contain a header which needs to be managed. + * A Gettext file can have multiple domains into one file. This is called + * a message context or msgctxt. + * + * To provide for both header and context this class provides for + * some static functions to (help) process their values. + * + * @see GettextTest + * @see PoFileLoader + * @see PoFileLoaderTest + * @see PoFileDumper + * @see PoFileDumperTest + */ + const HEADER_KEY = "__HEADER__"; + const CONTEXT_KEY = "__CONTEXT__"; + + /** + * Merge key/value pair into Gettext compatible item. + * + * Each combination is into substring: "key: value \n". + * + * If any key found the values are preceded by empty msgid and msgstr + * + * @param array $header + * @return array|NULL A Gettext compatible string. + */ + static public function headerToString(array $header) + { + $zipped = Gettext::zipHeader($header); + if (!empty($zipped)) { + $result = array( + 'msgid ""', + 'msgstr ""', + $zipped, + ); + + return implode("\n", $result); + } + } + + /** + * Ordered list of Gettext header keys + * + * TODO: this list is probably incomplete + * + * @return array Ordered list of Gettext keys + */ + static public function headerKeys() { + return array( + 'Project-Id-Version', + 'POT-Creation-Date', + 'PO-Revision-Date', + 'Last-Translator', + 'Language-Team', + 'MIME-Version', + 'Content-Type', + 'Content-Transfer-Encoding', + 'Plural-Forms' + ); + } + + static public function emptyHeader() { + return array_fill_keys(Gettext::headerKeys(), ""); + } + + /** + * Retrieve PO Header from messages. + * + * @param array $messages + * @return array containing key/value pair|empty array. + */ + static public function getHeader(array &$messages) + { + if (isset($messages[Gettext::HEADER_KEY])) { + return Gettext::unzipHeader($messages[Gettext::HEADER_KEY]); + } + + return array(); + } + + /** + * Adds or overwrite a header to the messages. + * + * @param array $messages + * @param array $header + */ + static public function addHeader(array &$messages, array $header) + { + $messages[Gettext::HEADER_KEY] = Gettext::zipHeader($header); + } + + /** + * Deletes a header from the messages if exists. + * + * @param array $messages + */ + static public function deleteHeader(array &$messages) { + if (isset($messages[Gettext::HEADER_KEY])) { + unset($messages[Gettext::HEADER_KEY]); + } + } + + /** + * Add context to the messages. + * + * Gettext supports for multiple context (domains) in one PO|MO file. + * By injecting these into the translated messages we can post process. + * + * @param array $messages + * @param type $context + */ + static public function addContext(array &$messages, $context) { + if (!isset($messages[Gettext::CONTEXT_KEY])) { + $messages[Gettext::CONTEXT_KEY] = ''; + } + $contexts = array_flip(explode('|', $messages[Gettext::CONTEXT_KEY])); + $contexts[$context] = $context; + unset($contexts['']); + $messages[Gettext::CONTEXT_KEY] = implode('|', array_keys($contexts)); + } + + static public function deleteContext(array &$messages) { + unset($messages[Gettext::CONTEXT_KEY]); + } + + static public function getContext(array &$messages) { + if (isset($messages[Gettext::CONTEXT_KEY])) { + return explode('|', $messages[Gettext::CONTEXT_KEY]); + } + + return array(); + } + + /** + * Parses a Gettext header string into a key/value pairs. + * + * @param $header + * The Gettext header. + * @return array + * Array with the key/value pair + */ + static private function unzipHeader($header) + { + $result = array(); + $lines = explode("\n", $header); + foreach ($lines as $line) { + $cleaned = trim($line); + $cleaned = preg_replace(array('/^\"/','/\\\n\"$/'), '', $cleaned); + if (strpos($cleaned, ':') > 0) { + list($key, $value) = explode(':', $cleaned, 2); + $result[trim($key)] = trim($value); + } + } + + return $result; + } + + /** + * Zips header into a Gettext formatted string. + * + * The returned value is what msgstr would contain when used by the header + * in a Gettext file. + * + * @param array $header + * @return string + * + * @see unzipHeader(). + * @see fixtures/full.po + */ + static private function zipHeader(array $header) + { + $lines = array(); + foreach ($header as $key => $value) { + $lines[] = '"' . $key . ": " . $value . '\n"'; + } + + return implode("\n", $lines); + } + +} diff --git a/src/Symfony/Component/Translation/Loader/PoFileLoader.php b/src/Symfony/Component/Translation/Loader/PoFileLoader.php index c783cb1eccfeb..b7fb1960797e7 100644 --- a/src/Symfony/Component/Translation/Loader/PoFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/PoFileLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Translation\Gettext; /** * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/) @@ -21,7 +22,7 @@ class PoFileLoader extends ArrayLoader implements LoaderInterface { public function load($resource, $locale, $domain = 'messages') { - $messages = $this->parse($resource); + $messages = $this->parse($resource, $domain); // empty file if (null === $messages) { @@ -84,13 +85,14 @@ public function load($resource, $locale, $domain = 'messages') * * @return array */ - private function parse($resource) + private function parse($resource, $domain) { $stream = fopen($resource, 'r'); $defaults = array( 'ids' => array(), 'translated' => null, + 'context' => null, ); $messages = array(); @@ -98,16 +100,23 @@ private function parse($resource) while ($line = fgets($stream)) { $line = trim($line); - if ($line === '') { // Whitespace indicated current item is done - $this->addMessage($messages, $item); + $this->addMessage($messages, $item, $domain); $item = $defaults; - } elseif (substr($line, 0, 7) === 'msgid "') { + } elseif (substr($line, 0, 9) === 'msgctxt "') { // We start a new msg so save previous - // TODO: this fails when comments or contexts are added - $this->addMessage($messages, $item); + // TODO: this fails when comments are added + $this->addMessage($messages, $item, $domain); $item = $defaults; + $item['context'] = substr($line, 9, -1); + } elseif (substr($line, 0, 7) === 'msgid "') { + // We start a new msg so save previous + // TODO: this fails when comments are added + if (!$item['context']) { + $this->addMessage($messages, $item, $domain); + $item = $defaults; + } $item['ids']['singular'] = substr($line, 7, -1); } elseif (substr($line, 0, 8) === 'msgstr "') { $item['translated'] = substr($line, 8, -1); @@ -129,7 +138,7 @@ private function parse($resource) } // save last item - $this->addMessage($messages, $item); + $this->addMessage($messages, $item, $domain); fclose($stream); return $messages; @@ -138,14 +147,32 @@ private function parse($resource) /** * Save a translation item to the messeages. * + * An item can belong to a particular context which is equivalent with + * a translation domain. + * + * The given item can also be the .po header which should only be added + * when on the default domain 'messages'. + * * A .po file could contain by error missing plural indexes. We need to * fix these before saving them. * * @param array $messages * @param array $item + * @param $domain */ - private function addMessage(array &$messages, array $item) + private function addMessage(array &$messages, array $item, $domain) { + $context = $item['context']; + // Only collect contexts when on default domain + if ($domain == 'messages' && $context) { + Gettext::addContext($messages, $item['context']); + } + if (empty($context)) { + $context = 'messages'; + } + if ($context != $domain) { + return; + } if (is_array($item['translated'])) { $messages[$item['ids']['singular']] = stripslashes($item['translated'][0]); if (isset($item['ids']['plural'])) { @@ -160,9 +187,19 @@ private function addMessage(array &$messages, array $item) $plurals += $empties; ksort($plurals); $messages[$item['ids']['plural']] = stripcslashes(implode('|', $plurals)); + // Add bundled ID for later combining by ie PoFileDumper or translation UI + $messageBundleId = $item['ids']['singular'].'|'.$item['ids']['plural']; + $messages[$messageBundleId] = $messages[$item['ids']['plural']]; } - } elseif (!empty($item['ids']['singular'])) { + } else if(!empty($item['ids']['singular'])) { $messages[$item['ids']['singular']] = stripslashes($item['translated']); + } else if(!empty($item['translated'])) { + // This is a header. + // We must clean it up a little and make it multi line + // The '\n' is still part of the text. So replace it by "\n" + // TODO: do we really have to do this here or for all $item(s)?!? + $header = implode("\n", explode('\n', $item['translated'])); + $messages[Gettext::HEADER_KEY] = $header; } } } diff --git a/src/Symfony/Component/Translation/MessageSelector.php b/src/Symfony/Component/Translation/MessageSelector.php index 6d8f0092aafe1..7cadc923e63ca 100644 --- a/src/Symfony/Component/Translation/MessageSelector.php +++ b/src/Symfony/Component/Translation/MessageSelector.php @@ -49,6 +49,33 @@ class MessageSelector * @api */ public function choose($message, $number, $locale) + { + list($explicitRules, $standardRules) = MessageSelector::getRules($message); + + // try to match an explicit rule, then fallback to the standard ones + foreach ($explicitRules as $interval => $m) { + if (Interval::test($number, $interval)) { + return $m; + } + } + + $position = PluralizationRules::get($number, $locale); + if (!isset($standardRules[$position])) { + throw new \InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s".', $message, $locale)); + } + + return $standardRules[$position]; + } + + /** + * Calculated the rules for the given message. + * + * @see MessageSelector::choose(). + * + * @param string $message + * @return array Containing the two rulesets (explicit, standard) + */ + static public function getRules($message) { $parts = explode('|', $message); $explicitRules = array(); @@ -65,18 +92,6 @@ public function choose($message, $number, $locale) } } - // try to match an explicit rule, then fallback to the standard ones - foreach ($explicitRules as $interval => $m) { - if (Interval::test($number, $interval)) { - return $m; - } - } - - $position = PluralizationRules::get($number, $locale); - if (!isset($standardRules[$position])) { - throw new \InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s".', $message, $locale)); - } - - return $standardRules[$position]; + return array($explicitRules, $standardRules); } } diff --git a/src/Symfony/Component/Translation/Tests/Dumper/PoFileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/PoFileDumperTest.php index 9c434f9781e41..356179983f23f 100644 --- a/src/Symfony/Component/Translation/Tests/Dumper/PoFileDumperTest.php +++ b/src/Symfony/Component/Translation/Tests/Dumper/PoFileDumperTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Dumper\PoFileDumper; +use Symfony\Component\Translation\Gettext; class PoFileDumperTest extends \PHPUnit_Framework_TestCase { @@ -24,8 +25,83 @@ public function testDump() $tempDir = sys_get_temp_dir(); $dumper = new PoFileDumper(); $dumperString = $dumper->dump($catalogue, array('path' => $tempDir)); - $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.po'), file_get_contents($tempDir.'/messages.en.po')); + $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.po'), file_get_contents($tempDir.'/messages.en.po'), 'Resource has whitelines added.'); unlink($tempDir.'/messages.en.po'); } + + public function testHeader() { + $header = Gettext::emptyHeader(); + $string = Gettext::headerToString($header); + $tempDir = sys_get_temp_dir(); + $filename = $tempDir . DIRECTORY_SEPARATOR . 'header.en.po'; + file_put_contents($filename, $string); + $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/empty-header.po'), file_get_contents($filename)); + unlink($filename); + } + + public function testDumpFullInterval() + { + /* + * We need a way to dump a plural message into a Gettext + * format with the structure according to + * http://www.gnu.org/software/gettext/manual/gettext.html#Translating-plural-forms + * + * msgid "One sheep" + * msgid_plural "%d sheep" + * msgstr[0] "Un mouton" + * msgstr[1] "@count sheep" + * + * but it is not yet posible to do as we cannot ask for ie an array + * containing a processed version of '{0} un mouton|{1} @count moutons' + * + * MessageSelector::choose has the algoritme for interval and indexed + * but Gettext PO (and MO?) does not understand interval. + */ + $this->markTestSkipped('We need to find a way for interval messages plural handling'); + $catalogue = new MessageCatalogue('en'); + + $header = Gettext::emptyHeader(); + + $messages = array(); + Gettext::addHeader($messages, $header); + $catalogue->add(array(Gettext::HEADER_KEY => $messages['__HEADER__'])); + $catalogue->add(array('One sheep' => 'un mouton')); + // interval + $catalogue->add(array('@count sheep' => '{0} un mouton|{1} @count moutons')); + $catalogue->add(array('Monday' => 'lundi')); + + $tempDir = sys_get_temp_dir(); + $fileName = 'messages.en.po'; + $fullpath = $tempDir . DIRECTORY_SEPARATOR . $fileName; + $dumper = new PoFileDumper(); + $dumperString = $dumper->dump($catalogue, array('path' => $tempDir)); + $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/full.po'), file_get_contents($fullpath)); + unlink($fullpath); + } + + public function testDumpFullIndexed() + { + $messages = array( + 'messages' => array( + 'One sheep' => 'un mouton', + '@count sheep' => 'un mouton|@count moutons', + 'One sheep|@count sheep' => 'un mouton|@count moutons', + 'Monday' => 'lundi', + ), + ); + + Gettext::addHeader($messages['messages'], Gettext::emptyHeader()); + + $catalogue = new MessageCatalogue('en', $messages); + + $tempDir = sys_get_temp_dir(); + $fileName = 'messages.en.po'; + $fullpath = $tempDir . DIRECTORY_SEPARATOR . $fileName; + $dumper = new PoFileDumper(); + $dumper->dump($catalogue, array('path' => $tempDir)); + $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/full.po'), file_get_contents($fullpath)); + unlink($fullpath); + } + } diff --git a/src/Symfony/Component/Translation/Tests/GettextTest.php b/src/Symfony/Component/Translation/Tests/GettextTest.php new file mode 100644 index 0000000000000..878b02311f045 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/GettextTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests\Loader; + +use Symfony\Component\Translation\Gettext; + +/** + * Description of GettextText + * + * @author clemens + */ +class GettextTest extends \PHPUnit_Framework_TestCase +{ + public function testHeaderToString() + { + $actual = Gettext::headerToString(array()); + $this->assertEquals(NULL, $actual, 'No header.'); + $header = array("A" => "B", "C" => "D"); + $actual = Gettext::headerToString($header); + $expected = implode("\n", array('msgid ""', 'msgstr ""', '"A: B\n"','"C: D\n"')); + $this->assertEquals($expected, $actual, 'Header string ok'); + } + + public function testValidHeader() + { + $header = Gettext::emptyHeader(); + $this->assertEquals(Gettext::headerKeys(), array_keys($header)); + $this->assertEquals("", implode('', $header)); + } + + public function testIdentityHeader() + { + // Make sure header keeps the same + $header = Gettext::emptyHeader(); + $resource = __DIR__.'/fixtures/empty-header.po'; + $this->assertEquals(file_get_contents($resource), Gettext::headerToString($header), 'Header from file maps to internal version'); + } + + public function testNoHeaderExists() + { + $messages = array(); + $header = Gettext::getHeader($messages); + $this->assertEquals(array(), $header, "Empty header is empty array"); + } + +} diff --git a/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php index 8b518fe9702e1..2c3ba56cc9657 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Translation\Loader\PoFileLoader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Translation\Gettext; class PoFileLoaderTest extends \PHPUnit_Framework_TestCase { @@ -26,9 +27,8 @@ public function testLoad() { $loader = new PoFileLoader(); $resource = __DIR__.'/../fixtures/resources.po'; - $catalogue = $loader->load($resource, 'en', 'domain1'); - - $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1')); + $catalogue = $loader->load($resource, 'en'); + $this->assertEquals(array('foo' => 'bar'), $catalogue->all('messages')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); } @@ -37,9 +37,9 @@ public function testLoadPlurals() { $loader = new PoFileLoader(); $resource = __DIR__.'/../fixtures/plurals.po'; - $catalogue = $loader->load($resource, 'en', 'domain1'); + $catalogue = $loader->load($resource, 'en'); - $this->assertEquals(array('foo' => 'bar', 'foos' => 'bar|bars'), $catalogue->all('domain1')); + $this->assertEquals(array('foo' => 'bar', 'foos' => 'bar|bars', 'foo|foos' => 'bar|bars'), $catalogue->all('messages')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); } @@ -48,21 +48,135 @@ public function testLoadDoesNothingIfEmpty() { $loader = new PoFileLoader(); $resource = __DIR__.'/../fixtures/empty.po'; - $catalogue = $loader->load($resource, 'en', 'domain1'); + $catalogue = $loader->load($resource, 'en'); - $this->assertEquals(array(), $catalogue->all('domain1')); + $this->assertEquals(array(), $catalogue->all('messages')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); } + public function testLoadMultiline() + { + $loader = new PoFileLoader(); + $resource = __DIR__.'/../fixtures/multiline.po'; + $catalogue = $loader->load($resource, 'en'); + + $this->assertEquals(3, count($catalogue->all('messages'))); + + $messages = $catalogue->all('messages'); + $this->assertEquals('trans single line', $messages['both single line']); + $this->assertEquals('trans multi line', $messages['source single line']); + $this->assertEquals('trans single line', $messages['source multi line']); + + } + + /** + * Read file with one item without whitespaces before and after. + */ + public function testLoadMinimalFile() + { + $loader = new PoFileLoader(); + $resource = __DIR__.'/../fixtures/minimal.po'; + $catalogue = $loader->load($resource, 'en'); + // TODO: This fails on 'source multi line' + $this->assertEquals(1, count($catalogue->all('messages'))); + } + + /** + * Read the PO header and check it's available. + */ + public function testLoadHeader() + { + $loader = new PoFileLoader(); + $resource = __DIR__.'/../fixtures/header.po'; + $catalogue = $loader->load($resource, 'en'); + $messages = $catalogue->all('messages'); + $this->assertEquals(1, count($catalogue->all('messages'))); + // Header exists + $header = Gettext::getHeader($messages); + $this->assertArrayHasKey('Plural-Forms', $header, 'Plural-Forms key ia part of header'); + // Is header removed + $header = Gettext::deleteHeader($messages); + $header = Gettext::getHeader($messages); + $this->assertEquals(array(), $header, 'PoFileLoader has no header.'); + // Add header + $messages = array(); + $expected = array('foo' => 'bar'); + Gettext::addHeader($messages, $expected); + $actual = Gettext::getHeader($messages); + $this->assertEquals($expected, $actual, 'PoFileLoader has a header.'); + } + + public function testLoadFullFile() + { + $loader = new PoFileLoader(); + $resource = __DIR__.'/../fixtures/full.po'; + $catalogue = $loader->load($resource, 'en'); + $messages = $catalogue->all('domain1'); + // File contains a Header, 2 msgid and 1 plural form and MessageBundleId + $this->assertEquals(5, count($catalogue->all('messages'))); + } + + public function testLoadPlural() + { + $loader = new PoFileLoader(); + $resource = __DIR__.'/../fixtures/plural.po'; + $catalogue = $loader->load($resource, 'en'); + $messages = $catalogue->all('messages'); + $singular = $messages["index singular"]; + $all = $messages["index plural"]; + $count = count(explode("|", $all)); + // File contains a Header, 2 msgid and 1 plural form + $this->assertEquals(6, $count); + + $singular = $messages["singular missing"]; + $all = $messages["plural missing"]; + $plurals = explode("|", $all); + $this->assertEquals($plurals[1], '-'); + $this->assertEquals($plurals[2], '-'); + $this->assertEquals($plurals[5], '-'); + // File contains a Header, 2 msgid and 1 plural form + $this->assertEquals(6, $count); + } + + /** + * Test .po context by iterate over their contexts. + * + * Each .po file may contain translation contexts. To load these we + * need to iterator over the found contexts. + */ + public function testLoadContext() + { + $loader = new PoFileLoader(); + $resource = __DIR__.'/../fixtures/context.po'; + $catalogue = $loader->load($resource, 'en'); + $messages = $catalogue->all('messages'); + + $domains = Gettext::getContext($messages); + $this->assertEquals(array('sheep', 'calendar'), $domains); + Gettext::deleteContext($messages); + $this->assertEquals(1, count($messages), 'Empty context has one message.'); + + foreach( $domains as $domain) { + $catalogue = $loader->load($resource, 'en', $domain); + $messages = $catalogue->all($domain); + $this->assertEquals(1, count($messages), 'Each context has one message.'); + } + } + + /** + * We should allow for importing POT files. + * + * A POT file has empty translation strings. + * TODO: decide whether add or extend PoFileLoader with '.pot' extension + */ public function testLoadEmptyTranslation() { $loader = new PoFileLoader(); $resource = __DIR__.'/../fixtures/empty-translation.po'; - $catalogue = $loader->load($resource, 'en', 'domain1'); + $catalogue = $loader->load($resource, 'en'); + $messages = $catalogue->all('messages'); - $this->assertEquals(array('foo' => ''), $catalogue->all('domain1')); - $this->assertEquals('en', $catalogue->getLocale()); - $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); + $this->assertEquals(array('One sheep|@count sheep' => '|', 'Monday' => '', 'One sheep' => '', '@count sheep' => '|'), $messages, 'Empty translation available.'); } } diff --git a/src/Symfony/Component/Translation/Tests/fixtures/context.po b/src/Symfony/Component/Translation/Tests/fixtures/context.po new file mode 100644 index 0000000000000..8dc6922c1cb6c --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/context.po @@ -0,0 +1,10 @@ +msgid "context less" +msgstr "context equals domain" + +msgctxt "sheep" +msgid "One sheep" +msgstr "@count sheep" + +msgctxt "calendar" +msgid "Monday" +msgstr "lundi" \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Tests/fixtures/empty-header.po b/src/Symfony/Component/Translation/Tests/fixtures/empty-header.po new file mode 100644 index 0000000000000..1e4ffc1bc7c69 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/empty-header.po @@ -0,0 +1,11 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: \n" +"Content-Type: \n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Tests/fixtures/empty-translation.po b/src/Symfony/Component/Translation/Tests/fixtures/empty-translation.po index ff6f22afb1c98..3fb68bd23d015 100644 --- a/src/Symfony/Component/Translation/Tests/fixtures/empty-translation.po +++ b/src/Symfony/Component/Translation/Tests/fixtures/empty-translation.po @@ -1,3 +1,7 @@ -msgid "foo" +msgid "Monday" msgstr "" +msgid "One sheep" +msgid_plural "@count sheep" +msgstr[0] "" +msgstr[1] "" \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Tests/fixtures/full.po b/src/Symfony/Component/Translation/Tests/fixtures/full.po new file mode 100644 index 0000000000000..391f0a29a6632 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/full.po @@ -0,0 +1,19 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: \n" +"Content-Type: \n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +msgid "One sheep" +msgid_plural "@count sheep" +msgstr[0] "un mouton" +msgstr[1] "@count moutons" + +msgid "Monday" +msgstr "lundi" \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Tests/fixtures/header.po b/src/Symfony/Component/Translation/Tests/fixtures/header.po new file mode 100644 index 0000000000000..40ed67fb51cac --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/header.po @@ -0,0 +1,11 @@ +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=((n==1)?(0):((n==0)?(1):((n==2)?(2):((((n%100)>=3)&&((n%100)<=10))?(3):((((n%100)>=11)&&((n%100)<=99))?(4):5)))));\n" diff --git a/src/Symfony/Component/Translation/Tests/fixtures/minimal.po b/src/Symfony/Component/Translation/Tests/fixtures/minimal.po new file mode 100644 index 0000000000000..b255c1fe29110 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/minimal.po @@ -0,0 +1,2 @@ +msgid "source no whitespace before" +msgstr "trans no whitespace after" \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Tests/fixtures/multiline.po b/src/Symfony/Component/Translation/Tests/fixtures/multiline.po new file mode 100644 index 0000000000000..1326ccdc89402 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/multiline.po @@ -0,0 +1,13 @@ + +msgid "both single line" +msgstr "trans single line" + +msgid "source single line" +msgstr "" +"trans " +"multi line" + +msgid "" +"source multi " +"line" +msgstr "trans single line" diff --git a/src/Symfony/Component/Translation/Tests/fixtures/plural.po b/src/Symfony/Component/Translation/Tests/fixtures/plural.po new file mode 100644 index 0000000000000..d067265188d08 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/plural.po @@ -0,0 +1,16 @@ + +msgid "index singular" +msgid_plural "index plural" +msgstr[0] "index singular" +msgstr[1] "index 1" +msgstr[2] "index 2" +msgstr[3] "index 3" +msgstr[4] "index 4" +msgstr[5] "index 5" + +msgid "singular missing" +msgid_plural "plural missing" +msgstr[0] "index singular" +msgstr[3] "index 3" +msgstr[4] "index 4" +msgstr[6] "index 6"