diff --git a/Gettext/Generators/PhpArray.php b/Gettext/Generators/PhpArray.php index 065be5f1..cd6b1728 100644 --- a/Gettext/Generators/PhpArray.php +++ b/Gettext/Generators/PhpArray.php @@ -32,6 +32,10 @@ static public function generate (Entries $entries, $string = false) { ) ); + if ($entries->getHeader('Plural-Forms') !== null) { + $translations[$domain]['']['plural-forms'] = $entries->getHeader('Plural-Forms'); + } + $translations[$domain] = array_merge($translations[$domain], $array); if ($string) { diff --git a/Gettext/Translator.php b/Gettext/Translator.php index d5bdf82a..f7d13f40 100644 --- a/Gettext/Translator.php +++ b/Gettext/Translator.php @@ -4,17 +4,34 @@ class Translator { static private $dictionary = array(); static private $domain = 'messages'; + static private $pluralCount = 2; + static private $pluralCode = 'return ($n != 1);'; static private $context_glue = '\u0004'; public static function loadTranslations ($file) { if (is_file($file)) { $dictionary = include($file); + self::loadTranslationsArray($dictionary); + } + } + + public static function loadTranslationsArray($dictionary) + { + if (is_array($dictionary)) { + $domain = isset($dictionary['messages']['']['domain']) ? $dictionary['messages']['']['domain'] : null; - if (is_array($dictionary)) { - $domain = isset($dictionary['messages']['']['domain']) ? $dictionary['messages']['']['domain'] : null; - unset($dictionary['messages']['']); - self::addTranslations($dictionary['messages'], $domain); + // If a plural form is set we extract those values + if (isset($dictionary['messages']['']['plural-forms'])) { + list($count, $code) = explode(';', $dictionary['messages']['']['plural-forms']); + self::$pluralCount = (int)str_replace('nplurals=','', $count); + + // extract just the expression turn 'n' into a php variable '$n'. + // Slap on a return keyword and semicolon at the end. + self::$pluralCode = str_replace('plural=', 'return ', str_replace('n', '$n', $code)) . ';'; } + + unset($dictionary['messages']['']); + self::addTranslations($dictionary['messages'], $domain); } } @@ -85,7 +102,69 @@ public static function dnpgettext ($domain, $context, $original, $plural, $value return ($key === 1) ? $original : $plural; } - public static function isPlural ($n) { - return ($n === 1) ? 1 : 2; + /** + * Executes the plural decision code given the number to decide which + * plural version to take. + * + * @param $n + * @return int + */ + public static function isPlural ($n) + { + $pluralFunc = create_function('$n', self::fixTerseIfs(self::$pluralCode)); + + if (self::$pluralCount <= 2) { + return ($pluralFunc($n)) ? 2 : 1; + } else { + // We need to +1 because while (GNU) gettext codes assume 0 based, + // this gettext actually stores 1 based. + return ($pluralFunc($n)) + 1; + } + } + + /** + * This function will recursively wrap failure states in brackets if they contain a nested terse if + * + * This because PHP can not handle nested terse if's unless they are wrapped in brackets. + * + * This code probably only works for the gettext plural decision codes. + * + * return ($n==1 ? 0 : $n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2); + * becomes + * return ($n==1 ? 0 : ($n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2)); + * + * @param string $code the terse if string + * @param bool $inner If inner is true we wrap it in brackets + * @return string A formatted terse If that PHP can work with. + */ + public static function fixTerseIfs($code, $inner=false) + { + /** + * (?P[^?]+) Capture everything up to ? as 'expression' + * \? ? + * (?P[^:]+) Capture everything up to : as 'success' + * : : + * (?P[^;]+) Capture everything up to ; as 'failure' + */ + preg_match('/(?P[^?]+)\?(?P[^:]+):(?P[^;]+)/', $code, $matches); + + // If no match was found then no terse if was present + if (!isset($matches[0])) + return $code; + + $expression = $matches['expression']; + $success = $matches['success']; + $failure = $matches['failure']; + + // Go look for another terse if in the failure state. + $failure = self::fixTerseIfs($failure, true); + $code = $expression . ' ? ' . $success . ' : ' . $failure; + + if ($inner) { + return "($code)"; + } else { + // note the semicolon. We need that for executing the code. + return "$code;"; + } } } diff --git a/tests/GettextTest.php b/tests/GettextTest.php index 7d0839a5..b197ec84 100644 --- a/tests/GettextTest.php +++ b/tests/GettextTest.php @@ -1,6 +1,11 @@ assertInstanceOf('Gettext\\Entries', $entries); + + $pluralHeader = "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"; + $this->assertEquals($pluralHeader, $entries->getHeader('Plural-Forms'), "Plural form did not get extracted correctly"); + + return $entries; + } /** * @depends testPhpCodeExtractor @@ -126,4 +141,37 @@ public function testPhpArrayGenerator ($entries) { unlink($filename); } + + /** + * @depends testPoFileExtractor + */ + public function testMultiPlural ($entries) { + + $translationArray = \Gettext\Generators\PhpArray::generate($entries); + \Gettext\Translator::loadTranslationsArray($translationArray); + + /** + * Test that nplural=3 plural translation check comes up with the correct translation key. + */ + $this->assertEquals('1 plik', n__ ("one file", "multiple files", 1), "plural calculation result bad"); + $this->assertEquals('2,3,4 pliki', n__ ("one file", "multiple files", 2), "plural calculation result bad"); + $this->assertEquals('2,3,4 pliki', n__ ("one file", "multiple files", 3), "plural calculation result bad"); + $this->assertEquals('2,3,4 pliki', n__ ("one file", "multiple files", 4), "plural calculation result bad"); + $this->assertEquals('5-21 plików', n__ ("one file", "multiple files", 5), "plural calculation result bad"); + $this->assertEquals('5-21 plików', n__ ("one file", "multiple files", 6), "plural calculation result bad"); + + + /** + * Test that when less then the nplural translations are available it still works. + */ + $this->assertEquals('1', n__ ("one", "more", 1), "non-plural fallback failed"); + $this->assertEquals('*', n__ ("one", "more", 2), "non-plural fallback failed"); + $this->assertEquals('*', n__ ("one", "more", 3), "non-plural fallback failed"); + + /** + * Test that non-plural translations the fallback still works. + */ + $this->assertEquals('more', n__ ("single", "more", 3), "non-plural fallback failed"); + + } } \ No newline at end of file diff --git a/tests/files/gettext_plural.po b/tests/files/gettext_plural.po new file mode 100644 index 00000000..3b30c3e1 --- /dev/null +++ b/tests/files/gettext_plural.po @@ -0,0 +1,19 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +# Taken from http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html +msgid "one file" +msgid_plural "multiple files" +msgstr[0] "1 plik" +msgstr[1] "2,3,4 pliki" +msgstr[2] "5-21 plików" + + +msgid "one" +msgid_plural "more" +msgstr[0] "1" +msgstr[1] "*" + +msgid "single" +msgstr "test"