diff --git a/framework/yii/i18n/FallbackMessageFormatter.php b/framework/yii/i18n/FallbackMessageFormatter.php deleted file mode 100644 index 17928f5..0000000 --- a/framework/yii/i18n/FallbackMessageFormatter.php +++ /dev/null @@ -1,306 +0,0 @@ - - * @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($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, $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/I18N.php b/framework/yii/i18n/I18N.php index b2b99b5..61b0f77 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -78,7 +78,7 @@ class I18N extends Component } /** - * Formats a message using using [[MessageFormatter]]. + * Formats a message using [[MessageFormatter]]. * * @param string $message the message to be formatted. * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. @@ -93,12 +93,8 @@ class I18N extends Component } if (preg_match('~{\s*[\d\w]+\s*,~u', $message)) { - $formatter = new MessageFormatter($language, $message); - if ($formatter === null) { - Yii::warning("Unable to format message in language '$language': $message."); - return $message; - } - $result = $formatter->format($params); + $formatter = new MessageFormatter(); + $result = $formatter->format($language, $message, $params); if ($result === false) { $errorMessage = $formatter->getErrorMessage(); Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message."); diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 9f198c9..55dc73d 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -7,10 +7,8 @@ namespace yii\i18n; -if (!class_exists('MessageFormatter', false)) { - require_once(__DIR__ . '/FallbackMessageFormatter.php'); -} -defined('YII_INTL_MESSAGE_FALLBACK') || define('YII_INTL_MESSAGE_FALLBACK', false); +use yii\base\Component; +use yii\base\NotSupportedException; /** * MessageFormatter enhances the message formatter class provided by PHP intl extension. @@ -25,53 +23,133 @@ defined('YII_INTL_MESSAGE_FALLBACK') || define('YII_INTL_MESSAGE_FALLBACK', fals * 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. + * + * This implementation only supports to following message formats: + * - plural formatting for english + * - select format + * - simple parameters + * + * 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 + * * @author Alexander Makarov * @author Carsten Brandt * @since 2.0 */ -class MessageFormatter extends \MessageFormatter +class MessageFormatter extends Component { + private $_errorCode = 0; + private $_errorMessage = ''; + /** - * Format the message. + * Get the error text from the last operation + * @link http://php.net/manual/en/messageformatter.geterrorcode.php + * @return string Description of the last 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; + } + + /** + * Formats a message via ICU message format. * - * @link http://php.net/manual/en/messageformatter.format.php - * @param array $args Arguments to insert into the format string. - * @return string|boolean The formatted string, or false if an error occurred. + * @link http://php.net/manual/en/messageformatter.formatmessage.php + * @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($args) + public function format($language, $message, $params) { - if ($args === []) { - return $this->getPattern(); + $this->_errorCode = 0; + $this->_errorMessage = ''; + + if ($params === []) { + return $message; + } + + if (!class_exists('MessageFormatter', false)) { + return $this->fallbackFormat($language, $message, $params); } - 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); + if (version_compare(PHP_VERSION, '5.5.0', '<')) { + $message = $this->replaceNamedArguments($message, $params); + $params = array_values($params); + } + $formatter = new \MessageFormatter($language, $message); + if ($formatter === null) { + $this->_errorMessage = "Unable to format message in language '$language'."; + return false; + } + $result = $formatter->format($params); + if ($result === false) { + $this->_errorCode = $formatter->getErrorCode(); + $this->_errorMessage = $formatter->getErrorMessage(); + return false; + } else { + return $result; } - return parent::format($args); } /** - * Quick format message. + * Parse input string according to a message pattern * - * @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|boolean The formatted pattern string or false if an error occurred. + * @link http://www.php.net/manual/en/messageformatter.parsemessage.php + * @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. + * @throws \yii\base\NotSupportedException when PHP intl extension is not installed. */ - public static function formatMessage($locale, $pattern, $args) + public function parse($language, $pattern, $message) { - if ($args === []) { - return $pattern; + $this->_errorCode = 0; + $this->_errorMessage = ''; + + if (!class_exists('MessageFormatter', false)) { + throw new NotSupportedException('You have to install PHP intl extension to use this feature.'); } - if (version_compare(PHP_VERSION, '5.5.0', '<') && !YII_INTL_MESSAGE_FALLBACK) { - $pattern = self::replaceNamedArguments($pattern, $args); - $args = array_values($args); + // TODO try to support named args +// if (version_compare(PHP_VERSION, '5.5.0', '<')) { +// $message = $this->replaceNamedArguments($message, $params); +// $params = array_values($params); +// } + $formatter = new \MessageFormatter($language, $pattern); + if ($formatter === null) { + $this->_errorMessage = "Unable to parse message in language '$language'."; + return false; } - return parent::formatMessage($locale, $pattern, $args); + $result = $formatter->parse($message); + if ($result === false) { + $this->_errorCode = $formatter->getErrorCode(); + $this->_errorMessage = $formatter->getErrorMessage(); + return false; + } else { + return $result; + } + } /** @@ -124,4 +202,155 @@ class MessageFormatter extends \MessageFormatter } return $pattern; } + + /** + * 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 + * @return string|boolean The formatted pattern string or `FALSE` if an error occurred + */ + protected function fallbackFormat($locale, $pattern, $args = []) + { + if (($tokens = $this->tokenizePattern($pattern)) === false) { + return false; + } + foreach($tokens as $i => $token) { + if (is_array($token)) { + if (($tokens[$i] = $this->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 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 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 $this->fallbackFormat($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 $this->fallbackFormat($locale, $message, $args); + } + break; + } + return false; + } } diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php index 407ec7f..044c450 100644 --- a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php @@ -7,7 +7,7 @@ namespace yiiunit\framework\i18n; -use yii\i18n\FallbackMessageFormatter; +use yii\i18n\MessageFormatter; use yiiunit\TestCase; /** @@ -115,19 +115,10 @@ _MSG_ /** * @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); + $formatter = new FallbackMessageFormatter(); + $result = $formatter->format('en_US', $pattern, $args); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -135,7 +126,8 @@ _MSG_ { $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; - $result = FallbackMessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.'}', [ + $formatter = new FallbackMessageFormatter(); + $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.'}', [ self::N => self::N_VALUE, ]); @@ -145,11 +137,17 @@ _MSG_ 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([]); + $formatter = new FallbackMessageFormatter(); + $result = $formatter->format('en_US', $pattern, []); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } +} + +class FallbackMessageFormatter extends MessageFormatter +{ + public function fallbackFormat($locale, $pattern, $args = []) + { + return parent::fallbackFormat($locale, $pattern, $args); + } } \ No newline at end of file diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index c392ce0..5daaab9 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -122,19 +122,10 @@ _MSG_ /** * @dataProvider patterns */ - public function testNamedArgumentsStatic($pattern, $expected, $args) - { - $result = MessageFormatter::formatMessage('en_US', $pattern, $args); - $this->assertEquals($expected, $result, intl_get_error_message()); - } - - /** - * @dataProvider patterns - */ public function testNamedArgumentsObject($pattern, $expected, $args) { - $formatter = new MessageFormatter('en_US', $pattern); - $result = $formatter->format($args); + $formatter = new MessageFormatter(); + $result = $formatter->format('en_US', $pattern, $args); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -142,36 +133,19 @@ _MSG_ { $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; - $result = MessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', [ + $formatter = new MessageFormatter(); + $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', [ self::N => self::N_VALUE, ]); - $this->assertEquals($expected, $result, intl_get_error_message()); - } - - /** - * When instantiating a MessageFormatter with invalid pattern it should be null with default settings. - * It will be IntlException if intl.use_exceptions=1 and PHP 5.5 or newer or an error if intl.error_level is not 0. - */ - public function testNullConstructor() - { - if(ini_get('intl.use_exceptions')) { - $this->setExpectedException('IntlException'); - } - - if (!ini_get('intl.error_level') || ini_get('intl.use_exceptions')) { - $this->assertNull(new MessageFormatter('en_US', '')); - } + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } public function testNoParams() { $pattern = '{'.self::SUBJECT.'} is '.self::N; - $result = MessageFormatter::formatMessage('en_US', $pattern, []); - $this->assertEquals($pattern, $result, intl_get_error_message()); - - $formatter = new MessageFormatter('en_US', $pattern); - $result = $formatter->format([]); + $formatter = new MessageFormatter(); + $result = $formatter->format('en_US', $pattern, []); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } } \ No newline at end of file