diff --git a/src/Translator/Loader/Ini.php b/src/Translator/Loader/Ini.php new file mode 100644 index 00000000..9b76c574 --- /dev/null +++ b/src/Translator/Loader/Ini.php @@ -0,0 +1,86 @@ +fromFile($filename); + + $list = $messagesNamespaced; + if (isset($messagesNamespaced['translation'])) { + $list = $messagesNamespaced['translation']; + } + + foreach ($list as $message) { + if (!is_array($message) || count($message) < 2) { + throw new Exception\InvalidArgumentException( + 'Each INI row must be an array with message and translation' + ); + } + if (isset($message['message']) && isset($message['translation'])) { + $messages[$message['message']] = $message['translation']; + continue; + } + $messages[array_shift($message)] = array_shift($message); + } + + if (!is_array($messages)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected an array, but received %s', + gettype($messages) + )); + } + + $textDomain = new TextDomain($messages); + + if (array_key_exists('plural', $messagesNamespaced) + && isset($messagesNamespaced['plural']['plural_forms']) + ) { + $textDomain->setPluralRule( + PluralRule::fromString($messagesNamespaced['plural']['plural_forms']) + ); + } + + return $textDomain; + } +} diff --git a/src/Translator/LoaderPluginManager.php b/src/Translator/LoaderPluginManager.php index 437e9064..752f5055 100644 --- a/src/Translator/LoaderPluginManager.php +++ b/src/Translator/LoaderPluginManager.php @@ -32,8 +32,9 @@ class LoaderPluginManager extends AbstractPluginManager * @var array */ protected $invokableClasses = array( - 'phparray' => 'Zend\I18n\Translator\Loader\PhpArray', 'gettext' => 'Zend\I18n\Translator\Loader\Gettext', + 'ini' => 'Zend\I18n\Translator\Loader\Ini', + 'phparray' => 'Zend\I18n\Translator\Loader\PhpArray', ); /** diff --git a/src/Translator/TranslatorAwareTrait.php b/src/Translator/TranslatorAwareTrait.php new file mode 100644 index 00000000..b6acf183 --- /dev/null +++ b/src/Translator/TranslatorAwareTrait.php @@ -0,0 +1,120 @@ +translator = $translator; + + if (!is_null($textDomain)) { + $this->setTranslatorTextDomain($textDomain); + } + + return $this; + } + + /** + * Returns translator used in object + * + * @return Translator + */ + public function getTranslator() + { + return $this->translator; + } + + /** + * Checks if the object has a translator + * + * @return bool + */ + public function hasTranslator() + { + return !is_null($this->translator); + } + + /** + * Sets whether translator is enabled and should be used + * + * @param bool $enabled + * @return mixed + */ + public function setTranslatorEnabled($enabled = true) + { + $this->translatorEnabled = $enabled; + + return $this; + } + + /** + * Returns whether translator is enabled and should be used + * + * @return bool + */ + public function isTranslatorEnabled() + { + return $this->translatorEnabled; + } + + /** + * Set translation text domain + * + * @param string $textDomain + * @return mixed + */ + public function setTranslatorTextDomain($textDomain = 'default') + { + $this->translatorTextDomain = $textDomain; + + return $this; + } + + /** + * Return the translation text domain + * + * @return string + */ + public function getTranslatorTextDomain() + { + return $this->translatorTextDomain; + } +} diff --git a/src/View/Helper/DateFormat.php b/src/View/Helper/DateFormat.php index 279f0b5c..7bcd9488 100644 --- a/src/View/Helper/DateFormat.php +++ b/src/View/Helper/DateFormat.php @@ -105,32 +105,35 @@ public function getlocale() /** * Format a date. * - * @param DateTime|integer|array $date - * @param integer $dateType - * @param integer $timeType - * @param string $locale + * @param DateTime|integer|array $date + * @param int $dateType + * @param int $timeType + * @param string $locale + * @param string|null $pattern * @return string - * @throws Exception\RuntimeException */ public function __invoke( $date, $dateType = IntlDateFormatter::NONE, $timeType = IntlDateFormatter::NONE, - $locale = null + $locale = null, + $pattern = null ) { if ($locale === null) { $locale = $this->getlocale(); } $timezone = $this->getTimezone(); - $formatterId = md5($dateType . "\0" . $timeType . "\0" . $locale); + $formatterId = md5($dateType . "\0" . $timeType . "\0" . $locale ."\0" . $pattern); if (!isset($this->formatters[$formatterId])) { $this->formatters[$formatterId] = new IntlDateFormatter( $locale, $dateType, $timeType, - $timezone + $timezone, + IntlDateFormatter::GREGORIAN, + $pattern ); } diff --git a/src/View/Helper/Plural.php b/src/View/Helper/Plural.php new file mode 100644 index 00000000..10faf2a0 --- /dev/null +++ b/src/View/Helper/Plural.php @@ -0,0 +1,84 @@ +rule = $pluralRule; + + return $this; + } + + /** + * Given an array of strings, a number and, if wanted, an optional locale (the default one is used + * otherwise), this picks the right string according to plural rules of the locale + * + * @param array|string $strings + * @param int $number + * @throws Exception\InvalidArgumentException + * @return string + */ + public function __invoke($strings, $number) + { + if ($this->rule === null) { + throw new Exception\InvalidArgumentException(sprintf( + 'No plural rule was set' + )); + } + + if (!is_array($strings)) { + $strings = (array) $strings; + } + + $pluralIndex = $this->rule->evaluate($number); + + return $strings[$pluralIndex]; + } +} diff --git a/src/View/HelperConfig.php b/src/View/HelperConfig.php index 3dfe2782..2034f194 100644 --- a/src/View/HelperConfig.php +++ b/src/View/HelperConfig.php @@ -29,6 +29,7 @@ class HelperConfig implements ConfigInterface 'currencyformat' => 'Zend\I18n\View\Helper\CurrencyFormat', 'dateformat' => 'Zend\I18n\View\Helper\DateFormat', 'numberformat' => 'Zend\I18n\View\Helper\NumberFormat', + 'plural' => 'Zend\I18n\View\Helper\Plural', 'translate' => 'Zend\I18n\View\Helper\Translate', 'translateplural' => 'Zend\I18n\View\Helper\TranslatePlural', ); diff --git a/test/Translator/Loader/IniTest.php b/test/Translator/Loader/IniTest.php new file mode 100644 index 00000000..735c3fb1 --- /dev/null +++ b/test/Translator/Loader/IniTest.php @@ -0,0 +1,94 @@ +testFilesDir = realpath(__DIR__ . '/../_files'); + } + + public function testLoaderFailsToLoadMissingFile() + { + $loader = new IniLoader(); + $this->setExpectedException('Zend\I18n\Exception\InvalidArgumentException', 'Could not open file'); + $loader->load('en_EN', 'missing'); + } + + public function testLoaderLoadsEmptyFile() + { + $loader = new IniLoader(); + $textDomain = $loader->load('en_EN', $this->testFilesDir . '/translation_empty.ini'); + $this->assertInstanceOf('Zend\I18n\Translator\TextDomain', $textDomain); + } + + public function testLoaderFailsToLoadNonArray() + { + $loader = new IniLoader(); + $this->setExpectedException('Zend\I18n\Exception\InvalidArgumentException', + 'Each INI row must be an array with message and translation'); + $loader->load('en_EN', $this->testFilesDir . '/failed.ini'); + } + + public function testLoaderFailsToLoadBadSyntax() + { + $loader = new IniLoader(); + $this->setExpectedException('Zend\I18n\Exception\InvalidArgumentException', + 'Each INI row must be an array with message and translation'); + $loader->load('en_EN', $this->testFilesDir . '/failed_syntax.ini'); + } + + public function testLoaderReturnsValidTextDomain() + { + $loader = new IniLoader(); + $textDomain = $loader->load('en_EN', $this->testFilesDir . '/translation_en.ini'); + + $this->assertEquals('Message 1 (en)', $textDomain['Message 1']); + $this->assertEquals('Message 4 (en)', $textDomain['Message 4']); + } + + public function testLoaderReturnsValidTextDomainWithFileWithoutPlural() + { + $loader = new IniLoader(); + $textDomain = $loader->load('en_EN', $this->testFilesDir . '/translation_en_without_plural.ini'); + + $this->assertEquals('Message 1 (en)', $textDomain['Message 1']); + $this->assertEquals('Message 4 (en)', $textDomain['Message 4']); + } + + public function testLoaderReturnsValidTextDomainWithSimpleSyntax() + { + $loader = new IniLoader(); + $textDomain = $loader->load('en_EN', $this->testFilesDir . '/translation_en_simple_syntax.ini'); + + $this->assertEquals('Message 1 (en)', $textDomain['Message 1']); + $this->assertEquals('Message 4 (en)', $textDomain['Message 4']); + } + + public function testLoaderLoadsPluralRules() + { + $loader = new IniLoader(); + $textDomain = $loader->load('en_EN', $this->testFilesDir . '/translation_en.ini'); + + $this->assertEquals(2, $textDomain->getPluralRule()->evaluate(0)); + $this->assertEquals(0, $textDomain->getPluralRule()->evaluate(1)); + $this->assertEquals(1, $textDomain->getPluralRule()->evaluate(2)); + $this->assertEquals(2, $textDomain->getPluralRule()->evaluate(10)); + } +} diff --git a/test/Translator/TranslatorAwareTraitTest.php b/test/Translator/TranslatorAwareTraitTest.php new file mode 100644 index 00000000..acaa62cf --- /dev/null +++ b/test/Translator/TranslatorAwareTraitTest.php @@ -0,0 +1,129 @@ +getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertAttributeEquals(null, 'translator', $object); + + $translator = new Translator; + + $object->setTranslator($translator); + + $this->assertAttributeEquals($translator, 'translator', $object); + } + + public function testSetTranslatorAndTextDomain() + { + $object = $this->getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertAttributeEquals(null, 'translator', $object); + $this->assertAttributeEquals('default', 'translatorTextDomain', $object); + + $translator = new Translator; + $textDomain = 'domain'; + + $object->setTranslator($translator, $textDomain); + + $this->assertAttributeEquals($translator, 'translator', $object); + $this->assertAttributeEquals($textDomain, 'translatorTextDomain', $object); + } + + public function testGetTranslator() + { + $object = $this->getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertNull($object->getTranslator()); + + $translator = new Translator; + + $object->setTranslator($translator); + + $this->assertEquals($translator, $object->getTranslator()); + } + + public function testHasTranslator() + { + $object = $this->getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertFalse($object->hasTranslator()); + + $translator = new Translator; + + $object->setTranslator($translator); + + $this->assertTrue($object->hasTranslator()); + } + + public function testSetTranslatorEnabled() + { + $object = $this->getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertAttributeEquals(true, 'translatorEnabled', $object); + + $enabled = false; + + $object->setTranslatorEnabled($enabled); + + $this->assertAttributeEquals($enabled, 'translatorEnabled', $object); + + $object->setTranslatorEnabled(); + + $this->assertAttributeEquals(true, 'translatorEnabled', $object); + } + + public function testIsTranslatorEnabled() + { + $object = $this->getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertTrue($object->isTranslatorEnabled()); + + $object->setTranslatorEnabled(false); + + $this->assertFalse($object->isTranslatorEnabled()); + } + + public function testSetTranslatorTextDomain() + { + $object = $this->getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertAttributeEquals('default', 'translatorTextDomain', $object); + + $textDomain = 'domain'; + + $object->setTranslatorTextDomain($textDomain); + + $this->assertAttributeEquals($textDomain, 'translatorTextDomain', $object); + } + + public function testGetTranslatorTextDomain() + { + $object = $this->getObjectForTrait('\Zend\I18n\Translator\TranslatorAwareTrait'); + + $this->assertEquals('default', $object->getTranslatorTextDomain()); + + $textDomain = 'domain'; + + $object->setTranslatorTextDomain($textDomain); + + $this->assertEquals($textDomain, $object->getTranslatorTextDomain()); + } +} diff --git a/test/Translator/_files/failed.ini b/test/Translator/_files/failed.ini new file mode 100644 index 00000000..e0437c1b --- /dev/null +++ b/test/Translator/_files/failed.ini @@ -0,0 +1 @@ +identifier1 = "Message 1" diff --git a/test/Translator/_files/failed_syntax.ini b/test/Translator/_files/failed_syntax.ini new file mode 100644 index 00000000..bf0edae7 --- /dev/null +++ b/test/Translator/_files/failed_syntax.ini @@ -0,0 +1 @@ +identifier1[] = "Message 1" diff --git a/test/Translator/_files/translation_empty.ini b/test/Translator/_files/translation_empty.ini new file mode 100644 index 00000000..e69de29b diff --git a/test/Translator/_files/translation_en.ini b/test/Translator/_files/translation_en.ini new file mode 100644 index 00000000..78a9407a --- /dev/null +++ b/test/Translator/_files/translation_en.ini @@ -0,0 +1,26 @@ +[plural] +plural_forms = 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);' + +[translation] +identifier1.message = "Message 1" +identifier1.translation = "Message 1 (en)" + +identifier2.message = "Message 2" +identifier2.translation = "Message 2 (en)" + +identifier3.message = "Message 3" +identifier3.translation = "Message 3 (en)" + +identifier4.message = "Message 4" +identifier4.translation = "Message 4 (en)" + +identifier5.message = "Message 5" +identifier5.translation.0 = "Message 5 (en) Plural 0" +identifier5.translation.1 = "Message 5 (en) Plural 1" +identifier5.translation.2 = "Message 5 (en) Plural 2" + +identifier6.message = "Cooking furniture" +identifier6.translation = "Küchen Möbel (en)" + +identifier7.message = "Küchen Möbel" +identifier7.translation = "Cooking furniture (en)" diff --git a/test/Translator/_files/translation_en_simple_syntax.ini b/test/Translator/_files/translation_en_simple_syntax.ini new file mode 100644 index 00000000..b9b8cdbb --- /dev/null +++ b/test/Translator/_files/translation_en_simple_syntax.ini @@ -0,0 +1,26 @@ +[plural] +plural_forms = 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);' + +[translation] +identifier1[] = "Message 1" +identifier1[] = "Message 1 (en)" + +identifier2[] = "Message 2" +identifier2[] = "Message 2 (en)" + +identifier3[] = "Message 3" +identifier3[] = "Message 3 (en)" + +identifier4[] = "Message 4" +identifier4[] = "Message 4 (en)" + +identifier5.message = "Message 5" +identifier5.translation.0 = "Message 5 (en) Plural 0" +identifier5.translation.1 = "Message 5 (en) Plural 1" +identifier5.translation.2 = "Message 5 (en) Plural 2" + +identifier6[] = "Cooking furniture" +identifier6[] = "Küchen Möbel (en)" + +identifier7[] = "Küchen Möbel" +identifier7[] = "Cooking furniture (en)" diff --git a/test/Translator/_files/translation_en_without_plural.ini b/test/Translator/_files/translation_en_without_plural.ini new file mode 100644 index 00000000..9401f256 --- /dev/null +++ b/test/Translator/_files/translation_en_without_plural.ini @@ -0,0 +1,22 @@ +identifier1.message = "Message 1" +identifier1.translation = "Message 1 (en)" + +identifier2.message = "Message 2" +identifier2.translation = "Message 2 (en)" + +identifier3.message = "Message 3" +identifier3.translation = "Message 3 (en)" + +identifier4.message = "Message 4" +identifier4.translation = "Message 4 (en)" + +identifier5.message = "Message 5" +identifier5.translation.0 = "Message 5 (en) Plural 0" +identifier5.translation.1 = "Message 5 (en) Plural 1" +identifier5.translation.2 = "Message 5 (en) Plural 2" + +identifier6.message = "Cooking furniture" +identifier6.translation = "Küchen Möbel (en)" + +identifier7.message = "Küchen Möbel" +identifier7.translation = "Cooking furniture (en)" diff --git a/test/Validator/FloatTest.php b/test/Validator/FloatTest.php index c9f82e27..802e800b 100644 --- a/test/Validator/FloatTest.php +++ b/test/Validator/FloatTest.php @@ -59,6 +59,7 @@ public function basicProvider() array(0.01, true), array(-0.1, true), array('10.1', true), + array('5.00', true), array('10.0', true), array('10.10', true), array(1, true), diff --git a/test/View/Helper/DateFormatTest.php b/test/View/Helper/DateFormatTest.php index 38873d57..157cd901 100644 --- a/test/View/Helper/DateFormatTest.php +++ b/test/View/Helper/DateFormatTest.php @@ -53,7 +53,7 @@ public function tearDown() unset($this->helper); } - public function currencyTestsDataProvider() + public function dateTestsDataProvider() { $date = new DateTime('2012-07-02T22:44:03Z'); return array( @@ -160,19 +160,62 @@ public function currencyTestsDataProvider() ); } + public function dateTestsDataProviderWithPattern() + { + $date = new DateTime('2012-07-02T22:44:03Z'); + return array( + // FULL format varies based on OS + // array( + // 'de_DE', + // 'Europe/Berlin', + // IntlDateFormatter::FULL, + // IntlDateFormatter::FULL, + // $date, + // 'Dienstag, 3. Juli 2012 00:44:03 Deutschland', + // ), + array( + 'de_DE', + 'Europe/Berlin', + null, + null, + 'MMMM', + $date, + 'Juli', + ), + array( + 'de_DE', + 'Europe/Berlin', + null, + null, + 'MMMM.Y', + $date, + 'Juli.2012', + ), + array( + 'de_DE', + 'Europe/Berlin', + null, + null, + 'dd/Y', + $date, + '03/2012', + ), + ); + } + /** - * @dataProvider currencyTestsDataProvider + * @dataProvider dateTestsDataProvider */ public function testBasic($locale, $timezone, $timeType, $dateType, $date, $expected) { $this->helper->setTimezone($timezone); $this->assertMbStringEquals($expected, $this->helper->__invoke( - $date, $dateType, $timeType, $locale + $date, $dateType, $timeType, $locale, null )); } /** - * @dataProvider currencyTestsDataProvider + * @dataProvider dateTestsDataProvider */ public function testSettersProvideDefaults($locale, $timezone, $timeType, $dateType, $date, $expected) { @@ -185,11 +228,32 @@ public function testSettersProvideDefaults($locale, $timezone, $timeType, $dateT )); } + /** + * @dataProvider dateTestsDataProviderWithPattern + */ + public function testUseCustomPattern($locale, $timezone, $timeType, $dateType, $pattern, $date, $expected) + { + $this->helper->setTimezone($timezone); + $this->assertMbStringEquals($expected, $this->helper->__invoke( + $date, $dateType, $timeType, $locale, $pattern + )); + } + public function testDefaultLocale() { $this->assertEquals(Locale::getDefault(), $this->helper->getLocale()); } + public function testBugTwoPatternOnSameHelperInstance() + { + $date = new DateTime('2012-07-02T22:44:03Z'); + + $helper = new DateFormatHelper(); + $helper->setTimezone('Europe/Berlin'); + $this->assertEquals('03/2012', $helper->__invoke($date, null, null, 'it_IT', 'dd/Y')); + $this->assertEquals('03-2012', $helper->__invoke($date, null, null, 'it_IT', 'dd-Y')); + } + public function assertMbStringEquals($expected, $test, $message = '') { $expected = str_replace(array("\xC2\xA0", ' '), '', $expected); diff --git a/test/View/Helper/PluralTest.php b/test/View/Helper/PluralTest.php new file mode 100644 index 00000000..dbb0d37b --- /dev/null +++ b/test/View/Helper/PluralTest.php @@ -0,0 +1,68 @@ +helper = new PluralHelper(); + } + + /** + * @return array + */ + public function pluralsTestProvider() + { + return array( + array('nplurals=1; plural=0', 'かさ', 0, 'かさ'), + array('nplurals=1; plural=0', 'かさ', 10, 'かさ'), + + array('nplurals=2; plural=(n==1 ? 0 : 1)', array('umbrella', 'umbrellas'), 0, 'umbrellas'), + array('nplurals=2; plural=(n==1 ? 0 : 1)', array('umbrella', 'umbrellas'), 1, 'umbrella'), + array('nplurals=2; plural=(n==1 ? 0 : 1)', array('umbrella', 'umbrellas'), 2, 'umbrellas'), + + array('nplurals=2; plural=(n==0 || n==1 ? 0 : 1)', array('parapluie', 'parapluies'), 0, 'parapluie'), + array('nplurals=2; plural=(n==0 || n==1 ? 0 : 1)', array('parapluie', 'parapluies'), 1, 'parapluie'), + array('nplurals=2; plural=(n==0 || n==1 ? 0 : 1)', array('parapluie', 'parapluies'), 2, 'parapluies'), + ); + } + + /** + * @dataProvider pluralsTestProvider + */ + public function testGetCorrectPlurals($pluralRule, $strings, $number, $expected) + { + $this->helper->setPluralRule($pluralRule); + $result = $this->helper->__invoke($strings, $number); + $this->assertEquals($expected, $result); + } +}