setupManifest(); } protected function tearDown(): void { $this->tearDownManifest(); parent::tearDown(); } public function testGetExistingTranslations() { $translations = i18n::getSources()->getKnownLocales(); $this->assertTrue(isset($translations['en_US']), 'Checking for en translation'); $this->assertEquals($translations['en_US'], 'English (United States)'); $this->assertTrue(isset($translations['de_DE']), 'Checking for de_DE translation'); } public function testGetClosestTranslation() { // Validate necessary assumptions for this test // As per set of locales loaded from _fakewebroot $translations = i18n::getSources()->getKnownLocales(); $this->assertEquals( [ 'en_GB', 'en_US', 'fr_FR', 'de_AT', 'de_DE', 'ja_JP', 'mi_NZ', 'pl_PL', 'es_AR', 'es_ES', ], array_keys($translations ?? []) ); // Test indeterminate locales $this->assertEmpty(i18n::get_closest_translation('zz_ZZ')); // Test english fallback $this->assertEquals('en_US', i18n::get_closest_translation('en_US')); $this->assertEquals('en_GB', i18n::get_closest_translation('en_GB')); $this->assertEquals('en_US', i18n::get_closest_translation('en_ZZ')); // Test spanish fallbacks $this->assertEquals('es_AR', i18n::get_closest_translation('es_AR')); $this->assertEquals('es_ES', i18n::get_closest_translation('es_ES')); $this->assertEquals('es_ES', i18n::get_closest_translation('es_XX')); } public function testDataObjectFieldLabels() { i18n::set_locale('de_DE'); // Load into the translator as a literal array data source /** @var SymfonyMessageProvider $provider */ $provider = Injector::inst()->get(MessageProvider::class); $provider->getTranslator()->addResource( 'array', [ i18nTest\TestDataObject::class . '.MyProperty' => 'MyProperty' ], 'en_US' ); $provider->getTranslator()->addResource( 'array', [ i18nTest\TestDataObject::class . '.MyProperty' => 'Mein Attribut' ], 'de_DE' ); $provider->getTranslator()->addResource( 'array', [ i18nTest\TestDataObject::class . '.MyUntranslatedProperty' => 'Mein Attribut' ], 'en_US' ); // Test field labels $obj = new i18nTest\TestDataObject(); $this->assertEquals( 'Mein Attribut', $obj->fieldLabel('MyProperty') ); $this->assertEquals( 'My untranslated property', $obj->fieldLabel('MyUntranslatedProperty') ); } public function testProvideI18nEntities() { /** @var SymfonyMessageProvider $provider */ $provider = Injector::inst()->get(MessageProvider::class); $provider->getTranslator()->addResource( 'array', [ i18nTest\TestObject::class . '.MyProperty' => 'Untranslated' ], 'en_US' ); $provider->getTranslator()->addResource( 'array', [ i18nTest\TestObject::class . '.my_translatable_property' => 'Übersetzt' ], 'de_DE' ); $this->assertEquals( i18nTest\TestObject::$my_translatable_property, 'Untranslated' ); $this->assertEquals( i18nTest\TestObject::my_translatable_property(), 'Untranslated' ); i18n::set_locale('en_US'); $this->assertEquals( i18nTest\TestObject::my_translatable_property(), 'Untranslated', 'Getter returns original static value when called in default locale' ); i18n::set_locale('de_DE'); $this->assertEquals( i18nTest\TestObject::my_translatable_property(), 'Übersetzt', 'Getter returns translated value when called in another locale' ); } public function testTemplateTranslation() { $oldLocale = i18n::get_locale(); i18n::config()->set('missing_default_warning', false); /** @var SymfonyMessageProvider $provider */ $provider = Injector::inst()->get(MessageProvider::class); $provider->getTranslator()->addResource( 'array', [ 'i18nTestModule.MAINTEMPLATE' => 'Main Template', 'REPLACEMENTNONAMESPACE' => 'My replacement no namespace: {replacement}', 'i18nTestModule.LAYOUTTEMPLATE' => 'Layout Template', 'LAYOUTTEMPLATENONAMESPACE' => 'Layout Template no namespace', 'i18nTestModule.REPLACEMENTNAMESPACE' => 'My replacement: {replacement}', 'i18nTestModule.WITHNAMESPACE' => 'Include Entity with Namespace', 'NONAMESPACE' => 'Include Entity without Namespace', 'i18nTestModuleInclude_ss.REPLACEMENTINCLUDENAMESPACE' => 'My include replacement: {replacement}', 'REPLACEMENTINCLUDENONAMESPACE' => 'My include replacement no namespace: {replacement}' ], 'en_US' ); $viewer = new SSViewer('i18nTestModule'); $parsedHtml = Convert::nl2os($viewer->process(new ArrayData([ 'TestProperty' => 'TestPropertyValue' ]))); $this->assertStringContainsString( Convert::nl2os("Layout Template\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("Layout Template no namespace\n"), $parsedHtml ); $provider->getTranslator()->addResource( 'array', [ 'i18nTestModule.MAINTEMPLATE' => 'TRANS Main Template', 'REPLACEMENTNONAMESPACE' => 'TRANS My replacement no namespace: {replacement}', 'i18nTestModule.LAYOUTTEMPLATE' => 'TRANS Layout Template', 'LAYOUTTEMPLATENONAMESPACE' => 'TRANS Layout Template no namespace', 'i18nTestModule.REPLACEMENTNAMESPACE' => 'TRANS My replacement: {replacement}', 'i18nTestModule.WITHNAMESPACE' => 'TRANS Include Entity with Namespace', 'NONAMESPACE' => 'TRANS Include Entity without Namespace', 'i18nTestModuleInclude_ss.REPLACEMENTINCLUDENAMESPACE' => 'TRANS My include replacement: {replacement}', 'REPLACEMENTINCLUDENONAMESPACE' => 'TRANS My include replacement no namespace: {replacement}', 'i18nTestModule.PLURALS' => 'An item|{count} items', ], 'de_DE' ); i18n::set_locale('de_DE'); $viewer = new SSViewer('i18nTestModule'); $parsedHtml = Convert::nl2os($viewer->process(new ArrayData(['TestProperty' => 'TestPropertyValue']))); $this->assertStringContainsString( Convert::nl2os("TRANS Main Template\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("TRANS Layout Template\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("TRANS Layout Template no namespace\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("TRANS My replacement: TestPropertyValue\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("TRANS Include Entity with Namespace\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("TRANS Include Entity without Namespace\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("TRANS My include replacement: TestPropertyValue\n"), $parsedHtml ); $this->assertStringContainsString( Convert::nl2os("TRANS My include replacement no namespace: TestPropertyValue\n"), $parsedHtml ); // Check plurals $this->assertStringContainsString('Single: An item', $parsedHtml); $this->assertStringContainsString('Multiple: 4 items', $parsedHtml); $this->assertStringContainsString('None: 0 items', $parsedHtml); i18n::set_locale($oldLocale); } public function testNewTMethodSignature() { /** @var SymfonyMessageProvider $provider */ $provider = Injector::inst()->get(MessageProvider::class); $provider->getTranslator()->addResource( 'array', [ 'i18nTestModule.NEWMETHODSIG' => 'TRANS New _t method signature test', 'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}' ], 'en_US' ); $entity = "i18nTestModule.INJECTIONS"; $default = "Hello {name} {greeting}. But it is late, {goodbye}"; // Test missing entity key $translated = i18n::_t( $entity . '_DOES_NOT_EXIST', $default, ["name"=>"Mark", "greeting"=>"welcome", "goodbye"=>"bye"] ); $this->assertStringContainsString( "Hello Mark welcome. But it is late, bye", $translated, "Testing fallback to the translation default (but using the injection array)" ); // Test standard injection $translated = i18n::_t( $entity, $default, ["name"=>"Paul", "greeting"=>"good you are here", "goodbye"=>"see you"] ); $this->assertStringContainsString( "TRANS Hello Paul good you are here. But it is late, see you", $translated, "Testing entity, default string and injection array" ); // @deprecated 5.0 Passing in context $translated = i18n::_t( $entity, $default, "New context (this should be ignored)", ["name"=>"Steffen", "greeting"=>"willkommen", "goodbye"=>"wiedersehen"] ); $this->assertStringContainsString( "TRANS Hello Steffen willkommen. But it is late, wiedersehen", $translated, "Full test of translation, using default, context and injection array" ); // Passing in non-associative arrays for placeholders is now an error $this->expectExceptionMessage(InvalidArgumentException::class); $this->expectExceptionMessage('Injection must be an associative array'); i18n::_t( $entity, // has {name} placeholders $default, ["Cat", "meow", "meow"] ); } /** * See @i18nTestModule.ss for the template that is being used for this test * */ public function testNewTemplateTranslation() { i18n::config()->set('missing_default_warning', false); /** @var SymfonyMessageProvider $provider */ $provider = Injector::inst()->get(MessageProvider::class); $provider->getTranslator()->addResource( 'array', [ 'i18nTestModule.NEWMETHODSIG' => 'TRANS New _t method signature test', 'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}' ], 'en_US' ); $viewer = new SSViewer('i18nTestModule'); $parsedHtml = Convert::nl2os($viewer->process(new ArrayData(['TestProperty' => 'TestPropertyValue']))); $this->assertStringContainsString( Convert::nl2os("Hello Mark welcome. But it is late, bye\n"), $parsedHtml, "Testing fallback to the translation default (but using the injection array)" ); $this->assertStringContainsString( Convert::nl2os("TRANS Hello Paul good you are here. But it is late, see you\n"), $parsedHtml, "Testing entity, default string and injection array" ); //test injected calls $this->assertStringContainsString( Convert::nl2os( "TRANS Hello " . Director::absoluteBaseURL() . " " . i18n::get_locale() . ". But it is late, global calls\n" ), $parsedHtml, "Testing a translation with just entity and injection array, but with global variables injected in" ); } public function testGetLocaleFromLang() { $this->assertEquals('en_US', i18n::getData()->localeFromLang('en')); $this->assertEquals('de_DE', i18n::getData()->localeFromLang('de_DE')); $this->assertEquals('xy_XY', i18n::getData()->localeFromLang('xy')); } public function testValidateLocale() { $this->assertTrue(i18n::getData()->validate('en_US'), 'Known locale in underscore format is valid'); $this->assertTrue(i18n::getData()->validate('en-US'), 'Known locale in dash format is valid'); $this->assertFalse(i18n::getData()->validate('en'), 'Short lang format is not valid'); $this->assertFalse(i18n::getData()->validate('xx_XX'), 'Unknown locale in correct format is not valid'); $this->assertFalse(i18n::getData()->validate(''), 'Empty string is not valid'); $this->assertTrue(i18n::getData()->validate('de_DE'), 'Known locale where language is same as region'); $this->assertTrue(i18n::getData()->validate('fr-FR'), 'Known locale where language is same as region'); $this->assertTrue(i18n::getData()->validate('zh_cmn'), 'Known locale with all lowercase letters'); } public function testTranslate() { /** @var SymfonyMessageProvider $provider */ $provider = Injector::inst()->get(MessageProvider::class); $provider->getTranslator()->addResource( 'array', [ 'i18nTestModule.ENTITY' => 'Entity with "Double Quotes"' ], 'en_US' ); $provider->getTranslator()->addResource( 'array', [ 'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de)', 'i18nTestModule.ADDITION' => 'Addition (de)', ], 'de' ); $provider->getTranslator()->addResource( 'array', [ 'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de_AT)', ], 'de_AT' ); $this->assertEquals( 'Entity with "Double Quotes"', i18n::_t('i18nTestModule.ENTITY', 'Ignored default'), 'Returns translation in default language' ); i18n::set_locale('de'); $this->assertEquals( 'Entity with "Double Quotes" (de)', i18n::_t('i18nTestModule.ENTITY', 'Entity with "Double Quotes"'), 'Returns translation according to current locale' ); i18n::set_locale('de_AT'); $this->assertEquals( 'Entity with "Double Quotes" (de_AT)', i18n::_t('i18nTestModule.ENTITY', 'Entity with "Double Quotes"'), 'Returns specific regional translation if available' ); $this->assertEquals( 'Addition (de)', i18n::_t('i18nTestModule.ADDITION', 'Addition'), 'Returns fallback non-regional translation if regional is not available' ); i18n::set_locale('fr'); $this->assertEquals( 'Entity with "Double Quotes" (fr)', i18n::_t('i18nTestModule.ENTITY', 'Entity with "Double Quotes"'), 'Non-specific locales fall back to language-only localisations' ); } public function pluralisationDataProvider() { return [ // English - 2 plural forms ['en_NZ', 0, '0 months'], ['en_NZ', 1, 'A month'], ['en_NZ', 2, '2 months'], ['en_NZ', 5, '5 months'], ['en_NZ', 10, '10 months'], // Polish - 4 plural forms ['pl_PL', 0, '0 miesięcy'], ['pl_PL', 1, '1 miesiąc'], ['pl_PL', 2, '2 miesiące'], ['pl_PL', 5, '5 miesięcy'], ['pl_PL', 10, '10 miesięcy'], // Japanese - 1 plural form ['ja_JP', 0, '0日'], ['ja_JP', 1, '1日'], ['ja_JP', 2, '2日'], ['ja_JP', 5, '5日'], ['ja_JP', 10, '10日'], ]; } /** * @dataProvider pluralisationDataProvider() * @param string $locale * @param int $count * @param string $expected */ public function testPluralisation($locale, $count, $expected) { i18n::set_locale($locale); $this->assertEquals( $expected, _t('Month.PLURALS', 'A month|{count} months', ['count' => $count]), "Plural form in locale $locale with count $count should be $expected" ); } public function testGetLanguageName() { i18n::config()->merge( 'common_languages', ['de_CGN' => ['name' => 'German (Cologne)', 'native' => 'Kölsch']] ); $this->assertEquals('German', i18n::getData()->languageName('de_CGN')); $this->assertEquals('Deutsch', i18n::with_locale('de_CGN', function () { return i18n::getData()->languageName('de_CGN'); })); } }