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}'."); } } } }