diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index 49b52d9..80e93a6 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -20,7 +20,7 @@ use yii\web\HttpException; * @property \yii\db\Connection $db The database connection. This property is read-only. * @property ErrorHandler $errorHandler The error handler application component. This property is read-only. * @property \yii\base\Formatter $formatter The formatter application component. This property is read-only. - * @property \yii\i18n\I18N $i18N The internationalization component. This property is read-only. + * @property \yii\i18n\I18N $i18n The internationalization component. This property is read-only. * @property \yii\log\Logger $log The log component. This property is read-only. * @property \yii\web\Request|\yii\console\Request $request The request component. This property is read-only. * @property string $runtimePath The directory that stores runtime files. Defaults to the "runtime" diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index 61b0f77..fe0b533 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -14,6 +14,10 @@ use yii\base\InvalidConfigException; /** * I18N provides features related with internationalization (I18N) and localization (L10N). * + * @property MessageFormatter $messageFormatter The message formatter to be used to format message via ICU + * message format. Note that the type of this property differs in getter and setter. See + * [[getMessageFormatter()]] and [[setMessageFormatter()]] for details. + * * @author Qiang Xue * @since 2.0 */ @@ -69,7 +73,7 @@ class I18N extends Component * @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. * @param string $language the language code (e.g. `en_US`, `en`). - * @return string the translated message. + * @return string the translated and formatted message. */ public function translate($category, $message, $params, $language) { @@ -93,8 +97,8 @@ class I18N extends Component } if (preg_match('~{\s*[\d\w]+\s*,~u', $message)) { - $formatter = new MessageFormatter(); - $result = $formatter->format($language, $message, $params); + $formatter = $this->getMessageFormatter(); + $result = $formatter->format($message, $params, $language); if ($result === false) { $errorMessage = $formatter->getErrorMessage(); Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message."); @@ -112,6 +116,35 @@ class I18N extends Component } /** + * @var string|array|MessageFormatter + */ + private $_messageFormatter; + + /** + * Returns the message formatter instance. + * @return MessageFormatter the message formatter to be used to format message via ICU message format. + */ + public function getMessageFormatter() + { + if ($this->_messageFormatter === null) { + $this->_messageFormatter = new MessageFormatter(); + } elseif (is_array($this->_messageFormatter) || is_string($this->_messageFormatter)) { + $this->_messageFormatter = Yii::createObject($this->_messageFormatter); + } + return $this->_messageFormatter; + } + + /** + * @param string|array|MessageFormatter $value the message formatter to be used to format message via ICU message format. + * Can be given as array or string configuration that will be given to [[Yii::createObject]] to create an instance + * or a [[MessageFormatter]] instance. + */ + public function setMessageFormatter($value) + { + $this->_messageFormatter = $value; + } + + /** * Returns the message source for the given category. * @param string $category the category name. * @return MessageSource the message source for the given category. diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 55dc73d..d04782e 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -11,11 +11,13 @@ use yii\base\Component; use yii\base\NotSupportedException; /** - * MessageFormatter enhances the message formatter class provided by PHP intl extension. + * MessageFormatter allows formatting messages via [ICU message format](http://userguide.icu-project.org/formatparse/messages) + * + * This class enhances the message formatter class provided by the PHP intl extension. * * The following enhancements are provided: * - * - Accepts named arguments and mixed numeric and named arguments. + * - It accepts named arguments and mixed numeric and named arguments. * - 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). @@ -23,24 +25,18 @@ use yii\base\NotSupportedException; * 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. * - * FallbackMessageFormatter is a fallback implementation for the PHP intl MessageFormatter that is - * used in case PHP intl extension is not installed. - * - * Do not use this class directly. Use [[MessageFormatter]] instead, which will automatically detect - * installed version of PHP intl and use the fallback if it is not installed. - * - * 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 + * - select format + * - simple parameters + * - integer number parameters * - * This implementation only supports to following message formats: - * - plural formatting for english - * - select format - * - simple parameters + * The fallback implementation does NOT support the ['apostrophe-friendly' syntax](http://www.php.net/manual/en/messageformatter.formatmessage.php). + * Also messages that are working with the fallback implementation are not necessarily compatible with the + * PHP intl MessageFormatter so do not rely on the fallback if you are able to install intl extension somehow. * - * The pattern also does NOT support the ['apostrophe-friendly' syntax](http://www.php.net/manual/en/messageformatter.formatmessage.php). - * - * Messages that can be parsed are also not necessarily compatible with the ICU formatting method so do not expect messages that work without intl installed - * to work with intl + * @property string $errorCode Code of the last error. This property is read-only. + * @property string $errorMessage Description of the last error. This property is read-only. * * @author Alexander Makarov * @author Carsten Brandt @@ -52,9 +48,9 @@ class MessageFormatter extends Component private $_errorMessage = ''; /** - * Get the error text from the last operation + * Get the error code from the last operation * @link http://php.net/manual/en/messageformatter.geterrorcode.php - * @return string Description of the last error. + * @return string Code of the last error. */ public function getErrorCode() { @@ -72,34 +68,38 @@ class MessageFormatter extends Component } /** - * Formats a message via ICU message format. + * Formats a message via [ICU message format](http://userguide.icu-project.org/formatparse/messages) + * + * It uses the PHP intl extension's [MessageFormatter](http://www.php.net/manual/en/class.messageformatter.php) + * and works around some issues. + * If PHP intl is not installed a fallback will be used that supports a subset of the ICU message format. * - * @link http://php.net/manual/en/messageformatter.formatmessage.php + * @param string $pattern The pattern string to insert parameters into. + * @param array $params The array of name value pairs to insert into the format string. * @param string $language The locale to use for formatting locale-dependent parts - * @param string $message The pattern string to insert things into. - * @param array $params The array of values to insert into the format string * @return string|boolean The formatted pattern string or `FALSE` if an error occurred */ - public function format($language, $message, $params) + public function format($pattern, $params, $language) { $this->_errorCode = 0; $this->_errorMessage = ''; if ($params === []) { - return $message; + return $pattern; } if (!class_exists('MessageFormatter', false)) { - return $this->fallbackFormat($language, $message, $params); + return $this->fallbackFormat($language, $pattern, $params); } if (version_compare(PHP_VERSION, '5.5.0', '<')) { - $message = $this->replaceNamedArguments($message, $params); + $pattern = $this->replaceNamedArguments($pattern, $params); $params = array_values($params); } - $formatter = new \MessageFormatter($language, $message); + $formatter = new \MessageFormatter($language, $pattern); if ($formatter === null) { - $this->_errorMessage = "Unable to format message in language '$language'."; + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; return false; } $result = $formatter->format($params); @@ -113,16 +113,19 @@ class MessageFormatter extends Component } /** - * Parse input string according to a message pattern + * Parses an input string according to an [ICU message format](http://userguide.icu-project.org/formatparse/messages) pattern. * - * @link http://www.php.net/manual/en/messageformatter.parsemessage.php + * It uses the PHP intl extension's [MessageFormatter::parse()](http://www.php.net/manual/en/messageformatter.parsemessage.php) + * and works around some issues. + * 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 - * @param string $pattern The pattern with which to parse the message. - * @param array $message The message to parse, conforming to the pattern. - * @return string|boolean An array containing items extracted, or `FALSE` on error. + * @return array|boolean An array containing items extracted, or `FALSE` on error. * @throws \yii\base\NotSupportedException when PHP intl extension is not installed. */ - public function parse($language, $pattern, $message) + public function parse($pattern, $message, $language) { $this->_errorCode = 0; $this->_errorMessage = ''; @@ -138,7 +141,8 @@ class MessageFormatter extends Component // } $formatter = new \MessageFormatter($language, $pattern); if ($formatter === null) { - $this->_errorMessage = "Unable to parse message in language '$language'."; + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; return false; } $result = $formatter->parse($message); @@ -205,12 +209,12 @@ class MessageFormatter extends Component /** * Fallback implementation for MessageFormatter::formatMessage - * @param string $language The locale to use for formatting locale-dependent parts - * @param string $message The pattern string to insert things into. - * @param array $params The array of values to insert into the format string + * @param string $pattern The pattern string to insert things into. + * @param array $args The array of values to insert into the format string + * @param string $locale The locale to use for formatting locale-dependent parts * @return string|boolean The formatted pattern string or `FALSE` if an error occurred */ - protected function fallbackFormat($locale, $pattern, $args = []) + protected function fallbackFormat($pattern, $args, $locale) { if (($tokens = $this->tokenizePattern($pattern)) === false) { return false; diff --git a/framework/yii/log/Logger.php b/framework/yii/log/Logger.php index 4e0e547..f79c40f 100644 --- a/framework/yii/log/Logger.php +++ b/framework/yii/log/Logger.php @@ -65,8 +65,8 @@ use yii\base\InvalidConfigException; * second element the total time spent in SQL execution. This property is read-only. * @property float $elapsedTime The total elapsed time in seconds for current request. This property is * read-only. - * @property array $profiling The profiling results. Each array element has the following structure: - * `[$token, $category, $time]`. This property is read-only. + * @property array $profiling The profiling results. Each array element has the following structure: `[$token, + * $category, $time]`. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php index 044c450..f674358 100644 --- a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php @@ -118,7 +118,7 @@ _MSG_ public function testNamedArgumentsObject($pattern, $expected, $args) { $formatter = new FallbackMessageFormatter(); - $result = $formatter->format('en_US', $pattern, $args); + $result = $formatter->fallbackFormat($pattern, $args, 'en_US'); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -127,9 +127,9 @@ _MSG_ $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; $formatter = new FallbackMessageFormatter(); - $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.'}', [ + $result = $formatter->fallbackFormat('{'.self::SUBJECT.'} is {'.self::N.'}', [ self::N => self::N_VALUE, - ]); + ], 'en_US'); $this->assertEquals($expected, $result); } @@ -139,15 +139,15 @@ _MSG_ $pattern = '{'.self::SUBJECT.'} is '.self::N; $formatter = new FallbackMessageFormatter(); - $result = $formatter->format('en_US', $pattern, []); + $result = $formatter->fallbackFormat($pattern, [], 'en_US'); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } } class FallbackMessageFormatter extends MessageFormatter { - public function fallbackFormat($locale, $pattern, $args = []) + public function fallbackFormat($pattern, $args, $locale) { - return parent::fallbackFormat($locale, $pattern, $args); + return parent::fallbackFormat($pattern, $args, $locale); } } \ No newline at end of file diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index 5daaab9..9030d87 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -122,10 +122,10 @@ _MSG_ /** * @dataProvider patterns */ - public function testNamedArgumentsObject($pattern, $expected, $args) + public function testNamedArguments($pattern, $expected, $args) { $formatter = new MessageFormatter(); - $result = $formatter->format('en_US', $pattern, $args); + $result = $formatter->format($pattern, $args, 'en_US'); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -134,9 +134,9 @@ _MSG_ $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; $formatter = new MessageFormatter(); - $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', [ + $result = $formatter->format('{'.self::SUBJECT.'} is {'.self::N.', number}', [ self::N => self::N_VALUE, - ]); + ], 'en_US'); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -145,7 +145,7 @@ _MSG_ { $pattern = '{'.self::SUBJECT.'} is '.self::N; $formatter = new MessageFormatter(); - $result = $formatter->format('en_US', $pattern, []); + $result = $formatter->format($pattern, [], 'en_US'); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } } \ No newline at end of file