diff --git a/src/internationalization/PhutilTranslator.php b/src/internationalization/PhutilTranslator.php
index 5ed00bc..875c8c4 100644
--- a/src/internationalization/PhutilTranslator.php
+++ b/src/internationalization/PhutilTranslator.php
@@ -1,152 +1,169 @@
language = $language;
return $this;
}
/**
* Add translations which will be later used by @{method:translate}.
* The parameter is an array of strings (for simple translations) or arrays
* (for translastions with variants). The number of items in the array is
* language specific. It is `array($singular, $plural)` for English.
*
* array(
* 'color' => 'colour',
* '%d beer(s)' => array('%d beer', '%d beers'),
* );
*
* The arrays can be nested for strings with more variant parts:
*
* array(
* '%d char(s) on %d row(s)' => array(
* array('%d char on %d row', '%d char on %d rows'),
* array('%d chars on %d row', '%d chars on %d rows'),
* ),
* );
*
* The translation should have the same placeholders as originals. Swapping
* parameter order is possible:
*
* array(
* '%s owns %s.' => '%2$s is owned by %1$s.',
* );
*
* @param array Identifier in key, translation in value.
* @return PhutilTranslator Provides fluent interface.
*/
public function addTranslations(array $translations) {
$this->translations = array_merge($this->translations, $translations);
return $this;
}
public function translate($text /* , ... */) {
$translation = idx($this->translations, $text, $text);
$args = func_get_args();
while (is_array($translation)) {
$translation = $this->chooseVariant($translation, next($args));
}
array_shift($args);
return vsprintf($translation, $args);
}
private function chooseVariant(array $translations, $variant) {
switch ($this->language) {
case 'en':
list($singular, $plural) = $translations;
if ($variant == 1) {
return $singular;
}
return $plural;
case 'cs':
if ($variant instanceof PhutilPerson) {
list($male, $female) = $translations;
if ($variant->getSex() == PhutilPerson::SEX_FEMALE) {
return $female;
}
return $male;
}
list($singular, $paucal, $plural) = $translations;
if ($variant == 1) {
return $singular;
}
if ($variant >= 2 && $variant <= 4) {
return $paucal;
}
return $plural;
default:
throw new Exception("Unknown language '{$this->language}'.");
}
}
/**
* Translate date formatted by `$date->format()`.
*
* @param string Format accepted by `DateTime::format()`.
* @param DateTime
* @return string Formatted and translated date.
*/
public function translateDate($format, DateTime $date) {
static $format_cache = array();
if (!isset($format_cache[$format])) {
$translatable = 'DlSFMaA';
preg_match_all(
'/['.$translatable.']|(\\\\.|[^'.$translatable.'])+/',
$format,
$format_cache[$format],
PREG_SET_ORDER);
}
$parts = array();
foreach ($format_cache[$format] as $match) {
$part = $date->format($match[0]);
if (!isset($match[1])) {
$part = $this->translate($part);
}
$parts[] = $part;
}
return implode('', $parts);
}
+ /**
+ * Format number with grouped thousands and optional decimal part. Requires
+ * translations of '.' (decimal point) and ',' (thousands separator). Both
+ * these translations must be 1 byte long.
+ *
+ * @param float
+ * @param int
+ * @return string
+ */
+ public function formatNumber($number, $decimals = 0) {
+ return number_format(
+ $number,
+ $decimals,
+ $this->translate('.'),
+ $this->translate(','));
+ }
+
public function validateTranslation($original, $translation) {
$pattern = '/<(\S[^>]*>?)?|&(\S[^;]*;?)?/i';
$original_matches = null;
$translation_matches = null;
preg_match_all($pattern, $original, $original_matches);
preg_match_all($pattern, $translation, $translation_matches);
sort($original_matches[0]);
sort($translation_matches[0]);
if ($original_matches[0] !== $translation_matches[0]) {
return false;
}
return true;
}
}
diff --git a/src/internationalization/__tests__/PhutilTranslatorTestCase.php b/src/internationalization/__tests__/PhutilTranslatorTestCase.php
index 1b13d2e..4c37f68 100644
--- a/src/internationalization/__tests__/PhutilTranslatorTestCase.php
+++ b/src/internationalization/__tests__/PhutilTranslatorTestCase.php
@@ -1,149 +1,165 @@
addTranslations(
array(
'%d line(s)' => array('%d line', '%d lines'),
'%d char(s) on %d row(s)' => array(
array('%d char on %d row', '%d char on %d rows'),
array('%d chars on %d row', '%d chars on %d rows'),
),
));
$this->assertEqual('line', $translator->translate('line'));
$this->assertEqual('param', $translator->translate('%s', 'param'));
$this->assertEqual('0 lines', $translator->translate('%d line(s)', 0));
$this->assertEqual('1 line', $translator->translate('%d line(s)', 1));
$this->assertEqual('2 lines', $translator->translate('%d line(s)', 2));
$this->assertEqual(
'1 char on 1 row',
$translator->translate('%d char(s) on %d row(s)', 1, 1));
$this->assertEqual(
'5 chars on 2 rows',
$translator->translate('%d char(s) on %d row(s)', 5, 2));
$this->assertEqual('1 beer(s)', $translator->translate('%d beer(s)', 1));
}
public function testCzech() {
$translator = new PhutilTranslator();
$translator->setLanguage('cs');
$translator->addTranslations(
array(
'%d beer(s)' => array('%d pivo', '%d piva', '%d piv'),
));
$this->assertEqual('0 piv', $translator->translate('%d beer(s)', 0));
$this->assertEqual('1 pivo', $translator->translate('%d beer(s)', 1));
$this->assertEqual('2 piva', $translator->translate('%d beer(s)', 2));
$this->assertEqual('5 piv', $translator->translate('%d beer(s)', 5));
$this->assertEqual('1 line(s)', $translator->translate('%d line(s)', 1));
}
public function testPerson() {
$translator = new PhutilTranslator();
$translator->setLanguage('cs');
$translator->addTranslations(
array(
'%s wrote.' => array('%s napsal.', '%s napsala.'),
));
$person = new PhutilPersonTest();
$this->assertEqual(
'Test () napsal.',
$translator->translate('%s wrote.', $person));
$person->setSex(PhutilPerson::SEX_MALE);
$this->assertEqual(
'Test (m) napsal.',
$translator->translate('%s wrote.', $person));
$person->setSex(PhutilPerson::SEX_FEMALE);
$this->assertEqual(
'Test (f) napsala.',
$translator->translate('%s wrote.', $person));
}
public function testTranslateDate() {
$date = new DateTime('2012-06-21');
$translator = new PhutilTranslator();
$this->assertEqual('June', $translator->translateDate('F', $date));
$this->assertEqual('June 21', $translator->translateDate('F d', $date));
$this->assertEqual('F', $translator->translateDate('\F', $date));
$translator->addTranslations(
array(
'June' => 'correct',
'21' => 'wrong',
'F' => 'wrong'
));
$this->assertEqual('correct', $translator->translateDate('F', $date));
$this->assertEqual('correct 21', $translator->translateDate('F d', $date));
$this->assertEqual('F', $translator->translateDate('\F', $date));
}
public function testSetInstance() {
PhutilTranslator::setInstance(new PhutilTranslator());
$original = PhutilTranslator::getInstance();
$this->assertEqual('color', pht('color'));
$british = new PhutilTranslator();
$british->addTranslations(
array(
'color' => 'colour',
));
PhutilTranslator::setInstance($british);
$this->assertEqual('colour', pht('color'));
PhutilTranslator::setInstance($original);
$this->assertEqual('color', pht('color'));
}
+ public function testFormatNumber() {
+ $translator = new PhutilTranslator();
+ $this->assertEqual('1,234', $translator->formatNumber(1234));
+ $this->assertEqual('1,234.5', $translator->formatNumber(1234.5, 1));
+ $this->assertEqual('1,234.5678', $translator->formatNumber(1234.5678, 4));
+
+ $translator->addTranslations(
+ array(
+ ',' => ' ',
+ '.' => ','
+ ));
+ $this->assertEqual('1 234', $translator->formatNumber(1234));
+ $this->assertEqual('1 234,5', $translator->formatNumber(1234.5, 1));
+ $this->assertEqual('1 234,5678', $translator->formatNumber(1234.5678, 4));
+ }
+
public function testValidateTranslation() {
$tests = array(
'a < 2' => array(
'a < 2' => true,
'b < 3' => true,
'2 > a' => false,
'a<2' => false,
),
'We win' => array(
'We win' => true,
'We win' => true, // false positive
'We win' => false,
'We win' => false,
),
'We win & triumph' => array(
'We triumph & win' => true,
'We win and triumph' => false,
),
'beer' => array(
'pivo' => true,
'b<>r' => false,
'b&&r' => false,
),
);
$translator = new PhutilTranslator();
foreach ($tests as $original => $translations) {
foreach ($translations as $translation => $expect) {
$valid = ($expect ? "valid" : "invalid");
$this->assertEqual(
$expect,
$translator->validateTranslation($original, $translation),
"'{$original}' should be {$valid} with '{$translation}'.");
}
}
}
}