diff --git a/admin/code/GroupImportForm.php b/admin/code/GroupImportForm.php index c084aeecc..850fe7e75 100644 --- a/admin/code/GroupImportForm.php +++ b/admin/code/GroupImportForm.php @@ -70,17 +70,17 @@ class GroupImportForm extends Form { // result message $msgArr = array(); - if($result->CreatedCount()) $msgArr[] = sprintf( - _t('GroupImportForm.ResultCreated', 'Created %d groups'), - $result->CreatedCount() + if($result->CreatedCount()) $msgArr[] = _t( + 'GroupImportForm.ResultCreated', 'Created {count} groups', + array('count' => $result->CreatedCount()) ); - if($result->UpdatedCount()) $msgArr[] = sprintf( - _t('GroupImportForm.ResultUpdated', 'Updated %d groups'), - $result->UpdatedCount() + if($result->UpdatedCount()) $msgArr[] = _t( + 'GroupImportForm.ResultUpdated', 'Updated %d groups', + array('count' => $result->UpdatedCount()) ); - if($result->DeletedCount()) $msgArr[] = sprintf( - _t('GroupImportForm.ResultDeleted', 'Deleted %d groups'), - $result->DeletedCount() + if($result->DeletedCount()) $msgArr[] = _t( + 'GroupImportForm.ResultDeleted', 'Deleted %d groups', + array('count' => $result->DeletedCount()) ); $msg = ($msgArr) ? implode(',', $msgArr) : _t('MemberImportForm.ResultNone', 'No changes'); diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index e0ec7e4d4..a394f5fc1 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -1299,12 +1299,12 @@ class LeftAndMain extends Controller implements PermissionProvider { $title = _t("{$class}.MENUTITLE", LeftAndMain::menu_title_for_class($class)); $perms["CMS_ACCESS_" . $class] = array( - 'name' => sprintf(_t( + 'name' => _t( 'CMSMain.ACCESS', - "Access to '%s' section", - - "Item in permission selection identifying the admin section. Example: Access to 'Files & Images'" - ), $title, null), + "Access to '{title}' section", + "Item in permission selection identifying the admin section. Example: Access to 'Files & Images'", + array('title' => $title) + ), 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access') ); } diff --git a/admin/code/MemberImportForm.php b/admin/code/MemberImportForm.php index 1b15692ea..bb6274738 100644 --- a/admin/code/MemberImportForm.php +++ b/admin/code/MemberImportForm.php @@ -75,17 +75,17 @@ class MemberImportForm extends Form { // result message $msgArr = array(); - if($result->CreatedCount()) $msgArr[] = sprintf( - _t('MemberImportForm.ResultCreated', 'Created %d members'), - $result->CreatedCount() + if($result->CreatedCount()) $msgArr[] = _t( + 'MemberImportForm.ResultCreated', 'Created {count} members', + array('count' => $result->CreatedCount()) ); - if($result->UpdatedCount()) $msgArr[] = sprintf( - _t('MemberImportForm.ResultUpdated', 'Updated %d members'), - $result->UpdatedCount() + if($result->UpdatedCount()) $msgArr[] = _t( + 'MemberImportForm.ResultUpdated', 'Updated {count} members', + array('count' => $result->UpdatedCount()) ); - if($result->DeletedCount()) $msgArr[] = sprintf( - _t('MemberImportForm.ResultDeleted', 'Deleted %d members'), - $result->DeletedCount() + if($result->DeletedCount()) $msgArr[] = _t( + 'MemberImportForm.ResultDeleted', 'Deleted %d members', + array('count' => $result->DeletedCount()) ); $msg = ($msgArr) ? implode(',', $msgArr) : _t('MemberImportForm.ResultNone', 'No changes'); diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index d55a3f1e4..e19dfbbc4 100644 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -387,17 +387,17 @@ abstract class ModelAdmin extends LeftAndMain { $results = $loader->load($_FILES['_CsvFile']['tmp_name']); $message = ''; - if($results->CreatedCount()) $message .= sprintf( - _t('ModelAdmin.IMPORTEDRECORDS', "Imported %s records."), - $results->CreatedCount() + if($results->CreatedCount()) $message .= _t( + 'ModelAdmin.IMPORTEDRECORDS', "Imported {count} records.", + array('count' => $results->CreatedCount()) ); - if($results->UpdatedCount()) $message .= sprintf( - _t('ModelAdmin.UPDATEDRECORDS', "Updated %s records."), - $results->UpdatedCount() + if($results->UpdatedCount()) $message .= _t( + 'ModelAdmin.UPDATEDRECORDS', "Updated {count} records.", + array('count' => $results->UpdatedCount()) ); - if($results->DeletedCount()) $message .= sprintf( - _t('ModelAdmin.DELETEDRECORDS', "Deleted %s records."), - $results->DeletedCount() + if($results->DeletedCount()) $message .= _t( + 'ModelAdmin.DELETEDRECORDS', "Deleted {count} records.", + array('count' => $results->DeletedCount()) ); if(!$results->CreatedCount() && !$results->UpdatedCount()) $message .= _t('ModelAdmin.NOIMPORT', "Nothing to import"); diff --git a/admin/code/SecurityAdmin.php b/admin/code/SecurityAdmin.php index 4e0690d90..c43d4d798 100755 --- a/admin/code/SecurityAdmin.php +++ b/admin/code/SecurityAdmin.php @@ -219,7 +219,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { $title = _t("SecurityAdmin.MENUTITLE", LeftAndMain::menu_title_for_class($this->class)); return array( "CMS_ACCESS_SecurityAdmin" => array( - 'name' => sprintf(_t('CMSMain.ACCESS', "Access to '%s' section"), $title), + 'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)), 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 'help' => _t( 'SecurityAdmin.ACCESS_HELP', diff --git a/core/Core.php b/core/Core.php index a340abeff..d2e25d102 100644 --- a/core/Core.php +++ b/core/Core.php @@ -386,8 +386,8 @@ function stripslashes_recursively(&$array) { /** * @see i18n::_t() */ -function _t($entity, $string = "", $context = "") { - return i18n::_t($entity, $string, $context); +function _t($entity, $string = "", $context = "", $injection = "") { + return i18n::_t($entity, $string, $context, $injection); } /** 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/filesystem/Upload.php b/filesystem/Upload.php index 31339dcec..7af603fb2 100644 --- a/filesystem/Upload.php +++ b/filesystem/Upload.php @@ -496,28 +496,22 @@ class Upload_Validator { if(!$this->isValidSize()) { $ext = (isset($pathInfo['extension'])) ? $pathInfo['extension'] : ''; $arg = File::format_size($this->getAllowedMaxFileSize($ext)); - $this->errors[] = sprintf( - _t( - 'File.TOOLARGE', - 'Filesize is too large, maximum %s allowed.', - - 'Argument 1: Filesize (e.g. 1MB)' - ), - $arg + $this->errors[] = _t( + 'File.TOOLARGE', + 'Filesize is too large, maximum {size} allowed.', + 'Argument 1: Filesize (e.g. 1MB)', + array('size' => $arg) ); return false; } // extension validation if(!$this->isValidExtension()) { - $this->errors[] = sprintf( - _t( - 'File.INVALIDEXTENSION', - 'Extension is not allowed (valid: %s)', - - 'Argument 1: Comma-separated list of valid extensions' - ), - wordwrap(implode(', ', $this->allowedExtensions)) + $this->errors[] = _t( + 'File.INVALIDEXTENSION', + 'Extension is not allowed (valid: {extensions})', + 'Argument 1: Comma-separated list of valid extensions', + array('extensions' => wordwrap(implode(', ', $this->allowedExtensions))) ); return false; } diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index ddb6d112f..ec04fd7aa 100644 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -485,12 +485,11 @@ JS; $editLink = Controller::join_links($this->Link(), 'item/' . $childData->ID . '/edit'); - $message = sprintf( - _t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'), - $childData->singular_name(), - '' . $childData->Title . '', - $closeLink + $message = _t( + 'ComplexTableField.SUCCESSADD2', 'Added {name}', + array('name' => $childData->singular_name()) ); + $message .= '' . $childData->Title . '' . $closeLink; $form->sessionMessage($message, 'good'); diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php index 889b3342a..213150b8b 100644 --- a/forms/ConfirmedPasswordField.php +++ b/forms/ConfirmedPasswordField.php @@ -247,13 +247,25 @@ class ConfirmedPasswordField extends FormField { if(($this->minLength || $this->maxLength)) { if($this->minLength && $this->maxLength) { $limit = "{{$this->minLength},{$this->maxLength}}"; - $errorMsg = sprintf(_t('ConfirmedPasswordField.BETWEEN', 'Passwords must be %s to %s characters long.'), $this->minLength, $this->maxLength); + $errorMsg = _t( + 'ConfirmedPasswordField.BETWEEN', + 'Passwords must be {min} to {max} characters long.', + array('min' => $this->minLength, 'max' => $this->maxLength) + ); } elseif($this->minLength) { $limit = "{{$this->minLength}}.*"; - $errorMsg = sprintf(_t('ConfirmedPasswordField.ATLEAST', 'Passwords must be at least %s characters long.'), $this->minLength); + $errorMsg = _t( + 'ConfirmedPasswordField.ATLEAST', + 'Passwords must be at least {min} characters long.', + array('min' => $this->minLength) + ); } elseif($this->maxLength) { $limit = "{0,{$this->maxLength}}"; - $errorMsg = sprintf(_t('ConfirmedPasswordField.MAXIMUM', 'Passwords must be at most %s characters long.'), $this->maxLength); + $errorMsg = _t( + 'ConfirmedPasswordField.MAXIMUM', + 'Passwords must be at most {max} characters long.', + array('max' => $this->maxLength) + ); } $limitRegex = '/^.' . $limit . '$/'; if(!empty($value) && !preg_match($limitRegex,$value)) { diff --git a/forms/CreditCardField.php b/forms/CreditCardField.php index bcf246095..e8f8e83ed 100644 --- a/forms/CreditCardField.php +++ b/forms/CreditCardField.php @@ -51,9 +51,10 @@ class CreditCardField extends TextField { } $validator->validationError( $this->name, - sprintf( - _t('Form.VALIDATIONCREDITNUMBER', "Please ensure you have entered the %s credit card number correctly."), - $number + _t( + 'Form.VALIDATIONCREDITNUMBER', + "Please ensure you have entered the {number} credit card number correctly.", + array('number' => $number) ), "validation", false diff --git a/forms/DateField.php b/forms/DateField.php index b352c6c2c..4d609abf9 100644 --- a/forms/DateField.php +++ b/forms/DateField.php @@ -310,9 +310,9 @@ class DateField extends TextField { if(!$valid) { $validator->validationError( $this->name, - sprintf( - _t('DateField.VALIDDATEFORMAT2', "Please enter a valid date format (%s)."), - $this->getConfig('dateformat') + _t( + 'DateField.VALIDDATEFORMAT2', "Please enter a valid date format ({format}).", + array('format' => $this->getConfig('dateformat')) ), "validation", false @@ -331,9 +331,9 @@ class DateField extends TextField { if(!$this->valueObj->isLater($minDate) && !$this->valueObj->equals($minDate)) { $validator->validationError( $this->name, - sprintf( - _t('DateField.VALIDDATEMINDATE', "Your date has to be newer or matching the minimum allowed date (%s)"), - $minDate->toString($this->getConfig('dateformat')) + _t( + 'DateField.VALIDDATEMINDATE', "Your date has to be newer or matching the minimum allowed date ({date})", + array('date' => $minDate->toString($this->getConfig('dateformat'))) ), "validation", false @@ -351,9 +351,9 @@ class DateField extends TextField { if(!$this->valueObj->isEarlier($maxDate) && !$this->valueObj->equals($maxDate)) { $validator->validationError( $this->name, - sprintf( - _t('DateField.VALIDDATEMAXDATE', "Your date has to be older or matching the maximum allowed date (%s)"), - $maxDate->toString($this->getConfig('dateformat')) + _t( + 'DateField.VALIDDATEMAXDATE', "Your date has to be older or matching the maximum allowed date ({date})", + array('date' => $maxDate->toString($this->getConfig('dateformat'))) ), "validation", false diff --git a/forms/FileIFrameField.php b/forms/FileIFrameField.php index a5b651352..cbf469dec 100644 --- a/forms/FileIFrameField.php +++ b/forms/FileIFrameField.php @@ -92,9 +92,11 @@ class FileIFrameField extends FileField { ) ); } else { - return sprintf(_t ( - 'FileIFrameField.ATTACHONCESAVED', '%ss can be attached once you have saved the record for the first time.' - ), $this->FileTypeName()); + return _t( + 'FileIFrameField.ATTACHONCESAVED', + '{type}s can be attached once you have saved the record for the first time.', + array('type' => $this->FileTypeName()) + ); } } @@ -130,9 +132,9 @@ class FileIFrameField extends FileField { $selectFile = _t('FileIFrameField.FROMFILESTORE', 'From the File Store'); if($this->AttachedFile() && $this->AttachedFile()->ID) { - $title = sprintf(_t('FileIFrameField.REPLACE', 'Replace %s'), $this->FileTypeName()); + $title = _t('FileIFrameField.REPLACE', 'Replace {type}', array('type' => $this->FileTypeName())); } else { - $title = sprintf(_t('FileIFrameField.ATTACH', 'Attach %s'), $this->FileTypeName()); + $title = _t('FileIFrameField.ATTACH', 'Attach {type}', array('type' => $this->FileTypeName())); } $fileSources = array(); @@ -235,7 +237,7 @@ class FileIFrameField extends FileField { ), new FieldList ( $deleteButton = new FormAction ( - 'delete', sprintf(_t('FileIFrameField.DELETE', 'Delete %s'), $this->FileTypeName()) + 'delete', _t('FileIFrameField.DELETE', 'Delete {type}', array('type' => $this->FileTypeName())) ) ) ); diff --git a/forms/NumericField.php b/forms/NumericField.php index 872d9cbdf..218d777d4 100644 --- a/forms/NumericField.php +++ b/forms/NumericField.php @@ -16,9 +16,9 @@ class NumericField extends TextField{ if($this->value && !is_numeric(trim($this->value))){ $validator->validationError( $this->name, - sprintf( - _t('NumericField.VALIDATION', "'%s' is not a number, only numbers can be accepted for this field"), - $this->value + _t( + 'NumericField.VALIDATION', "'{value}' is not a number, only numbers can be accepted for this field", + array('value' => $this->value) ), "validation" ); diff --git a/forms/TimeField.php b/forms/TimeField.php index 38b096d4a..330223793 100644 --- a/forms/TimeField.php +++ b/forms/TimeField.php @@ -143,9 +143,9 @@ class TimeField extends TextField { if(!Zend_Date::isDate($this->value, $this->getConfig('timeformat'), $this->locale)) { $validator->validationError( $this->name, - sprintf( - _t('TimeField.VALIDATEFORMAT', "Please enter a valid time format (%s)"), - $this->getConfig('timeformat') + _t( + 'TimeField.VALIDATEFORMAT', "Please enter a valid time format ({format})", + array('format' => $this->getConfig('timeformat')) ), "validation", false diff --git a/forms/UploadField.php b/forms/UploadField.php index e23f89944..e8dcd4aca 100644 --- a/forms/UploadField.php +++ b/forms/UploadField.php @@ -348,23 +348,26 @@ class UploadField extends FileField { if (count($this->getValidator()->getAllowedExtensions())) { $allowedExtensions = $this->getValidator()->getAllowedExtensions(); $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; - $config['errorMessages']['acceptFileTypes'] = sprintf(_t( + $config['errorMessages']['acceptFileTypes'] = _t( 'File.INVALIDEXTENSION', - 'Extension is not allowed (valid: %s)' - ), wordwrap(implode(', ', $allowedExtensions))); + 'Extension is not allowed (valid: {extensions})', + array('extensions' => wordwrap(implode(', ', $allowedExtensions))) + ); } if ($this->getValidator()->getAllowedMaxFileSize()) { $config['maxFileSize'] = $this->getValidator()->getAllowedMaxFileSize(); - $config['errorMessages']['maxFileSize'] = sprintf(_t( + $config['errorMessages']['maxFileSize'] = _t( 'File.TOOLARGE', - 'Filesize is too large, maximum %s allowed.' - ), File::format_size($config['maxFileSize'])); + 'Filesize is too large, maximum {size} allowed.', + array('size' => File::format_size($config['maxFileSize'])) + ); } if ($config['maxNumberOfFiles'] > 1) { - $config['errorMessages']['maxNumberOfFiles'] = sprintf(_t( + $config['errorMessages']['maxNumberOfFiles'] = _t( 'UploadField.MAXNUMBEROFFILES', - 'Max number of %s file(s) exceeded.' - ), $config['maxNumberOfFiles']); + 'Max number of {count} file(s) exceeded.', + array('count' => $config['maxNumberOfFiles']) + ); } $configOverwrite = array(); if (is_numeric($config['maxNumberOfFiles']) && $this->getItems()->count()) { @@ -459,10 +462,11 @@ class UploadField extends FileField { // Report the constraint violation. if ($tooManyFiles) { if(!$this->getConfig('allowedMaxFileNumber')) $this->setConfig('allowedMaxFileNumber', 1); - $return['error'] = sprintf(_t( + $return['error'] = _t( 'UploadField.MAXNUMBEROFFILES', - 'Max number of %s file(s) exceeded.' - ), $this->getConfig('allowedMaxFileNumber')); + 'Max number of {count} file(s) exceeded.', + array('count' => $this->getConfig('allowedMaxFileNumber')) + ); } } diff --git a/forms/gridfield/GridFieldAddExistingAutocompleter.php b/forms/gridfield/GridFieldAddExistingAutocompleter.php index a9f1f4629..1f4cfe036 100755 --- a/forms/gridfield/GridFieldAddExistingAutocompleter.php +++ b/forms/gridfield/GridFieldAddExistingAutocompleter.php @@ -251,15 +251,15 @@ class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridF if($label) $labels[] = $label; } if($labels) { - return sprintf( - _t('GridField.PlaceHolderWithLabels', 'Find %s by %s', 'Find by '), - singleton($dataClass)->plural_name(), - implode(', ', $labels) + return _t( + 'GridField.PlaceHolderWithLabels', + 'Find {type} by {name}', + array('type' => singleton($dataClass)->plural_name(), 'name' => implode(', ', $labels)) ); } else { - return sprintf( - _t('GridField.PlaceHolder', 'Find %s', 'Find '), - singleton($dataClass)->plural_name() + return _t( + 'GridField.PlaceHolder', 'Find {type}', + array('type' => singleton($dataClass)->plural_name()) ); } } diff --git a/i18n/i18n.php b/i18n/i18n.php index 4e8460a06..c350797f6 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -1488,7 +1488,7 @@ class i18n extends Object implements TemplateGlobalProvider { $translatorsByPrio = self::$translators; if(!$translatorsByPrio) $translatorsByPrio = self::get_translators(); - $returnValue = $string; // Fall back to default string argument + $returnValue = (is_string($string)) ? $string : ''; // Fall back to default string argument foreach($translatorsByPrio as $priority => $translators) { foreach($translators as $name => $translator) { @@ -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/i18n/i18nTextCollector.php b/i18n/i18nTextCollector.php index d4a054523..fb5503830 100644 --- a/i18n/i18nTextCollector.php +++ b/i18n/i18nTextCollector.php @@ -162,6 +162,7 @@ class i18nTextCollector extends Object { //Debug::message("Processing Module '{$module}'", false); // Search for calls in code files if these exists + $fileList = array(); if(is_dir("$this->basePath/$module/code")) { $fileList = $this->getFilesRecursive("$this->basePath/$module/code"); } else if($module == FRAMEWORK_DIR || substr($module, 0, 7) == 'themes/') { diff --git a/lang/en.yml b/lang/en.yml index 24a67903f..751121af7 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -55,7 +55,7 @@ en: NO: No YES: Yes CMSMain: - ACCESS: 'Access to ''%s'' section' + ACCESS: 'Access to ''{title}'' section' ACCESSALLINTERFACES: 'Access to all CMS sections' ACCESSALLINTERFACESHELP: 'Overrules more specific access settings.' SAVE: Save @@ -72,7 +72,7 @@ en: YES: Yes ComplexTableField: CLOSEPOPUP: 'Close Popup' - SUCCESSADD: 'Added %s %s %s' + SUCCESSADD2: 'Added {name}' SUCCESSEDIT: 'Saved %s %s %s' SUCCESSEDIT2: 'Deleted %s %s' ComplexTableField.ss: @@ -83,9 +83,9 @@ en: NEXT: Next PREVIOUS: Previous ConfirmedPasswordField: - ATLEAST: 'Passwords must be at least %s characters long.' - BETWEEN: 'Passwords must be %s to %s characters long.' - MAXIMUM: 'Passwords must be at most %s characters long.' + ATLEAST: 'Passwords must be at least {min} characters long.' + BETWEEN: 'Passwords must be {min} to {max} characters long.' + MAXIMUM: 'Passwords must be at most {max} characters long.' SHOWONCLICKTITLE: 'Change Password' CreditCardField: FIRST: first @@ -108,16 +108,16 @@ en: MONTHS: ' months' SEC: ' sec' SECS: ' secs' - TIMEDIFFAGO: '%s ago' - TIMEDIFFIN: 'in %s' + TIMEDIFFAGO: '{difference} ago' + TIMEDIFFIN: 'in {difference}' YEAR: ' year' YEARS: ' years' DateField: NOTSET: 'not set' TODAY: today - VALIDDATEFORMAT2: 'Please enter a valid date format (%s).' - VALIDDATEMAXDATE: 'Your date has to be older or matching the maximum allowed date (%s)' - VALIDDATEMINDATE: 'Your date has to be newer or matching the minimum allowed date (%s)' + VALIDDATEFORMAT2: 'Please enter a valid date format ({format}).' + VALIDDATEMAXDATE: 'Your date has to be older or matching the maximum allowed date ({date})' + VALIDDATEMINDATE: 'Your date has to be newer or matching the minimum allowed date ({date})' DropdownField: CHOOSE: (Choose) EmailField: @@ -130,24 +130,24 @@ en: File: Content: Content Filename: Filename - INVALIDEXTENSION: 'Extension is not allowed (valid: %s)' + INVALIDEXTENSION: 'Extension is not allowed (valid: {extensions})' NOFILESIZE: 'Filesize is zero bytes.' NOVALIDUPLOAD: 'File is not a valid upload' Name: Name PLURALNAME: Files SINGULARNAME: File - TOOLARGE: 'Filesize is too large, maximum %s allowed.' + TOOLARGE: 'Filesize is too large, maximum {size} allowed.' Title: Title FileIFrameField: - ATTACH: 'Attach %s' - ATTACHONCESAVED: '%ss can be attached once you have saved the record for the first time.' - DELETE: 'Delete %s' + ATTACH: 'Attach {type}' + ATTACHONCESAVED: '{type}s can be attached once you have saved the record for the first time.' + DELETE: 'Delete {type}' DISALLOWEDFILETYPE: 'This filetype is not allowed to be uploaded' FILE: File FROMCOMPUTER: 'From your Computer' FROMFILESTORE: 'From the File Store' NOSOURCE: 'Please select a source file to attach' - REPLACE: 'Replace %s' + REPLACE: 'Replace {type}' FileIFrameField_iframe.ss: TITLE: 'Image Uploading Iframe' ForgotPasswordEmail.ss: @@ -157,8 +157,7 @@ en: TEXT3: for Form: FIELDISREQUIRED: '%s is required' - VALIDATIONCREDITNUMBER: 'Please ensure you have entered the %s credit card number correctly.' - VALIDATIONFAILED: 'Validation failed' + VALIDATIONCREDITNUMBER: 'Please ensure you have entered the {number} credit card number correctly.' VALIDATIONNOTUNIQUE: 'The value entered is not unique' VALIDATIONPASSWORDSDONTMATCH: 'Passwords don''t match' VALIDATIONPASSWORDSNOTEMPTY: 'Passwords can''t be empty' @@ -179,8 +178,8 @@ en: NoItemsFound: 'No items found' PRINTEDAT: 'Printed at' PRINTEDBY: 'Printed by' - PlaceHolder: 'Find %s' - PlaceHolderWithLabels: 'Find %s by %s' + PlaceHolder: 'Find {type}' + PlaceHolderWithLabels: 'Find {type} by {name}' RelationSearch: 'Relation search' ResetFilter: Reset GridFieldAction_Delete: @@ -209,16 +208,17 @@ en: GroupImportForm: Help1: '

Import one or more groups in CSV format (comma-separated values). Show advanced usage

' Help2: "
\n

Advanced usage

\n
    \n
  • Allowed columns: %s
  • \n
  • Existing groups are matched by their unique Code value, and updated with any new values from the imported file
  • \n
  • Group hierarchies can be created by using a ParentCode column.
  • \n
  • Permission codes can be assigned by the PermissionCode column. Existing permission codes are not cleared.
  • \n
\n
" - ResultCreated: 'Created %d groups' + ResultCreated: 'Created {count} groups' ResultDeleted: 'Deleted %d groups' ResultUpdated: 'Updated %d groups' Hierarchy: - InfiniteLoopNotAllowed: 'Infinite loop found within the "%s" hierarchy. Please change the parent to resolve this' + InfiniteLoopNotAllowed: 'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this' HtmlEditorField: ANCHORVALUE: Anchor BUTTONINSERT: Insert BUTTONINSERTLINK: 'Insert link' BUTTONREMOVELINK: 'Remove link' + BUTTONUpdate: Update CAPTIONTEXT: 'Caption text' CSSCLASS: 'Alignment / style' CSSCLASSCENTER: 'Centered, on its own.' @@ -233,12 +233,12 @@ en: FROMCMS: 'From the CMS' FROMCOMPUTER: 'From your computer' Find: Find - IMAGE: 'Insert Image' IMAGEALTTEXT: 'Alternative text (alt) - shown if image cannot be displayed' IMAGEDIMENSIONS: Dimensions IMAGEHEIGHTPX: Height IMAGETITLE: 'Title text (tooltip) - for additional information about the image' IMAGEWIDTHPX: Width + INSERTIMAGE: 'Insert Image' LINK: 'Insert Link' LINKANCHOR: 'Anchor on this page' LINKDESCR: 'Link description' @@ -250,6 +250,7 @@ en: LINKTO: 'Link to' PAGE: Page URL: URL + UpdateIMAGE: 'Update Image' ImageField: IMAGE: Image Image_iframe.ss: @@ -270,6 +271,7 @@ en: IP: 'IP Address' Status: Status Member: + ADDGROUP: 'Add group' ADDRESS: Address BUTTONCHANGEPASSWORD: 'Change Password' BUTTONLOGIN: 'Log in' @@ -293,8 +295,8 @@ en: FIRSTNAME: 'First Name' GREETING: Welcome INTERFACELANG: 'Interface Language' - INVALIDNEWPASSWORD: 'We couldn''t accept that password: %s' - LOGGEDINAS: 'You''re logged in as %s.' + INVALIDNEWPASSWORD: 'We couldn''t accept that password: {password}' + LOGGEDINAS: 'You''re logged in as {name}.' MOBILE: Mobile NAME: Name NEWPASSWORD: 'New Password' @@ -308,8 +310,8 @@ en: SUBJECTPASSWORDRESET: 'Your password reset link' SURNAME: Surname VALIDATIONMEMBEREXISTS: 'A member already exists with the same %s' - ValidationIdentifierFailed: 'Can''t overwrite existing member #%d with identical identifier (%s = %s))' - WELCOMEBACK: 'Welcome Back, %s' + ValidationIdentifierFailed: 'Can''t overwrite existing member #{id} with identical identifier ({name} = {value}))' + WELCOMEBACK: 'Welcome Back, {firstname}' YOUROLDPASSWORD: 'Your old password' belongs_many_many_Groups: Groups db_LastVisited: 'Last Visited Date' @@ -343,37 +345,37 @@ en: MemberImportForm: Help1: '

Import users in CSV format (comma-separated values). Show advanced usage

' Help2: "
\n

Advanced usage

\n
    \n
  • Allowed columns: %s
  • \n
  • Existing users are matched by their unique Code property, and updated with any new values from the imported file.
  • \n
  • Groups can be assigned by the Groups column. Groups are identified by their Code property, multiple groups can be separated by comma. Existing group memberships are not cleared.
  • \n
\n
" - ResultCreated: 'Created %d members' + ResultCreated: 'Created {count} members' ResultDeleted: 'Deleted %d members' ResultNone: 'No changes' - ResultUpdated: 'Updated %d members' + ResultUpdated: 'Updated {count} members' MemberTableField: 'APPLY FILTER': 'Apply Filter' ModelAdmin: DELETE: Delete - DELETEDRECORDS: 'Deleted %s records.' + DELETEDRECORDS: 'Deleted {count} records.' IMPORT: 'Import from CSV' - IMPORTEDRECORDS: 'Imported %s records.' + IMPORTEDRECORDS: 'Imported {count} records.' NOCSVFILE: 'Please browse for a CSV file to import' NOIMPORT: 'Nothing to import' RESET: Reset - UPDATEDRECORDS: 'Updated %s records.' + UPDATEDRECORDS: 'Updated {count} records.' MoneyField: FIELDLABELAMOUNT: Amount FIELDLABELCURRENCY: Currency NullableField: IsNullLabel: 'Is Null' NumericField: - VALIDATION: '''%s'' is not a number, only numbers can be accepted for this field' + VALIDATION: '''{value}'' is not a number, only numbers can be accepted for this field' Permission: AdminGroup: Administrator CMS_ACCESS_CATEGORY: 'CMS Access' FULLADMINRIGHTS: 'Full administrative rights' FULLADMINRIGHTS_HELP: 'Implies and overrules all other assigned permissions.' PermissionCheckboxSetField: - AssignedTo: 'assigned to "%s"' - FromGroup: 'inherited from group "%s"' - FromRole: 'inherited from role "%s"' + AssignedTo: 'assigned to "{title}"' + FromGroup: 'inherited from group "{title}"' + FromRole: 'inherited from role "{title}"' FromRoleOnGroup: 'inherited from role "%s" on group "%s"' Permissions: PERMISSIONS_CATEGORY: 'Roles and access permissions' @@ -391,10 +393,10 @@ en: LOGGEDOUT: 'You have been logged out. If you would like to log in again, enter your credentials below.' LOGIN: 'Log in' NOTEPAGESECURED: 'That page is secured. Enter your credentials below and we will send you right along.' - NOTERESETLINKINVALID: '

The password reset link is invalid or expired.

You can request a new one here or change your password after you logged in.

' + NOTERESETLINKINVALID: '

The password reset link is invalid or expired.

You can request a new one here or change your password after you logged in.

' NOTERESETPASSWORD: 'Enter your e-mail address and we will send you a link with which you can reset your password' - PASSWORDSENTHEADER: 'Password reset link sent to ''%s''' - PASSWORDSENTTEXT: 'Thank you! A reset link has been sent to ''%s'', provided an account exists for this email address.' + PASSWORDSENTHEADER: 'Password reset link sent to ''{email}''' + PASSWORDSENTTEXT: 'Thank you! A reset link has been sent to ''{email}'', provided an account exists for this email address.' SecurityAdmin: ACCESS_HELP: 'Allow viewing, adding and editing users, as well as assigning permissions and roles to them.' APPLY_ROLES: 'Apply roles to groups' @@ -440,7 +442,7 @@ en: TextareaField_Readonly.ss: NONE: none TimeField: - VALIDATEFORMAT: 'Please enter a valid time format (%s)' + VALIDATEFORMAT: 'Please enter a valid time format ({format})' ToggleCompositeField.ss: HIDE: Hide SHOW: Show @@ -459,7 +461,7 @@ en: FIELDNOTSET: 'File information not found' FROMCOMPUTER: 'From your computer' FROMFILES: 'From files' - MAXNUMBEROFFILES: 'Max number of %s file(s) exceeded.' + MAXNUMBEROFFILES: 'Max number of {count} file(s) exceeded.' REMOVEERROR: 'Error removing file' REMOVEINFO: 'Remove this file from here, but do not delete it from the file store' STARTALL: 'Start all' diff --git a/model/Hierarchy.php b/model/Hierarchy.php index 48ad13889..dbec83d1f 100644 --- a/model/Hierarchy.php +++ b/model/Hierarchy.php @@ -44,13 +44,11 @@ class Hierarchy extends DataExtension { if ($node->ParentID==$this->owner->ID) { // Hierarchy is looping. $validationResult->error( - sprintf( - _t( - 'Hierarchy.InfiniteLoopNotAllowed', - 'Infinite loop found within the "%s" hierarchy. Please change the parent to resolve this', - 'First argument is the class that makes up the hierarchy.' - ), - $this->owner->class + _t( + 'Hierarchy.InfiniteLoopNotAllowed', + 'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this', + 'First argument is the class that makes up the hierarchy.', + array('type' => $this->owner->class) ), 'INFINITE_LOOP' ); diff --git a/model/fieldtypes/Date.php b/model/fieldtypes/Date.php index 7de0cca95..269c93df8 100644 --- a/model/fieldtypes/Date.php +++ b/model/fieldtypes/Date.php @@ -191,24 +191,18 @@ class Date extends DBField { function Ago() { if($this->value) { if(strtotime($this->value) == time() || time() > strtotime($this->value)) { - return sprintf( - _t( - 'Date.TIMEDIFFAGO', - "%s ago", - - 'Natural language time difference, e.g. 2 hours ago' - ), - $this->TimeDiff() + return _t( + 'Date.TIMEDIFFAGO', + "{difference} ago", + 'Natural language time difference, e.g. 2 hours ago', + array('difference' => $this->TimeDiff()) ); } else { - return sprintf( - _t( - 'Date.TIMEDIFFIN', - "in %s", - - 'Natural language time difference, e.g. in 2 hours' - ), - $this->TimeDiff() + return _t( + 'Date.TIMEDIFFIN', + "in {difference}", + 'Natural language time difference, e.g. in 2 hours', + array('difference' => $this->TimeDiff()) ); } } diff --git a/security/ChangePasswordForm.php b/security/ChangePasswordForm.php index cb218e6f0..53b21b394 100644 --- a/security/ChangePasswordForm.php +++ b/security/ChangePasswordForm.php @@ -117,7 +117,11 @@ class ChangePasswordForm extends Form { } else { $this->clearMessage(); $this->sessionMessage( - sprintf(_t('Member.INVALIDNEWPASSWORD', "We couldn't accept that password: %s"), nl2br("\n".$isValid->starredList())), + _t( + 'Member.INVALIDNEWPASSWORD', + "We couldn't accept that password: {password}", + array('password' => nl2br("\n".$isValid->starredList())) + ), "bad" ); Director::redirectBack(); diff --git a/security/Member.php b/security/Member.php index a7774f981..0558c28d6 100644 --- a/security/Member.php +++ b/security/Member.php @@ -626,16 +626,15 @@ class Member extends DataObject implements TemplateGlobalProvider { ) ); if($existingRecord) { - throw new ValidationException(new ValidationResult(false, sprintf( - _t( - 'Member.ValidationIdentifierFailed', - 'Can\'t overwrite existing member #%d with identical identifier (%s = %s))', - - 'The values in brackets show a fieldname mapped to a value, usually denoting an existing email address' - ), - $existingRecord->ID, - $identifierField, - $this->$identifierField + throw new ValidationException(new ValidationResult(false, _t( + 'Member.ValidationIdentifierFailed', + 'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))', + 'The values in brackets show a fieldname mapped to a value, usually denoting an existing email address', + array( + 'id' => $existingRecord->ID, + 'name' => $identifierField, + 'value' => $this->$identifierField + ) ))); } } @@ -1624,12 +1623,10 @@ class Member_Validator extends RequiredFields { $uniqueField = $this->form->dataFieldByName($identifierField); $this->validationError( $uniqueField->id(), - sprintf( - _t( - 'Member.VALIDATIONMEMBEREXISTS', - 'A member already exists with the same %s' - ), - strtolower($identifierField) + _t( + 'Member.VALIDATIONMEMBEREXISTS', + 'A member already exists with the same %s', + array('identifier' => strtolower($identifierField)) ), 'required' ); diff --git a/security/MemberLoginForm.php b/security/MemberLoginForm.php index 0299e181f..538824453 100644 --- a/security/MemberLoginForm.php +++ b/security/MemberLoginForm.php @@ -105,7 +105,11 @@ JS protected function getMessageFromSession() { parent::getMessageFromSession(); if(($member = Member::currentUser()) && !Session::get('MemberLoginForm.force_message')) { - $this->message = sprintf(_t('Member.LOGGEDINAS', "You're logged in as %s."), $member->{$this->loggedInAsField}); + $this->message = _t( + 'Member.LOGGEDINAS', + "You're logged in as {name}.", + array('name' => $member->{$this->loggedInAsField}) + ); } Session::set('MemberLoginForm.force_message', false); } @@ -198,7 +202,7 @@ JS } Session::set('Security.Message.message', - sprintf(_t('Member.WELCOMEBACK', "Welcome Back, %s"), $firstname) + _t('Member.WELCOMEBACK', "Welcome Back, {firstname}", array('firstname' => $firstname)) ); Session::set("Security.Message.type", "good"); } diff --git a/security/PermissionCheckboxSetField.php b/security/PermissionCheckboxSetField.php index 4f37bb854..03f639242 100644 --- a/security/PermissionCheckboxSetField.php +++ b/security/PermissionCheckboxSetField.php @@ -96,9 +96,9 @@ class PermissionCheckboxSetField extends FormField { $relationMethod = $this->name; foreach($record->$relationMethod() as $permission) { if(!isset($uninheritedCodes[$permission->Code])) $uninheritedCodes[$permission->Code] = array(); - $uninheritedCodes[$permission->Code][] = sprintf( - _t('PermissionCheckboxSetField.AssignedTo', 'assigned to "%s"'), - $record->Title + $uninheritedCodes[$permission->Code][] = _t( + 'PermissionCheckboxSetField.AssignedTo', 'assigned to "{title}"', + array('title' => $record->Title) ); } @@ -110,14 +110,11 @@ class PermissionCheckboxSetField extends FormField { foreach($record->Roles() as $role) { foreach($role->Codes() as $code) { if (!isset($inheritedCodes[$code->Code])) $inheritedCodes[$code->Code] = array(); - $inheritedCodes[$code->Code][] = sprintf( - _t( - 'PermissionCheckboxSetField.FromRole', - 'inherited from role "%s"', - - 'A permission inherited from a certain permission role' - ), - $role->Title + $inheritedCodes[$code->Code][] = _t( + 'PermissionCheckboxSetField.FromRole', + 'inherited from role "{title}"', + 'A permission inherited from a certain permission role', + array('title' => $role->Title) ); } } @@ -132,15 +129,11 @@ class PermissionCheckboxSetField extends FormField { if ($role->Codes()) { foreach($role->Codes() as $code) { if (!isset($inheritedCodes[$code->Code])) $inheritedCodes[$code->Code] = array(); - $inheritedCodes[$code->Code][] = sprintf( - _t( - 'PermissionCheckboxSetField.FromRoleOnGroup', - 'inherited from role "%s" on group "%s"', - - 'A permission inherited from a role on a certain group' - ), - $role->Title, - $parent->Title + $inheritedCodes[$code->Code][] = _t( + 'PermissionCheckboxSetField.FromRoleOnGroup', + 'inherited from role "%s" on group "%s"', + 'A permission inherited from a role on a certain group', + array('roletitle' => $role->Title, 'grouptitle' => $parent->Title) ); } } @@ -149,14 +142,11 @@ class PermissionCheckboxSetField extends FormField { foreach($parent->Permissions() as $permission) { if (!isset($inheritedCodes[$permission->Code])) $inheritedCodes[$permission->Code] = array(); $inheritedCodes[$permission->Code][] = - sprintf( - _t( - 'PermissionCheckboxSetField.FromGroup', - 'inherited from group "%s"', - - 'A permission inherited from a certain group' - ), - $parent->Title + _t( + 'PermissionCheckboxSetField.FromGroup', + 'inherited from group "{title}"', + 'A permission inherited from a certain group', + array('title' => $parent->Title) ); } } diff --git a/security/Security.php b/security/Security.php index 29765ec96..1d07d7318 100644 --- a/security/Security.php +++ b/security/Security.php @@ -494,10 +494,10 @@ class Security extends Controller { $email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension()); $customisedController = $controller->customise(array( - 'Title' => sprintf(_t('Security.PASSWORDSENTHEADER', "Password reset link sent to '%s'"), $email), + 'Title' => _t('Security.PASSWORDSENTHEADER', "Password reset link sent to '{email}'", array('email' => $email)), 'Content' => "

" . - sprintf(_t('Security.PASSWORDSENTTEXT', "Thank you! A reset link has been sent to '%s', provided an account exists for this email address."), $email) . + _t('Security.PASSWORDSENTTEXT', "Thank you! A reset link has been sent to '{email}', provided an account exists for this email address.", array('email' => $email)) . "

", 'Email' => $email )); @@ -571,12 +571,10 @@ class Security extends Controller { if(isset($_REQUEST['h'])) { $customisedController = $controller->customise( array('Content' => - sprintf( - _t('Security.NOTERESETLINKINVALID', - '

The password reset link is invalid or expired.

You can request a new one here or change your password after you logged in.

' - ), - $this->Link('lostpassword'), - $this->link('login') + _t( + 'Security.NOTERESETLINKINVALID', + '

The password reset link is invalid or expired.

You can request a new one here or change your password after you logged in.

', + array('link1' => $this->Link('lostpassword'), 'link2' => $this->link('login')) ) ) ); diff --git a/tests/filesystem/UploadTest.php b/tests/filesystem/UploadTest.php index 8816d00b5..d3f112384 100644 --- a/tests/filesystem/UploadTest.php +++ b/tests/filesystem/UploadTest.php @@ -333,28 +333,22 @@ class UploadTest_Validator extends Upload_Validator implements TestOnly { if(!$this->isValidSize()) { $ext = (isset($pathInfo['extension'])) ? $pathInfo['extension'] : ''; $arg = File::format_size($this->getAllowedMaxFileSize($ext)); - $this->errors[] = sprintf( - _t( - 'File.TOOLARGE', - 'Filesize is too large, maximum %s allowed.', - - 'Argument 1: Filesize (e.g. 1MB)' - ), - $arg + $this->errors[] = _t( + 'File.TOOLARGE', + 'Filesize is too large, maximum {size} allowed.', + 'Argument 1: Filesize (e.g. 1MB)', + array('size' => $arg) ); return false; } // extension validation if(!$this->isValidExtension()) { - $this->errors[] = sprintf( - _t( - 'File.INVALIDEXTENSION', - 'Extension is not allowed (valid: %s)', - - 'Argument 1: Comma-separated list of valid extensions' - ), - implode(',', $this->allowedExtensions) + $this->errors[] = _t( + 'File.INVALIDEXTENSION', + 'Extension is not allowed (valid: {extensions})', + 'Argument 1: Comma-separated list of valid extensions', + array('extensions' => implode(',', $this->allowedExtensions)) ); return false; } 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); }