setupManifest(); $this->alternateBaseSavePath = TEMP_PATH . DIRECTORY_SEPARATOR . 'i18nTextCollectorTest_webroot'; Filesystem::makeFolder($this->alternateBaseSavePath); } protected function tearDown(): void { if (is_dir($this->alternateBaseSavePath ?? '')) { Filesystem::removeFolder($this->alternateBaseSavePath); } $this->tearDownManifest(); parent::tearDown(); } public function testConcatenationInEntityValues() { $c = i18nTextCollector::create(); $module = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $php = <<assertEquals( [ 'Test.CONCATENATED' => [ 'default' => "Line 1 and Line '2' and Line \"3\"", 'comment' => 'Comment' ], 'Test.CONCATENATED2' => "Line \"4\" and Line 5" ], $c->collectFromCode($php, null, $module) ); } public function testCollectFromNewTemplateSyntaxUsingParserSubclass() { $c = i18nTextCollector::create(); $c->setWarnOnEmptyDefault(false); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $html = << <%t i18nTestModule.NEWMETHODSIG "New _t method signature test" %> <%t i18nTestModule.INJECTIONS_0 "Hello {name} {greeting}, and {goodbye}" name="Mark" greeting="welcome" goodbye="bye" %> <%t i18nTestModule.INJECTIONS_1 "Hello {name} {greeting}, and {goodbye}" name="Paul" greeting="welcome" goodbye="cya" %> <%t i18nTestModule.INJECTIONS_2 "Hello {name} {greeting}" is "context (ignored)" name="Steffen" greeting="Wilkommen" %> <%t i18nTestModule.INJECTIONS_3 name="Cat" greeting='meow' goodbye="meow" %> <%t i18nTestModule.INJECTIONS_4 name=\$absoluteBaseURL greeting=\$get_locale goodbye="global calls" %> <%t i18nTestModule.INJECTIONS_9 "An item|{count} items" is "Test Pluralisation" count=4 %> <%t SilverStripe\\TestModule\\i18nTestModule.INJECTIONS_10 "This string is namespaced" %> <%t SilverStripe\\\\TestModule\\\\i18nTestModule.INJECTIONS_11 "Escaped namespaced string" %> SS; $c->collectFromTemplate($html, null, $mymodule); $this->assertEquals( [ 'Test.SINGLEQUOTE' => 'Single Quote', 'i18nTestModule.NEWMETHODSIG' => "New _t method signature test", 'i18nTestModule.INJECTIONS_0' => "Hello {name} {greeting}, and {goodbye}", 'i18nTestModule.INJECTIONS_1' => "Hello {name} {greeting}, and {goodbye}", 'i18nTestModule.INJECTIONS_2' => [ 'default' => "Hello {name} {greeting}", 'comment' => 'context (ignored)', ], 'i18nTestModule.INJECTIONS_9' => [ 'one' => 'An item', 'other' => '{count} items', 'comment' => 'Test Pluralisation' ], 'SilverStripe\\TestModule\\i18nTestModule.INJECTIONS_10' => 'This string is namespaced', 'SilverStripe\\TestModule\\i18nTestModule.INJECTIONS_11' => 'Escaped namespaced string' ], $c->collectFromTemplate($html, null, $mymodule) ); // Test warning is raised on empty default $c->setWarnOnEmptyDefault(true); $this->expectNotice(); $this->expectNoticeMessage('Missing localisation default for key i18nTestModule.INJECTIONS_3'); $c->collectFromTemplate($html, null, $mymodule); } public function testCollectFromTemplateSimple() { $c = i18nTextCollector::create(); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $html = << SS; $this->assertEquals( [ 'Test.SINGLEQUOTE' => 'Single Quote' ], $c->collectFromTemplate($html, null, $mymodule) ); $html = << SS; $this->assertEquals( [ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ], $c->collectFromTemplate($html, null, $mymodule) ); $html = << SS; $this->assertEquals( [ 'Test.NOSEMICOLON' => "No Semicolon" ], $c->collectFromTemplate($html, null, $mymodule) ); } public function testCollectFromTemplateAdvanced() { $c = i18nTextCollector::create(); $c->setWarnOnEmptyDefault(false); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $html = << SS; $this->assertEquals( [ 'Test.PRIOANDCOMMENT' => [ 'default' => ' Prio and Value with "Double Quotes"', 'comment' => 'Comment with "Double Quotes"', ]], $c->collectFromTemplate($html, 'Test', $mymodule) ); $html = << SS; $this->assertEquals( [ 'Test.PRIOANDCOMMENT' => [ 'default' => " Prio and Value with 'Single Quotes'", 'comment' => "Comment with 'Single Quotes'", ]], $c->collectFromTemplate($html, 'Test', $mymodule) ); // Test empty $html = << SS; $this->assertEquals( [], $c->collectFromTemplate($html, null, $mymodule) ); // Test warning is raised on empty default $c->setWarnOnEmptyDefault(true); $this->expectNotice(); $this->expectNoticeMessage('Missing localisation default for key Test.PRIOANDCOMMENT'); $c->collectFromTemplate($html, 'Test', $mymodule); } public function testCollectFromCodeSimple() { $c = i18nTextCollector::create(); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $php = <<assertEquals( [ 'Test.SINGLEQUOTE' => 'Single Quote' ], $c->collectFromCode($php, null, $mymodule) ); $php = <<assertEquals( [ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ], $c->collectFromCode($php, null, $mymodule) ); } public function testCollectFromCodeAdvanced() { $c = i18nTextCollector::create(); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $php = <<assertEquals( [ 'Test.NEWLINES' => "New Lines" ], $c->collectFromCode($php, null, $mymodule) ); $php = <<assertEquals( [ 'Test.PRIOANDCOMMENT' => [ 'default' => ' Value with "Double Quotes"', 'comment' => 'Comment with "Double Quotes"', ] ], $c->collectFromCode($php, null, $mymodule) ); $php = <<assertEquals( [ 'Test.PRIOANDCOMMENT' => [ 'default' => " Value with 'Single Quotes'", 'comment' => "Comment with 'Single Quotes'" ] ], $c->collectFromCode($php, null, $mymodule) ); $php = <<assertEquals( [ 'Test.PRIOANDCOMMENT' => "Value with 'Escaped Single Quotes'" ], $c->collectFromCode($php, null, $mymodule) ); $php = <<assertEquals( [ 'Test.PRIOANDCOMMENT' => "Doublequoted Value with 'Unescaped Single Quotes'"], $c->collectFromCode($php, null, $mymodule) ); } public function testCollectFromCodeNamespace() { $c = i18nTextCollector::create(); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $php = <<assertEquals( [ 'SilverStripe\\Framework\\Core\\MyClass.NEWLINES' => "New Lines", 'SilverStripe\\Framework\\MyClass.ANOTHER_STRING' => 'Slash=\\, Quote=\'', 'SilverStripe\\Framework\\MyClass.DOUBLE_STRING' => 'Slash=\\, Quote="', 'SilverStripe\\Framework\\Core\\MyClass.SELF_CLASS' => 'Self Class', ], $c->collectFromCode($php, null, $mymodule) ); } public function testNewlinesInEntityValues() { $c = i18nTextCollector::create(); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $php = <<assertEquals( [ 'Test.NEWLINESINGLEQUOTE' => "Line 1{$eol}Line 2" ], $c->collectFromCode($php, null, $mymodule) ); $php = <<assertEquals( [ 'Test.NEWLINEDOUBLEQUOTE' => "Line 1{$eol}Line 2" ], $c->collectFromCode($php, null, $mymodule) ); } /** * Test extracting entities from the new _t method signature */ public function testCollectFromCodeNewSignature() { $c = i18nTextCollector::create(); $c->setWarnOnEmptyDefault(false); // Disable warnings for tests $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $php = <<"Paul", "greeting"=>"good you are here", "goodbye"=>"see you"]); _t("i18nTestModule.INJECTIONS3", "Hello {name} {greeting}. But it is late, {goodbye}", "New context (this should be ignored)", ["name"=>"Steffen", "greeting"=>"willkommen", "goodbye"=>"wiedersehen"]); _t('i18nTestModule.INJECTIONS4', ["name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"]); _t('i18nTestModule.INJECTIONS6', "Hello {name} {greeting}. But it is late, {goodbye}", ["name"=>"Paul", "greeting"=>"good you are here", "goodbye"=>"see you"]); _t("i18nTestModule.INJECTIONS7", "Hello {name} {greeting}. But it is late, {goodbye}", "New context (this should be ignored)", ["name"=>"Steffen", "greeting"=>"willkommen", "goodbye"=>"wiedersehen"]); _t('i18nTestModule.INJECTIONS8', ["name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"]); _t('i18nTestModule.INJECTIONS9', "An item|{count} items", ['count' => 4], "Test Pluralisation"); PHP; $collectedTranslatables = $c->collectFromCode($php, null, $mymodule); $expectedArray = [ 'i18nTestModule.INJECTIONS2' => "Hello {name} {greeting}. But it is late, {goodbye}", 'i18nTestModule.INJECTIONS3' => [ 'default' => "Hello {name} {greeting}. But it is late, {goodbye}", 'comment' => 'New context (this should be ignored)' ], 'i18nTestModule.INJECTIONS6' => "Hello {name} {greeting}. But it is late, {goodbye}", 'i18nTestModule.INJECTIONS7' => [ 'default' => "Hello {name} {greeting}. But it is late, {goodbye}", 'comment' => "New context (this should be ignored)", ], 'i18nTestModule.INJECTIONS9' => [ 'one' => 'An item', 'other' => '{count} items', 'comment' => 'Test Pluralisation', ], 'i18nTestModule.NEWMETHODSIG' => "New _t method signature test", ]; $this->assertEquals($expectedArray, $collectedTranslatables); // Test warning is raised on empty default $this->expectNotice(); $this->expectNoticeMessage('Missing localisation default for key i18nTestModule.INJECTIONS4'); $php = <<"Cat", "greeting"=>"meow", "goodbye"=>"meow"]); PHP; $c->setWarnOnEmptyDefault(true); $c->collectFromCode($php, null, $mymodule); } public function testUncollectableCode() { $c = i18nTextCollector::create(); $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $php = <<collectFromCode($php, null, $mymodule); // Only one item is collectable $expectedArray = [ 'Collectable.KEY4' => 'Default' ]; $this->assertEquals($expectedArray, $collectedTranslatables); } public function testCollectFromIncludedTemplates() { $c = i18nTextCollector::create(); $c->setWarnOnEmptyDefault(false); // Disable warnings for tests $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule'); $templateFilePath = $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss'; $html = file_get_contents($templateFilePath ?? ''); $matches = $c->collectFromTemplate($html, $templateFilePath, $mymodule); $this->assertArrayHasKey('i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE', $matches); $this->assertEquals( 'Layout Template no namespace', $matches['i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE'] ); $this->assertArrayHasKey('i18nTestModule.ss.REPLACEMENTNONAMESPACE', $matches); $this->assertEquals( 'My replacement no namespace: {replacement}', $matches['i18nTestModule.ss.REPLACEMENTNONAMESPACE'] ); $this->assertArrayHasKey('i18nTestModule.LAYOUTTEMPLATE', $matches); $this->assertEquals( 'Layout Template', $matches['i18nTestModule.LAYOUTTEMPLATE'] ); $this->assertArrayHasKey('i18nTestModule.REPLACEMENTNAMESPACE', $matches); $this->assertEquals( 'My replacement: {replacement}', $matches['i18nTestModule.REPLACEMENTNAMESPACE'] ); // Includes should not automatically inject translations into parent templates $this->assertArrayNotHasKey('i18nTestModule.WITHNAMESPACE', $matches); $this->assertArrayNotHasKey('i18nTestModuleInclude_ss.NONAMESPACE', $matches); $this->assertArrayNotHasKey('i18nTestModuleInclude_ss.REPLACEMENTINCLUDENAMESPACE', $matches); $this->assertArrayNotHasKey('i18nTestModuleInclude_ss.REPLACEMENTINCLUDENONAMESPACE', $matches); } public function testCollectMergesWithExisting() { $c = i18nTextCollector::create(); $c->setWarnOnEmptyDefault(false); $c->setWriter(new YamlWriter()); $c->basePath = $this->alternateBasePath; $c->baseSavePath = $this->alternateBaseSavePath; $entitiesByModule = $c->collect(null, true /* merge */); $this->assertArrayHasKey( 'i18nTestModule.ENTITY', $entitiesByModule['i18ntestmodule'], 'Retains existing entities' ); $this->assertArrayHasKey( 'i18nTestModule.NEWENTITY', $entitiesByModule['i18ntestmodule'], 'Adds new entities' ); // Test cross-module strings are set correctly $this->assertArrayHasKey( 'i18nProviderClass.OTHER_MODULE', $entitiesByModule['i18ntestmodule'] ); $this->assertEquals( [ 'comment' => 'Test string in another module', 'default' => 'i18ntestmodule string defined in i18nothermodule', ], $entitiesByModule['i18ntestmodule']['i18nProviderClass.OTHER_MODULE'] ); } public function testCollectFromFilesystemAndWriteMasterTables() { i18n::set_locale('en_US'); //set the locale to the US locale expected in the asserts i18n::config()->update('default_locale', 'en_US'); i18n::config()->update('missing_default_warning', false); $c = i18nTextCollector::create(); $c->setWarnOnEmptyDefault(false); $c->setWriter(new YamlWriter()); $c->basePath = $this->alternateBasePath; $c->baseSavePath = $this->alternateBaseSavePath; $c->run(); // i18ntestmodule $moduleLangFile = "{$this->alternateBaseSavePath}/i18ntestmodule/lang/" . $c->getDefaultLocale() . '.yml'; $this->assertTrue( file_exists($moduleLangFile ?? ''), 'Master language file can be written to modules /lang folder' ); $moduleLangFileContent = file_get_contents($moduleLangFile ?? ''); $this->assertStringContainsString( " ADDITION: Addition\n", $moduleLangFileContent ); $this->assertStringContainsString( " ENTITY: 'Entity with \"Double Quotes\"'\n", $moduleLangFileContent ); $this->assertStringContainsString( " MAINTEMPLATE: 'Main Template'\n", $moduleLangFileContent ); $this->assertStringContainsString( " OTHERENTITY: 'Other Entity'\n", $moduleLangFileContent ); $this->assertStringContainsString( " WITHNAMESPACE: 'Include Entity with Namespace'\n", $moduleLangFileContent ); $this->assertStringContainsString( " NONAMESPACE: 'Include Entity without Namespace'\n", $moduleLangFileContent ); // i18nothermodule $otherModuleLangFile = "{$this->alternateBaseSavePath}/i18nothermodule/lang/" . $c->getDefaultLocale() . '.yml'; $this->assertTrue( file_exists($otherModuleLangFile ?? ''), 'Master language file can be written to modules /lang folder' ); $otherModuleLangFileContent = file_get_contents($otherModuleLangFile ?? ''); $this->assertStringContainsString( " ENTITY: 'Other Module Entity'\n", $otherModuleLangFileContent ); $this->assertStringContainsString( " MAINTEMPLATE: 'Main Template Other Module'\n", $otherModuleLangFileContent ); } public function testCollectFromEntityProvidersInCustomObject() { // note: Disable _fakewebroot manifest for this test $this->popManifests(); $c = i18nTextCollector::create(); // Collect from MyObject.php $filePath = __DIR__ . '/i18nTest/MyObject.php'; $matches = $c->collectFromEntityProviders($filePath); $this->assertEquals( [ 'SilverStripe\Admin\LeftAndMain.OTHER_TITLE' => [ 'default' => 'Other title', 'module' => 'admin', ], 'SilverStripe\i18n\Tests\i18nTest\MyObject.PLURALNAME' => 'My Objects', 'SilverStripe\i18n\Tests\i18nTest\MyObject.PLURALS' => [ 'one' => 'A My Object', 'other' => '{count} My Objects', ], 'SilverStripe\i18n\Tests\i18nTest\MyObject.SINGULARNAME' => 'My Object', ], $matches ); } public function testCollectFromEntityProvidersInWebRoot() { // Collect from i18nProviderClass $c = i18nTextCollector::create(); $c->setWarnOnEmptyDefault(false); $c->setWriter(new YamlWriter()); $c->basePath = $this->alternateBasePath; $c->baseSavePath = $this->alternateBaseSavePath; $entitiesByModule = $c->collect(null, false); $this->assertEquals( [ 'comment' => 'Plural forms for the test class', 'one' => 'A class', 'other' => '{count} classes', ], $entitiesByModule['i18nothermodule']['i18nProviderClass.PLURALS'] ); $this->assertEquals( 'My Provider Class', $entitiesByModule['i18nothermodule']['i18nProviderClass.TITLE'] ); $this->assertEquals( [ 'comment' => 'Test string in another module', 'default' => 'i18ntestmodule string defined in i18nothermodule', ], $entitiesByModule['i18ntestmodule']['i18nProviderClass.OTHER_MODULE'] ); } /** * Test that duplicate keys are resolved to the appropriate modules */ public function testResolveDuplicates() { $collector = new Collector(); // Dummy data as collected $data1 = [ 'i18ntestmodule' => [ 'i18nTestModule.PLURALNAME' => 'Data Objects', 'i18nTestModule.SINGULARNAME' => 'Data Object', ], 'mymodule' => [ 'i18nTestModule.PLURALNAME' => 'Ignored String', 'i18nTestModule.STREETNAME' => 'Shortland Street', ], ]; $expected = [ 'i18ntestmodule' => [ 'i18nTestModule.PLURALNAME' => 'Data Objects', 'i18nTestModule.SINGULARNAME' => 'Data Object', ], 'mymodule' => [ // Removed PLURALNAME because this key doesn't exist in i18ntestmodule strings 'i18nTestModule.STREETNAME' => 'Shortland Street' ] ]; $resolved = $collector->resolveDuplicateConflicts_Test($data1); $this->assertEquals($expected, $resolved); // Test getConflicts $data2 = [ 'module1' => [ 'i18ntestmodule.ONE' => 'One', 'i18ntestmodule.TWO' => 'Two', 'i18ntestmodule.THREE' => 'Three', ], 'module2' => [ 'i18ntestmodule.THREE' => 'Three', ], 'module3' => [ 'i18ntestmodule.TWO' => 'Two', 'i18ntestmodule.THREE' => 'Three', ], ]; $conflictsA = $collector->getConflicts_Test($data2); sort($conflictsA); $this->assertEquals( ['i18ntestmodule.THREE', 'i18ntestmodule.TWO'], $conflictsA ); // Removing module3 should remove a conflict unset($data2['module3']); $conflictsB = $collector->getConflicts_Test($data2); $this->assertEquals( ['i18ntestmodule.THREE'], $conflictsB ); } /** * Test ability for textcollector to detect modules */ public function testModuleDetection() { $collector = new Collector(); $modules = ModuleLoader::inst()->getManifest()->getModules(); $this->assertEquals( [ 'i18nnonstandardmodule', 'i18nothermodule', 'i18ntestmodule', ], array_keys($modules ?? []) ); $this->assertEquals('i18ntestmodule', $collector->findModuleForClass_Test('i18nTestNamespacedClass')); $this->assertEquals( 'i18ntestmodule', $collector->findModuleForClass_Test('i18nTest\\i18nTestNamespacedClass') ); $this->assertEquals('i18ntestmodule', $collector->findModuleForClass_Test('i18nTestSubModule')); } /** * Test that text collector can detect module file lists properly */ public function testModuleFileList() { $collector = new Collector(); $collector->basePath = $this->alternateBasePath; $collector->baseSavePath = $this->alternateBaseSavePath; // Non-standard modules can't be safely filtered, so just index everything $nonStandardFiles = $collector->getFileListForModule_Test('i18nnonstandardmodule'); $nonStandardRoot = $this->alternateBasePath . '/i18nnonstandardmodule'; $this->assertEquals(3, count($nonStandardFiles ?? [])); $this->assertArrayHasKey("{$nonStandardRoot}/_config.php", $nonStandardFiles); $this->assertArrayHasKey("{$nonStandardRoot}/phpfile.php", $nonStandardFiles); $this->assertArrayHasKey("{$nonStandardRoot}/template.ss", $nonStandardFiles); // Normal module should have predictable dir structure $testFiles = $collector->getFileListForModule_Test('i18ntestmodule'); $testRoot = $this->alternateBasePath . '/i18ntestmodule'; $this->assertEquals(7, count($testFiles ?? [])); // Code in code folder is detected $this->assertArrayHasKey("{$testRoot}/code/i18nTestModule.php", $testFiles); $this->assertArrayHasKey("{$testRoot}/code/subfolder/_config.php", $testFiles); $this->assertArrayHasKey("{$testRoot}/code/subfolder/i18nTestSubModule.php", $testFiles); $this->assertArrayHasKey("{$testRoot}/code/subfolder/i18nTestNamespacedClass.php", $testFiles); // Templates in templates folder is detected $this->assertArrayHasKey("{$testRoot}/templates/Includes/i18nTestModuleInclude.ss", $testFiles); $this->assertArrayHasKey("{$testRoot}/templates/Layout/i18nTestModule.ss", $testFiles); $this->assertArrayHasKey("{$testRoot}/templates/i18nTestModule.ss", $testFiles); // Standard modules with code in odd places should only have code in those directories detected $otherFiles = $collector->getFileListForModule_Test('i18nothermodule'); $otherRoot = $this->alternateBasePath . '/i18nothermodule'; $this->assertEquals(4, count($otherFiles ?? [])); // Only detect well-behaved files $this->assertArrayHasKey("{$otherRoot}/code/i18nOtherModule.php", $otherFiles); $this->assertArrayHasKey("{$otherRoot}/code/i18nProviderClass.php", $otherFiles); $this->assertArrayHasKey("{$otherRoot}/code/i18nTestModuleDecorator.php", $otherFiles); $this->assertArrayHasKey("{$otherRoot}/templates/i18nOtherModule.ss", $otherFiles); } }