blob: c2c30a9172909714b86adb914d08bffd111e5b54 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Contracts\Translation\Test;
13
14use PHPUnit\Framework\TestCase;
15use Symfony\Contracts\Translation\TranslatorInterface;
16use Symfony\Contracts\Translation\TranslatorTrait;
17
18/**
19 * Test should cover all languages mentioned on http://translate.sourceforge.net/wiki/l10n/pluralforms
20 * and Plural forms mentioned on http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms.
21 *
22 * See also https://developer.mozilla.org/en/Localization_and_Plurals which mentions 15 rules having a maximum of 6 forms.
23 * The mozilla code is also interesting to check for.
24 *
25 * As mentioned by chx http://drupal.org/node/1273968 we can cover all by testing number from 0 to 199
26 *
27 * The goal to cover all languages is to far fetched so this test case is smaller.
28 *
29 * @author Clemens Tolboom clemens@build2be.nl
30 */
31class TranslatorTest extends TestCase
32{
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010033 private $defaultLocale;
34
35 protected function setUp(): void
36 {
37 $this->defaultLocale = \Locale::getDefault();
38 \Locale::setDefault('en');
39 }
40
41 protected function tearDown(): void
42 {
43 \Locale::setDefault($this->defaultLocale);
44 }
45
46 public function getTranslator(): TranslatorInterface
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020047 {
48 return new class() implements TranslatorInterface {
49 use TranslatorTrait;
50 };
51 }
52
53 /**
54 * @dataProvider getTransTests
55 */
56 public function testTrans($expected, $id, $parameters)
57 {
58 $translator = $this->getTranslator();
59
60 $this->assertEquals($expected, $translator->trans($id, $parameters));
61 }
62
63 /**
64 * @dataProvider getTransChoiceTests
65 */
66 public function testTransChoiceWithExplicitLocale($expected, $id, $number)
67 {
68 $translator = $this->getTranslator();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020069
70 $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
71 }
72
73 /**
74 * @requires extension intl
75 *
76 * @dataProvider getTransChoiceTests
77 */
78 public function testTransChoiceWithDefaultLocale($expected, $id, $number)
79 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020080 $translator = $this->getTranslator();
81
82 $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
83 }
84
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010085 /**
86 * @dataProvider getTransChoiceTests
87 */
88 public function testTransChoiceWithEnUsPosix($expected, $id, $number)
89 {
90 $translator = $this->getTranslator();
91 $translator->setLocale('en_US_POSIX');
92
93 $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
94 }
95
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020096 public function testGetSetLocale()
97 {
98 $translator = $this->getTranslator();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020099
100 $this->assertEquals('en', $translator->getLocale());
101 }
102
103 /**
104 * @requires extension intl
105 */
106 public function testGetLocaleReturnsDefaultLocaleIfNotSet()
107 {
108 $translator = $this->getTranslator();
109
110 \Locale::setDefault('pt_BR');
111 $this->assertEquals('pt_BR', $translator->getLocale());
112
113 \Locale::setDefault('en');
114 $this->assertEquals('en', $translator->getLocale());
115 }
116
117 public function getTransTests()
118 {
119 return [
120 ['Symfony is great!', 'Symfony is great!', []],
121 ['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']],
122 ];
123 }
124
125 public function getTransChoiceTests()
126 {
127 return [
128 ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
129 ['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1],
130 ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
131 ['There are 0 apples', 'There is 1 apple|There are %count% apples', 0],
132 ['There is 1 apple', 'There is 1 apple|There are %count% apples', 1],
133 ['There are 10 apples', 'There is 1 apple|There are %count% apples', 10],
134 // custom validation messages may be coded with a fixed value
135 ['There are 2 apples', 'There are 2 apples', 2],
136 ];
137 }
138
139 /**
140 * @dataProvider getInternal
141 */
142 public function testInterval($expected, $number, $interval)
143 {
144 $translator = $this->getTranslator();
145
146 $this->assertEquals($expected, $translator->trans($interval.' foo|[1,Inf[ bar', ['%count%' => $number]));
147 }
148
149 public function getInternal()
150 {
151 return [
152 ['foo', 3, '{1,2, 3 ,4}'],
153 ['bar', 10, '{1,2, 3 ,4}'],
154 ['bar', 3, '[1,2]'],
155 ['foo', 1, '[1,2]'],
156 ['foo', 2, '[1,2]'],
157 ['bar', 1, ']1,2['],
158 ['bar', 2, ']1,2['],
159 ['foo', log(0), '[-Inf,2['],
160 ['foo', -log(0), '[-2,+Inf]'],
161 ];
162 }
163
164 /**
165 * @dataProvider getChooseTests
166 */
167 public function testChoose($expected, $id, $number, $locale = null)
168 {
169 $translator = $this->getTranslator();
170
171 $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number], null, $locale));
172 }
173
174 public function testReturnMessageIfExactlyOneStandardRuleIsGiven()
175 {
176 $translator = $this->getTranslator();
177
178 $this->assertEquals('There are two apples', $translator->trans('There are two apples', ['%count%' => 2]));
179 }
180
181 /**
182 * @dataProvider getNonMatchingMessages
183 */
184 public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number)
185 {
186 $this->expectException(\InvalidArgumentException::class);
187 $translator = $this->getTranslator();
188
189 $translator->trans($id, ['%count%' => $number]);
190 }
191
192 public function getNonMatchingMessages()
193 {
194 return [
195 ['{0} There are no apples|{1} There is one apple', 2],
196 ['{1} There is one apple|]1,Inf] There are %count% apples', 0],
197 ['{1} There is one apple|]2,Inf] There are %count% apples', 2],
198 ['{0} There are no apples|There is one apple', 2],
199 ];
200 }
201
202 public function getChooseTests()
203 {
204 return [
205 ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
206 ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
207 ['There are no apples', '{0}There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
208
209 ['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1],
210
211 ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
212 ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10],
213 ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
214
215 ['There are 0 apples', 'There is one apple|There are %count% apples', 0],
216 ['There is one apple', 'There is one apple|There are %count% apples', 1],
217 ['There are 10 apples', 'There is one apple|There are %count% apples', 10],
218
219 ['There are 0 apples', 'one: There is one apple|more: There are %count% apples', 0],
220 ['There is one apple', 'one: There is one apple|more: There are %count% apples', 1],
221 ['There are 10 apples', 'one: There is one apple|more: There are %count% apples', 10],
222
223 ['There are no apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 0],
224 ['There is one apple', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 1],
225 ['There are 10 apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 10],
226
227 ['', '{0}|{1} There is one apple|]1,Inf] There are %count% apples', 0],
228 ['', '{0} There are no apples|{1}|]1,Inf] There are %count% apples', 1],
229
230 // Indexed only tests which are Gettext PoFile* compatible strings.
231 ['There are 0 apples', 'There is one apple|There are %count% apples', 0],
232 ['There is one apple', 'There is one apple|There are %count% apples', 1],
233 ['There are 2 apples', 'There is one apple|There are %count% apples', 2],
234
235 // Tests for float numbers
236 ['There is almost one apple', '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7],
237 ['There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1],
238 ['There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7],
239 ['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0],
240 ['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0],
241 ['There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0],
242
243 // Test texts with new-lines
244 // with double-quotes and \n in id & double-quotes and actual newlines in text
245 ["This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a
246 new-line in it. Selector = 0.|{1}This is a text with a
247 new-line in it. Selector = 1.|[1,Inf]This is a text with a
248 new-line in it. Selector > 1.', 0],
249 // with double-quotes and \n in id and single-quotes and actual newlines in text
250 ["This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a
251 new-line in it. Selector = 0.|{1}This is a text with a
252 new-line in it. Selector = 1.|[1,Inf]This is a text with a
253 new-line in it. Selector > 1.', 1],
254 ["This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a
255 new-line in it. Selector = 0.|{1}This is a text with a
256 new-line in it. Selector = 1.|[1,Inf]This is a text with a
257 new-line in it. Selector > 1.', 5],
258 // with double-quotes and id split accros lines
259 ['This is a text with a
260 new-line in it. Selector = 1.', '{0}This is a text with a
261 new-line in it. Selector = 0.|{1}This is a text with a
262 new-line in it. Selector = 1.|[1,Inf]This is a text with a
263 new-line in it. Selector > 1.', 1],
264 // with single-quotes and id split accros lines
265 ['This is a text with a
266 new-line in it. Selector > 1.', '{0}This is a text with a
267 new-line in it. Selector = 0.|{1}This is a text with a
268 new-line in it. Selector = 1.|[1,Inf]This is a text with a
269 new-line in it. Selector > 1.', 5],
270 // with single-quotes and \n in text
271 ['This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0],
272 // with double-quotes and id split accros lines
273 ["This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1],
274 // esacape pipe
275 ['This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0],
276 // Empty plural set (2 plural forms) from a .PO file
277 ['', '|', 1],
278 // Empty plural set (3 plural forms) from a .PO file
279 ['', '||', 1],
280
281 // Floating values
282 ['1.5 liters', '%count% liter|%count% liters', 1.5],
283 ['1.5 litre', '%count% litre|%count% litres', 1.5, 'fr'],
284
285 // Negative values
286 ['-1 degree', '%count% degree|%count% degrees', -1],
287 ['-1 degré', '%count% degré|%count% degrés', -1],
288 ['-1.5 degrees', '%count% degree|%count% degrees', -1.5],
289 ['-1.5 degré', '%count% degré|%count% degrés', -1.5, 'fr'],
290 ['-2 degrees', '%count% degree|%count% degrees', -2],
291 ['-2 degrés', '%count% degré|%count% degrés', -2],
292 ];
293 }
294
295 /**
296 * @dataProvider failingLangcodes
297 */
298 public function testFailedLangcodes($nplural, $langCodes)
299 {
300 $matrix = $this->generateTestData($langCodes);
301 $this->validateMatrix($nplural, $matrix, false);
302 }
303
304 /**
305 * @dataProvider successLangcodes
306 */
307 public function testLangcodes($nplural, $langCodes)
308 {
309 $matrix = $this->generateTestData($langCodes);
310 $this->validateMatrix($nplural, $matrix);
311 }
312
313 /**
314 * This array should contain all currently known langcodes.
315 *
316 * As it is impossible to have this ever complete we should try as hard as possible to have it almost complete.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200317 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100318 public function successLangcodes(): array
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200319 {
320 return [
321 ['1', ['ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky']],
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100322 ['2', ['nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM', 'en_US_POSIX']],
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200323 ['3', ['be', 'bs', 'cs', 'hr']],
324 ['4', ['cy', 'mt', 'sl']],
325 ['6', ['ar']],
326 ];
327 }
328
329 /**
330 * This array should be at least empty within the near future.
331 *
332 * This both depends on a complete list trying to add above as understanding
333 * the plural rules of the current failing languages.
334 *
335 * @return array with nplural together with langcodes
336 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100337 public function failingLangcodes(): array
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200338 {
339 return [
340 ['1', ['fa']],
341 ['2', ['jbo']],
342 ['3', ['cbs']],
343 ['4', ['gd', 'kw']],
344 ['5', ['ga']],
345 ];
346 }
347
348 /**
349 * We validate only on the plural coverage. Thus the real rules is not tested.
350 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100351 * @param string $nplural Plural expected
352 * @param array $matrix Containing langcodes and their plural index values
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200353 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100354 protected function validateMatrix(string $nplural, array $matrix, bool $expectSuccess = true)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200355 {
356 foreach ($matrix as $langCode => $data) {
357 $indexes = array_flip($data);
358 if ($expectSuccess) {
359 $this->assertEquals($nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms.");
360 } else {
361 $this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms.");
362 }
363 }
364 }
365
366 protected function generateTestData($langCodes)
367 {
368 $translator = new class() {
369 use TranslatorTrait {
370 getPluralizationRule as public;
371 }
372 };
373
374 $matrix = [];
375 foreach ($langCodes as $langCode) {
376 for ($count = 0; $count < 200; ++$count) {
377 $plural = $translator->getPluralizationRule($count, $langCode);
378 $matrix[$langCode][$count] = $plural;
379 }
380 }
381
382 return $matrix;
383 }
384}