diff --git a/docs/en/changelogs/3.0.0.md b/docs/en/changelogs/3.0.0.md index 3757dbc93..63c9b1fac 100644 --- a/docs/en/changelogs/3.0.0.md +++ b/docs/en/changelogs/3.0.0.md @@ -455,13 +455,36 @@ and was rarely used in practice - so we moved it to a "[homepagefordomain](https ### New syntax for translatable _t functions [i18n-t]### -You can now call the _t() function in both templates and code with a namespace and string to translate, as well as a -comment and injection array. Note that the proxity arguement to _t is no longer supported. +You can now call the `_t()` function in both templates and code with a namespace and string to translate, as well as a comment and injection array. The new syntax supports injecting variables into the translation. For example: :::php - _t('i18nTestModule.INJECTIONS2', "Hello {name} {greeting}", array("name"=>"Paul", "greeting"=>"good you are here")); + _t( + 'i18nTestModule.INJECTIONS2', + "Hello {name} {greeting}", + array("name"=>"Paul", "greeting"=>"good you are here") + ); + +We've written the injection logic in a way that keeps backwards compatible with +existing translations. This means that you can migrate from `sprintf()` to the new injection +API incrementally. The following to "mixed usage" examples still work, although they +don't get the advantage of flexible ordering in substitutions. + + :::php + _t( + 'i18nTestModule.INJECTIONS2', + "Hello {name} {greeting}", + array("Paul", "good you are here") + ); + _t( + 'i18nTestModule.INJECTIONS2', + "Hello %s, %s", + array("name"=>"Paul", "greeting"=>"good you are here") + ); + +Of course, you can keep using `sprintf()` for variable substitution in your own code. + ### Default translation source in YML instead of PHP $lang array, using Zend_Translate {#zend-translate} diff --git a/docs/en/topics/i18n.md b/docs/en/topics/i18n.md index ab16c430c..fd61ff134 100644 --- a/docs/en/topics/i18n.md +++ b/docs/en/topics/i18n.md @@ -180,9 +180,10 @@ Therefore, the following would be a valid use in templates: Using SS templating variables in the translatable string (e.g. $Author, $Date..) is not currently supported. -### Injection-support +### Injection Support -Variable injection in _t allows us to dynamically replace parts of a translated string, e.g. by a username or a page-title. +Variable injection in `_t()` allows us to dynamically replace parts of a translated string, e.g. by a username or a page-title. The named parameters also allow flexible ordering of placeholders, +which might vary depending on the used language. :::php // in PHP-file @@ -196,6 +197,10 @@ Variable injection in _t allows us to dynamically replace parts of a translated // in SS-template ($Name must be available in the current template-scope) <%t MYPROJECT.INJECTIONS "Hello {name} {greeting}" name="$Name" greeting="good to see you" %> +Note that you can still use `sprintf()` wrapped around a `_t()` call +for your substitutions. In contrast to `sprintf()`, our API has a more translator friendly +placeholder syntax, as well as more graceful fallback if not all placeholders are found +(an outdated translation with less placeholders will throw a notice rather than a fatal error). ## Collecting text diff --git a/i18n/i18n.php b/i18n/i18n.php index 4e8460a06..d66d1979b 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -1512,9 +1512,36 @@ class i18n extends Object implements TemplateGlobalProvider { } // inject the variables from injectionArray (if present) - if ($injectionArray && count($injectionArray) > 0) { - foreach($injectionArray as $variable => $injection) { - $returnValue = str_replace('{'.$variable.'}', $injection, $returnValue); + if($injectionArray) { + $regex = '/\{[\w\d]*\}/i'; + if(!preg_match($regex, $returnValue)) { + // Legacy mode: If no injection placeholders are found, + // replace sprintf placeholders in fixed order. + $returnValue = vsprintf($returnValue, array_values($injectionArray)); + } else if(!ArrayLib::is_associative($injectionArray)) { + // Legacy mode: If injection placeholders are found, + // but parameters are passed without names, replace them in fixed order. + $returnValue = preg_replace_callback( + $regex, + function($matches) use(&$injectionArray) { + return $injectionArray ? array_shift($injectionArray) : ''; + }, + $returnValue + ); + } else { + // Standard placeholder replacement with named injections and variable order. + foreach($injectionArray as $variable => $injection) { + $placeholder = '{'.$variable.'}'; + $returnValue = str_replace($placeholder, $injection, $returnValue, $count); + if(!$count) { + SS_Log::log(sprintf( + "Couldn't find placeholder '%s' in translation string '%s' (id: '%s')", + $placeholder, + $returnValue, + $entity + ), SS_Log::NOTICE); + } + } } } diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index 7c5a40c84..ca0ac6392 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -251,7 +251,8 @@ class i18nTest extends SapphireTest { i18n::get_translator('core')->getAdapter()->addTranslation(array( 'i18nTestModule.NEWMETHODSIG' => 'TRANS New _t method signature test', - 'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}' + 'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}', + 'i18nTestModule.INJECTIONSLEGACY' => 'TRANS Hello %s %s. But it is late, %s', ), 'en_US'); $entity = "i18nTestModule.INJECTIONS"; @@ -287,6 +288,24 @@ class i18nTest extends SapphireTest { $translated, "Testing a translation with just entity and injection array" ); + $translated = i18n::_t( + 'i18nTestModule.INJECTIONSLEGACY', // has %s placeholders + array("name"=>"Cat", "greeting2"=>"meow", "goodbye"=>"meow") + ); + $this->assertContains( + "TRANS Hello Cat meow. But it is late, meow", + $translated, "Testing sprintf placeholders with named injections" + ); + + $translated = i18n::_t( + 'i18nTestModule.INJECTIONS', // has {name} placeholders + array("Cat", "meow", "meow") + ); + $this->assertContains( + "TRANS Hello Cat meow. But it is late, meow", + $translated, "Testing named injection placeholders with unnamed injections" + ); + i18n::set_locale($oldLocale); }