@ -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