1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @link http://www.yiiframework.com/ |
4
|
|
|
* @copyright Copyright (c) 2008 Yii Software LLC |
5
|
|
|
* @license http://www.yiiframework.com/license/ |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace yii\i18n; |
9
|
|
|
|
10
|
|
|
use Yii; |
11
|
|
|
use yii\base\Component; |
12
|
|
|
use yii\base\NotSupportedException; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* MessageFormatter allows formatting messages via [ICU message format](http://userguide.icu-project.org/formatparse/messages). |
16
|
|
|
* |
17
|
|
|
* This class enhances the message formatter class provided by the PHP intl extension. |
18
|
|
|
* |
19
|
|
|
* The following enhancements are provided: |
20
|
|
|
* |
21
|
|
|
* - Issues no error if format is invalid returning false and holding error for retrieval via `getErrorCode()` |
22
|
|
|
* and `getErrorMessage()` methods. |
23
|
|
|
* - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be |
24
|
|
|
* substituted. It prevents translation mistakes to crash whole page. |
25
|
|
|
* - Offers limited support for message formatting in case PHP intl extension is not installed. |
26
|
|
|
* However it is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if |
27
|
|
|
* you want to use MessageFormatter features. |
28
|
|
|
* |
29
|
|
|
* The fallback implementation only supports the following message formats: |
30
|
|
|
* - plural formatting for english ('one' and 'other' selectors) |
31
|
|
|
* - select format |
32
|
|
|
* - simple parameters |
33
|
|
|
* - integer number parameters |
34
|
|
|
* |
35
|
|
|
* The fallback implementation does NOT support the ['apostrophe-friendly' syntax](http://www.php.net/manual/en/messageformatter.formatmessage.php). |
36
|
|
|
* Also messages that are working with the fallback implementation are not necessarily compatible with the |
37
|
|
|
* PHP intl MessageFormatter so do not rely on the fallback if you are able to install intl extension somehow. |
38
|
|
|
* |
39
|
|
|
* @property string $errorCode Code of the last error. This property is read-only. |
40
|
|
|
* @property string $errorMessage Description of the last error. This property is read-only. |
41
|
|
|
* |
42
|
|
|
* @author Alexander Makarov <[email protected]> |
43
|
|
|
* @author Carsten Brandt <[email protected]> |
44
|
|
|
* @since 2.0 |
45
|
|
|
*/ |
46
|
|
|
class MessageFormatter extends Component |
47
|
|
|
{ |
48
|
|
|
private $_errorCode = 0; |
49
|
|
|
private $_errorMessage = ''; |
50
|
|
|
|
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Get the error code from the last operation. |
54
|
|
|
* @link http://php.net/manual/en/messageformatter.geterrorcode.php |
55
|
|
|
* @return string Code of the last error. |
56
|
|
|
*/ |
57
|
|
|
public function getErrorCode() |
58
|
|
|
{ |
59
|
|
|
return $this->_errorCode; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Get the error text from the last operation. |
64
|
|
|
* @link http://php.net/manual/en/messageformatter.geterrormessage.php |
65
|
|
|
* @return string Description of the last error. |
66
|
|
|
*/ |
67
|
42 |
|
public function getErrorMessage() |
68
|
|
|
{ |
69
|
42 |
|
return $this->_errorMessage; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Formats a message via [ICU message format](http://userguide.icu-project.org/formatparse/messages). |
74
|
|
|
* |
75
|
|
|
* It uses the PHP intl extension's [MessageFormatter](http://www.php.net/manual/en/class.messageformatter.php) |
76
|
|
|
* and works around some issues. |
77
|
|
|
* If PHP intl is not installed a fallback will be used that supports a subset of the ICU message format. |
78
|
|
|
* |
79
|
|
|
* @param string $pattern The pattern string to insert parameters into. |
80
|
|
|
* @param array $params The array of name value pairs to insert into the format string. |
81
|
|
|
* @param string $language The locale to use for formatting locale-dependent parts |
82
|
|
|
* @return string|false The formatted pattern string or `false` if an error occurred |
83
|
|
|
*/ |
84
|
152 |
|
public function format($pattern, $params, $language) |
85
|
|
|
{ |
86
|
152 |
|
$this->_errorCode = 0; |
87
|
152 |
|
$this->_errorMessage = ''; |
88
|
|
|
|
89
|
152 |
|
if ($params === []) { |
90
|
4 |
|
return $pattern; |
91
|
|
|
} |
92
|
|
|
|
93
|
148 |
|
if (!class_exists('MessageFormatter', false)) { |
94
|
|
|
return $this->fallbackFormat($pattern, $params, $language); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
try { |
98
|
148 |
|
$formatter = new \MessageFormatter($language, $pattern); |
99
|
2 |
|
} catch (\IntlException $e) { |
|
|
|
|
100
|
2 |
|
$this->_errorCode = $e->getCode(); |
101
|
2 |
|
$this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage(); |
102
|
2 |
|
return false; |
103
|
|
|
} |
104
|
|
|
|
105
|
146 |
|
$result = $formatter->format($params); |
106
|
|
|
|
107
|
146 |
|
if ($result === false) { |
108
|
|
|
$this->_errorCode = $formatter->getErrorCode(); |
109
|
|
|
$this->_errorMessage = $formatter->getErrorMessage(); |
110
|
|
|
return false; |
111
|
|
|
} |
112
|
|
|
|
113
|
146 |
|
return $result; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Fallback implementation for MessageFormatter::formatMessage. |
118
|
|
|
* @param string $pattern The pattern string to insert things into. |
119
|
|
|
* @param array $args The array of values to insert into the format string |
120
|
|
|
* @param string $locale The locale to use for formatting locale-dependent parts |
121
|
|
|
* @return false|string The formatted pattern string or `false` if an error occurred |
122
|
|
|
*/ |
123
|
18 |
|
protected function fallbackFormat($pattern, $args, $locale) |
124
|
|
|
{ |
125
|
18 |
|
if (($tokens = self::tokenizePattern($pattern)) === false) { |
126
|
|
|
$this->_errorCode = -1; |
127
|
|
|
$this->_errorMessage = 'Message pattern is invalid.'; |
128
|
|
|
|
129
|
|
|
return false; |
130
|
|
|
} |
131
|
18 |
|
foreach ($tokens as $i => $token) { |
132
|
18 |
|
if (is_array($token)) { |
133
|
18 |
|
if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) { |
134
|
|
|
$this->_errorCode = -1; |
135
|
|
|
$this->_errorMessage = 'Message pattern is invalid.'; |
136
|
|
|
|
137
|
18 |
|
return false; |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
16 |
|
return implode('', $tokens); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Tokenizes a pattern by separating normal text from replaceable patterns. |
147
|
|
|
* @param string $pattern patter to tokenize |
148
|
|
|
* @return array|bool array of tokens or false on failure |
149
|
|
|
*/ |
150
|
18 |
|
private static function tokenizePattern($pattern) |
151
|
|
|
{ |
152
|
18 |
|
$charset = Yii::$app ? Yii::$app->charset : 'UTF-8'; |
153
|
18 |
|
$depth = 1; |
154
|
18 |
|
if (($start = $pos = mb_strpos($pattern, '{', 0, $charset)) === false) { |
155
|
3 |
|
return [$pattern]; |
156
|
|
|
} |
157
|
18 |
|
$tokens = [mb_substr($pattern, 0, $pos, $charset)]; |
158
|
18 |
|
while (true) { |
159
|
18 |
|
$open = mb_strpos($pattern, '{', $pos + 1, $charset); |
160
|
18 |
|
$close = mb_strpos($pattern, '}', $pos + 1, $charset); |
161
|
18 |
|
if ($open === false && $close === false) { |
162
|
18 |
|
break; |
163
|
|
|
} |
164
|
18 |
|
if ($open === false) { |
165
|
18 |
|
$open = mb_strlen($pattern, $charset); |
166
|
|
|
} |
167
|
18 |
|
if ($close > $open) { |
168
|
11 |
|
$depth++; |
169
|
11 |
|
$pos = $open; |
170
|
|
|
} else { |
171
|
18 |
|
$depth--; |
172
|
18 |
|
$pos = $close; |
173
|
|
|
} |
174
|
18 |
|
if ($depth === 0) { |
175
|
18 |
|
$tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1, $charset), 3); |
176
|
18 |
|
$start = $pos + 1; |
177
|
18 |
|
$tokens[] = mb_substr($pattern, $start, $open - $start, $charset); |
178
|
18 |
|
$start = $open; |
179
|
|
|
} |
180
|
|
|
|
181
|
18 |
|
if ($depth !== 0 && ($open === false || $close === false)) { |
182
|
|
|
break; |
183
|
|
|
} |
184
|
|
|
} |
185
|
18 |
|
if ($depth !== 0) { |
186
|
|
|
return false; |
187
|
|
|
} |
188
|
|
|
|
189
|
18 |
|
return $tokens; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Parses a token. |
194
|
|
|
* @param array $token the token to parse |
195
|
|
|
* @param array $args arguments to replace |
196
|
|
|
* @param string $locale the locale |
197
|
|
|
* @return bool|string parsed token or false on failure |
198
|
|
|
* @throws \yii\base\NotSupportedException when unsupported formatting is used. |
199
|
|
|
*/ |
200
|
18 |
|
private function parseToken($token, $args, $locale) |
201
|
|
|
{ |
202
|
|
|
// parsing pattern based on ICU grammar: |
203
|
|
|
// http://icu-project.org/apiref/icu4c/classMessageFormat.html#details |
204
|
18 |
|
$charset = Yii::$app ? Yii::$app->charset : 'UTF-8'; |
205
|
18 |
|
$param = trim($token[0]); |
206
|
18 |
|
if (isset($args[$param])) { |
207
|
16 |
|
$arg = $args[$param]; |
208
|
|
|
} else { |
209
|
3 |
|
return '{' . implode(',', $token) . '}'; |
210
|
|
|
} |
211
|
16 |
|
$type = isset($token[1]) ? trim($token[1]) : 'none'; |
212
|
|
|
switch ($type) { |
213
|
16 |
|
case 'date': |
214
|
16 |
|
case 'time': |
215
|
16 |
|
case 'spellout': |
216
|
16 |
|
case 'ordinal': |
217
|
16 |
|
case 'duration': |
218
|
16 |
|
case 'choice': |
219
|
16 |
|
case 'selectordinal': |
220
|
|
|
throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature."); |
221
|
16 |
|
case 'number': |
222
|
9 |
|
$format = isset($token[2]) ? trim($token[2]) : null; |
223
|
9 |
|
if (is_numeric($arg) && ($format === null || $format === 'integer')) { |
224
|
7 |
|
$number = number_format($arg); |
225
|
7 |
|
if ($format === null && ($pos = strpos($arg, '.')) !== false) { |
226
|
|
|
// add decimals with unknown length |
227
|
1 |
|
$number .= '.' . substr($arg, $pos + 1); |
228
|
|
|
} |
229
|
|
|
|
230
|
7 |
|
return $number; |
231
|
|
|
} |
232
|
2 |
|
throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature."); |
233
|
10 |
|
case 'none': |
234
|
9 |
|
return $arg; |
235
|
6 |
|
case 'select': |
236
|
|
|
/* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html |
237
|
|
|
selectStyle = (selector '{' message '}')+ |
238
|
|
|
*/ |
239
|
5 |
|
if (!isset($token[2])) { |
240
|
|
|
return false; |
241
|
|
|
} |
242
|
5 |
|
$select = self::tokenizePattern($token[2]); |
243
|
5 |
|
$c = count($select); |
244
|
5 |
|
$message = false; |
245
|
5 |
|
for ($i = 0; $i + 1 < $c; $i++) { |
246
|
5 |
|
if (is_array($select[$i]) || !is_array($select[$i + 1])) { |
247
|
|
|
return false; |
248
|
|
|
} |
249
|
5 |
|
$selector = trim($select[$i++]); |
250
|
5 |
|
if ($message === false && $selector === 'other' || $selector == $arg) { |
251
|
5 |
|
$message = implode(',', $select[$i]); |
252
|
|
|
} |
253
|
|
|
} |
254
|
5 |
|
if ($message !== false) { |
255
|
5 |
|
return $this->fallbackFormat($message, $args, $locale); |
256
|
|
|
} |
257
|
|
|
break; |
258
|
2 |
|
case 'plural': |
259
|
|
|
/* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html |
260
|
|
|
pluralStyle = [offsetValue] (selector '{' message '}')+ |
261
|
|
|
offsetValue = "offset:" number |
262
|
|
|
selector = explicitValue | keyword |
263
|
|
|
explicitValue = '=' number // adjacent, no white space in between |
264
|
|
|
keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ |
265
|
|
|
message: see MessageFormat |
266
|
|
|
*/ |
267
|
2 |
|
if (!isset($token[2])) { |
268
|
|
|
return false; |
269
|
|
|
} |
270
|
2 |
|
$plural = self::tokenizePattern($token[2]); |
271
|
2 |
|
$c = count($plural); |
272
|
2 |
|
$message = false; |
273
|
2 |
|
$offset = 0; |
274
|
2 |
|
for ($i = 0; $i + 1 < $c; $i++) { |
275
|
2 |
|
if (is_array($plural[$i]) || !is_array($plural[$i + 1])) { |
276
|
|
|
return false; |
277
|
|
|
} |
278
|
2 |
|
$selector = trim($plural[$i++]); |
279
|
|
|
|
280
|
2 |
|
if ($i == 1 && strncmp($selector, 'offset:', 7) === 0) { |
281
|
1 |
|
$offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7, $charset)) - 7, $charset)); |
282
|
1 |
|
$selector = trim(mb_substr($selector, $pos + 1, mb_strlen($selector, $charset), $charset)); |
283
|
|
|
} |
284
|
2 |
|
if ($message === false && $selector === 'other' || |
285
|
2 |
|
$selector[0] === '=' && (int) mb_substr($selector, 1, mb_strlen($selector, $charset), $charset) === $arg || |
286
|
2 |
|
$selector === 'one' && $arg - $offset == 1 |
287
|
|
|
) { |
288
|
2 |
|
$message = implode(',', str_replace('#', $arg - $offset, $plural[$i])); |
289
|
|
|
} |
290
|
|
|
} |
291
|
2 |
|
if ($message !== false) { |
292
|
2 |
|
return $this->fallbackFormat($message, $args, $locale); |
293
|
|
|
} |
294
|
|
|
break; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
return false; |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
Scrutinizer analyzes your
composer.json
/composer.lock
file if available to determine the classes, and functions that are defined by your dependencies.It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.