Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gettext/Generators/PhpArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
91 changes: 85 additions & 6 deletions Gettext/Translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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<expression>[^?]+) Capture everything up to ? as 'expression'
* \? ?
* (?P<success>[^:]+) Capture everything up to : as 'success'
* : :
* (?P<failure>[^;]+) Capture everything up to ; as 'failure'
*/
preg_match('/(?P<expression>[^?]+)\?(?P<success>[^:]+):(?P<failure>[^;]+)/', $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;";
}
}
}
48 changes: 48 additions & 0 deletions tests/GettextTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<?php
include dirname(__DIR__).'/Gettext/autoloader.php';

if (!function_exists('n__')) {
require_once(__DIR__ . '/../Gettext/translator_functions.php');
}


class GettextTest extends PHPUnit_Framework_TestCase {

public function testPhpCodeExtractor () {
Expand All @@ -12,6 +17,16 @@ public function testPhpCodeExtractor () {
return $entries;
}

public function testPoFileExtractor () {
$entries = Gettext\Extractors\Po::extract(__DIR__.'/files/gettext_plural.po');

$this->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
Expand Down Expand Up @@ -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");

}
}
19 changes: 19 additions & 0 deletions tests/files/gettext_plural.po
Original file line number Diff line number Diff line change
@@ -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"