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
This commit is contained in:
Ingo Schommer 2008-11-06 02:50:14 +00:00
parent fdeaececb1
commit 728e691a1a
7 changed files with 211 additions and 17 deletions

View File

@ -433,14 +433,20 @@ class SSViewer extends Object {
$content = ereg_replace('<' . '% +else +%' . '>', '<? } 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\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '<?= _t(\''. $path[1] . '.\\2\\3\'\\4) ?>', $content);
// i18n - sprintf => "sprintf(_t(...),$argument)"
// CAUTION: No spaces allowed between arguments!
$content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\),\<\?= +([^\?]*) +\?\>) +%' . '>', '<?= sprintf(_t(\''. $path[1] . '.\\2\\3\'\\4),\\6) ?>', $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\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '<?= _t(\''. $path[1] . '.\\2\\3\'\\4) ?>', $content);
// i18n _t(...)
$content = ereg_replace('<' . '% +_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '<?= _t(\'\\2\\3\'\\4) ?>', $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\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\),\<\?= +([^\?]*) +\?\>) +%' . '>', '<?= sprintf(_t(\''. $path[1] . '.\\2\\3\'\\4),\\6) ?>', $content);
// i18n sprintf(_t(...),$argument)
$content = ereg_replace('<' . '% +sprintf\(_t\((\'([^\']*)\'|"([^"]*)")(([^)]|\)[^ ]|\) +[^% ])*)\),\<\?= +([^\?]*) +\?\>) +%' . '>', '<?= sprintf(_t(\'\\2\\3\'\\4),\\6) ?>', $content);
// </base> isnt valid html? !?
$content = ereg_replace('<' . '% +base_tag +%' . '>', '<base href="<?= Director::absoluteBaseURL(); ?>" />', $content);

View File

@ -1,8 +1,24 @@
<?php
/**
* Base-class for storage and retrieval of translated entities.
* Most common use is translation of the CMS-interface through the _t()-method
* (in controller/model) and the <% _t() %> template variable.
*
* Usage PHP:
* <code>
* _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);
* </code>
*
* Usage Templates:
* <code>
* <% _t('MyNamespace.MYENTITY', 'My default natural language value') %>
* <% sprintf(_t('MyNamespace.MYENTITY','Counting %s things'),$ThingsCount) %>
* </code>
*
* Usage Javascript (see sapphire/javascript/i18n.js):
* <code>
* ss.i18n._t('MyEntity.MyNamespace','My default natural language value');
* </code>
*
* File-based i18n-translations always have a "locale" (e.g. 'en_US').
* Common language names (e.g. 'en') are mainly used in {Translatable} for

View File

@ -1,2 +1,4 @@
<% _t("i18nTestModule.WITHNAMESPACE", 'Include Entity with Namespace') %>
<% _t("NONAMESPACE", 'Include Entity without Namespace') %>
<% _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) %>

View File

@ -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 %>

View File

@ -1,2 +1,3 @@
<% _t('i18nTestModule.MAINTEMPLATE',"Main Template")%>
<% _t('i18nTestModule.MAINTEMPLATE',"Main Template") %>
$Layout
lonely _t() call that should be ignored

View File

@ -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 {

View File

@ -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)
);
}