Browse Source

Fixes #11397: `yii\i18n\MessageFormatter` polyfills and `yii\i18n\MessageFormatter::parse()` method were removed resulting in performance boost. See UPGRADE for compatibility notes

tags/3.0.0-alpha1
Alexander Makarov 6 years ago committed by GitHub
parent
commit
32efb09902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      docs/guide/tutorial-i18n.md
  2. 1
      framework/CHANGELOG.md
  3. 5
      framework/UPGRADE.md
  4. 146
      framework/i18n/MessageFormatter.php
  5. 3
      framework/i18n/MessageSource.php
  6. 12
      tests/framework/i18n/I18NTest.php
  7. 93
      tests/framework/i18n/MessageFormatterTest.php

4
docs/guide/tutorial-i18n.md

@ -193,12 +193,12 @@ echo \Yii::t('app', 'Hello, {username}!', [
While translating a message containing placeholders, you should leave the placeholders as is. This is because the placeholders
will be replaced with the actual values when you call `Yii::t()` to translate a message.
You can use either *named placeholders* or *positional placeholders*, but not both, in a single message.
The previous example shows how you can use named placeholders. That is, each placeholder is written in the format of
`{name}`, and you provide an associative array whose keys are the placeholder names
(without the curly brackets) and whose values are the corresponding values placeholder to be replaced with.
> Note: Some characters such as `.`, `-` or `=` are not allowed in placeholder names. Use `_` instead.
Positional placeholders use zero-based integer sequence as names which are replaced by the provided values
according to their positions in the call of `Yii::t()`. In the following example, the positional placeholders
`{0}`, `{1}` and `{2}` will be replaced by the values of `$price`, `$count` and `$subtotal`, respectively.

1
framework/CHANGELOG.md

@ -50,6 +50,7 @@ Yii Framework 2 Change Log
- Chg #15811: Fixed issue with additional parameters on `yii\base\View::renderDynamic()` while parameters contains single quote introduced in #12938 (xicond)
- Enh #16054: Callback execution with mutex synchronization (zhuravljov)
- Enh #16126: Allows to configure `Connection::dsn` by config array (leandrogehlen)
- Chg #11397: `yii\i18n\MessageFormatter` polyfills and `yii\i18n\MessageFormatter::parse()` method were removed resulting in performance boost. See UPGRADE for compatibility notes (samdark)
2.0.14.2 under development
------------------------

5
framework/UPGRADE.md

@ -177,6 +177,11 @@ Upgrade from Yii 2.0.x
this package manually for your project.
* `yii\BaseYii::powered()` method has been removed. Please add "Powered by Yii" link either right into HTML or using
`yii\helpers\Html::a()`.
* `yii\i18n\MessageFormatter` no longer supports parameter names with `.`, `-`, `=` and other symbols that are used in
pattern syntax following directly how it works in intl/ICU. If you use such parameters names, replace special
symbols with `_`.
* `yii\i18n\MessageFormatter::parse()` method was removed. If you have a rare case where it's used copy-paste it from
2.0 branch to your project.
Upgrade from Yii 2.0.15

146
framework/i18n/MessageFormatter.php

@ -18,13 +18,13 @@ use yii\base\NotSupportedException;
*
* The following enhancements are provided:
*
* - It accepts named arguments and mixed numeric and named arguments.
* - Issues no error if format is invalid returning false and holding error for retrieval via `getErrorCode()`
* and `getErrorMessage()` methods.
* - 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 replacement in case no arguments are provided at all (https://bugs.php.net/bug.php?id=65920).
* substituted. It prevents translation mistakes to crash whole page.
* - Offers limited support for message formatting in case PHP intl extension is not installed.
* However it is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if you want
* to use MessageFormatter features.
* However it is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if
* you want to use MessageFormatter features.
*
* The fallback implementation only supports the following message formats:
* - plural formatting for english ('one' and 'other' selectors)
@ -94,22 +94,9 @@ class MessageFormatter extends Component
return $this->fallbackFormat($pattern, $params, $language);
}
// replace named arguments (https://github.com/yiisoft/yii2/issues/9678)
$newParams = [];
$pattern = $this->replaceNamedArguments($pattern, $params, $newParams);
$params = $newParams;
try {
$formatter = new \MessageFormatter($language, $pattern);
if ($formatter === null) {
// formatter may be null in PHP 5.x
$this->_errorCode = intl_get_error_code();
$this->_errorMessage = 'Message pattern is invalid: ' . intl_get_error_message();
return false;
}
} catch (\IntlException $e) {
// IntlException is thrown since PHP 7
$this->_errorCode = $e->getCode();
$this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage();
return false;
@ -127,129 +114,6 @@ class MessageFormatter extends Component
}
/**
* Parses an input string according to an [ICU message format](http://userguide.icu-project.org/formatparse/messages) pattern.
*
* It uses the PHP intl extension's [MessageFormatter::parse()](http://www.php.net/manual/en/messageformatter.parsemessage.php)
* and adds support for named arguments.
* Usage of this method requires PHP intl extension to be installed.
*
* @param string $pattern The pattern to use for parsing the message.
* @param string $message The message to parse, conforming to the pattern.
* @param string $language The locale to use for formatting locale-dependent parts
* @return array|bool An array containing items extracted, or `FALSE` on error.
* @throws \yii\base\NotSupportedException when PHP intl extension is not installed.
*/
public function parse($pattern, $message, $language)
{
$this->_errorCode = 0;
$this->_errorMessage = '';
if (!class_exists('MessageFormatter', false)) {
throw new NotSupportedException('You have to install PHP intl extension to use this feature.');
}
// replace named arguments
if (($tokens = self::tokenizePattern($pattern)) === false) {
$this->_errorCode = -1;
$this->_errorMessage = 'Message pattern is invalid.';
return false;
}
$map = [];
foreach ($tokens as $i => $token) {
if (is_array($token)) {
$param = trim($token[0]);
if (!isset($map[$param])) {
$map[$param] = count($map);
}
$token[0] = $map[$param];
$tokens[$i] = '{' . implode(',', $token) . '}';
}
}
$pattern = implode('', $tokens);
$map = array_flip($map);
$formatter = new \MessageFormatter($language, $pattern);
if ($formatter === null) {
$this->_errorCode = -1;
$this->_errorMessage = 'Message pattern is invalid.';
return false;
}
$result = $formatter->parse($message);
if ($result === false) {
$this->_errorCode = $formatter->getErrorCode();
$this->_errorMessage = $formatter->getErrorMessage();
return false;
}
$values = [];
foreach ($result as $key => $value) {
$values[$map[$key]] = $value;
}
return $values;
}
/**
* Replace named placeholders with numeric placeholders and quote unused.
*
* @param string $pattern The pattern string to replace things into.
* @param array $givenParams The array of values to insert into the format string.
* @param array $resultingParams Modified array of parameters.
* @param array $map
* @return string The pattern string with placeholders replaced.
*/
private function replaceNamedArguments($pattern, $givenParams, &$resultingParams = [], &$map = [])
{
if (($tokens = self::tokenizePattern($pattern)) === false) {
return false;
}
foreach ($tokens as $i => $token) {
if (!is_array($token)) {
continue;
}
$param = trim($token[0]);
if (array_key_exists($param, $givenParams)) {
// if param is given, replace it with a number
if (!isset($map[$param])) {
$map[$param] = count($map);
// make sure only used params are passed to format method
$resultingParams[$map[$param]] = $givenParams[$param];
}
$token[0] = $map[$param];
$quote = '';
} else {
// quote unused token
$quote = "'";
}
$type = isset($token[1]) ? trim($token[1]) : 'none';
// replace plural and select format recursively
if ($type === 'plural' || $type === 'select') {
if (!isset($token[2])) {
return false;
}
if (($subtokens = self::tokenizePattern($token[2])) === false) {
return false;
}
$c = count($subtokens);
for ($k = 0; $k + 1 < $c; $k++) {
if (is_array($subtokens[$k]) || !is_array($subtokens[++$k])) {
return false;
}
$subpattern = $this->replaceNamedArguments(implode(',', $subtokens[$k]), $givenParams, $resultingParams, $map);
$subtokens[$k] = $quote . '{' . $quote . $subpattern . $quote . '}' . $quote;
}
$token[2] = implode('', $subtokens);
}
$tokens[$i] = $quote . '{' . $quote . implode(',', $token) . $quote . '}' . $quote;
}
return implode('', $tokens);
}
/**
* Fallback implementation for MessageFormatter::formatMessage.
* @param string $pattern The pattern string to insert things into.
* @param array $args The array of values to insert into the format string

3
framework/i18n/MessageSource.php

@ -108,7 +108,8 @@ class MessageSource extends Component
}
if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') {
return $this->_messages[$key][$message];
} elseif ($this->hasEventHandlers(self::EVENT_MISSING_TRANSLATION)) {
}
if ($this->hasEventHandlers(self::EVENT_MISSING_TRANSLATION)) {
$event = new MissingTranslationEvent([
'category' => $category,
'message' => $message,

12
tests/framework/i18n/I18NTest.php

@ -273,15 +273,7 @@ class I18NTest extends TestCase
public function testFormatMessageWithNoParam()
{
$message = 'Incorrect password (length must be from {min, number} to {max, number} symbols).';
$this->assertEquals($message, $this->i18n->format($message, ['attribute' => 'password'], 'en'));
}
public function testFormatMessageWithDottedParameters()
{
$message = 'date: {dt.test}';
$this->assertEquals('date: 1510147434', $this->i18n->format($message, ['dt.test' => 1510147434], 'en'));
$message = 'date: {dt.test,date}';
$this->assertEquals('date: Nov 8, 2017', $this->i18n->format($message, ['dt.test' => 1510147434], 'en'));
$expected = 'Incorrect password (length must be from {min} to {max} symbols).';
$this->assertEquals($expected, $this->i18n->format($message, ['attribute' => 'password'], 'en'));
}
}

93
tests/framework/i18n/MessageFormatterTest.php

@ -172,7 +172,7 @@ _MSG_
// formatting a message that contains params but they are not provided.
[
'Incorrect password (length must be from {min, number} to {max, number} symbols).',
'Incorrect password (length must be from {min, number} to {max, number} symbols).',
'Incorrect password (length must be from {min} to {max} symbols).',
['attribute' => 'password'],
],
@ -275,79 +275,6 @@ _MSG_
];
}
public function parsePatterns()
{
return [
[
self::SUBJECT_VALUE . ' is {0, number}', // pattern
self::SUBJECT_VALUE . ' is ' . self::N_VALUE, // expected
[ // params
0 => self::N_VALUE,
],
],
[
self::SUBJECT_VALUE . ' is {' . self::N . ', number}', // pattern
self::SUBJECT_VALUE . ' is ' . self::N_VALUE, // expected
[ // params
self::N => self::N_VALUE,
],
],
[
self::SUBJECT_VALUE . ' is {' . self::N . ', number, integer}', // pattern
self::SUBJECT_VALUE . ' is ' . self::N_VALUE, // expected
[ // params
self::N => self::N_VALUE,
],
],
[
'{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree',
'4,560 monkeys on 123 trees make 37.073 monkeys per tree',
[
0 => 4560,
1 => 123,
2 => 37.073,
],
'en-US',
],
[
'{0,number,integer} Affen auf {1,number,integer} Bäumen sind {2,number} Affen pro Baum',
'4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum',
[
0 => 4560,
1 => 123,
2 => 37.073,
],
'de',
],
[
'{monkeyCount,number,integer} monkeys on {trees,number,integer} trees make {monkeysPerTree,number} monkeys per tree',
'4,560 monkeys on 123 trees make 37.073 monkeys per tree',
[
'monkeyCount' => 4560,
'trees' => 123,
'monkeysPerTree' => 37.073,
],
'en-US',
],
[
'{monkeyCount,number,integer} Affen auf {trees,number,integer} Bäumen sind {monkeysPerTree,number} Affen pro Baum',
'4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum',
[
'monkeyCount' => 4560,
'trees' => 123,
'monkeysPerTree' => 37.073,
],
'de',
],
];
}
/**
* @dataProvider patterns
* @param string $pattern
@ -366,24 +293,6 @@ _MSG_
$this->assertEquals($expected, $result, $formatter->getErrorMessage());
}
/**
* @dataProvider parsePatterns
* @param string $pattern
* @param string $expected
* @param array $args
* @param string $locale
*/
public function testParseNamedArguments($pattern, $expected, $args, $locale = 'en-US')
{
if (!extension_loaded('intl')) {
$this->markTestSkipped('intl not installed. Skipping.');
}
$formatter = new MessageFormatter();
$result = $formatter->parse($pattern, $expected, $locale);
$this->assertEquals($args, $result, $formatter->getErrorMessage() . ' Pattern: ' . $pattern);
}
public function testInsufficientArguments()
{
$expected = '{' . self::SUBJECT . '} is ' . self::N_VALUE;

Loading…
Cancel
Save