From c629ad776abe36501fd6c034cff8fc115fdc7f59 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 1 Apr 2013 17:59:14 -0400 Subject: [PATCH] Finished plural forms. --- build/build | 0 build/controllers/LocaleController.php | 17 ++++-- framework/YiiBase.php | 39 ++++++++------ framework/base/ErrorHandler.php | 10 ++-- framework/i18n/I18N.php | 97 ++++++++++++++++++++++++++++++---- framework/i18n/data/plurals.php | 66 +++++++++++------------ 6 files changed, 162 insertions(+), 67 deletions(-) mode change 100644 => 100755 build/build diff --git a/build/build b/build/build old mode 100644 new mode 100755 diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php index 51de6a0..00de787 100644 --- a/build/controllers/LocaleController.php +++ b/build/controllers/LocaleController.php @@ -42,9 +42,10 @@ class LocaleController extends Controller '/\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|within)\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not in, not within + '/^(.*?)\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 - '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3&&fmod($1,1)==0)', //in ); foreach ($xml->plurals->pluralRules as $node) { $attributes = $node->attributes(); @@ -59,7 +60,15 @@ class LocaleController extends Controller $expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and); $expr_or[$key_or] = implode('&&', $expr_and); } - $rules[] = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { + if ($matches[2] - $matches[1] <= 2) { + return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; + } else { + return $matches[0]; + } + }, $expr); + } foreach ($locales as $locale) { $allRules[$locale] = $rules; @@ -70,7 +79,7 @@ class LocaleController extends Controller $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))))', + 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)) { diff --git a/framework/YiiBase.php b/framework/YiiBase.php index e4a01b4..e81a288 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -192,6 +192,7 @@ class YiiBase * @param boolean $throwException whether to throw an exception if the given alias is invalid. * If this is false and an invalid alias is given, false will be returned by this method. * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. + * @throws InvalidParamException if the alias is invalid while $throwException is true. * @see setAlias */ public static function getAlias($alias, $throwException = true) @@ -231,6 +232,7 @@ class YiiBase * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias]]. + * * @throws Exception if $path is an invalid alias * @see getAlias */ @@ -504,21 +506,28 @@ class YiiBase /** * Translates a message to the specified language. - * This method supports choice format (see {@link CChoiceFormat}), - * i.e., the message returned will be chosen from a few candidates according to the given - * number value. This feature is mainly used to solve plural format issue in case - * a message has different plural forms in some languages. - * @param string $message the original message - * @param array $params parameters to be applied to the message using strtr. - * The first parameter can be a number without key. - * And in this case, the method will call {@link CChoiceFormat::format} to choose - * an appropriate message translation. - * You can pass parameter for {@link CChoiceFormat::format} - * or plural forms format without wrapping it with array. - * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. - * @return string the translated message - * @see CMessageSource - * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + * + * The translation will be conducted according to the message category and the target language. + * To specify the category of the message, prefix the message with the category name and separate it + * with "|". For example, "app|hello world". If the category is not specified, the default category "app" + * will be used. The actual message translation is done by a [[\yii\i18n\MessageSource|message source]]. + * + * In case when a translated message has different plural forms (separated by "|"), this method + * will also attempt to choose an appropriate one according to a given numeric value which is + * specified as the first parameter (indexed by 0) in `$params`. + * + * For example, if a translated message is "I have an apple.|I have {n} apples.", and the first + * parameter is 2, the message returned will be "I have 2 apples.". Note that the placeholder "{n}" + * will be replaced with the given number. + * + * For more details on how plural rules are applied, please refer to: + * [[http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html]] + * + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. */ public static function t($message, $params = array(), $language = null) { diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index f71b8c8..a2f372c 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -16,8 +16,6 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -use yii\helpers\VarDumper; - class ErrorHandler extends Component { /** @@ -63,10 +61,10 @@ class ErrorHandler extends Component $this->clearOutput(); } - $this->render($exception); + $this->renderException($exception); } - protected function render($exception) + protected function renderException($exception) { if ($this->errorAction !== null) { \Yii::$app->runAction($this->errorAction); @@ -84,7 +82,7 @@ class ErrorHandler extends Component } else { $viewName = $this->exceptionView; } - echo $view->render($viewName, array( + echo $view->renderFile($viewName, array( 'exception' => $exception, ), $this); } @@ -255,7 +253,7 @@ class ErrorHandler extends Component { $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($name, array( + echo $view->renderFile($name, array( 'exception' => $exception, ), $this); } diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 0409da3..8667abc 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -1,11 +1,23 @@ + * @since 2.0 + */ class I18N extends Component { /** @@ -13,11 +25,36 @@ class I18N extends Component * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. + * + * This property may be modified on the fly by extensions who want to have their own message sources + * registered under their own namespaces. + * + * The category "yii" and "app" are always defined. The former refers to the messages used in the Yii core + * framework code, while the latter refers to the default message category for custom application code. + * By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are + * stored under "@yii/messages" and "@app/messages", respectively. + * + * You may override the configuration of both categories. */ public $translations; + /** + * @var string the path or path alias of the file that contains the plural rules. + * By default, this refers to a file shipped with the Yii distribution. The file is obtained + * by converting from the data file in the CLDR project. + * + * If the default rule file does not contain the expected rules, you may copy and modify it + * for your application, and then configure this property to point to your modified copy. + * + * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + */ + public $pluralRuleFile = '@yii/i18n/data/plurals.php'; + /** + * Initializes the component by configuring the default message categories. + */ public function init() { + parent::init(); if (!isset($this->translations['yii'])) { $this->translations['yii'] = array( 'class' => 'yii\i18n\PhpMessageSource', @@ -34,6 +71,16 @@ class I18N extends Component } } + /** + * Translates a message to the specified language. + * If the first parameter in `$params` is a number and it is indexed by 0, appropriate plural rules + * will be applied to the translated message. + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. + */ public function translate($message, $params = array(), $language = null) { if ($language === null) { @@ -55,7 +102,7 @@ class I18N extends Component } if (isset($params[0])) { - $message = $this->getPluralForm($message, $params[0], $language); + $message = $this->applyPluralRules($message, $params[0], $language); if (!isset($params['{n}'])) { $params['{n}'] = $params[0]; } @@ -65,6 +112,12 @@ class I18N extends Component return $params === array() ? $message : strtr($message, $params); } + /** + * Returns the message source for the given category. + * @param string $category the category name. + * @return MessageSource the message source for the given category. + * @throws InvalidConfigException if there is no message source available for the specified category. + */ public function getMessageSource($category) { if (isset($this->translations[$category])) { @@ -85,18 +138,21 @@ class I18N extends Component } } - public function getLocale($language) - { - - } - - protected function getPluralForm($message, $number, $language) + /** + * 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->getLocale($language)->getPluralRules(); + + $rules = $this->getPluralRules($language); foreach ($rules as $i => $rule) { if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; @@ -106,6 +162,29 @@ class I18N extends Component 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; + } + $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 @@ -114,6 +193,6 @@ class I18N extends Component */ protected function evaluate($expression, $n) { - return @eval("return $expression;"); + return eval("return $expression;"); } } diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php index 3ed5619..b82fec0 100644 --- a/framework/i18n/data/plurals.php +++ b/framework/i18n/data/plurals.php @@ -21,8 +21,8 @@ return array ( 0 => '$n==0', 1 => '$n==1', 2 => '$n==2', - 3 => '(fmod($n,100)>=3&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', - 4 => '(fmod($n,100)>=11&&fmod($n,100)<=99&&fmod(fmod($n,100),1)==0)', + 3 => 'in_array(fmod($n,100),range(3,10))', + 4 => 'in_array(fmod($n,100),range(11,99))', ), 'asa' => array ( @@ -494,93 +494,93 @@ return array ( array ( 0 => '$n==1', 1 => '$n==2', - 2 => '($n>=3&&$n<=6&&fmod($n,1)==0)', - 3 => '($n>=7&&$n<=10&&fmod($n,1)==0)', + 2 => 'in_array($n,range(3,6))', + 3 => 'in_array($n,range(7,10))', ), 'ro' => array ( 0 => '$n==1', - 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', ), 'mo' => array ( 0 => '$n==1', - 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', ), 'lt' => array ( - 0 => 'fmod($n,10)==1&&(fmod($n,100)<11||fmod($n,100)>19)', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<11||fmod($n,100)>19)', + 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 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'bs' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'hr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'ru' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'sh' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'sr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'uk' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'cs' => array ( 0 => '$n==1', - 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + 1 => 'in_array($n,array(2,3,4))', ), 'sk' => array ( 0 => '$n==1', - 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + 1 => 'in_array($n,array(2,3,4))', ), 'pl' => array ( 0 => '$n==1', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => '$n!=1&&(fmod($n,10)>=0&&fmod($n,10)<=1&&fmod(fmod($n,10),1)==0)||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=12&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),array(12,13,14))', ), 'sl' => array ( 0 => 'fmod($n,100)==1', 1 => 'fmod($n,100)==2', - 2 => '(fmod($n,100)>=3&&fmod($n,100)<=4&&fmod(fmod($n,100),1)==0)', + 2 => 'in_array(fmod($n,100),array(3,4))', ), 'mt' => array ( 0 => '$n==1', - 1 => '$n==0||(fmod($n,100)>=2&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', - 2 => '(fmod($n,100)>=11&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||in_array(fmod($n,100),range(2,10))', + 2 => 'in_array(fmod($n,100),range(11,19))', ), 'mk' => array ( @@ -602,13 +602,13 @@ return array ( 'shi' => array ( 0 => '($n>=0&&$n<=1)', - 1 => '($n>=2&&$n<=10&&fmod($n,1)==0)', + 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))))', + 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' => @@ -618,10 +618,10 @@ return array ( ), 'tzm' => array ( - 0 => '($n==0||$n==1)||($n>=11&&$n<=99&&fmod($n,1)==0)', + 0 => '($n==0||$n==1)||in_array($n,range(11,99))', ), 'gv' => array ( - 0 => '(fmod($n,10)>=1&&fmod($n,10)<=2&&fmod(fmod($n,10),1)==0)||fmod($n,20)==0', + 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', ), ); \ No newline at end of file