From bf722c0423c69a0606abd539bfce4f3988325ca8 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 28 Sep 2013 02:50:25 +0400 Subject: [PATCH 01/19] Used intl ICU for message translation --- build/controllers/LocaleController.php | 114 ---- docs/guide/i18n.md | 48 ++ framework/yii/i18n/I18N.php | 95 +--- framework/yii/i18n/MessageFormatter.php | 74 +++ framework/yii/i18n/data/plurals.php | 627 --------------------- framework/yii/i18n/data/plurals.xml | 109 ---- framework/yii/requirements/requirements.php | 4 +- tests/unit/framework/i18n/MessageFormatterTest.php | 47 ++ 8 files changed, 187 insertions(+), 931 deletions(-) delete mode 100644 build/controllers/LocaleController.php create mode 100644 framework/yii/i18n/MessageFormatter.php delete mode 100644 framework/yii/i18n/data/plurals.php delete mode 100644 framework/yii/i18n/data/plurals.xml create mode 100644 tests/unit/framework/i18n/MessageFormatterTest.php diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php deleted file mode 100644 index 7b2a5df..0000000 --- a/build/controllers/LocaleController.php +++ /dev/null @@ -1,114 +0,0 @@ - - * @since 2.0 - */ -class LocaleController extends Controller -{ - public $defaultAction = 'plural'; - - /** - * Generates the plural rules data. - * - * This command will parse the plural rule XML file from CLDR and convert them - * into appropriate PHP representation to support Yii message translation feature. - * @param string $xmlFile the original plural rule XML file (from CLDR). This file may be found in - * http://www.unicode.org/Public/cldr/latest/core.zip - * Extract the zip file and locate the file "common/supplemental/plurals.xml". - * @throws Exception - */ - public function actionPlural($xmlFile) - { - if (!is_file($xmlFile)) { - throw new Exception("The source plural rule file does not exist: $xmlFile"); - } - - $xml = simplexml_load_file($xmlFile); - - $allRules = array(); - - $patterns = array( - '/n in 0..1/' => '(n==0||n==1)', - '/\s+is\s+not\s+/i' => '!=', //is not - '/\s+is\s+/i' => '==', //is - '/n\s+mod\s+(\d+)/i' => 'fmod(n,$1)', //mod (CLDR's "mod" is "fmod()", not "%") - '/^(.*?)\s+not\s+in\s+(\d+)\.\.(\d+)/i' => '!in_array($1,range($2,$3))', //not in - '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => 'in_array($1,range($2,$3))', //in - '/^(.*?)\s+not\s+within\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not within - '/^(.*?)\s+within\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3)', //within - ); - foreach ($xml->plurals->pluralRules as $node) { - $attributes = $node->attributes(); - $locales = explode(' ', $attributes['locales']); - $rules = array(); - - if (!empty($node->pluralRule)) { - foreach ($node->pluralRule as $rule) { - $expr_or = preg_split('/\s+or\s+/i', $rule); - foreach ($expr_or as $key_or => $val_or) { - $expr_and = preg_split('/\s+and\s+/i', $val_or); - $expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and); - $expr_or[$key_or] = implode('&&', $expr_and); - } - $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); - $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { - if ($matches[2] - $matches[1] <= 5) { - return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; - } else { - return $matches[0]; - } - }, $expr); - - } - foreach ($locales as $locale) { - $allRules[$locale] = $rules; - } - } - } - // hard fix for "br": the rule is too complex - $allRules['br'] = array( - 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', - 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', - 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', - 3 => 'fmod($n,1000000)==0&&$n!=0', - ); - if (preg_match('/\d+/', $xml->version['number'], $matches)) { - $revision = $matches[0]; - } else { - $revision = -1; - } - - echo "getMessageSource($category)->translate($category, $message, $language); + $params = (array)$params; - if (!is_array($params)) { - $params = array($params); - } - - if (isset($params[0])) { - $message = $this->applyPluralRules($message, $params[0], $language); - if (!isset($params['{n}'])) { - $params['{n}'] = $params[0]; + if (class_exists('MessageFormatter', false) && preg_match('~{\s*[\d\w]+\s*,~u', $message)) { + $formatter = new MessageFormatter($language, $message); + if ($formatter === null) { + \Yii::$app->getLog()->log("$language message from category $category failed. Message is: $message.", Logger::LEVEL_WARNING, 'application'); + } + $result = $formatter->format($params); + if ($result === false) { + $errorMessage = $formatter->getErrorMessage(); + \Yii::$app->getLog()->log("$language message from category $category failed with error: $errorMessage. Message is: $message.", Logger::LEVEL_WARNING, 'application'); + } + else { + return $result; } - unset($params[0]); } return empty($params) ? $message : strtr($message, $params); @@ -125,62 +118,4 @@ class I18N extends Component throw new InvalidConfigException("Unable to locate message source for category '$category'."); } } - - /** - * Applies appropriate plural rules to the given message. - * @param string $message the message to be applied with plural rules - * @param mixed $number the number by which plural rules will be applied - * @param string $language the language code that determines which set of plural rules to be applied. - * @return string the message that has applied plural rules - */ - protected function applyPluralRules($message, $number, $language) - { - if (strpos($message, '|') === false) { - return $message; - } - $chunks = explode('|', $message); - - $rules = $this->getPluralRules($language); - foreach ($rules as $i => $rule) { - if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { - return $chunks[$i]; - } - } - $n = count($rules); - return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; - } - - private $_pluralRules = array(); // language => rule set - - /** - * Returns the plural rules for the given language code. - * @param string $language the language code (e.g. `en_US`, `en`). - * @return array the plural rules - * @throws InvalidParamException if the language code is invalid. - */ - protected function getPluralRules($language) - { - if (isset($this->_pluralRules[$language])) { - return $this->_pluralRules[$language]; - } - $allRules = require(Yii::getAlias($this->pluralRuleFile)); - if (isset($allRules[$language])) { - return $this->_pluralRules[$language] = $allRules[$language]; - } elseif (preg_match('/^[a-z]+/', strtolower($language), $matches)) { - return $this->_pluralRules[$language] = isset($allRules[$matches[0]]) ? $allRules[$matches[0]] : array(); - } else { - throw new InvalidParamException("Invalid language code: $language"); - } - } - - /** - * Evaluates a PHP expression with the given number value. - * @param string $expression the PHP expression - * @param mixed $n the number value - * @return boolean the expression result - */ - protected function evaluate($expression, $n) - { - return eval("return $expression;"); - } } diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php new file mode 100644 index 0000000..e7163c0 --- /dev/null +++ b/framework/yii/i18n/MessageFormatter.php @@ -0,0 +1,74 @@ + + * @since 2.0 + */ +class MessageFormatter extends \MessageFormatter +{ + /** + * Format the message. + * + * @link http://php.net/manual/en/messageformatter.format.php + * @param array $args Arguments to insert into the format string. + * @return string|boolean The formatted string, or false if an error occurred. + */ + public function format($args) + { + $pattern = self::replaceNamedArguments($this->getPattern(), $args); + $this->setPattern($pattern); + return parent::format(array_values($args)); + } + + /** + * Quick format message. + * + * @link http://php.net/manual/en/messageformatter.formatmessage.php + * @param string $locale The locale to use for formatting locale-dependent parts. + * @param string $pattern The pattern string to insert things into. + * @param array $args The array of values to insert into the format string. + * @return string|boolean The formatted pattern string or false if an error occurred. + */ + public static function formatMessage($locale, $pattern, $args) + { + $pattern = self::replaceNamedArguments($pattern, $args); + return parent::formatMessage($locale, $pattern, array_values($args)); + } + + /** + * Replace named placeholders with numeric placeholders. + * + * @param string $pattern The pattern string to relace things into. + * @param array $args The array of values to insert into the format string. + * @return string The pattern string with placeholders replaced. + */ + private static function replaceNamedArguments($pattern, $args) + { + $map = array_flip(array_keys($args)); + return preg_replace_callback('~({\s*)([\d\w]+)(\s*[,}])~u', function ($input) use ($map) { + $name = $input[2]; + if (isset($map[$name])) { + return $input[1] . $map[$name] . $input[3]; + } + else { + return "'" . $input[1] . $name . $input[3] . "'"; + } + }, $pattern); + } +} + \ No newline at end of file diff --git a/framework/yii/i18n/data/plurals.php b/framework/yii/i18n/data/plurals.php deleted file mode 100644 index cb51307..0000000 --- a/framework/yii/i18n/data/plurals.php +++ /dev/null @@ -1,627 +0,0 @@ - - array ( - 0 => '$n==0', - 1 => '$n==1', - 2 => '$n==2', - 3 => 'in_array(fmod($n,100),range(3,10))', - 4 => 'in_array(fmod($n,100),range(11,99))', - ), - 'asa' => - array ( - 0 => '$n==1', - ), - 'af' => - array ( - 0 => '$n==1', - ), - 'bem' => - array ( - 0 => '$n==1', - ), - 'bez' => - array ( - 0 => '$n==1', - ), - 'bg' => - array ( - 0 => '$n==1', - ), - 'bn' => - array ( - 0 => '$n==1', - ), - 'brx' => - array ( - 0 => '$n==1', - ), - 'ca' => - array ( - 0 => '$n==1', - ), - 'cgg' => - array ( - 0 => '$n==1', - ), - 'chr' => - array ( - 0 => '$n==1', - ), - 'da' => - array ( - 0 => '$n==1', - ), - 'de' => - array ( - 0 => '$n==1', - ), - 'dv' => - array ( - 0 => '$n==1', - ), - 'ee' => - array ( - 0 => '$n==1', - ), - 'el' => - array ( - 0 => '$n==1', - ), - 'en' => - array ( - 0 => '$n==1', - ), - 'eo' => - array ( - 0 => '$n==1', - ), - 'es' => - array ( - 0 => '$n==1', - ), - 'et' => - array ( - 0 => '$n==1', - ), - 'eu' => - array ( - 0 => '$n==1', - ), - 'fi' => - array ( - 0 => '$n==1', - ), - 'fo' => - array ( - 0 => '$n==1', - ), - 'fur' => - array ( - 0 => '$n==1', - ), - 'fy' => - array ( - 0 => '$n==1', - ), - 'gl' => - array ( - 0 => '$n==1', - ), - 'gsw' => - array ( - 0 => '$n==1', - ), - 'gu' => - array ( - 0 => '$n==1', - ), - 'ha' => - array ( - 0 => '$n==1', - ), - 'haw' => - array ( - 0 => '$n==1', - ), - 'he' => - array ( - 0 => '$n==1', - ), - 'is' => - array ( - 0 => '$n==1', - ), - 'it' => - array ( - 0 => '$n==1', - ), - 'jmc' => - array ( - 0 => '$n==1', - ), - 'kaj' => - array ( - 0 => '$n==1', - ), - 'kcg' => - array ( - 0 => '$n==1', - ), - 'kk' => - array ( - 0 => '$n==1', - ), - 'kl' => - array ( - 0 => '$n==1', - ), - 'ksb' => - array ( - 0 => '$n==1', - ), - 'ku' => - array ( - 0 => '$n==1', - ), - 'lb' => - array ( - 0 => '$n==1', - ), - 'lg' => - array ( - 0 => '$n==1', - ), - 'mas' => - array ( - 0 => '$n==1', - ), - 'ml' => - array ( - 0 => '$n==1', - ), - 'mn' => - array ( - 0 => '$n==1', - ), - 'mr' => - array ( - 0 => '$n==1', - ), - 'nah' => - array ( - 0 => '$n==1', - ), - 'nb' => - array ( - 0 => '$n==1', - ), - 'nd' => - array ( - 0 => '$n==1', - ), - 'ne' => - array ( - 0 => '$n==1', - ), - 'nl' => - array ( - 0 => '$n==1', - ), - 'nn' => - array ( - 0 => '$n==1', - ), - 'no' => - array ( - 0 => '$n==1', - ), - 'nr' => - array ( - 0 => '$n==1', - ), - 'ny' => - array ( - 0 => '$n==1', - ), - 'nyn' => - array ( - 0 => '$n==1', - ), - 'om' => - array ( - 0 => '$n==1', - ), - 'or' => - array ( - 0 => '$n==1', - ), - 'pa' => - array ( - 0 => '$n==1', - ), - 'pap' => - array ( - 0 => '$n==1', - ), - 'ps' => - array ( - 0 => '$n==1', - ), - 'pt' => - array ( - 0 => '$n==1', - ), - 'rof' => - array ( - 0 => '$n==1', - ), - 'rm' => - array ( - 0 => '$n==1', - ), - 'rwk' => - array ( - 0 => '$n==1', - ), - 'saq' => - array ( - 0 => '$n==1', - ), - 'seh' => - array ( - 0 => '$n==1', - ), - 'sn' => - array ( - 0 => '$n==1', - ), - 'so' => - array ( - 0 => '$n==1', - ), - 'sq' => - array ( - 0 => '$n==1', - ), - 'ss' => - array ( - 0 => '$n==1', - ), - 'ssy' => - array ( - 0 => '$n==1', - ), - 'st' => - array ( - 0 => '$n==1', - ), - 'sv' => - array ( - 0 => '$n==1', - ), - 'sw' => - array ( - 0 => '$n==1', - ), - 'syr' => - array ( - 0 => '$n==1', - ), - 'ta' => - array ( - 0 => '$n==1', - ), - 'te' => - array ( - 0 => '$n==1', - ), - 'teo' => - array ( - 0 => '$n==1', - ), - 'tig' => - array ( - 0 => '$n==1', - ), - 'tk' => - array ( - 0 => '$n==1', - ), - 'tn' => - array ( - 0 => '$n==1', - ), - 'ts' => - array ( - 0 => '$n==1', - ), - 'ur' => - array ( - 0 => '$n==1', - ), - 'wae' => - array ( - 0 => '$n==1', - ), - 've' => - array ( - 0 => '$n==1', - ), - 'vun' => - array ( - 0 => '$n==1', - ), - 'xh' => - array ( - 0 => '$n==1', - ), - 'xog' => - array ( - 0 => '$n==1', - ), - 'zu' => - array ( - 0 => '$n==1', - ), - 'ak' => - array ( - 0 => '($n==0||$n==1)', - ), - 'am' => - array ( - 0 => '($n==0||$n==1)', - ), - 'bh' => - array ( - 0 => '($n==0||$n==1)', - ), - 'fil' => - array ( - 0 => '($n==0||$n==1)', - ), - 'tl' => - array ( - 0 => '($n==0||$n==1)', - ), - 'guw' => - array ( - 0 => '($n==0||$n==1)', - ), - 'hi' => - array ( - 0 => '($n==0||$n==1)', - ), - 'ln' => - array ( - 0 => '($n==0||$n==1)', - ), - 'mg' => - array ( - 0 => '($n==0||$n==1)', - ), - 'nso' => - array ( - 0 => '($n==0||$n==1)', - ), - 'ti' => - array ( - 0 => '($n==0||$n==1)', - ), - 'wa' => - array ( - 0 => '($n==0||$n==1)', - ), - 'ff' => - array ( - 0 => '($n>=0&&$n<=2)&&$n!=2', - ), - 'fr' => - array ( - 0 => '($n>=0&&$n<=2)&&$n!=2', - ), - 'kab' => - array ( - 0 => '($n>=0&&$n<=2)&&$n!=2', - ), - 'lv' => - array ( - 0 => '$n==0', - 1 => 'fmod($n,10)==1&&fmod($n,100)!=11', - ), - 'iu' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'kw' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'naq' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'se' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'sma' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'smi' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'smj' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'smn' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'sms' => - array ( - 0 => '$n==1', - 1 => '$n==2', - ), - 'ga' => - array ( - 0 => '$n==1', - 1 => '$n==2', - 2 => 'in_array($n,array(3,4,5,6))', - 3 => 'in_array($n,array(7,8,9,10))', - ), - 'ro' => - array ( - 0 => '$n==1', - 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', - ), - 'mo' => - array ( - 0 => '$n==1', - 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', - ), - 'lt' => - array ( - 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),range(11,19))', - 1 => 'in_array(fmod($n,10),range(2,9))&&!in_array(fmod($n,100),range(11,19))', - ), - 'be' => - array ( - 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'bs' => - array ( - 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'hr' => - array ( - 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'ru' => - array ( - 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'sh' => - array ( - 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'sr' => - array ( - 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'uk' => - array ( - 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', - ), - 'cs' => - array ( - 0 => '$n==1', - 1 => 'in_array($n,array(2,3,4))', - ), - 'sk' => - array ( - 0 => '$n==1', - 1 => 'in_array($n,array(2,3,4))', - ), - 'pl' => - array ( - 0 => '$n==1', - 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', - 2 => '$n!=1&&in_array(fmod($n,10),array(0,1))||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(12,13,14))', - ), - 'sl' => - array ( - 0 => 'fmod($n,100)==1', - 1 => 'fmod($n,100)==2', - 2 => 'in_array(fmod($n,100),array(3,4))', - ), - 'mt' => - array ( - 0 => '$n==1', - 1 => '$n==0||in_array(fmod($n,100),range(2,10))', - 2 => 'in_array(fmod($n,100),range(11,19))', - ), - 'mk' => - array ( - 0 => 'fmod($n,10)==1&&$n!=11', - ), - 'cy' => - array ( - 0 => '$n==0', - 1 => '$n==1', - 2 => '$n==2', - 3 => '$n==3', - 4 => '$n==6', - ), - 'lag' => - array ( - 0 => '$n==0', - 1 => '($n>=0&&$n<=2)&&$n!=0&&$n!=2', - ), - 'shi' => - array ( - 0 => '($n>=0&&$n<=1)', - 1 => 'in_array($n,range(2,10))', - ), - 'br' => - array ( - 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', - 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', - 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', - 3 => 'fmod($n,1000000)==0&&$n!=0', - ), - 'ksh' => - array ( - 0 => '$n==0', - 1 => '$n==1', - ), - 'tzm' => - array ( - 0 => '($n==0||$n==1)||in_array($n,range(11,99))', - ), - 'gv' => - array ( - 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', - ), -); diff --git a/framework/yii/i18n/data/plurals.xml b/framework/yii/i18n/data/plurals.xml deleted file mode 100644 index 9227dc6..0000000 --- a/framework/yii/i18n/data/plurals.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - n is 0 - n is 1 - n is 2 - n mod 100 in 3..10 - n mod 100 in 11..99 - - - n is 1 - - - n in 0..1 - - - n within 0..2 and n is not 2 - - - n is 0 - n mod 10 is 1 and n mod 100 is not 11 - - - n is 1 - n is 2 - - - n is 1 - n is 2 - n in 3..6 - n in 7..10 - - - n is 1 - n is 0 OR n is not 1 AND n mod 100 in 1..19 - - - n mod 10 is 1 and n mod 100 not in 11..19 - n mod 10 in 2..9 and n mod 100 not in 11..19 - - - n mod 10 is 1 and n mod 100 is not 11 - n mod 10 in 2..4 and n mod 100 not in 12..14 - n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14 - - - - n is 1 - n in 2..4 - - - n is 1 - n mod 10 in 2..4 and n mod 100 not in 12..14 - n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14 - - - - - n mod 100 is 1 - n mod 100 is 2 - n mod 100 in 3..4 - - - n is 1 - n is 0 or n mod 100 in 2..10 - n mod 100 in 11..19 - - - n mod 10 is 1 and n is not 11 - - - n is 0 - n is 1 - n is 2 - n is 3 - n is 6 - - - n is 0 - n within 0..2 and n is not 0 and n is not 2 - - - n within 0..1 - n in 2..10 - - - n mod 10 is 1 and n mod 100 not in 11,71,91 - n mod 10 is 2 and n mod 100 not in 12,72,92 - n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99 - n mod 1000000 is 0 and n is not 0 - - - n is 0 - n is 1 - - - n in 0..1 or n in 11..99 - - - n mod 10 in 1..2 or n mod 20 is 0 - - - diff --git a/framework/yii/requirements/requirements.php b/framework/yii/requirements/requirements.php index 005a205..571aa57 100644 --- a/framework/yii/requirements/requirements.php +++ b/framework/yii/requirements/requirements.php @@ -43,6 +43,8 @@ return array( 'mandatory' => false, 'condition' => $this->checkPhpExtensionVersion('intl', '1.0.2', '>='), 'by' => 'Internationalization support', - 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use IDN-feature of EmailValidator or UrlValidator or the yii\i18n\Formatter class.' + 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use advanced parameters formatting + in \Yii::t(), IDN-feature of + EmailValidator or UrlValidator or the yii\i18n\Formatter class.' ), ); diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php new file mode 100644 index 0000000..7595fe0 --- /dev/null +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -0,0 +1,47 @@ + + * @since 2.0 + * @group i18n + */ +class MessageFormatterTest extends TestCase +{ + const N = 'n'; + const N_VALUE = 42; + const SUBJECT = 'сабж'; + const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; + + public function testNamedArguments() + { + $expected = self::SUBJECT_VALUE.' is '.self::N_VALUE; + + $result = MessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', array( + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + )); + + $this->assertEquals($expected, $result); + } + + public function testInsufficientArguments() + { + $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; + + $result = MessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', array( + self::N => self::N_VALUE, + )); + + $this->assertEquals($expected, $result); + } +} \ No newline at end of file From dafbeda301bb1eb85eec55a468fbd043efadd348 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 28 Sep 2013 22:30:16 +0400 Subject: [PATCH 02/19] More i18n tests, docs, added check to skip fixes where possible --- docs/guide/i18n.md | 245 +++++++++++++++++---- framework/yii/i18n/MessageFormatter.php | 34 ++- tests/unit/framework/i18n/MessageFormatterTest.php | 33 +++ 3 files changed, 268 insertions(+), 44 deletions(-) diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index 214b7d2..a1c6314 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -5,44 +5,215 @@ Internationalization (I18N) refers to the process of designing a software applic various languages and regions without engineering changes. For Web applications, this is of particular importance because the potential users may be worldwide. -When developing an application it's assumed that we're relying on -[PHP internationalization extension](http://www.php.net/manual/en/intro.intl.php). While extension covers a lot of aspects -Yii adds a bit more: +Locale and Language +------------------- -- It handles message translation. +There are two languages defined in Yii application: [[\yii\base\Application::$sourceLanguage|source language]] and +[[\yii\base\Application::$language|target language]]. +Source language is the language original application messages are written in such as: -Locale and Language -------------------- +```php +echo \Yii::t('app', 'I am a message!'); +``` + +> **Tip**: Default is English and it's not recommended to change it. The reason is that it's easier to find people translating from +> English to any language than from non-English to non-English. + +Target language is what's currently used. It's defined in application configuration like the following: + +```php +// ... +return array( + 'id' => 'applicationID', + 'basePath' => dirname(__DIR__), + 'language' => 'ru_RU' // ← here! +``` + +Later you can easily change it in runtime: + +```php +\Yii::$app->language = 'zh_CN'; +``` + +Basic message translation +------------------------- + +### Strings translation + +Yii basic message translation that works without additional PHP extension and + + +### Named placeholders + +```php +$username = 'Alexander'; +echo \Yii::t('app', 'Hello, {username}!', array( + 'username' => $username, +)); +``` + +### Positional placeholders + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0}', $sum); +``` + +> **Tip**: When messages are extracted and passed to translator, he sees strings only. For the code above extracted message will be +> "Balance: {0}". It's not recommended to use positional placeholders except when there's only one and message context is +> clear as above. + +Advanced placeholder formatting +------------------------------- + +In order to use advanced features you need to install and enable [intl](http://www.php.net/manual/en/intro.intl.php) PHP +extension. After installing and enabling it you will be able to use extended syntax for placeholders. Either short form +`{placeholderName, argumentType}` that means default setting or full form `{placeholderName, argumentType, argumentStyle}` +that allows you to specify formatting style. + +Full reference is [available at ICU website](http://icu-project.org/apiref/icu4c/classMessageFormat.html) but since it's +a bit crypric we have our own reference below. + +### Numbers + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0, number}', $sum); +``` + +You can specify one of the built-in styles (`integer`, `currency`, `percent`): + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0, number, currency}', $sum); +``` + +Or specify custom pattern: + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0, number, ,000,000000}', $sum); +``` + +[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1DecimalFormat.html). + +### Dates + +```php +echo \Yii::t('app', 'Today is {0, date}', time()); +``` + +Built in formats (`short`, `medium`, `long`, `full`): + +```php +echo \Yii::t('app', 'Today is {0, date, short}', time()); +``` + +Custom pattern: + +```php +echo \Yii::t('app', 'Today is {0, date, YYYY-MM-dd}', time()); +``` + +[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1SimpleDateFormat.html). + +### Time + +```php +echo \Yii::t('app', 'It is {0, time}', time()); +``` + +Built in formats (`short`, `medium`, `long`, `full`): + +```php +echo \Yii::t('app', 'It is {0, time, short}', time()); +``` + +Custom pattern: + +```php +echo \Yii::t('app', 'It is {0, date, HH:mm}', time()); +``` + +[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1SimpleDateFormat.html). + + +### Spellout + +```php +echo \Yii::t('app', '{n,number} is spelled as {n, spellout}', array( + 'n' => 42, +)); +``` + +### Ordinal + +```php +echo \Yii::t('app', 'You are {n, ordinal} visitor here!', array( + 'n' => 42, +)); +``` + +Will produce "You are 42nd visitor here!". + +### Duration + + +```php +echo \Yii::t('app', 'You are here for {n, duration} already!', array( + 'n' => 42, +)); +``` + +Will produce "You are here for 47 sec. already!". + +### Plurals + +Different languages have different ways to inflect plurals. Some rules are very complex so it's very handy that this +functionality is provided without the need to specify inflection rule. Instead it only requires your input of inflected +word in certain situations. + +```php +echo \Yii::t('app', 'There {n, plural, =0{are no cats} =1{is one cat} other{are # cats}}!', array( + 'n' => 0, +)); +``` + +Will give us "There are no cats!". + +In the plural rule arguments above `=0` means exactly zero, `=1` stands for exactly one `other` is for any other number. +`#` is replaced with the `n` argument value. It's not that simple for languages other than English. Here's an example +for Russian: + +``` +Здесь {n, plural, =0{котов нет} =1{есть один кот} one{# кот} few{# кота} many{# котов} other{# кота}}! +``` + +In the above it worth mentioning that `=1` matches exactly `n = 1` while `one` matches `21` or `101`. + +To learn which inflection forms you should specify for your language you can referer to +[rules reference at unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). + +### Selections + +You can select phrases based on keywords. The pattern in this case specifies how to map keywords to phrases and +provides a default phrase. + +```php +echo \Yii::t('app', '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', array( + 'name' => 'Snoopy', + 'gender' => 'dog', +)); +``` + +Will produce "Snoopy is dog and it loves Yii!". + +In the expression `female` and `male` are possible values. `other` handler values that do not match. Strings inside +brackets are sub-expressions so could be just a string or a string with more placeholders. + +Formatters +---------- -Translation ------------ - -/* - -numeric arg \{\s*\d+\s*\} -named arg \{\s*(\w|(\w|\d){2,})\s*\} - -named placeholder can be unicode!!! - - -argName [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ - -message = messageText (argument messageText)* - argument = noneArg | simpleArg | complexArg - complexArg = choiceArg | pluralArg | selectArg | selectordinalArg - noneArg = '{' argNameOrNumber '}' - simpleArg = '{' argNameOrNumber ',' argType [',' argStyle] '}' - choiceArg = '{' argNameOrNumber ',' "choice" ',' choiceStyle '}' - pluralArg = '{' argNameOrNumber ',' "plural" ',' pluralStyle '}' - selectArg = '{' argNameOrNumber ',' "select" ',' selectStyle '}' - selectordinalArg = '{' argNameOrNumber ',' "selectordinal" ',' pluralStyle '}' - choiceStyle: see ChoiceFormat - pluralStyle: see PluralFormat - selectStyle: see SelectFormat - argNameOrNumber = argName | argNumber - argName = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ - argNumber = '0' | ('1'..'9' ('0'..'9')*) - argType = "number" | "date" | "time" | "spellout" | "ordinal" | "duration" - argStyle = "short" | "medium" | "long" | "full" | "integer" | "currency" | "percent" | argStyleText - */ \ No newline at end of file +In order to use formatters you need to install and enable [intl](http://www.php.net/manual/en/intro.intl.php) PHP +extension. diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index e7163c0..de717dc 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -14,8 +14,6 @@ namespace yii\i18n; * - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be * substituted. * - * @see http://php.net/manual/en/migration55.changed-functions.php - * * @author Alexander Makarov * @since 2.0 */ @@ -30,9 +28,12 @@ class MessageFormatter extends \MessageFormatter */ public function format($args) { - $pattern = self::replaceNamedArguments($this->getPattern(), $args); - $this->setPattern($pattern); - return parent::format(array_values($args)); + if (self::needFix()) { + $pattern = self::replaceNamedArguments($this->getPattern(), $args); + $this->setPattern($pattern); + $args = array_values($args); + } + return parent::format($args); } /** @@ -46,8 +47,11 @@ class MessageFormatter extends \MessageFormatter */ public static function formatMessage($locale, $pattern, $args) { - $pattern = self::replaceNamedArguments($pattern, $args); - return parent::formatMessage($locale, $pattern, array_values($args)); + if (self::needFix()) { + $pattern = self::replaceNamedArguments($pattern, $args); + $args = array_values($args); + } + return parent::formatMessage($locale, $pattern, $args); } /** @@ -66,9 +70,25 @@ class MessageFormatter extends \MessageFormatter return $input[1] . $map[$name] . $input[3]; } else { + //return $input[1] . $name . $input[3]; return "'" . $input[1] . $name . $input[3] . "'"; } }, $pattern); } + + /** + * Checks if fix should be applied + * + * @see http://php.net/manual/en/migration55.changed-functions.php + * @return boolean if fix should be applied + */ + private static function needFix() + { + return ( + !defined('INTL_ICU_VERSION') || + version_compare(INTL_ICU_VERSION, '48.0.0', '<') || + version_compare(PHP_VERSION, '5.5.0', '<') + ); + } } \ No newline at end of file diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index 7595fe0..51e512c 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -32,6 +32,39 @@ class MessageFormatterTest extends TestCase )); $this->assertEquals($expected, $result); + + $pattern = <<<_MSG_ +{gender_of_host, select, + female {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to her party.} + =2 {{host} invites {guest} and one other person to her party.} + other {{host} invites {guest} and # other people to her party.}}} + male {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to his party.} + =2 {{host} invites {guest} and one other person to his party.} + other {{host} invites {guest} and # other people to his party.}}} + other {{num_guests, plural, offset:1 + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to their party.} + =2 {{host} invites {guest} and one other person to their party.} + other {{host} invites {guest} and # other people to their party.}}}} +_MSG_; + $result = MessageFormatter::formatMessage('en_US', $pattern, array( + 'gender_of_host' => 'male', + 'num_guests' => 4, + 'host' => 'ralph', + 'guest' => 'beep' + )); + $this->assertEquals('ralph invites beep and 3 other people to his party.', $result); + + $pattern = '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!'; + $result = MessageFormatter::formatMessage('en_US', $pattern, array( + 'name' => 'Alexander', + 'gender' => 'male', + )); + $this->assertEquals('Alexander is male and he loves Yii!', $result); } public function testInsufficientArguments() From b45b16d84502b6d557c72839d17b4b8b229806c1 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 29 Sep 2013 05:05:57 +0400 Subject: [PATCH 03/19] Renamed test to match class name --- tests/unit/framework/BaseYiiTest.php | 63 ++++++++++++++++++++++++++++++++++++ tests/unit/framework/YiiBaseTest.php | 63 ------------------------------------ 2 files changed, 63 insertions(+), 63 deletions(-) create mode 100644 tests/unit/framework/BaseYiiTest.php delete mode 100644 tests/unit/framework/YiiBaseTest.php diff --git a/tests/unit/framework/BaseYiiTest.php b/tests/unit/framework/BaseYiiTest.php new file mode 100644 index 0000000..a04de99 --- /dev/null +++ b/tests/unit/framework/BaseYiiTest.php @@ -0,0 +1,63 @@ +aliases = Yii::$aliases; + } + + protected function tearDown() + { + parent::tearDown(); + Yii::$aliases = $this->aliases; + } + + public function testAlias() + { + $this->assertEquals(YII_PATH, Yii::getAlias('@yii')); + + Yii::$aliases = array(); + $this->assertFalse(Yii::getAlias('@yii', false)); + + Yii::setAlias('@yii', '/yii/framework'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + Yii::setAlias('@yii/gii', '/yii/gii'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii')); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@tii', '@yii/test'); + $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii')); + + Yii::setAlias('@yii', null); + $this->assertFalse(Yii::getAlias('@yii', false)); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@some/alias', '/www'); + $this->assertEquals('/www', Yii::getAlias('@some/alias')); + } + + public function testGetVersion() + { + $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); + } + + public function testPowered() + { + $this->assertTrue(is_string(Yii::powered())); + } +} diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php deleted file mode 100644 index 72b5f24..0000000 --- a/tests/unit/framework/YiiBaseTest.php +++ /dev/null @@ -1,63 +0,0 @@ -aliases = Yii::$aliases; - } - - protected function tearDown() - { - parent::tearDown(); - Yii::$aliases = $this->aliases; - } - - public function testAlias() - { - $this->assertEquals(YII_PATH, Yii::getAlias('@yii')); - - Yii::$aliases = array(); - $this->assertFalse(Yii::getAlias('@yii', false)); - - Yii::setAlias('@yii', '/yii/framework'); - $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); - $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); - Yii::setAlias('@yii/gii', '/yii/gii'); - $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); - $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); - $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii')); - $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); - - Yii::setAlias('@tii', '@yii/test'); - $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii')); - - Yii::setAlias('@yii', null); - $this->assertFalse(Yii::getAlias('@yii', false)); - $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); - - Yii::setAlias('@some/alias', '/www'); - $this->assertEquals('/www', Yii::getAlias('@some/alias')); - } - - public function testGetVersion() - { - $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); - } - - public function testPowered() - { - $this->assertTrue(is_string(Yii::powered())); - } -} From fb684774db5870abb3ecb0bf91d2924704d6bba6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 29 Sep 2013 05:11:46 +0400 Subject: [PATCH 04/19] better wording --- docs/guide/i18n.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index a1c6314..6532717 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -41,7 +41,11 @@ Basic message translation ### Strings translation -Yii basic message translation that works without additional PHP extension and +Yii basic message translation that works without additional PHP extension. + +```php +echo \Yii::t('app', 'This is a string to translate!'); +``` ### Named placeholders @@ -60,9 +64,8 @@ $sum = 42; echo \Yii::t('app', 'Balance: {0}', $sum); ``` -> **Tip**: When messages are extracted and passed to translator, he sees strings only. For the code above extracted message will be -> "Balance: {0}". It's not recommended to use positional placeholders except when there's only one and message context is -> clear as above. +> **Tip**: Try keep message strings meaningful and avoid using too many positional parameters. Remember that +> translator has source string only so it should be obvious about what will replace each placeholder. Advanced placeholder formatting ------------------------------- From abc0df6145bda4e4531d5b9482d39aa1a9ba09be Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 29 Sep 2013 05:12:36 +0400 Subject: [PATCH 05/19] typo --- framework/yii/i18n/MessageFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index de717dc..76197d1 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -57,7 +57,7 @@ class MessageFormatter extends \MessageFormatter /** * Replace named placeholders with numeric placeholders. * - * @param string $pattern The pattern string to relace things into. + * @param string $pattern The pattern string to replace things into. * @param array $args The array of values to insert into the format string. * @return string The pattern string with placeholders replaced. */ From 6fc5f0a4caceaddbc10daa775d24fb0d399817ae Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 3 Oct 2013 01:54:58 +0400 Subject: [PATCH 06/19] Minor additions to i18n docs --- docs/guide/i18n.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index 6532717..2e17ce0 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -39,14 +39,17 @@ Later you can easily change it in runtime: Basic message translation ------------------------- -### Strings translation - -Yii basic message translation that works without additional PHP extension. +Yii basic message translation in its basic variant works without additional PHP extension. What it does is finding a +translation of the message from source language into targer language. Message itself is specified as the first +`\Yii::t` method parameter: ```php echo \Yii::t('app', 'This is a string to translate!'); ``` +Yii tries to load approprite translation from one of the message sources defined via `i18n` component configuration. + +TBD: https://github.com/yiisoft/yii2/issues/930 ### Named placeholders From b8099cbf28e9e0deba2fc55a2645dd7b01ff753d Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 04:14:58 +0200 Subject: [PATCH 07/19] a parser that is able to deal with the right number of args --- framework/yii/i18n/MessageFormatter.php | 43 ++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 76197d1..cba0091 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -64,16 +64,41 @@ class MessageFormatter extends \MessageFormatter private static function replaceNamedArguments($pattern, $args) { $map = array_flip(array_keys($args)); - return preg_replace_callback('~({\s*)([\d\w]+)(\s*[,}])~u', function ($input) use ($map) { - $name = $input[2]; - if (isset($map[$name])) { - return $input[1] . $map[$name] . $input[3]; - } - else { - //return $input[1] . $name . $input[3]; - return "'" . $input[1] . $name . $input[3] . "'"; + + // parsing http://icu-project.org/apiref/icu4c/classMessageFormat.html#details + $parts = explode('{', $pattern); + $c = count($parts); + $pattern = $parts[0]; + $d = 0; + $stack = array(); + for($i = 1; $i < $c; $i++) { + if (preg_match('~^\A(\s*)([\d\w]+)(\s*)([},])(\s*)(.*)\z$~u', $parts[$i], $matches)) { + $d++; + // replace normal arg if it was set + if (isset($map[$matches[2]])) { + $q = ''; + $pattern .= '{' . $matches[1] . $map[$matches[2]] . $matches[3]; + } else { + // quote unused args + $q = '';//($matches[4] == '}' && isset($stack[$d]) && !($stack[$d] == 'plural' || $stack[$d] == 'select')) ? "'" : ""; + $pattern .= "$q{" . $matches[1] . $matches[2] . $matches[3]; + } + $pattern .= ($term = $matches[4] . $q . $matches[5] . $matches[6]); + // check type current level + $stack[$d] = ($matches[4] == ',') ? substr($matches[6], 0, 6) : 'none'; + // if it's plural or select, the next bracket is NOT begin of a message then! + if ($stack[$d] == 'plural' || $stack[$d] == 'select') { + $i++; + $d -= substr_count($term, '}'); + } else { + $d -= substr_count($term, '}'); + continue; + } } - }, $pattern); + $pattern .= '{' . $parts[$i]; + $d += 1 - substr_count($parts[$i], '}'); + } + return $pattern; } /** From fa9a975dbbca6723cda1772cec74d8e5251b2db4 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 04:41:03 +0200 Subject: [PATCH 08/19] intl message parser now handles too many or too less args --- framework/yii/i18n/MessageFormatter.php | 46 ++++++++++++---------- tests/unit/framework/i18n/MessageFormatterTest.php | 32 +++++++++++++++ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index cba0091..d83541c 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -65,7 +65,8 @@ class MessageFormatter extends \MessageFormatter { $map = array_flip(array_keys($args)); - // parsing http://icu-project.org/apiref/icu4c/classMessageFormat.html#details + // parsing pattern base on ICU grammar: + // http://icu-project.org/apiref/icu4c/classMessageFormat.html#details $parts = explode('{', $pattern); $c = count($parts); $pattern = $parts[0]; @@ -73,26 +74,29 @@ class MessageFormatter extends \MessageFormatter $stack = array(); for($i = 1; $i < $c; $i++) { if (preg_match('~^\A(\s*)([\d\w]+)(\s*)([},])(\s*)(.*)\z$~u', $parts[$i], $matches)) { - $d++; - // replace normal arg if it was set - if (isset($map[$matches[2]])) { - $q = ''; - $pattern .= '{' . $matches[1] . $map[$matches[2]] . $matches[3]; - } else { - // quote unused args - $q = '';//($matches[4] == '}' && isset($stack[$d]) && !($stack[$d] == 'plural' || $stack[$d] == 'select')) ? "'" : ""; - $pattern .= "$q{" . $matches[1] . $matches[2] . $matches[3]; - } - $pattern .= ($term = $matches[4] . $q . $matches[5] . $matches[6]); - // check type current level - $stack[$d] = ($matches[4] == ',') ? substr($matches[6], 0, 6) : 'none'; - // if it's plural or select, the next bracket is NOT begin of a message then! - if ($stack[$d] == 'plural' || $stack[$d] == 'select') { - $i++; - $d -= substr_count($term, '}'); - } else { - $d -= substr_count($term, '}'); - continue; + // if we are not inside a plural or select this is a message + if (!isset($stack[$d]) || $stack[$d] != 'plural' && $stack[$d] != 'select') { + $d++; + // replace normal arg if it is available + if (isset($map[$matches[2]])) { + $q = ''; + $pattern .= '{' . $matches[1] . $map[$matches[2]] . $matches[3]; + } else { + // quote unused args + $q = ($matches[4] == '}') ? "'" : ""; + $pattern .= "$q{" . $matches[1] . $matches[2] . $matches[3]; + } + $pattern .= ($term = $matches[4] . $q . $matches[5] . $matches[6]); + // store type of current level + $stack[$d] = ($matches[4] == ',') ? substr($matches[6], 0, 6) : 'none'; + // if it's plural or select, the next bracket is NOT begin of a message then! + if ($stack[$d] == 'plural' || $stack[$d] == 'select') { + $i++; + $d -= substr_count($term, '}'); + } else { + $d -= substr_count($term, '}'); + continue; + } } } $pattern .= '{' . $parts[$i]; diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index 51e512c..65f9ea4 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -65,6 +65,38 @@ _MSG_; 'gender' => 'male', )); $this->assertEquals('Alexander is male and he loves Yii!', $result); + + // verify pattern in select does not get replaced + $pattern = '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!'; + $result = MessageFormatter::formatMessage('en_US', $pattern, array( + 'name' => 'Alexander', + 'gender' => 'male', + // following should not be replaced + 'he' => 'wtf', + 'she' => 'wtf', + 'it' => 'wtf', + )); + $this->assertEquals('Alexander is male and he loves Yii!', $result); + + // verify pattern in select message gets replaced + $pattern = '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!'; + $result = MessageFormatter::formatMessage('en_US', $pattern, array( + 'name' => 'Alexander', + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + )); + $this->assertEquals('Alexander is male and wtf loves Yii!', $result); + + // some parser specific verifications + $pattern = '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr, number} is {gender}!'; + $result = MessageFormatter::formatMessage('en_US', $pattern, array( + 'nr' => 42, + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + )); + $this->assertEquals('male and wtf loves 42 is male!', $result); } public function testInsufficientArguments() From 150b9366fcf385fb279fb59bf4293d95fd407c9d Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 05:24:02 +0200 Subject: [PATCH 09/19] fixed regex for multiline patterns --- framework/yii/i18n/MessageFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index d83541c..8ae6c1e 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -73,7 +73,7 @@ class MessageFormatter extends \MessageFormatter $d = 0; $stack = array(); for($i = 1; $i < $c; $i++) { - if (preg_match('~^\A(\s*)([\d\w]+)(\s*)([},])(\s*)(.*)\z$~u', $parts[$i], $matches)) { + if (preg_match('~^(\s*)([\d\w]+)(\s*)([},])(\s*)(.*)$~us', $parts[$i], $matches)) { // if we are not inside a plural or select this is a message if (!isset($stack[$d]) || $stack[$d] != 'plural' && $stack[$d] != 'select') { $d++; From 2eb5abbfcbbf220a91e9423251b6cd54daed980c Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 05:54:10 +0200 Subject: [PATCH 10/19] improved unit tests for ICU message formatter --- framework/yii/i18n/MessageFormatter.php | 5 +- tests/unit/framework/i18n/MessageFormatterTest.php | 137 +++++++++++++-------- 2 files changed, 87 insertions(+), 55 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 8ae6c1e..c412420 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -15,6 +15,7 @@ namespace yii\i18n; * substituted. * * @author Alexander Makarov + * @author Carsten Brandt * @since 2.0 */ class MessageFormatter extends \MessageFormatter @@ -55,7 +56,7 @@ class MessageFormatter extends \MessageFormatter } /** - * Replace named placeholders with numeric placeholders. + * Replace named placeholders with numeric placeholders and quote unused. * * @param string $pattern The pattern string to replace things into. * @param array $args The array of values to insert into the format string. @@ -65,7 +66,7 @@ class MessageFormatter extends \MessageFormatter { $map = array_flip(array_keys($args)); - // parsing pattern base on ICU grammar: + // parsing pattern based on ICU grammar: // http://icu-project.org/apiref/icu4c/classMessageFormat.html#details $parts = explode('{', $pattern); $c = count($parts); diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index 65f9ea4..d9ae728 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -22,18 +22,19 @@ class MessageFormatterTest extends TestCase const SUBJECT = 'сабж'; const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; - public function testNamedArguments() + public function patterns() { - $expected = self::SUBJECT_VALUE.' is '.self::N_VALUE; + return array( + array( + '{'.self::SUBJECT.'} is {'.self::N.', number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + array( // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ) + ), - $result = MessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', array( - self::N => self::N_VALUE, - self::SUBJECT => self::SUBJECT_VALUE, - )); - - $this->assertEquals($expected, $result); - - $pattern = <<<_MSG_ + array(<<<_MSG_ {gender_of_host, select, female {{num_guests, plural, offset:1 =0 {{host} does not give a party.} @@ -50,53 +51,83 @@ class MessageFormatterTest extends TestCase =1 {{host} invites {guest} to their party.} =2 {{host} invites {guest} and one other person to their party.} other {{host} invites {guest} and # other people to their party.}}}} -_MSG_; - $result = MessageFormatter::formatMessage('en_US', $pattern, array( - 'gender_of_host' => 'male', - 'num_guests' => 4, - 'host' => 'ralph', - 'guest' => 'beep' - )); - $this->assertEquals('ralph invites beep and 3 other people to his party.', $result); +_MSG_ + , + 'ralph invites beep and 3 other people to his party.', + array( + 'gender_of_host' => 'male', + 'num_guests' => 4, + 'host' => 'ralph', + 'guest' => 'beep' + ) + ), - $pattern = '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!'; - $result = MessageFormatter::formatMessage('en_US', $pattern, array( - 'name' => 'Alexander', - 'gender' => 'male', - )); - $this->assertEquals('Alexander is male and he loves Yii!', $result); + array( + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + array( + 'name' => 'Alexander', + 'gender' => 'male', + ), + ), - // verify pattern in select does not get replaced - $pattern = '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!'; - $result = MessageFormatter::formatMessage('en_US', $pattern, array( - 'name' => 'Alexander', - 'gender' => 'male', - // following should not be replaced - 'he' => 'wtf', - 'she' => 'wtf', - 'it' => 'wtf', - )); - $this->assertEquals('Alexander is male and he loves Yii!', $result); + // verify pattern in select does not get replaced + array( + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + array( + 'name' => 'Alexander', + 'gender' => 'male', + // following should not be replaced + 'he' => 'wtf', + 'she' => 'wtf', + 'it' => 'wtf', + ) + ), - // verify pattern in select message gets replaced - $pattern = '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!'; - $result = MessageFormatter::formatMessage('en_US', $pattern, array( - 'name' => 'Alexander', - 'gender' => 'male', - 'he' => 'wtf', - 'she' => 'wtf', - )); - $this->assertEquals('Alexander is male and wtf loves Yii!', $result); + // verify pattern in select message gets replaced + array( + '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!', + 'Alexander is male and wtf loves Yii!', + array( + 'name' => 'Alexander', + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ), + ), - // some parser specific verifications - $pattern = '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr, number} is {gender}!'; - $result = MessageFormatter::formatMessage('en_US', $pattern, array( - 'nr' => 42, - 'gender' => 'male', - 'he' => 'wtf', - 'she' => 'wtf', - )); - $this->assertEquals('male and wtf loves 42 is male!', $result); + // some parser specific verifications + array( + '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr, number} is {gender}!', + 'male and wtf loves 42 is male!', + array( + 'nr' => 42, + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ), + ), + ); + } + + /** + * @dataProvider patterns + */ + public function testNamedArgumentsStatic($pattern, $expected, $args) + { + $result = MessageFormatter::formatMessage('en_US', $pattern, $args); + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider patterns + */ + public function testNamedArgumentsObject($pattern, $expected, $args) + { + $formatter = new MessageFormatter('en_US', $pattern); + $result = $formatter->format($args); + $this->assertEquals($expected, $result); } public function testInsufficientArguments() From bbcee326be4bc14bad40b3e161951faa5a08c356 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 15:59:39 +0200 Subject: [PATCH 11/19] adjusted I18N to be consistent with intl message formatting --- docs/guide/i18n.md | 5 ++ framework/yii/i18n/I18N.php | 29 ++++++---- tests/unit/data/i18n/messages/de_DE/test.php | 9 +++ tests/unit/data/i18n/messages/en_US/test.php | 7 +++ tests/unit/framework/i18n/I18NTest.php | 65 ++++++++++++++++++++++ tests/unit/framework/i18n/MessageFormatterTest.php | 19 +++++++ 6 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 tests/unit/data/i18n/messages/de_DE/test.php create mode 100644 tests/unit/data/i18n/messages/en_US/test.php create mode 100644 tests/unit/framework/i18n/I18NTest.php diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index 2e17ce0..9d12f65 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -53,6 +53,9 @@ TBD: https://github.com/yiisoft/yii2/issues/930 ### Named placeholders +You can add parameters to a translation message that will be substituted with the corresponding value after translation. +The format for this is to use curly brackets around the parameter name as you can see in the following example: + ```php $username = 'Alexander'; echo \Yii::t('app', 'Hello, {username}!', array( @@ -60,6 +63,8 @@ echo \Yii::t('app', 'Hello, {username}!', array( )); ``` +Note that the parameter assignment is without the brackets. + ### Positional placeholders ```php diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index a96f081..3c32609 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -73,24 +73,31 @@ class I18N extends Component public function translate($category, $message, $params, $language) { $message = $this->getMessageSource($category)->translate($category, $message, $language); - $params = (array)$params; + if (empty($params)) { + return $message; + } + $params = (array)$params; if (class_exists('MessageFormatter', false) && preg_match('~{\s*[\d\w]+\s*,~u', $message)) { $formatter = new MessageFormatter($language, $message); if ($formatter === null) { - \Yii::$app->getLog()->log("$language message from category $category failed. Message is: $message.", Logger::LEVEL_WARNING, 'application'); - } - $result = $formatter->format($params); - if ($result === false) { - $errorMessage = $formatter->getErrorMessage(); - \Yii::$app->getLog()->log("$language message from category $category failed with error: $errorMessage. Message is: $message.", Logger::LEVEL_WARNING, 'application'); - } - else { - return $result; + \Yii::$app->getLog()->log("$language message from category $category is invalid. Message is: $message.", Logger::LEVEL_WARNING, 'application'); + } else { + $result = $formatter->format($params); + if ($result === false) { + $errorMessage = $formatter->getErrorMessage(); + \Yii::$app->getLog()->log("$language message from category $category failed with error: $errorMessage. Message is: $message.", Logger::LEVEL_WARNING, 'application'); + } else { + return $result; + } } } - return empty($params) ? $message : strtr($message, $params); + $p = array(); + foreach($params as $name => $value) { + $p['{' . $name . '}'] = $value; + } + return strtr($message, $p); } /** diff --git a/tests/unit/data/i18n/messages/de_DE/test.php b/tests/unit/data/i18n/messages/de_DE/test.php new file mode 100644 index 0000000..9cb1369 --- /dev/null +++ b/tests/unit/data/i18n/messages/de_DE/test.php @@ -0,0 +1,9 @@ + 'Der Hund rennt schnell.', + 'His speed is about {n} km/h.' => 'Seine Geschwindigkeit beträgt {n} km/h.', + 'His name is {name} and his speed is about {n, number} km/h.' => 'Er heißt {name} und ist {n, number} km/h schnell.', +); \ No newline at end of file diff --git a/tests/unit/data/i18n/messages/en_US/test.php b/tests/unit/data/i18n/messages/en_US/test.php new file mode 100644 index 0000000..dcd3bc9 --- /dev/null +++ b/tests/unit/data/i18n/messages/en_US/test.php @@ -0,0 +1,7 @@ + 'Der Hund rennt schell.', +); \ No newline at end of file diff --git a/tests/unit/framework/i18n/I18NTest.php b/tests/unit/framework/i18n/I18NTest.php new file mode 100644 index 0000000..e46b8cd --- /dev/null +++ b/tests/unit/framework/i18n/I18NTest.php @@ -0,0 +1,65 @@ + + * @since 2.0 + * @group i18n + */ +class I18NTest extends TestCase +{ + /** + * @var I18N + */ + public $i18n; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->i18n = new I18N(array( + 'translations' => array( + 'test' => new PhpMessageSource(array( + 'basePath' => '@yiiunit/data/i18n/messages', + )) + ) + )); + } + + public function testTranslate() + { + $msg = 'The dog runs fast.'; + $this->assertEquals('The dog runs fast.', $this->i18n->translate('test', $msg, array(), 'en_US')); + $this->assertEquals('Der Hund rennt schnell.', $this->i18n->translate('test', $msg, array(), 'de_DE')); + } + + public function testTranslateParams() + { + $msg = 'His speed is about {n} km/h.'; + $params = array( + 'n' => 42, + ); + $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en_US')); + $this->assertEquals('Seine Geschwindigkeit beträgt 42 km/h.', $this->i18n->translate('test', $msg, $params, 'de_DE')); + + $msg = 'His name is {name} and his speed is about {n, number} km/h.'; + $params = array( + 'n' => 42, + 'name' => 'DA VINCI', // http://petrix.com/dognames/d.html + ); + $this->assertEquals('His name is DA VINCI and his speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en_US')); + $this->assertEquals('Er heißt DA VINCI und ist 42 km/h schnell.', $this->i18n->translate('test', $msg, $params, 'de_DE')); + } + +} \ No newline at end of file diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index d9ae728..e65e3e9 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -140,4 +140,23 @@ _MSG_ $this->assertEquals($expected, $result); } + + /** + * when instantiating a MessageFormatter with invalid pattern it should be null + */ + public function testNullConstructor() + { + $this->assertNull(new MessageFormatter('en_US', '')); + } + + public function testNoParams() + { + $pattern = '{'.self::SUBJECT.'} is '.self::N; + $result = MessageFormatter::formatMessage('en_US', $pattern, array()); + $this->assertEquals($pattern, $result); + + $formatter = new MessageFormatter('en_US', $pattern); + $result = $formatter->format(array()); + $this->assertEquals($pattern, $result); + } } \ No newline at end of file From 5d6bad5faab6087e4feff014ad3783cafe368028 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 20:59:54 +0200 Subject: [PATCH 12/19] fixed I18N handling of special param values and broken message tags --- framework/yii/i18n/I18N.php | 23 +++++++++++----------- .../framework/i18n/GettextMessageSourceTest.php | 2 +- tests/unit/framework/i18n/I18NTest.php | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index 3c32609..625ba5c 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -10,7 +10,6 @@ namespace yii\i18n; use Yii; use yii\base\Component; use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; use yii\log\Logger; /** @@ -73,23 +72,25 @@ class I18N extends Component public function translate($category, $message, $params, $language) { $message = $this->getMessageSource($category)->translate($category, $message, $language); - if (empty($params)) { + + $params = (array)$params; + if ($params === array()) { return $message; } - $params = (array)$params; if (class_exists('MessageFormatter', false) && preg_match('~{\s*[\d\w]+\s*,~u', $message)) { $formatter = new MessageFormatter($language, $message); if ($formatter === null) { - \Yii::$app->getLog()->log("$language message from category $category is invalid. Message is: $message.", Logger::LEVEL_WARNING, 'application'); + Yii::warning("$language message from category $category is invalid. Message is: $message."); + return $message; + } + $result = $formatter->format($params); + if ($result === false) { + $errorMessage = $formatter->getErrorMessage(); + Yii::warning("$language message from category $category failed with error: $errorMessage. Message is: $message."); + return $message; } else { - $result = $formatter->format($params); - if ($result === false) { - $errorMessage = $formatter->getErrorMessage(); - \Yii::$app->getLog()->log("$language message from category $category failed with error: $errorMessage. Message is: $message.", Logger::LEVEL_WARNING, 'application'); - } else { - return $result; - } + return $result; } } diff --git a/tests/unit/framework/i18n/GettextMessageSourceTest.php b/tests/unit/framework/i18n/GettextMessageSourceTest.php index d039629..5c10ff2 100644 --- a/tests/unit/framework/i18n/GettextMessageSourceTest.php +++ b/tests/unit/framework/i18n/GettextMessageSourceTest.php @@ -12,6 +12,6 @@ class GettextMessageSourceTest extends TestCase { public function testLoadMessages() { - $this->markTestSkipped(); + $this->markTestIncomplete(); } } diff --git a/tests/unit/framework/i18n/I18NTest.php b/tests/unit/framework/i18n/I18NTest.php index e46b8cd..ac19c02 100644 --- a/tests/unit/framework/i18n/I18NTest.php +++ b/tests/unit/framework/i18n/I18NTest.php @@ -7,6 +7,7 @@ namespace yiiunit\framework\i18n; +use yii\base\Model; use yii\i18n\I18N; use yii\i18n\MessageFormatter; use yii\i18n\PhpMessageSource; @@ -62,4 +63,23 @@ class I18NTest extends TestCase $this->assertEquals('Er heißt DA VINCI und ist 42 km/h schnell.', $this->i18n->translate('test', $msg, $params, 'de_DE')); } + public function testSpecialParams() + { + $msg = 'His speed is about {0} km/h.'; + + $this->assertEquals('His speed is about 0 km/h.', $this->i18n->translate('test', $msg, 0, 'en_US')); + $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, 42, 'en_US')); + $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, null, 'en_US')); + $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, array(), 'en_US')); + + $msg = 'His name is {name} and he is {age} years old.'; + $model = new ParamModel(); + $this->assertEquals('His name is peer and he is 5 years old.', $this->i18n->translate('test', $msg, $model, 'en_US')); + } +} + +class ParamModel extends Model +{ + public $name = 'peer'; + public $age = 5; } \ No newline at end of file From 6507e2e31619ab7ef2c7df508373f723e8f44cf9 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 22:03:26 +0200 Subject: [PATCH 13/19] applied new style of Yii::t params to all occurences --- framework/yii/console/Application.php | 3 ++- framework/yii/console/Controller.php | 4 ++-- framework/yii/console/controllers/HelpController.php | 4 ++-- framework/yii/web/Controller.php | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/framework/yii/console/Application.php b/framework/yii/console/Application.php index bbdd4e1..9ca7393 100644 --- a/framework/yii/console/Application.php +++ b/framework/yii/console/Application.php @@ -9,6 +9,7 @@ namespace yii\console; +use Yii; use yii\base\InvalidRouteException; /** @@ -129,7 +130,7 @@ class Application extends \yii\base\Application try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - throw new Exception(\Yii::t('yii', 'Unknown command "{command}".', array('{command}' => $route)), 0, $e); + throw new Exception(Yii::t('yii', 'Unknown command "{command}".', array('command' => $route)), 0, $e); } } diff --git a/framework/yii/console/Controller.php b/framework/yii/console/Controller.php index fd6d0de..73b74f8 100644 --- a/framework/yii/console/Controller.php +++ b/framework/yii/console/Controller.php @@ -99,7 +99,7 @@ class Controller extends \yii\base\Controller $args[] = $value; } else { throw new Exception(Yii::t('yii', 'Unknown option: --{name}', array( - '{name}' => $name, + 'name' => $name, ))); } } @@ -125,7 +125,7 @@ class Controller extends \yii\base\Controller if (!empty($missing)) { throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', array( - '{params}' => implode(', ', $missing), + 'params' => implode(', ', $missing), ))); } diff --git a/framework/yii/console/controllers/HelpController.php b/framework/yii/console/controllers/HelpController.php index f0e72cd..678d9f9 100644 --- a/framework/yii/console/controllers/HelpController.php +++ b/framework/yii/console/controllers/HelpController.php @@ -58,7 +58,7 @@ class HelpController extends Controller $result = Yii::$app->createController($command); if ($result === false) { throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', array( - '{command}' => $this->ansiFormat($command, Console::FG_YELLOW), + 'command' => $this->ansiFormat($command, Console::FG_YELLOW), ))); } @@ -243,7 +243,7 @@ class HelpController extends Controller $action = $controller->createAction($actionID); if ($action === null) { throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', array( - '{command}' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), + 'command' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), ))); } if ($action instanceof InlineAction) { diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php index 6b8afa4..87edf82 100644 --- a/framework/yii/web/Controller.php +++ b/framework/yii/web/Controller.php @@ -60,7 +60,7 @@ class Controller extends \yii\base\Controller if (!empty($missing)) { throw new HttpException(400, Yii::t('yii', 'Missing required parameters: {params}', array( - '{params}' => implode(', ', $missing), + 'params' => implode(', ', $missing), ))); } From 677893eb79f5dfc639e01c6412a28991bdba8d53 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 16 Oct 2013 22:13:28 +0200 Subject: [PATCH 14/19] changed plural format for BaseListView --- framework/yii/widgets/BaseListView.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/yii/widgets/BaseListView.php b/framework/yii/widgets/BaseListView.php index 8e827d6..d90ac89 100644 --- a/framework/yii/widgets/BaseListView.php +++ b/framework/yii/widgets/BaseListView.php @@ -139,13 +139,13 @@ abstract class BaseListView extends Widget $page = $pagination->getPage() + 1; $pageCount = $pagination->pageCount; if (($summaryContent = $this->summary) === null) { - $summaryContent = '
' . Yii::t('yii', 'Showing {begin}-{end} of {totalCount} item.|Showing {begin}-{end} of {totalCount} items.', $totalCount) . '
'; + $summaryContent = '
' . Yii::t('yii', 'Showing {begin}-{end} of {totalCount} {0, plural, =1{item} other{items}}.', $totalCount) . '
'; } } else { $begin = $page = $pageCount = 1; $end = $totalCount = $count; if (($summaryContent = $this->summary) === null) { - $summaryContent = '
' . Yii::t('yii', 'Total 1 item.|Total {count} items.', $count) . '
'; + $summaryContent = '
' . Yii::t('yii', 'Total {count} {0, plural, =1{item} other{items}}.', $count) . '
'; } } return strtr($summaryContent, array( From cdb9fbf679405f32eb421d2209e64bd960a0a858 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 17 Oct 2013 00:36:48 +0400 Subject: [PATCH 15/19] Fixes PHP 5.5 weird placeholder replacememt in case no arguments are provided at all (https://bugs.php.net/bug.php?id=65920). --- framework/yii/i18n/MessageFormatter.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index c412420..464f3f7 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -13,6 +13,7 @@ namespace yii\i18n; * - Accepts named arguments and mixed numeric and named arguments. * - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be * substituted. + * - Fixes PHP 5.5 weird placeholder replacememt in case no arguments are provided at all (https://bugs.php.net/bug.php?id=65920). * * @author Alexander Makarov * @author Carsten Brandt @@ -29,6 +30,10 @@ class MessageFormatter extends \MessageFormatter */ public function format($args) { + if ($args === array()) { + return $this->getPattern(); + } + if (self::needFix()) { $pattern = self::replaceNamedArguments($this->getPattern(), $args); $this->setPattern($pattern); @@ -48,6 +53,10 @@ class MessageFormatter extends \MessageFormatter */ public static function formatMessage($locale, $pattern, $args) { + if ($args === array()) { + return $pattern; + } + if (self::needFix()) { $pattern = self::replaceNamedArguments($pattern, $args); $args = array_values($args); From da8f17955fdfccbfa863231655a53f1db29015a4 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 17 Oct 2013 00:41:40 +0400 Subject: [PATCH 16/19] Removed intl version check since it's very inconsistent and it seems bundled versions are always the same ones for the same PHP versions --- framework/yii/i18n/MessageFormatter.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 464f3f7..8479425 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -123,11 +123,7 @@ class MessageFormatter extends \MessageFormatter */ private static function needFix() { - return ( - !defined('INTL_ICU_VERSION') || - version_compare(INTL_ICU_VERSION, '48.0.0', '<') || - version_compare(PHP_VERSION, '5.5.0', '<') - ); + return version_compare(PHP_VERSION, '5.5.0', '<'); } } \ No newline at end of file From bf7a0842249e04e33063c4fa21c9451bb7fa4eb9 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 17 Oct 2013 01:04:42 +0400 Subject: [PATCH 17/19] removed needFix method --- framework/yii/i18n/MessageFormatter.php | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 8479425..eb85773 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -34,7 +34,7 @@ class MessageFormatter extends \MessageFormatter return $this->getPattern(); } - if (self::needFix()) { + if (version_compare(PHP_VERSION, '5.5.0', '<')) { $pattern = self::replaceNamedArguments($this->getPattern(), $args); $this->setPattern($pattern); $args = array_values($args); @@ -57,7 +57,7 @@ class MessageFormatter extends \MessageFormatter return $pattern; } - if (self::needFix()) { + if (version_compare(PHP_VERSION, '5.5.0', '<')) { $pattern = self::replaceNamedArguments($pattern, $args); $args = array_values($args); } @@ -114,16 +114,5 @@ class MessageFormatter extends \MessageFormatter } return $pattern; } - - /** - * Checks if fix should be applied - * - * @see http://php.net/manual/en/migration55.changed-functions.php - * @return boolean if fix should be applied - */ - private static function needFix() - { - return version_compare(PHP_VERSION, '5.5.0', '<'); - } } \ No newline at end of file From 71e2dcc6d5486a8fbc5ac3a416bec145b857d873 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 17 Oct 2013 01:53:32 +0400 Subject: [PATCH 18/19] Better MessageFormatter tests --- tests/unit/framework/i18n/MessageFormatterTest.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index e65e3e9..caffffe 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -142,11 +142,18 @@ _MSG_ } /** - * when instantiating a MessageFormatter with invalid pattern it should be null + * When instantiating a MessageFormatter with invalid pattern it should be null with default settings. + * It will be IntlException if intl.use_exceptions=1 and PHP 5.5 or newer or an error if intl.error_level is not 0. */ public function testNullConstructor() { - $this->assertNull(new MessageFormatter('en_US', '')); + if(ini_get('intl.use_exceptions')) { + $this->setExpectedException('IntlException'); + } + + if (!ini_get('intl.error_level') || ini_get('intl.use_exceptions')) { + $this->assertNull(new MessageFormatter('en_US', '')); + } } public function testNoParams() From 6b65ef58826a520c691b520b1b02135d6155b4e7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 17 Oct 2013 02:15:46 +0400 Subject: [PATCH 19/19] Added intl check to tests, better error reporting, credited Aura.Intl for the most complex test pattern --- tests/unit/framework/i18n/MessageFormatterTest.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index caffffe..2465409 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -22,6 +22,13 @@ class MessageFormatterTest extends TestCase const SUBJECT = 'сабж'; const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; + protected function setUp() + { + if (!extension_loaded("intl")) { + $this->markTestSkipped("intl not installed. Skipping."); + } + } + public function patterns() { return array( @@ -34,6 +41,7 @@ class MessageFormatterTest extends TestCase ) ), + // This one was provided by Aura.Intl. Thanks! array(<<<_MSG_ {gender_of_host, select, female {{num_guests, plural, offset:1 @@ -117,7 +125,7 @@ _MSG_ public function testNamedArgumentsStatic($pattern, $expected, $args) { $result = MessageFormatter::formatMessage('en_US', $pattern, $args); - $this->assertEquals($expected, $result); + $this->assertEquals($expected, $result, intl_get_error_message()); } /** @@ -127,7 +135,7 @@ _MSG_ { $formatter = new MessageFormatter('en_US', $pattern); $result = $formatter->format($args); - $this->assertEquals($expected, $result); + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } public function testInsufficientArguments() @@ -138,7 +146,7 @@ _MSG_ self::N => self::N_VALUE, )); - $this->assertEquals($expected, $result); + $this->assertEquals($expected, $result, intl_get_error_message()); } /** @@ -160,10 +168,10 @@ _MSG_ { $pattern = '{'.self::SUBJECT.'} is '.self::N; $result = MessageFormatter::formatMessage('en_US', $pattern, array()); - $this->assertEquals($pattern, $result); + $this->assertEquals($pattern, $result, intl_get_error_message()); $formatter = new MessageFormatter('en_US', $pattern); $result = $formatter->format(array()); - $this->assertEquals($pattern, $result); + $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } } \ No newline at end of file