TranslatorTest.php 16 KB

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