| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 | <?php/** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */namespace yii\i18n;use Yii;use yii\base\Component;use yii\base\NotSupportedException;/** * 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: * * - 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). * - 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](https://secure.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) *   - select format *   - simple parameters *   - integer number parameters * *   The fallback implementation does NOT support the ['apostrophe-friendly' syntax](https://secure.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. * * @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 <sam@rmcreative.ru> * @author Carsten Brandt <mail@cebe.cc> * @since 2.0 */class MessageFormatter extends Component{    private $_errorCode = 0;    private $_errorMessage = '';    /**     * Get the error code from the last operation.     * @link https://secure.php.net/manual/en/messageformatter.geterrorcode.php     * @return string Code of the last error.     */    public function getErrorCode()    {        return $this->_errorCode;    }    /**     * Get the error text from the last operation.     * @link https://secure.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](http://userguide.icu-project.org/formatparse/messages).     *     * It uses the PHP intl extension's [MessageFormatter](https://secure.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.     *     * @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     * @return string|false The formatted pattern string or `false` if an error occurred     */    public function format($pattern, $params, $language)    {        $this->_errorCode = 0;        $this->_errorMessage = '';        if ($params === []) {            return $pattern;        }        if (!class_exists('MessageFormatter', false)) {            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;        } catch (\Exception $e) {            // Exception is thrown by HHVM            $this->_errorCode = $e->getCode();            $this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage();            return false;        }        $result = $formatter->format($params);        if ($result === false) {            $this->_errorCode = $formatter->getErrorCode();            $this->_errorMessage = $formatter->getErrorMessage();            return false;        }        return $result;    }    /**     * 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()](https://secure.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     * @param string $locale The locale to use for formatting locale-dependent parts     * @return false|string The formatted pattern string or `false` if an error occurred     */    protected function fallbackFormat($pattern, $args, $locale)    {        if (($tokens = self::tokenizePattern($pattern)) === false) {            $this->_errorCode = -1;            $this->_errorMessage = 'Message pattern is invalid.';            return false;        }        foreach ($tokens as $i => $token) {            if (is_array($token)) {                if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) {                    $this->_errorCode = -1;                    $this->_errorMessage = 'Message pattern is invalid.';                    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)    {        $charset = Yii::$app ? Yii::$app->charset : 'UTF-8';        $depth = 1;        if (($start = $pos = mb_strpos($pattern, '{', 0, $charset)) === false) {            return [$pattern];        }        $tokens = [mb_substr($pattern, 0, $pos, $charset)];        while (true) {            $open = mb_strpos($pattern, '{', $pos + 1, $charset);            $close = mb_strpos($pattern, '}', $pos + 1, $charset);            if ($open === false && $close === false) {                break;            }            if ($open === false) {                $open = mb_strlen($pattern, $charset);            }            if ($close > $open) {                $depth++;                $pos = $open;            } else {                $depth--;                $pos = $close;            }            if ($depth === 0) {                $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1, $charset), 3);                $start = $pos + 1;                $tokens[] = mb_substr($pattern, $start, $open - $start, $charset);                $start = $open;            }            if ($depth !== 0 && ($open === false || $close === false)) {                break;            }        }        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)    {        // parsing pattern based on ICU grammar:        // http://icu-project.org/apiref/icu4c/classMessageFormat.html#details        $charset = Yii::$app ? Yii::$app->charset : 'UTF-8';        $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 '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 'number':                $format = isset($token[2]) ? trim($token[2]) : null;                if (is_numeric($arg) && ($format === null || $format === 'integer')) {                    $number = number_format($arg);                    if ($format === null && ($pos = strpos($arg, '.')) !== false) {                        // add decimals with unknown length                        $number .= '.' . substr($arg, $pos + 1);                    }                    return $number;                }                throw new NotSupportedException("Message format 'number' is only supported for integer values. 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 '}')+                */                if (!isset($token[2])) {                    return false;                }                $select = self::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($message, $args, $locale);                }                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                */                if (!isset($token[2])) {                    return false;                }                $plural = self::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 && strncmp($selector, 'offset:', 7) === 0) {                        $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7, $charset)) - 7, $charset));                        $selector = trim(mb_substr($selector, $pos + 1, mb_strlen($selector, $charset), $charset));                    }                    if ($message === false && $selector === 'other' ||                        $selector[0] === '=' && (int) mb_substr($selector, 1, mb_strlen($selector, $charset), $charset) === $arg ||                        $selector === 'one' && $arg - $offset == 1                    ) {                        $message = implode(',', str_replace('#', $arg - $offset, $plural[$i]));                    }                }                if ($message !== false) {                    return $this->fallbackFormat($message, $args, $locale);                }                break;        }        return false;    }}
 |