PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / vendor / gettext / languages / src / Language.php

Language.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at vendor/gettext/languages/src/Language.php

413 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Gettext\Languages;
4
5 use Exception;
6
7 /**
8 * Main class to convert the plural rules of a language from CLDR to gettext.
9 */
10 class Language
11 {
12 /**
13 * The language ID.
14 *
15 * @var string
16 */
17 public $id;
18
19 /**
20 * The language name.
21 *
22 * @var string
23 */
24 public $name;
25
26 /**
27 * If this language is deprecated: the gettext code of the new language.
28 *
29 * @var string|null
30 */
31 public $supersededBy;
32
33 /**
34 * The script name.
35 *
36 * @var string|null
37 */
38 public $script;
39
40 /**
41 * The territory name.
42 *
43 * @var string|null
44 */
45 public $territory;
46
47 /**
48 * The name of the base language.
49 *
50 * @var string|null
51 */
52 public $baseLanguage;
53
54 /**
55 * The list of categories.
56 *
57 * @var \Gettext\Languages\Category[]
58 */
59 public $categories;
60
61 /**
62 * The gettext formula to decide which category should be applied.
63 *
64 * @var string
65 */
66 public $formula;
67
68 /**
69 * Initialize the instance and parse the language code.
70 *
71 * @param array $info The result of CldrData::getLanguageInfo()
72 *
73 * @throws \Exception throws an Exception if $fullId is not valid
74 */
75 private function __construct($info)
76 {
77 $this->id = $info['id'];
78 $this->name = $info['name'];
79 $this->supersededBy = isset($info['supersededBy']) ? $info['supersededBy'] : null;
80 $this->script = isset($info['script']) ? $info['script'] : null;
81 $this->territory = isset($info['territory']) ? $info['territory'] : null;
82 $this->baseLanguage = isset($info['baseLanguage']) ? $info['baseLanguage'] : null;
83 // Let's build the category list
84 $this->categories = array();
85 foreach ($info['categories'] as $cldrCategoryId => $cldrFormulaAndExamples) {
86 $category = new Category($cldrCategoryId, $cldrFormulaAndExamples);
87 foreach ($this->categories as $c) {
88 if ($category->id === $c->id) {
89 throw new Exception("The category '{$category->id}' is specified more than once");
90 }
91 }
92 $this->categories[] = $category;
93 }
94 if (empty($this->categories)) {
95 throw new Exception("The language '{$info['id']}' does not have any plural category");
96 }
97 // Let's sort the categories from 'zero' to 'other'
98 usort($this->categories, function (Category $category1, Category $category2) {
99 return array_search($category1->id, CldrData::$categories) - array_search($category2->id, CldrData::$categories);
100 });
101 // The 'other' category should always be there
102 if ($this->categories[count($this->categories) - 1]->id !== CldrData::OTHER_CATEGORY) {
103 throw new Exception("The language '{$info['id']}' does not have the '" . CldrData::OTHER_CATEGORY . "' plural category");
104 }
105 $this->checkAlwaysTrueCategories();
106 $this->checkAlwaysFalseCategories();
107 $this->checkAllCategoriesWithExamples();
108 $this->formula = $this->buildFormula();
109 }
110
111 /**
112 * Return a list of all languages available.
113 *
114 * @throws \Exception
115 *
116 * @return \Gettext\Languages\Language[]
117 */
118 public static function getAll()
119 {
120 $result = array();
121 foreach (array_keys(CldrData::getLanguageNames()) as $cldrLanguageId) {
122 $result[] = new self(CldrData::getLanguageInfo($cldrLanguageId));
123 }
124
125 return $result;
126 }
127
128 /**
129 * Return a Language instance given the language id.
130 *
131 * @param string $id
132 *
133 * @return \Gettext\Languages\Language|null
134 */
135 public static function getById($id)
136 {
137 $result = null;
138 $info = CldrData::getLanguageInfo($id);
139 if (isset($info)) {
140 $result = new self($info);
141 }
142
143 return $result;
144 }
145
146 /**
147 * Returns a clone of this instance with all the strings to US-ASCII.
148 *
149 * @return \Gettext\Languages\Language
150 */
151 public function getUSAsciiClone()
152 {
153 $clone = clone $this;
154 self::asciifier($clone->name);
155 self::asciifier($clone->formula);
156 $clone->categories = array();
157 foreach ($this->categories as $category) {
158 $categoryClone = clone $category;
159 self::asciifier($categoryClone->examples);
160 $clone->categories[] = $categoryClone;
161 }
162
163 return $clone;
164 }
165
166 /**
167 * Build the formula starting from the currently defined categories.
168 *
169 * @param bool $withoutParenthesis TRUE to build a formula in standard gettext format, FALSE (default) to build a PHP-compatible formula
170 *
171 * @return string
172 */
173 public function buildFormula($withoutParenthesis = false)
174 {
175 $numCategories = count($this->categories);
176 switch ($numCategories) {
177 case 1:
178 // Just one category
179 return '0';
180 case 2:
181 return self::reduceFormula(self::reverseFormula($this->categories[0]->formula));
182 default:
183 $formula = (string) ($numCategories - 1);
184 for ($i = $numCategories - 2; $i >= 0; $i--) {
185 $f = self::reduceFormula($this->categories[$i]->formula);
186 if (!$withoutParenthesis && !preg_match('/^\([^()]+\)$/', $f)) {
187 $f = "({$f})";
188 }
189 $formula = "{$f} ? {$i} : {$formula}";
190 if (!$withoutParenthesis && $i > 0) {
191 $formula = "({$formula})";
192 }
193 }
194
195 return $formula;
196 }
197 }
198
199 /**
200 * Let's look for categories that will always occur.
201 * This because with decimals (CLDR) we may have more cases, with integers (gettext) we have just one case.
202 * If we found that (single) category we reduce the categories to that one only.
203 *
204 * @throws \Exception
205 */
206 private function checkAlwaysTrueCategories()
207 {
208 $alwaysTrueCategory = null;
209 foreach ($this->categories as $category) {
210 if ($category->formula === true) {
211 if (!isset($category->examples)) {
212 throw new Exception("The category '{$category->id}' should always occur, but it does not have examples (so for CLDR it will never occur for integers!)");
213 }
214 $alwaysTrueCategory = $category;
215 break;
216 }
217 }
218 if (isset($alwaysTrueCategory)) {
219 foreach ($this->categories as $category) {
220 if (($category !== $alwaysTrueCategory) && isset($category->examples)) {
221 throw new Exception("The category '{$category->id}' should never occur, but it has some examples (so for CLDR it will occur!)");
222 }
223 }
224 $alwaysTrueCategory->id = CldrData::OTHER_CATEGORY;
225 $alwaysTrueCategory->formula = null;
226 $this->categories = array($alwaysTrueCategory);
227 }
228 }
229
230 /**
231 * Let's look for categories that will never occur.
232 * This because with decimals (CLDR) we may have more cases, with integers (gettext) we have some less cases.
233 * If we found those categories we strip them out.
234 *
235 * @throws \Exception
236 */
237 private function checkAlwaysFalseCategories()
238 {
239 $filtered = array();
240 foreach ($this->categories as $category) {
241 if ($category->formula === false) {
242 if (isset($category->examples)) {
243 throw new Exception("The category '{$category->id}' should never occur, but it has examples (so for CLDR it may occur!)");
244 }
245 } else {
246 $filtered[] = $category;
247 }
248 }
249 $this->categories = $filtered;
250 }
251
252 /**
253 * Let's look for categories that don't have examples.
254 * This because with decimals (CLDR) we may have more cases, with integers (gettext) we have some less cases.
255 * If we found those categories, we check that they never occur and we strip them out.
256 *
257 * @throws \Exception
258 */
259 private function checkAllCategoriesWithExamples()
260 {
261 $allCategoriesIds = array();
262 $goodCategories = array();
263 $badCategories = array();
264 $badCategoriesIds = array();
265 foreach ($this->categories as $category) {
266 $allCategoriesIds[] = $category->id;
267 if (isset($category->examples)) {
268 $goodCategories[] = $category;
269 } else {
270 $badCategories[] = $category;
271 $badCategoriesIds[] = $category->id;
272 }
273 }
274 if (empty($badCategories)) {
275 return;
276 }
277 $removeCategoriesWithoutExamples = false;
278 switch (implode(',', $badCategoriesIds) . '@' . implode(',', $allCategoriesIds)) {
279 case CldrData::OTHER_CATEGORY . '@one,few,many,' . CldrData::OTHER_CATEGORY:
280 switch ($this->buildFormula()) {
281 case '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : ((n % 10 == 0 || n % 10 >= 5 && n % 10 <= 9 || n % 100 >= 11 && n % 100 <= 14) ? 2 : 3))':
282 // Numbers ending with 0 => case 2 ('many')
283 // Numbers ending with 1 but not with 11 => case 0 ('one')
284 // Numbers ending with 11 => case 2 ('many')
285 // Numbers ending with 2 but not with 12 => case 1 ('few')
286 // Numbers ending with 12 => case 2 ('many')
287 // Numbers ending with 3 but not with 13 => case 1 ('few')
288 // Numbers ending with 13 => case 2 ('many')
289 // Numbers ending with 4 but not with 14 => case 1 ('few')
290 // Numbers ending with 14 => case 2 ('many')
291 // Numbers ending with 5 => case 2 ('many')
292 // Numbers ending with 6 => case 2 ('many')
293 // Numbers ending with 7 => case 2 ('many')
294 // Numbers ending with 8 => case 2 ('many')
295 // Numbers ending with 9 => case 2 ('many')
296 // => the 'other' case never occurs: use 'other' for 'many'
297 $removeCategoriesWithoutExamples = true;
298 break;
299 case '(n == 1) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : ((n != 1 && (n % 10 == 0 || n % 10 == 1) || n % 10 >= 5 && n % 10 <= 9 || n % 100 >= 12 && n % 100 <= 14) ? 2 : 3))':
300 // Numbers ending with 0 => case 2 ('many')
301 // Numbers ending with 1 but not number 1 => case 2 ('many')
302 // Number 1 => case 0 ('one')
303 // Numbers ending with 2 but not with 12 => case 1 ('few')
304 // Numbers ending with 12 => case 2 ('many')
305 // Numbers ending with 3 but not with 13 => case 1 ('few')
306 // Numbers ending with 13 => case 2 ('many')
307 // Numbers ending with 4 but not with 14 => case 1 ('few')
308 // Numbers ending with 14 => case 2 ('many')
309 // Numbers ending with 5 => case 2 ('many')
310 // Numbers ending with 6 => case 2 ('many')
311 // Numbers ending with 7 => case 2 ('many')
312 // Numbers ending with 8 => case 2 ('many')
313 // Numbers ending with 9 => case 2 ('many')
314 // => the 'other' case never occurs: use 'other' for 'many'
315 $removeCategoriesWithoutExamples = true;
316 break;
317 }
318 }
319 if (!$removeCategoriesWithoutExamples) {
320 throw new Exception("Unhandled case of plural categories without examples '" . implode(', ', $badCategoriesIds) . "' out of '" . implode(', ', $allCategoriesIds) . "'");
321 }
322 if ($badCategories[count($badCategories) - 1]->id === CldrData::OTHER_CATEGORY) {
323 // We're removing the 'other' cagory: let's change the last good category to 'other'
324 $lastGood = $goodCategories[count($goodCategories) - 1];
325 $lastGood->id = CldrData::OTHER_CATEGORY;
326 $lastGood->formula = null;
327 }
328 $this->categories = $goodCategories;
329 }
330
331 /**
332 * Reverse a formula.
333 *
334 * @param string $formula
335 *
336 * @throws \Exception
337 *
338 * @return string
339 */
340 private static function reverseFormula($formula)
341 {
342 if (preg_match('/^n( % \d+)? == \d+(\.\.\d+|,\d+)*?$/', $formula)) {
343 return str_replace(' == ', ' != ', $formula);
344 }
345 if (preg_match('/^n( % \d+)? != \d+(\.\.\d+|,\d+)*?$/', $formula)) {
346 return str_replace(' != ', ' == ', $formula);
347 }
348 if (preg_match('/^\(?n == \d+ \|\| n == \d+\)?$/', $formula)) {
349 return trim(str_replace(array(' == ', ' || '), array(' != ', ' && '), $formula), '()');
350 }
351 $m = null;
352 if (preg_match('/^(n(?: % \d+)?) == (\d+) && (n(?: % \d+)?) != (\d+)$/', $formula, $m)) {
353 return "{$m[1]} != {$m[2]} || {$m[3]} == {$m[4]}";
354 }
355 switch ($formula) {
356 case '(n == 1 || n == 2 || n == 3) || n % 10 != 4 && n % 10 != 6 && n % 10 != 9':
357 return 'n != 1 && n != 2 && n != 3 && (n % 10 == 4 || n % 10 == 6 || n % 10 == 9)';
358 case '(n == 0 || n == 1) || n >= 11 && n <= 99':
359 return 'n >= 2 && (n < 11 || n > 99)';
360 }
361 throw new Exception("Unable to reverse the formula '{$formula}'");
362 }
363
364 /**
365 * Reduce some excessively complex formulas.
366 *
367 * @param string $formula
368 *
369 * @return string
370 */
371 private static function reduceFormula($formula)
372 {
373 $map = array(
374 'n != 0 && n != 1' => 'n > 1',
375 '(n == 0 || n == 1) && n != 0' => 'n == 1',
376 );
377
378 return isset($map[$formula]) ? $map[$formula] : $formula;
379 }
380
381 /**
382 * Take one variable and, if it's a string, we transliterate it to US-ASCII.
383 *
384 * @param mixed $value the variable to work on
385 *
386 * @throws \Exception
387 */
388 private static function asciifier(&$value)
389 {
390 if (is_string($value) && $value !== '') {
391 // Avoid converting from 'Ÿ' to '"Y', let's prefer 'Y'
392 $value = strtr($value, array(
393 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', '�
394 ' => 'A',
395 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',
396 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
397 'Ñ' => 'N',
398 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O',
399 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U',
400 'Ÿ' => 'Y', 'Ý' => 'Y',
401 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a',
402 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
403 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
404 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o',
405 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',
406 'ý' => 'y', 'ÿ' => 'y',
407 '…' => '...',
408 'ʼ' => "'", '’' => "'",
409 ));
410 }
411 }
412 }
413