From dafbeda301bb1eb85eec55a468fbd043efadd348 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 28 Sep 2013 22:30:16 +0400 Subject: [PATCH] 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()