From 728e691a1a9cba4653afbcddb561c1a4c9df876f Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 6 Nov 2008 02:50:14 +0000 Subject: [PATCH] BUGFIX Added SSViwer support for i18n namespaces in templates with <% _t('MyNamespace.MyEntity', ... %>, to work around magically added namespaces from the parsed template file. Those auto-namespaces were logically not working in includes, as the parsing context is always the including template. Legacy support for auto-namespaces is still present due to its high usage. ENHANCEMENT Added unit tests for i18n template parsing git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@65361 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/SSViewer.php | 18 ++- core/i18n.php | 20 ++- .../Includes/i18nTestModuleInclude.ss | 4 +- .../templates/Layout/i18nTestModule.ss | 5 +- .../templates/i18nTestModule.ss | 3 +- tests/i18n/i18nTest.php | 130 ++++++++++++++++++ tests/i18n/i18nTextCollectorTest.php | 48 ++++++- 7 files changed, 211 insertions(+), 17 deletions(-) diff --git a/core/SSViewer.php b/core/SSViewer.php index 2befda7e3..4f22c4167 100644 --- a/core/SSViewer.php +++ b/core/SSViewer.php @@ -433,14 +433,20 @@ class SSViewer extends Object { $content = ereg_replace('<' . '% +else +%' . '>', '', $content); $content = ereg_replace('<' . '% +end_if +%' . '>', '', $content); - // i18n + // i18n - get filename of currently parsed template + // CAUTION: No spaces allowed between arguments for all i18n calls! ereg('.*[\/](.*)',$template,$path); - $content = ereg_replace('<' . '% +_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '', $content); - - // i18n - sprintf => "sprintf(_t(...),$argument)" - // CAUTION: No spaces allowed between arguments! - $content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\),\<\?= +([^\?]*) +\?\>) +%' . '>', '', $content); + // i18n _t(...) - with entity only (no dots in namespace), meaning the current template filename will be added as a namespace + $content = ereg_replace('<' . '% +_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '', $content); + // i18n _t(...) + $content = ereg_replace('<' . '% +_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '', $content); + + // i18n sprintf(_t(...),$argument) with entity only (no dots in namespace), meaning the current template filename will be added as a namespace + $content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\),\<\?= +([^\?]*) +\?\>) +%' . '>', '', $content); + // i18n sprintf(_t(...),$argument) + $content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\),\<\?= +([^\?]*) +\?\>) +%' . '>', '', $content); + // isnt valid html? !? $content = ereg_replace('<' . '% +base_tag +%' . '>', '', $content); diff --git a/core/i18n.php b/core/i18n.php index 0678f6675..a10154487 100755 --- a/core/i18n.php +++ b/core/i18n.php @@ -1,8 +1,24 @@ template variable. + * + * Usage PHP: + * + * _t('MyNamespace.MYENTITY', 'My default natural language value'); + * _t('MyNamespace.MYENTITY', 'My default natural language value', PR_MEDIUM, 'My explanatory context'); + * sprintf(_t('MyNamespace.MYENTITY', 'Counting %s things'), 42); + * + * + * Usage Templates: + * + * <% _t('MyNamespace.MYENTITY', 'My default natural language value') %> + * <% sprintf(_t('MyNamespace.MYENTITY','Counting %s things'),$ThingsCount) %> + * + * + * Usage Javascript (see sapphire/javascript/i18n.js): + * + * ss.i18n._t('MyEntity.MyNamespace','My default natural language value'); + * * * File-based i18n-translations always have a "locale" (e.g. 'en_US'). * Common language names (e.g. 'en') are mainly used in {Translatable} for diff --git a/tests/i18n/_fakewebroot/i18ntestmodule/templates/Includes/i18nTestModuleInclude.ss b/tests/i18n/_fakewebroot/i18ntestmodule/templates/Includes/i18nTestModuleInclude.ss index 3fa5151a9..93187c01f 100644 --- a/tests/i18n/_fakewebroot/i18ntestmodule/templates/Includes/i18nTestModuleInclude.ss +++ b/tests/i18n/_fakewebroot/i18ntestmodule/templates/Includes/i18nTestModuleInclude.ss @@ -1,2 +1,4 @@ <% _t("i18nTestModule.WITHNAMESPACE", 'Include Entity with Namespace') %> -<% _t("NONAMESPACE", 'Include Entity without Namespace') %> \ No newline at end of file +<% _t("NONAMESPACE", 'Include Entity without Namespace') %> +<% sprintf(_t('i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE','My include replacement: %s'),$TestProperty) %> +<% sprintf(_t('SPRINTFINCLUDENONAMESPACE','My include replacement no namespace: %s'),$TestProperty) %> \ No newline at end of file diff --git a/tests/i18n/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss b/tests/i18n/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss index 1930114e4..1ec3bbcaf 100644 --- a/tests/i18n/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss +++ b/tests/i18n/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss @@ -1,2 +1,5 @@ -<% _t('i18nTestModule.LAYOUTTEMPLATE',"Layout Template")%> +<% _t('i18nTestModule.LAYOUTTEMPLATE',"Layout Template") %> +<% _t('LAYOUTTEMPLATENONAMESPACE',"Layout Template no namespace") %> +<% sprintf(_t('i18nTestModule.SPRINTFNAMESPACE','My replacement: %s'),$TestProperty) %> +<% sprintf(_t('SPRINTFNONAMESPACE','My replacement no namespace: %s'),$TestProperty) %> <% include i18nTestModuleInclude %> \ No newline at end of file diff --git a/tests/i18n/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss b/tests/i18n/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss index bf2fbc702..5cc818354 100644 --- a/tests/i18n/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss +++ b/tests/i18n/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss @@ -1,2 +1,3 @@ -<% _t('i18nTestModule.MAINTEMPLATE',"Main Template")%> +<% _t('i18nTestModule.MAINTEMPLATE',"Main Template") %> +$Layout lonely _t() call that should be ignored \ No newline at end of file diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index b7cab5668..ebd28d309 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -4,6 +4,64 @@ * @subpackage tests */ class i18nTest extends SapphireTest { + + /** + * @var string $tmpBasePath Used to write language files. + * We don't want to store them inside sapphire (or in any web-accessible place) + * in case something goes wrong with the file parsing. + */ + protected $alternateBaseSavePath; + + /** + * @var string $alternateBasePath Fake webroot with a single module + * /i18ntestmodule which contains some files with _t() calls. + */ + protected $alternateBasePath; + + function setUp() { + parent::setUp(); + + $this->alternateBasePath = Director::baseFolder() . "/sapphire/tests/i18n/_fakewebroot"; + $this->alternateBaseSavePath = TEMP_FOLDER . '/i18nTextCollectorTest_webroot'; + FileSystem::makeFolder($this->alternateBaseSavePath); + + // SSViewer and ManifestBuilder don't support different webroots, hence we set the paths manually + global $_CLASS_MANIFEST; + $_CLASS_MANIFEST['i18nTestModule'] = $this->alternateBasePath . '/i18ntestmodule/code/i18nTestModule.php'; + $_CLASS_MANIFEST['i18nTestModule_Addition'] = $this->alternateBasePath . '/i18ntestmodule/code/i18nTestModule.php'; + $_CLASS_MANIFEST['i18nTestModuleDecorator'] = $this->alternateBasePath . '/i18nothermodule/code/i18nTestModuleDecorator.php'; + + global $_ALL_CLASSES; + $_ALL_CLASSES['parents']['i18nTestModule'] = array('DataObject'=>'DataObject','Object'=>'Object'); + $_ALL_CLASSES['parents']['i18nTestModule_Addition'] = array('Object'=>'Object'); + $_ALL_CLASSES['parents']['i18nTestModuleDecorator'] = array('DataObjectDecorator'=>'DataObjectDecorator','Object'=>'Object'); + + global $_TEMPLATE_MANIFEST; + $_TEMPLATE_MANIFEST['i18nTestModule.ss'] = array( + 'main' => $this->alternateBasePath . '/i18ntestmodule/templates/i18nTestModule.ss', + 'Layout' => $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss', + ); + $_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss'] = array( + 'Includes' => $this->alternateBasePath . '/i18ntestmodule/templates/Includes/i18nTestModuleInclude.ss', + ); + $_TEMPLATE_MANIFEST['i18nTestModule.ss'] = array( + 'main' => $this->alternateBasePath . '/i18ntestmodule/templates/i18nTestModule.ss', + 'Layout' => $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss', + ); + } + + function tearDown() { + //FileSystem::removeFolder($this->tmpBasePath); + + global $_CLASS_MANIFEST; + unset($_CLASS_MANIFEST['i18nTestModule']); + unset($_CLASS_MANIFEST['i18nTestModule_Addition']); + + global $_TEMPLATE_MANIFEST; + unset($_TEMPLATE_MANIFEST['i18nTestModule.ss']); + unset($_TEMPLATE_MANIFEST['i18nTestModuleInclude.ss']); + } + function testGetExistingTranslations() { $translations = i18n::get_existing_translations(); $this->assertTrue(isset($translations['en_US']), 'Checking for en_US translation'); @@ -64,6 +122,78 @@ class i18nTest extends SapphireTest { ); } + function testTemplateTranslation() { + global $lang; + $oldLocale = i18n::get_locale(); + + i18n::set_locale('en_US'); + $lang['en_US']['i18nTestModule']['MAINTEMPLATE'] = 'Main Template'; + $lang['en_US']['i18nTestModule.ss']['SPRINTFNONAMESPACE'] = 'My replacement no namespace: %s'; + $lang['en_US']['i18nTestModule']['LAYOUTTEMPLATE'] = 'Layout Template'; + $lang['en_US']['i18nTestModule.ss']['LAYOUTTEMPLATENONAMESPACE'] = 'Layout Template no namespace'; + $lang['en_US']['i18nTestModule']['SPRINTFNAMESPACE'] = 'My replacement: %s'; + $lang['en_US']['i18nTestModule']['WITHNAMESPACE'] = 'Include Entity with Namespace'; + $lang['en_US']['i18nTestModule.ss']['NONAMESPACE'] = 'Include Entity without Namespace'; + $lang['en_US']['i18nTestModuleInclude.ss']['SPRINTFINCLUDENAMESPACE'] = 'My include replacement: %s'; + $lang['en_US']['i18nTestModule.ss']['SPRINTFINCLUDENONAMESPACE'] = 'My include replacement no namespace: %s'; + $viewer = new SSViewer('i18nTestModule'); + $parsedHtml = $viewer->process(new ArrayData(array('TestProperty' => 'TestPropertyValue'))); + $this->assertContains( + "Layout Template\n", + $parsedHtml + ); + $this->assertContains( + "Layout Template no namespace\n", + $parsedHtml + ); + + i18n::set_locale('de_DE'); + $lang['de_DE']['i18nTestModule']['MAINTEMPLATE'] = 'TRANS Main Template'; + $lang['de_DE']['i18nTestModule.ss']['SPRINTFNONAMESPACE'] = 'TRANS My replacement no namespace: %s'; + $lang['de_DE']['i18nTestModule']['LAYOUTTEMPLATE'] = 'TRANS Layout Template'; + $lang['de_DE']['i18nTestModule.ss']['LAYOUTTEMPLATENONAMESPACE'] = 'TRANS Layout Template no namespace'; + $lang['de_DE']['i18nTestModule']['SPRINTFNAMESPACE'] = 'TRANS My replacement: %s'; + $lang['de_DE']['i18nTestModule']['WITHNAMESPACE'] = 'TRANS Include Entity with Namespace'; + $lang['de_DE']['i18nTestModule.ss']['NONAMESPACE'] = 'TRANS Include Entity without Namespace'; + $lang['de_DE']['i18nTestModuleInclude.ss']['SPRINTFINCLUDENAMESPACE'] = 'TRANS My include replacement: %s'; + $lang['de_DE']['i18nTestModule.ss']['SPRINTFINCLUDENONAMESPACE'] = 'TRANS My include replacement no namespace: %s'; + $viewer = new SSViewer('i18nTestModule'); + $parsedHtml = $viewer->process(new ArrayData(array('TestProperty' => 'TestPropertyValue'))); + $this->assertContains( + "TRANS Main Template\n", + $parsedHtml + ); + $this->assertContains( + "TRANS Layout Template\n", + $parsedHtml + ); + $this->assertContains( + "TRANS Layout Template no namespace", + $parsedHtml + ); + $this->assertContains( + "TRANS My replacement: TestPropertyValue", + $parsedHtml + ); + $this->assertContains( + "TRANS Include Entity with Namespace", + $parsedHtml + ); + $this->assertContains( + "TRANS Include Entity without Namespace", + $parsedHtml + ); + $this->assertContains( + "TRANS My include replacement: TestPropertyValue", + $parsedHtml + ); + $this->assertContains( + "TRANS My include replacement no namespace: TestPropertyValue", + $parsedHtml + ); + + i18n::set_locale($oldLocale); + } } class i18nTest_DataObject extends DataObject implements TestOnly { diff --git a/tests/i18n/i18nTextCollectorTest.php b/tests/i18n/i18nTextCollectorTest.php index 44f377049..8655c2ca0 100644 --- a/tests/i18n/i18nTextCollectorTest.php +++ b/tests/i18n/i18nTextCollectorTest.php @@ -330,13 +330,49 @@ PHP; $templateFilePath = $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss'; $html = file_get_contents($templateFilePath); + $matches = $c->collectFromTemplate($html, 'mymodule', 'RandomNamespace'); + + /* + $this->assertArrayHasKey('i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE', $matches); $this->assertEquals( - $c->collectFromTemplate($html, 'mymodule', 'RandomNamespace'), - array( - 'i18nTestModule.WITHNAMESPACE' => array('Include Entity with Namespace', null, null), - 'i18nTestModuleInclude.ss.NONAMESPACE' => array('Include Entity without Namespace', null, null), - 'i18nTestModule.LAYOUTTEMPLATE' => array('Layout Template', null, null), - ) + $matches['i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE'], + array('Layout Template no namespace', null, null) + ); + */ + $this->assertArrayHasKey('RandomNamespace.SPRINTFNONAMESPACE', $matches); + $this->assertEquals( + $matches['RandomNamespace.SPRINTFNONAMESPACE'], + array('My replacement no namespace: %s', null, null) + ); + $this->assertArrayHasKey('i18nTestModule.LAYOUTTEMPLATE', $matches); + $this->assertEquals( + $matches['i18nTestModule.LAYOUTTEMPLATE'], + array('Layout Template', null, null) + ); + $this->assertArrayHasKey('i18nTestModule.SPRINTFNAMESPACE', $matches); + $this->assertEquals( + $matches['i18nTestModule.SPRINTFNAMESPACE'], + array('My replacement: %s', null, null) + ); + $this->assertArrayHasKey('i18nTestModule.WITHNAMESPACE', $matches); + $this->assertEquals( + $matches['i18nTestModule.WITHNAMESPACE'], + array('Include Entity with Namespace', null, null) + ); + $this->assertArrayHasKey('i18nTestModuleInclude.ss.NONAMESPACE', $matches); + $this->assertEquals( + $matches['i18nTestModuleInclude.ss.NONAMESPACE'], + array('Include Entity without Namespace', null, null) + ); + $this->assertArrayHasKey('i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE', $matches); + $this->assertEquals( + $matches['i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE'], + array('My include replacement: %s', null, null) + ); + $this->assertArrayHasKey('i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE', $matches); + $this->assertEquals( + $matches['i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE'], + array('My include replacement no namespace: %s', null, null) ); }