TranslatorTest.php 16 KB

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