├── LICENSE ├── MessageFormatter.php ├── README.md ├── Resources └── stubs │ ├── IntlException.php │ └── MessageFormatter.php ├── bootstrap.php └── composer.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MessageFormatter.php: -------------------------------------------------------------------------------- 1 | 54 | * @author Carsten Brandt 55 | * @author Nicolas Grekas 56 | * 57 | * @internal 58 | */ 59 | class MessageFormatter 60 | { 61 | private $locale; 62 | private $pattern; 63 | private $tokens; 64 | private $errorCode = 0; 65 | private $errorMessage = ''; 66 | 67 | public function __construct(string $locale, string $pattern) 68 | { 69 | $this->locale = $locale; 70 | 71 | if (!$this->setPattern($pattern)) { 72 | throw new \IntlException('Message pattern is invalid.'); 73 | } 74 | } 75 | 76 | public static function create(string $locale, string $pattern) 77 | { 78 | $formatter = new static($locale, '-'); 79 | 80 | return $formatter->setPattern($pattern) ? $formatter : null; 81 | } 82 | 83 | public static function formatMessage(string $locale, string $pattern, array $values) 84 | { 85 | if (null === $formatter = self::create($locale, $pattern)) { 86 | return false; 87 | } 88 | 89 | return $formatter->format($values); 90 | } 91 | 92 | public function getLocale() 93 | { 94 | return $this->locale; 95 | } 96 | 97 | public function getPattern() 98 | { 99 | return $this->pattern; 100 | } 101 | 102 | public function getErrorCode() 103 | { 104 | return $this->errorCode; 105 | } 106 | 107 | public function getErrorMessage() 108 | { 109 | return $this->errorMessage; 110 | } 111 | 112 | public function setPattern(string $pattern) 113 | { 114 | try { 115 | $this->tokens = self::tokenizePattern($pattern); 116 | $this->pattern = $pattern; 117 | } catch (\DomainException $e) { 118 | return false; 119 | } 120 | 121 | return true; 122 | } 123 | 124 | public function format(array $values) 125 | { 126 | $this->errorCode = 0; 127 | $this->errorMessage = ''; 128 | 129 | if (!$values) { 130 | return $this->pattern; 131 | } 132 | 133 | try { 134 | return self::parseTokens($this->tokens, $values, $this->locale); 135 | } catch (\DomainException $e) { 136 | $this->errorCode = -1; 137 | $this->errorMessage = $e->getMessage(); 138 | 139 | return false; 140 | } 141 | } 142 | 143 | public function parse(string $string) 144 | { 145 | $this->errorCode = -1; 146 | $this->errorMessage = sprintf('The PHP intl extension is required to use "MessageFormatter::%s()".', __FUNCTION__); 147 | 148 | return false; 149 | } 150 | 151 | private static function parseTokens(array $tokens, array $values, $locale) 152 | { 153 | foreach ($tokens as $i => $token) { 154 | if (\is_array($token)) { 155 | $tokens[$i] = self::parseToken($token, $values, $locale); 156 | } 157 | } 158 | 159 | return implode('', $tokens); 160 | } 161 | 162 | private static function tokenizePattern($pattern) 163 | { 164 | if (false === $start = $pos = strpos($pattern, '{')) { 165 | return [$pattern]; 166 | } 167 | 168 | $depth = 1; 169 | $tokens = [substr($pattern, 0, $pos)]; 170 | 171 | while (true) { 172 | $open = strpos($pattern, '{', 1 + $pos); 173 | $close = strpos($pattern, '}', 1 + $pos); 174 | 175 | if (false === $open) { 176 | if (false === $close) { 177 | break; 178 | } 179 | $open = \strlen($pattern); 180 | } 181 | 182 | if ($close > $open) { 183 | ++$depth; 184 | $pos = $open; 185 | } else { 186 | --$depth; 187 | $pos = $close; 188 | } 189 | 190 | if (0 === $depth) { 191 | $tokens[] = explode(',', substr($pattern, 1 + $start, $pos - $start - 1), 3); 192 | $start = 1 + $pos; 193 | $tokens[] = substr($pattern, $start, $open - $start); 194 | $start = $open; 195 | } 196 | 197 | if (0 !== $depth && (false === $open || false === $close)) { 198 | break; 199 | } 200 | } 201 | 202 | if ($depth) { 203 | throw new \DomainException('Message pattern is invalid.'); 204 | } 205 | 206 | return $tokens; 207 | } 208 | 209 | /** 210 | * Parses pattern based on ICU grammar. 211 | * 212 | * @see http://icu-project.org/apiref/icu4c/classMessageFormat.html#details 213 | */ 214 | private static function parseToken(array $token, array $values, $locale) 215 | { 216 | if (!isset($values[$param = trim($token[0])])) { 217 | return '{'.$param.'}'; 218 | } 219 | 220 | $arg = $values[$param]; 221 | $type = isset($token[1]) ? trim($token[1]) : 'none'; 222 | switch ($type) { 223 | case 'date': // XXX use DateFormatter? 224 | case 'time': 225 | case 'spellout': 226 | case 'ordinal': 227 | case 'duration': 228 | case 'choice': 229 | case 'selectordinal': 230 | throw new \DomainException(sprintf('The PHP intl extension is required to use the "%s" message format.', $type)); 231 | case 'number': 232 | $format = isset($token[2]) ? trim($token[2]) : null; 233 | if (!is_numeric($arg) || (null !== $format && 'integer' !== $format)) { 234 | throw new \DomainException('The PHP intl extension is required to use the "number" message format with non-integer values.'); 235 | } 236 | 237 | $number = number_format($arg); // XXX use NumberFormatter? 238 | if (null === $format && false !== $pos = strpos($arg, '.')) { 239 | // add decimals with unknown length 240 | $number .= '.'.substr($arg, $pos + 1); 241 | } 242 | 243 | return $number; 244 | 245 | case 'none': 246 | return $arg; 247 | 248 | case 'select': 249 | /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html 250 | selectStyle = (selector '{' message '}')+ 251 | */ 252 | if (!isset($token[2])) { 253 | throw new \DomainException('Message pattern is invalid.'); 254 | } 255 | $select = self::tokenizePattern($token[2]); 256 | $c = \count($select); 257 | $message = false; 258 | for ($i = 0; 1 + $i < $c; ++$i) { 259 | if (\is_array($select[$i]) || !\is_array($select[1 + $i])) { 260 | throw new \DomainException('Message pattern is invalid.'); 261 | } 262 | $selector = trim($select[$i++]); 263 | if (false === $message && 'other' === $selector || $selector == $arg) { 264 | $message = implode(',', $select[$i]); 265 | } 266 | } 267 | if (false !== $message) { 268 | return self::parseTokens(self::tokenizePattern($message), $values, $locale); 269 | } 270 | break; 271 | 272 | case 'plural': // TODO make it locale-dependent based on symfony/translation rules 273 | /* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html 274 | pluralStyle = [offsetValue] (selector '{' message '}')+ 275 | offsetValue = "offset:" number 276 | selector = explicitValue | keyword 277 | explicitValue = '=' number // adjacent, no white space in between 278 | keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ 279 | message: see MessageFormat 280 | */ 281 | if (!isset($token[2])) { 282 | throw new \DomainException('Message pattern is invalid.'); 283 | } 284 | $plural = self::tokenizePattern($token[2]); 285 | $c = \count($plural); 286 | $message = false; 287 | $offset = 0; 288 | for ($i = 0; 1 + $i < $c; ++$i) { 289 | if (\is_array($plural[$i]) || !\is_array($plural[1 + $i])) { 290 | throw new \DomainException('Message pattern is invalid.'); 291 | } 292 | $selector = trim($plural[$i++]); 293 | 294 | if (1 === $i && 0 === strncmp($selector, 'offset:', 7)) { 295 | $pos = strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7); 296 | $offset = (int) trim(substr($selector, 7, $pos - 7)); 297 | $selector = trim(substr($selector, 1 + $pos, \strlen($selector))); 298 | } 299 | if (false === $message && 'other' === $selector 300 | || '=' === $selector[0] && (int) substr($selector, 1, \strlen($selector)) === $arg 301 | || 'one' === $selector && 1 == $arg - $offset 302 | ) { 303 | $message = implode(',', str_replace('#', $arg - $offset, $plural[$i])); 304 | } 305 | } 306 | if (false !== $message) { 307 | return self::parseTokens(self::tokenizePattern($message), $values, $locale); 308 | } 309 | break; 310 | } 311 | 312 | throw new \DomainException('Message pattern is invalid.'); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Symfony Polyfill / Intl: MessageFormatter 2 | ========================================= 3 | 4 | This component provides a fallback implementation for the 5 | [`MessageFormatter`](https://php.net/MessageFormatter) class provided 6 | by the [Intl](https://php.net/intl) extension. 7 | 8 | More information can be found in the 9 | [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). 10 | 11 | License 12 | ======= 13 | 14 | This library is released under the [MIT license](LICENSE). 15 | -------------------------------------------------------------------------------- /Resources/stubs/IntlException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | class IntlException extends Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /Resources/stubs/MessageFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | class MessageFormatter extends Symfony\Polyfill\Intl\MessageFormatter\MessageFormatter 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | use Symfony\Polyfill\Intl\MessageFormatter\MessageFormatter as p; 13 | 14 | if (!function_exists('msgfmt_format_message')) { 15 | function msgfmt_format_message($locale, $pattern, array $values) { return p::formatMessage($locale, $pattern, $values); } 16 | } 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/polyfill-intl-messageformatter", 3 | "type": "library", 4 | "description": "Symfony polyfill for intl's MessageFormatter class and related functions", 5 | "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "messageformatter"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Nicolas Grekas", 11 | "email": "p@tchwork.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.2" 20 | }, 21 | "autoload": { 22 | "psr-4": { "Symfony\\Polyfill\\Intl\\MessageFormatter\\": "" }, 23 | "files": [ "bootstrap.php" ], 24 | "classmap": [ "Resources/stubs" ] 25 | }, 26 | "suggest": { 27 | "ext-intl": "For best performance" 28 | }, 29 | "minimum-stability": "dev", 30 | "extra": { 31 | "thanks": { 32 | "name": "symfony/polyfill", 33 | "url": "https://github.com/symfony/polyfill" 34 | } 35 | } 36 | } 37 | --------------------------------------------------------------------------------