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 " **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 +------------------------- + +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 + +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( + 'username' => $username, +)); +``` + +Note that the parameter assignment is without the brackets. + +### Positional placeholders + +```php +$sum = 42; +echo \Yii::t('app', 'Balance: {0}', $sum); +``` + +> **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 +------------------------------- + +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 +---------- + +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/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/i18n/I18N.php b/framework/yii/i18n/I18N.php index 171b5d4..625ba5c 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -10,7 +10,7 @@ namespace yii\i18n; use Yii; use yii\base\Component; use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; +use yii\log\Logger; /** * I18N provides features related with internationalization (I18N) and localization (L10N). @@ -37,17 +37,6 @@ class I18N extends Component * 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. @@ -73,8 +62,7 @@ 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 $category the message category. * @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. @@ -85,19 +73,32 @@ class I18N extends Component { $message = $this->getMessageSource($category)->translate($category, $message, $language); - if (!is_array($params)) { - $params = array($params); + $params = (array)$params; + if ($params === array()) { + return $message; } - 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::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 { + return $result; } - unset($params[0]); } - return empty($params) ? $message : strtr($message, $params); + $p = array(); + foreach($params as $name => $value) { + $p['{' . $name . '}'] = $value; + } + return strtr($message, $p); } /** @@ -125,62 +126,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..eb85773 --- /dev/null +++ b/framework/yii/i18n/MessageFormatter.php @@ -0,0 +1,118 @@ + + * @author Carsten Brandt + * @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) + { + if ($args === array()) { + return $this->getPattern(); + } + + if (version_compare(PHP_VERSION, '5.5.0', '<')) { + $pattern = self::replaceNamedArguments($this->getPattern(), $args); + $this->setPattern($pattern); + $args = array_values($args); + } + return parent::format($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) + { + if ($args === array()) { + return $pattern; + } + + if (version_compare(PHP_VERSION, '5.5.0', '<')) { + $pattern = self::replaceNamedArguments($pattern, $args); + $args = array_values($args); + } + return parent::formatMessage($locale, $pattern, $args); + } + + /** + * 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. + * @return string The pattern string with placeholders replaced. + */ + private static function replaceNamedArguments($pattern, $args) + { + $map = array_flip(array_keys($args)); + + // parsing pattern based on ICU grammar: + // 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('~^(\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++; + // 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]; + $d += 1 - substr_count($parts[$i], '}'); + } + return $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/framework/yii/web/Controller.php b/framework/yii/web/Controller.php index 7509186..a6eaf3f 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), ))); } 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( 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/YiiBaseTest.php b/tests/unit/framework/BaseYiiTest.php similarity index 96% rename from tests/unit/framework/YiiBaseTest.php rename to tests/unit/framework/BaseYiiTest.php index 72b5f24..a04de99 100644 --- a/tests/unit/framework/YiiBaseTest.php +++ b/tests/unit/framework/BaseYiiTest.php @@ -5,10 +5,10 @@ use Yii; use yiiunit\TestCase; /** - * YiiBaseTest + * BaseYiiTest * @group base */ -class YiiBaseTest extends TestCase +class BaseYiiTest extends TestCase { public $aliases; 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 new file mode 100644 index 0000000..ac19c02 --- /dev/null +++ b/tests/unit/framework/i18n/I18NTest.php @@ -0,0 +1,85 @@ + + * @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')); + } + + 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 diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php new file mode 100644 index 0000000..2465409 --- /dev/null +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -0,0 +1,177 @@ + + * @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'; + + protected function setUp() + { + if (!extension_loaded("intl")) { + $this->markTestSkipped("intl not installed. Skipping."); + } + } + + public function patterns() + { + 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, + ) + ), + + // This one was provided by Aura.Intl. Thanks! + array(<<<_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_ + , + 'ralph invites beep and 3 other people to his party.', + array( + 'gender_of_host' => 'male', + 'num_guests' => 4, + 'host' => 'ralph', + 'guest' => 'beep' + ) + ), + + 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 + 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 + 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 + 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, intl_get_error_message()); + } + + /** + * @dataProvider patterns + */ + public function testNamedArgumentsObject($pattern, $expected, $args) + { + $formatter = new MessageFormatter('en_US', $pattern); + $result = $formatter->format($args); + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); + } + + 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, intl_get_error_message()); + } + + /** + * 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() + { + 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() + { + $pattern = '{'.self::SUBJECT.'} is '.self::N; + $result = MessageFormatter::formatMessage('en_US', $pattern, array()); + $this->assertEquals($pattern, $result, intl_get_error_message()); + + $formatter = new MessageFormatter('en_US', $pattern); + $result = $formatter->format(array()); + $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); + } +} \ No newline at end of file