diff --git a/framework/yii/i18n/BaseMessageFormatter.php b/framework/yii/i18n/BaseMessageFormatter.php
deleted file mode 100644
index 73968c0..0000000
--- a/framework/yii/i18n/BaseMessageFormatter.php
+++ /dev/null
@@ -1,168 +0,0 @@
-
- * @since 2.0
- */
-class BaseMessageFormatter
-{
- private $_locale;
- private $_pattern;
-
- /**
- * Constructs a new Message Formatter
- * @link http://php.net/manual/en/messageformatter.create.php
- * @param string $locale The locale to use when formatting arguments
- * @param string $pattern The pattern string to stick arguments into.
- * The pattern uses an 'apostrophe-friendly' syntax; it is run through
- * umsg_autoQuoteApostrophe before being interpreted.
- */
- public function __construct($locale, $pattern)
- {
- $this->_locale = $locale;
- $this->_pattern = $pattern;
- }
-
- /**
- * Constructs a new Message Formatter
- * @link http://php.net/manual/en/messageformatter.create.php
- * @param string $locale The locale to use when formatting arguments
- * @param string $pattern The pattern string to stick arguments into.
- * The pattern uses an 'apostrophe-friendly' syntax; it is run through
- * umsg_autoQuoteApostrophe before being interpreted.
- * @return MessageFormatter The formatter object
- */
- public static function create($locale, $pattern)
- {
- return new static($locale, $pattern);
- }
-
- /**
- * Format the message
- * @link http://php.net/manual/en/messageformatter.format.php
- * @param array $args Arguments to insert into the format string
- * @return string The formatted string, or FALSE if an error occurred
- */
- public function format(array $args)
- {
- return static::formatMessage($this->_locale, $this->_pattern, $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.
- * The pattern uses an 'apostrophe-friendly' syntax; it is run through
- * umsg_autoQuoteApostrophe before being interpreted.
- * @param array $args The array of values to insert into the format string
- * @return string The formatted pattern string or FALSE if an error occurred
- */
- public static function formatMessage($locale, $pattern, array $args)
- {
- // TODO implement plural format
-
- $a = [];
- foreach($args as $name => $value) {
- $a['{' . $name . '}'] = $value;
- }
- return strtr($pattern, $a);
- }
-
- /**
- * Parse input string according to pattern
- * @link http://php.net/manual/en/messageformatter.parse.php
- * @param string $value The string to parse
- * @return array An array containing the items extracted, or FALSE on error
- */
- public function parse ($value)
- {
- throw new NotSupportedException('You have to install PHP intl extension to use this feature.');
- }
-
- /**
- * Quick parse input string
- * @link http://php.net/manual/en/messageformatter.parsemessage.php
- * @param string $locale The locale to use for parsing locale-dependent parts
- * @param string $pattern The pattern with which to parse the value.
- * @param string $source The string to parse, conforming to the pattern.
- * @return array An array containing items extracted, or FALSE on error
- */
- public static function parseMessage ($locale, $pattern, $source)
- {
- throw new NotSupportedException('You have to install PHP intl extension to use this feature.');
- }
-
- /**
- * Set the pattern used by the formatter
- * @link http://php.net/manual/en/messageformatter.setpattern.php
- * @param string $pattern The pattern string to use in this message formatter.
- * The pattern uses an 'apostrophe-friendly' syntax; it is run through
- * umsg_autoQuoteApostrophe before being interpreted.
- * @return bool TRUE on success or FALSE on failure.
- */
- public function setPattern ($pattern)
- {
- $this->_pattern = $pattern;
- return true;
- }
-
- /**
- * Get the pattern used by the formatter
- * @link http://php.net/manual/en/messageformatter.getpattern.php
- * @return string The pattern string for this message formatter
- */
- public function getPattern()
- {
- return $this->_pattern;
- }
-
- /**
- * Get the locale for which the formatter was created.
- * @link http://php.net/manual/en/messageformatter.getlocale.php
- * @return string The locale name
- */
- public function getLocale()
- {
- return $this->_locale;
- }
-
- /**
- * Get the error code from last operation
- * @link http://php.net/manual/en/messageformatter.geterrorcode.php
- * @return int The error code, one of UErrorCode values. Initial value is U_ZERO_ERROR.
- */
- public function getErrorCode()
- {
- return 0;
- }
-
- /**
- * Get the error text from the last operation
- * @link http://php.net/manual/en/messageformatter.geterrormessage.php
- * @return string Description of the last error.
- */
- public function getErrorMessage()
- {
- return '';
- }
-}
-
-if (!class_exists('MessageFormatter', false)) {
- class_alias('yii\\i18n\\BaseMessageFormatter', 'MessageFormatter');
-}
-
diff --git a/framework/yii/i18n/FallbackMessageFormatter.php b/framework/yii/i18n/FallbackMessageFormatter.php
new file mode 100644
index 0000000..16d38f5
--- /dev/null
+++ b/framework/yii/i18n/FallbackMessageFormatter.php
@@ -0,0 +1,306 @@
+
+ * @since 2.0
+ */
+class FallbackMessageFormatter
+{
+ private $_locale;
+ private $_pattern;
+ private $_errorMessage = '';
+ private $_errorCode = 0;
+
+ /**
+ * Constructs a new Message Formatter
+ * @link http://php.net/manual/en/messageformatter.create.php
+ * @param string $locale The locale to use when formatting arguments
+ * @param string $pattern The pattern string to stick arguments into.
+ */
+ public function __construct($locale, $pattern)
+ {
+ $this->_locale = $locale;
+ $this->_pattern = $pattern;
+ }
+
+ /**
+ * Constructs a new Message Formatter
+ * @link http://php.net/manual/en/messageformatter.create.php
+ * @param string $locale The locale to use when formatting arguments
+ * @param string $pattern The pattern string to stick arguments into.
+ * @return MessageFormatter The formatter object
+ */
+ public static function create($locale, $pattern)
+ {
+ return new static($locale, $pattern);
+ }
+
+ /**
+ * Format the message
+ * @link http://php.net/manual/en/messageformatter.format.php
+ * @param array $args Arguments to insert into the format string
+ * @return string The formatted string, or `FALSE` if an error occurred
+ */
+ public function format(array $args)
+ {
+ return static::formatMessage($this->_locale, $this->_pattern, $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 The formatted pattern string or `FALSE` if an error occurred
+ */
+ public static function formatMessage($locale, $pattern, array $args)
+ {
+ if (($tokens = static::tokenizePattern($pattern)) === false) {
+ return false;
+ }
+ foreach($tokens as $i => $token) {
+ if (is_array($token)) {
+ if (($tokens[$i] = static::parseToken($token, $args, $locale)) === false) {
+ return false;
+ }
+ }
+ }
+ return implode('', $tokens);
+ }
+
+ /**
+ * Tokenizes a pattern by separating normal text from replaceable patterns
+ * @param string $pattern patter to tokenize
+ * @return array|bool array of tokens or false on failure
+ */
+ private static function tokenizePattern($pattern)
+ {
+ $depth = 1;
+ if (($start = $pos = mb_strpos($pattern, '{')) === false) {
+ return [$pattern];
+ }
+ $tokens = [mb_substr($pattern, 0, $pos)];
+ while(true) {
+ $open = mb_strpos($pattern, '{', $pos + 1);
+ $close = mb_strpos($pattern, '}', $pos + 1);
+ if ($open === false && $close === false) {
+ break;
+ }
+ if ($open === false) {
+ $open = mb_strlen($pattern);
+ }
+ if ($close > $open) {
+ $depth++;
+ $pos = $open;
+ } else {
+ $depth--;
+ $pos = $close;
+ }
+ if ($depth == 0) {
+ $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1), 3);
+ $start = $pos + 1;
+ $tokens[] = mb_substr($pattern, $start, $open - $start);
+ $start = $open;
+ }
+ }
+ if ($depth != 0) {
+ return false;
+ }
+ return $tokens;
+ }
+
+ /**
+ * Parses a token
+ * @param array $token the token to parse
+ * @param array $args arguments to replace
+ * @param string $locale the locale
+ * @return bool|string parsed token or false on failure
+ * @throws \yii\base\NotSupportedException when unsupported formatting is used.
+ */
+ private static function parseToken($token, $args, $locale)
+ {
+ $param = trim($token[0]);
+ if (isset($args[$param])) {
+ $arg = $args[$param];
+ } else {
+ return '{' . implode(',', $token) . '}';
+ }
+ $type = isset($token[1]) ? trim($token[1]) : 'none';
+ switch($type)
+ {
+ case 'number':
+ case 'date':
+ case 'time':
+ case 'spellout':
+ case 'ordinal':
+ case 'duration':
+ case 'choice':
+ case 'selectordinal':
+ throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature.");
+ case 'none': return $arg;
+ case 'select':
+ /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html
+ selectStyle = (selector '{' message '}')+
+ */
+ $select = static::tokenizePattern($token[2]);
+ $c = count($select);
+ $message = false;
+ for($i = 0; $i + 1 < $c; $i++) {
+ if (is_array($select[$i]) || !is_array($select[$i + 1])) {
+ return false;
+ }
+ $selector = trim($select[$i++]);
+ if ($message === false && $selector == 'other' || $selector == $arg) {
+ $message = implode(',', $select[$i]);
+ }
+ }
+ if ($message !== false) {
+ return static::formatMessage($locale, $message, $args);
+ }
+ break;
+ case 'plural':
+ /* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html
+ pluralStyle = [offsetValue] (selector '{' message '}')+
+ offsetValue = "offset:" number
+ selector = explicitValue | keyword
+ explicitValue = '=' number // adjacent, no white space in between
+ keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
+ message: see MessageFormat
+ */
+ $plural = static::tokenizePattern($token[2]);
+ $c = count($plural);
+ $message = false;
+ $offset = 0;
+ for($i = 0; $i + 1 < $c; $i++) {
+ if (is_array($plural[$i]) || !is_array($plural[$i + 1])) {
+ return false;
+ }
+ $selector = trim($plural[$i++]);
+ if ($i == 1 && substr($selector, 0, 7) == 'offset:') {
+ $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7)) - 7));
+ $selector = trim(mb_substr($selector, $pos + 1));
+ }
+ if ($message === false && $selector == 'other' ||
+ $selector[0] == '=' && (int) mb_substr($selector, 1) == $arg ||
+ $selector == 'zero' && $arg - $offset == 0 ||
+ $selector == 'one' && $arg - $offset == 1 ||
+ $selector == 'two' && $arg - $offset == 2
+ ) {
+ $message = implode(',', str_replace('#', $arg - $offset, $plural[$i]));
+ }
+ }
+ if ($message !== false) {
+ return static::formatMessage($locale, $message, $args);
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Parse input string according to pattern
+ * @link http://php.net/manual/en/messageformatter.parse.php
+ * @param string $value The string to parse
+ * @return array An array containing the items extracted, or `FALSE` on error
+ */
+ public function parse($value)
+ {
+ throw new NotSupportedException('You have to install PHP intl extension to use this feature.');
+ }
+
+ /**
+ * Quick parse input string
+ * @link http://php.net/manual/en/messageformatter.parsemessage.php
+ * @param string $locale The locale to use for parsing locale-dependent parts
+ * @param string $pattern The pattern with which to parse the `value`.
+ * @param string $source The string to parse, conforming to the `pattern`.
+ * @return array An array containing items extracted, or `FALSE` on error
+ */
+ public static function parseMessage($locale, $pattern, $source)
+ {
+ throw new NotSupportedException('You have to install PHP intl extension to use this feature.');
+ }
+
+ /**
+ * Set the pattern used by the formatter
+ * @link http://php.net/manual/en/messageformatter.setpattern.php
+ * @param string $pattern The pattern string to use in this message formatter.
+ * @return bool `TRUE` on success or `FALSE` on failure.
+ */
+ public function setPattern($pattern)
+ {
+ $this->_pattern = $pattern;
+ return true;
+ }
+
+ /**
+ * Get the pattern used by the formatter
+ * @link http://php.net/manual/en/messageformatter.getpattern.php
+ * @return string The pattern string for this message formatter
+ */
+ public function getPattern()
+ {
+ return $this->_pattern;
+ }
+
+ /**
+ * Get the locale for which the formatter was created.
+ * @link http://php.net/manual/en/messageformatter.getlocale.php
+ * @return string The locale name
+ */
+ public function getLocale()
+ {
+ return $this->_locale;
+ }
+
+ /**
+ * Get the error code from last operation
+ * @link http://php.net/manual/en/messageformatter.geterrorcode.php
+ * @return int The error code, one of UErrorCode values. Initial value is U_ZERO_ERROR.
+ */
+ public function getErrorCode()
+ {
+ return $this->_errorCode;
+ }
+
+ /**
+ * Get the error text from the last operation
+ * @link http://php.net/manual/en/messageformatter.geterrormessage.php
+ * @return string Description of the last error.
+ */
+ public function getErrorMessage()
+ {
+ return $this->_errorMessage;
+ }
+}
+
+if (!class_exists('MessageFormatter', false)) {
+ class_alias('yii\\i18n\\FallbackMessageFormatter', 'MessageFormatter');
+ define('YII_INTL_MESSAGE_FALLBACK', true);
+}
diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php
index 6521d65..106bf18 100644
--- a/framework/yii/i18n/MessageFormatter.php
+++ b/framework/yii/i18n/MessageFormatter.php
@@ -8,8 +8,9 @@
namespace yii\i18n;
if (!class_exists('MessageFormatter', false)) {
- require_once(__DIR__ . '/BaseMessageFormatter.php');
+ require_once(__DIR__ . '/FallbackMessageFormatter.php');
}
+defined('YII_INTL_MESSAGE_FALLBACK') || define('YII_INTL_MESSAGE_FALLBACK', false);
/**
* MessageFormatter is an enhanced version of PHP intl class that no matter which PHP and ICU versions are used:
@@ -18,6 +19,9 @@ if (!class_exists('MessageFormatter', false)) {
* - 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).
+ * - 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.
*
* @author Alexander Makarov
* @author Carsten Brandt
@@ -38,7 +42,7 @@ class MessageFormatter extends \MessageFormatter
return $this->getPattern();
}
- if (version_compare(PHP_VERSION, '5.5.0', '<')) {
+ if (version_compare(PHP_VERSION, '5.5.0', '<') && !YII_INTL_MESSAGE_FALLBACK) {
$pattern = self::replaceNamedArguments($this->getPattern(), $args);
$this->setPattern($pattern);
$args = array_values($args);
@@ -61,7 +65,7 @@ class MessageFormatter extends \MessageFormatter
return $pattern;
}
- if (version_compare(PHP_VERSION, '5.5.0', '<')) {
+ if (version_compare(PHP_VERSION, '5.5.0', '<') && !YII_INTL_MESSAGE_FALLBACK) {
$pattern = self::replaceNamedArguments($pattern, $args);
$args = array_values($args);
}
diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php
new file mode 100644
index 0000000..407ec7f
--- /dev/null
+++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php
@@ -0,0 +1,155 @@
+
+ * @since 2.0
+ * @group i18n
+ */
+class FallbackMessageFormatterTest 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';
+
+ public function patterns()
+ {
+ return [
+ [
+ '{'.self::SUBJECT.'} is {'.self::N.'}', // pattern
+ self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected
+ [ // params
+ self::N => self::N_VALUE,
+ self::SUBJECT => self::SUBJECT_VALUE,
+ ]
+ ],
+
+ // This one was provided by Aura.Intl. Thanks!
+ [<<<_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.',
+ [
+ 'gender_of_host' => 'male',
+ 'num_guests' => 4,
+ 'host' => 'ralph',
+ 'guest' => 'beep'
+ ]
+ ],
+
+ [
+ '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!',
+ 'Alexander is male and he loves Yii!',
+ [
+ 'name' => 'Alexander',
+ 'gender' => 'male',
+ ],
+ ],
+
+ // verify pattern in select does not get replaced
+ [
+ '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!',
+ 'Alexander is male and he loves Yii!',
+ [
+ 'name' => 'Alexander',
+ 'gender' => 'male',
+ // following should not be replaced
+ 'he' => 'wtf',
+ 'she' => 'wtf',
+ 'it' => 'wtf',
+ ]
+ ],
+
+ // verify pattern in select message gets replaced
+ [
+ '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!',
+ 'Alexander is male and wtf loves Yii!',
+ [
+ 'name' => 'Alexander',
+ 'gender' => 'male',
+ 'he' => 'wtf',
+ 'she' => 'wtf',
+ ],
+ ],
+
+ // some parser specific verifications
+ [
+ '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr} is {gender}!',
+ 'male and wtf loves 42 is male!',
+ [
+ 'nr' => 42,
+ 'gender' => 'male',
+ 'he' => 'wtf',
+ 'she' => 'wtf',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider patterns
+ */
+ public function testNamedArgumentsStatic($pattern, $expected, $args)
+ {
+ $result = FallbackMessageFormatter::formatMessage('en_US', $pattern, $args);
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * @dataProvider patterns
+ */
+ public function testNamedArgumentsObject($pattern, $expected, $args)
+ {
+ $formatter = new FallbackMessageFormatter('en_US', $pattern);
+ $result = $formatter->format($args);
+ $this->assertEquals($expected, $result, $formatter->getErrorMessage());
+ }
+
+ public function testInsufficientArguments()
+ {
+ $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE;
+
+ $result = FallbackMessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.'}', [
+ self::N => self::N_VALUE,
+ ]);
+
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testNoParams()
+ {
+ $pattern = '{'.self::SUBJECT.'} is '.self::N;
+ $result = FallbackMessageFormatter::formatMessage('en_US', $pattern, []);
+ $this->assertEquals($pattern, $result);
+
+ $formatter = new FallbackMessageFormatter('en_US', $pattern);
+ $result = $formatter->format([]);
+ $this->assertEquals($pattern, $result, $formatter->getErrorMessage());
+ }
+}
\ No newline at end of file