Permalink
Browse files

Merge pull request #342 from silverstripe-big-o/sapphire

---

This is the new _t syntax. All i18n unit tests pass.
  • Loading branch information...
2 parents f8719e5 + 93fcb86 commit c2797f3ffa63803c4f14df03f57330d3bcb6da7d @chillu chillu committed Apr 18, 2012
@@ -365,7 +365,17 @@ placeholder and the `PageComment` class. See the ['comments' module](https://git
The setting determines difference homepages at arbitrary locations in the page tree,
and was rarely used in practice - so we moved it to a "[homepagefordomain](https://github.com/silverstripe-labs/silverstripe-homepagefordomain)" module.
-
+
+### New syntax for translatable _t functions ###
+
+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.
+
+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"));
+
### Default translation source in YML instead of PHP $lang array, using Zend_Translate ###
This allows for a more flexible handling of translation sources in various formats.
View
@@ -139,20 +139,18 @@ The `_t()` function is the main gateway to localized text, and takes four parame
* **$entity:** Unique identifier, composed by a namespace and an entity name, with a dot separating them. Both are arbitrary names, although by convention we use the name of the containing class or template. Use this identifier to reference the same translation elsewhere in your code.
* **$string:** (optional) The original language string to be translated. Only needs to be declared once, and gets picked up the [text collector](#collecting-text).
- * **$string:** (optional) Natural language (particularly short phrases and individual words)
+ * **$string:** (optional) Natural language comment (particularly short phrases and individual words)
are very context dependent. This parameter allows the developer to convey this information
-to the translator. Can also be used to explain `sprintf()` placeholders.
+to the translator.
+ * **$array::** (optional) An array of injecting variables into the second parameter
:::php
//Example 4: Using context to hint information about a parameter
- sprintf(
- _t('CMSMain.RESTORED',
- "Restored '%s' successfully",
- 'Param %s is a title'
- ),
- $title
- )
-
+ _t('CMSMain.RESTORED',
+ "Restored {value} successfully",
+ 'This is a message when restoring a broken part of the CMS',
+ array('value' => $itemRestored)
+ );
### Usage
@@ -182,25 +180,21 @@ 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.
-### sprintf()-support
+### Injection-support
-Sprintf enables 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.
:::php
// in PHP-file
- sprintf(
- _t('CMSMain.RESTORED',"Restored '%s' successfully"),
- $title
- )
-
-<div class="warning" markdown='1'>
-**Caution**: In templates (*.ss)-files you can only use ONE argument for your sprintf-support, and can't use spaces
-between parameters.
-</div>
+ _t(
+ 'CMSMain.RESTORED',
+ "Restored {title} successfully"),
+ array('title' => $title)
+ );
:::php
- // in SS-template ($title must be available in the current template-scope)
- <% sprintf(_t('CMSMain.RESTORED',"Restored '%s' successfully"),$title) %>
+ // 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" %>
## Collecting text
@@ -339,14 +333,12 @@ Example Translation Table (mymodule/javascript/lang/de_DE.js)
alert(ss.i18n._t('MYMODULE.MYENTITY'));
-### Advanced Usage with sprintf()
+### Advanced Usage with injection
:::js
- // MYMODULE.MYENTITY contains "Really delete %s articles by %s authors?"
- alert(ss.i18n.sprintf(
- ss.i18n._t('MYMODULE.MYENTITY'),
- 42,
- 'Douglas Adams'
+ // MYMODULE.MYENTITY contains "Really delete {answer} articles by {author} authors?"
+ alert(ss.i18n._t('MYMODULE.MYENTITY'),
+ array('answer' => 42, 'author' => 'Douglas Adams')
));
// Displays: "Really delete 42 articles by Douglas Adams?"
View
@@ -1456,25 +1456,40 @@ public static function get_time_format() {
* the class name where this string is used and Entity identifies the string inside the namespace.
* @param string $string The original string itself. In a usual call this is a mandatory parameter, but if you are reusing a string which
* has already been "declared" (using another call to this function, with the same class and entity), you can omit it.
- * @param string $context If the string can be difficult to translate by any reason, you can help translators with some more info using this param
+ * @param string $context (optional) If the string can be difficult to translate by any reason, you can help translators with some more info using this param
+ * @param string injectionArray (optional) array of key value pairs that are used to replace corresponding expressions in {curly brackets} in the $string.
+ * The injection array can also be used as the their argument to the _t() function
* @return string The translated string, according to the currently set locale {@link i18n::set_locale()}
*/
- static function _t($entity, $string = "", $context = "") {
+ static function _t($entity, $string = "", $context = "", $injection = "") {
if(is_numeric($context) && in_array($context, array(PR_LOW, PR_MEDIUM, PR_HIGH))) {
- $context = func_get_arg(4);
Deprecation::notice(
- '3.0',
+ '3.0',
'The $priority argument to _t() is deprecated, please use module inclusion priorities instead'
);
}
+
+ //fetch the injection array out of the parameters (if it is present)
+ $argList = func_get_args();
+ $argNum = func_num_args();
+ //_t($entity, $string = "", $context (optional), $injectionArray (optional))
+ $injectionArray = null;
+ for($i = 0; $i < $argNum; $i++) {
+ if (is_array($argList[$i])) { //we have reached the injectionArray
+ $injectionArray = $argList[$i]; //any array in the args will be the injection array
+ }
+ }
+
// get current locale (either default or user preference)
$locale = i18n::get_locale();
$lang = i18n::get_lang_from_locale($locale);
-
+
// Only call getter if static isn't already defined (for performance reasons)
$translatorsByPrio = self::$translators;
if(!$translatorsByPrio) $translatorsByPrio = self::get_translators();
-
+
+ $returnValue = $string; // Fall back to default string argument
+
foreach($translatorsByPrio as $priority => $translators) {
foreach($translators as $name => $translator) {
$adapter = $translator->getAdapter();
@@ -1489,14 +1504,24 @@ static function _t($entity, $string = "", $context = "") {
$translation = $adapter->translate($entity, $locale);
// Return translation only if we found a match thats not the entity itself (Zend fallback)
- if($translation && $translation != $entity) return $translation;
+ if($translation && $translation != $entity) {
+ $returnValue = $translation;
+ break 2;
+ }
}
}
-
- // Fall back to default string argument
- return $string;
+
+ // inject the variables from injectionArray (if present)
+ if ($injectionArray && count($injectionArray) > 0) {
+ foreach($injectionArray as $variable => $injection) {
+ $returnValue = str_replace('{'.$variable.'}', $injection, $returnValue);
+ }
+ }
+
+ return $returnValue;
}
+
/**
* @return array Array of priority keys to instances of Zend_Translate, mapped by name.
*/
View
@@ -193,17 +193,24 @@ protected function processModule($module) {
return $entities;
}
-
+
public function collectFromCode($content, $module) {
$entities = array();
$tokens = token_get_all("<?php\n" . $content);
$inTransFn = false;
$inConcat = false;
+ $finalTokenDueToArray = false;
$currentEntity = array();
foreach($tokens as $token) {
if(is_array($token)) {
list($id, $text) = $token;
+
+ if($inTransFn && $id == T_ARRAY) {
+ //raw 'array' token found in _t function, stop processing the tokens for this _t now
+ $finalTokenDueToArray = true;
+ }
+
if($id == T_STRING && $text == '_t') {
// start definition
$inTransFn = true;
@@ -223,39 +230,40 @@ public function collectFromCode($content, $module) {
$text = preg_replace('/^"/', '', $text);
$text = preg_replace('/"$/', '', $text);
}
-
+
if($inConcat) {
$currentEntity[count($currentEntity)-1] .= $text;
} else {
$currentEntity[] = $text;
- }
- }
+ }
+ }
} elseif($inTransFn && $token == '.') {
- $inConcat = true;
+ $inConcat = true;
} elseif($inTransFn && $token == ',') {
- $inConcat = false;
- } elseif($inTransFn && $token == ')') {
+ $inConcat = false;
+ } elseif($inTransFn && ($token == ')' || $finalTokenDueToArray)) {
// finalize definition
$inTransFn = false;
$inConcat = false;
$entity = array_shift($currentEntity);
$entities[$entity] = $currentEntity;
$currentEntity = array();
+ $finalTokenDueToArray = false;
}
}
-
+
foreach($entities as $entity => $spec) {
// call without master language definition
if(!$spec) {
unset($entities[$entity]);
- continue;
+ continue;
}
unset($entities[$entity]);
$entities[$this->normalizeEntity($entity, $module)] = $spec;
}
ksort($entities);
-
+
return $entities;
}
@@ -276,6 +284,11 @@ public function collectFromTemplate($content, $fileName, $module) {
// @todo Will get massively confused if you include the includer -> infinite loop
}
+ // use parser to extract <%t style translatable entities
+ $translatables = i18nTextCollector_Parser::GetTranslatables($content);
+ $entities = array_merge($entities,(array)$translatables);
+
+ // use the old method of getting _t() style translatable entities
// Collect in actual template
if(preg_match_all('/<%\s*(_t\(.*)%>/ms', $content, $matches)) {
foreach($matches as $match) {
@@ -516,4 +529,49 @@ public function getYaml($entities, $locale) {
// TODO Dumper can't handle YAML comments, so the context information is currently discarded
return $yamlHandler->dump(array($locale => $entitiesNested), 99);
}
+}
+
+/**
+ * Parser that scans through a template and extracts the parameters to the _t and <%t calls
+ */
+class i18nTextCollector_Parser extends SSTemplateParser {
+
+ static $entities = array();
+ static $currentEntity = array();
+
+ function Translate__construct(&$res) {
+ self::$currentEntity = array(null,null,null); //start with empty array
+ }
+
+ function Translate_Entity(&$res, $sub) {
+ self::$currentEntity[0] = $sub['text']; //entity
+ }
+
+ function Translate_Default(&$res, $sub) {
+ self::$currentEntity[1] = $sub['String']['text']; //value
+ }
+
+ function Translate_Context(&$res, $sub) {
+ self::$currentEntity[2] = $sub['String']['text']; //comment
+ }
+
+ function Translate__finalise(&$res) {
+ // set the entity name and the value (default), as well as the context (comment)
+ // priority is no longer used, so that is blank
+ self::$entities[self::$currentEntity[0]] = array(self::$currentEntity[1],null,self::$currentEntity[2]);
+ }
+
+ /**
+ * Parses a template and returns any translatable entities
+ */
+ static function GetTranslatables($template) {
+ self::$entities = array();
+
+ // Run the parser and throw away the result
+ $parser = new i18nTextCollector_Parser($template);
+ if(substr($template, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $parser->pos = 3;
+ $parser->match_TopTemplate();
+
+ return self::$entities;
+ }
}
@@ -2,4 +2,11 @@
<% _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 %>
+<% include i18nTestModuleInclude %>
+
+<%t i18nTestModule.NEWMETHODSIG "New _t method signature test" %>
+<%t i18nTestModule.INJECTIONS_DOES_NOT_EXIST "Hello {name} {greeting}. But it is late, {goodbye}" name="Mark" greeting="welcome" goodbye="bye" %>
+<%t i18nTestModule.INJECTIONS "Hello {name} {greeting}. But it is late, {goodbye}" name="Paul" greeting="good you are here" goodbye="see you" %>
+<%t i18nTestModule.INJECTIONS "Hello {name} {greeting}. But it is late, {goodbye}" is "New context (this should be ignored)" name="Steffen" greeting="willkommen" goodbye="wiedersehen" %>
+<%t i18nTestModule.INJECTIONS name="Cat" greeting='meow' goodbye="meow" %>
+<%t i18nTestModule.INJECTIONS name=$absoluteBaseURL greeting=$get_locale goodbye="global calls" %>
Oops, something went wrong.

0 comments on commit c2797f3

Please sign in to comment.