Skip to content
Browse files

Merge pull request #388 from chillu/trac/7170-i18n-sprintf-injections

#7170 i18n sprintf injections
  • Loading branch information...
2 parents e929753 + 431b958 commit 151abde17d104b0e30e638834473a3fcb1ec8518 @halkyon halkyon committed
View
18 admin/code/GroupImportForm.php
@@ -70,17 +70,17 @@ function doImport($data, $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');
View
10 admin/code/LeftAndMain.php
@@ -1299,12 +1299,12 @@ function providePermissions() {
$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')
);
}
View
18 admin/code/MemberImportForm.php
@@ -75,17 +75,17 @@ function doImport($data, $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');
View
18 admin/code/ModelAdmin.php
@@ -387,17 +387,17 @@ function import($data, $form, $request) {
$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");
View
2 admin/code/SecurityAdmin.php
@@ -219,7 +219,7 @@ function providePermissions() {
$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',
View
4 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);
}
/**
View
29 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}
View
9 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
View
26 filesystem/Upload.php
@@ -496,28 +496,22 @@ public function validate() {
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;
}
View
9 forms/ComplexTableField.php
@@ -485,12 +485,11 @@ function saveComplexTableField($data, $form, $params) {
$editLink = Controller::join_links($this->Link(), 'item/' . $childData->ID . '/edit');
- $message = sprintf(
- _t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'),
- $childData->singular_name(),
- '<a href="' . $editLink . '">' . $childData->Title . '</a>',
- $closeLink
+ $message = _t(
+ 'ComplexTableField.SUCCESSADD2', 'Added {name}',
+ array('name' => $childData->singular_name())
);
+ $message .= '<a href="' . $editLink . '">' . $childData->Title . '</a>' . $closeLink;
$form->sessionMessage($message, 'good');
View
18 forms/ConfirmedPasswordField.php
@@ -247,13 +247,25 @@ function validate($validator) {
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)) {
View
7 forms/CreditCardField.php
@@ -51,9 +51,10 @@ function validate($validator){
}
$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
View
18 forms/DateField.php
@@ -310,9 +310,9 @@ function validate($validator) {
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 @@ function validate($validator) {
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 @@ function validate($validator) {
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
View
14 forms/FileIFrameField.php
@@ -92,9 +92,11 @@ public function Field($properties = array()) {
)
);
} 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 @@ public function EditFileForm() {
$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 @@ public function DeleteFileForm() {
),
new FieldList (
$deleteButton = new FormAction (
- 'delete', sprintf(_t('FileIFrameField.DELETE', 'Delete %s'), $this->FileTypeName())
+ 'delete', _t('FileIFrameField.DELETE', 'Delete {type}', array('type' => $this->FileTypeName()))
)
)
);
View
6 forms/NumericField.php
@@ -16,9 +16,9 @@ function validate($validator){
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"
);
View
6 forms/TimeField.php
@@ -143,9 +143,9 @@ function validate($validator) {
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
View
28 forms/UploadField.php
@@ -348,23 +348,26 @@ public function Field($properties = array()) {
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 @@ public function upload(SS_HTTPRequest $request) {
// 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'))
+ );
}
}
View
14 forms/gridfield/GridFieldAddExistingAutocompleter.php
@@ -251,15 +251,15 @@ public function getPlaceholderText($dataClass) {
if($label) $labels[] = $label;
}
if($labels) {
- return sprintf(
- _t('GridField.PlaceHolderWithLabels', 'Find %s by %s', 'Find <object type> by <field names>'),
- 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 <object type>'),
- singleton($dataClass)->plural_name()
+ return _t(
+ 'GridField.PlaceHolder', 'Find {type}',
+ array('type' => singleton($dataClass)->plural_name())
);
}
}
View
35 i18n/i18n.php
@@ -1488,7 +1488,7 @@ static function _t($entity, $string = "", $context = "", $injection = "") {
$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 @@ static function _t($entity, $string = "", $context = "", $injection = "") {
}
// 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);
+ }
+ }
}
}
View
1 i18n/i18nTextCollector.php
@@ -162,6 +162,7 @@ protected function processModule($module) {
//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/') {
View
84 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: '<p>Import one or more groups in <em>CSV</em> format (comma-separated values). <small><a href="#" class="toggle-advanced">Show advanced usage</a></small></p>'
Help2: "<div class=\"advanced\">\n <h4>Advanced usage</h4>\n <ul>\n <li>Allowed columns: <em>%s</em></li>\n <li>Existing groups are matched by their unique <em>Code</em> value, and updated with any new values from the imported file</li>\n <li>Group hierarchies can be created by using a <em>ParentCode</em> column.</li>\n <li>Permission codes can be assigned by the <em>PermissionCode</em> column. Existing permission codes are not cleared.</li>\n </ul>\n</div>"
- 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: '<p>Import users in <em>CSV format</em> (comma-separated values). <small><a href="#" class="toggle-advanced">Show advanced usage</a></small></p>'
Help2: "<div class=\"advanced\">\n <h4>Advanced usage</h4>\n <ul>\n <li>Allowed columns: <em>%s</em></li>\n <li>Existing users are matched by their unique <em>Code</em> property, and updated with any new values from the imported file.</li>\n <li>Groups can be assigned by the <em>Groups</em> column. Groups are identified by their <em>Code</em> property, multiple groups can be separated by comma. Existing group memberships are not cleared.</li>\n </ul>\n</div>"
- 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: '<p>The password reset link is invalid or expired.</p><p>You can request a new one <a href="%s">here</a> or change your password after you <a href="%s">logged in</a>.</p>'
+ NOTERESETLINKINVALID: '<p>The password reset link is invalid or expired.</p><p>You can request a new one <a href="{link1}">here</a> or change your password after you <a href="{link2}">logged in</a>.</p>'
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'
View
12 model/Hierarchy.php
@@ -44,13 +44,11 @@ function validate(ValidationResult $validationResult) {
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'
);
View
26 model/fieldtypes/Date.php
@@ -191,24 +191,18 @@ function Rfc3339() {
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())
);
}
}
View
6 security/ChangePasswordForm.php
@@ -117,7 +117,11 @@ function doChangePassword(array $data) {
} 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();
View
29 security/Member.php
@@ -626,16 +626,15 @@ function onBeforeWrite() {
)
);
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 @@ function php($data) {
$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'
);
View
8 security/MemberLoginForm.php
@@ -105,7 +105,11 @@ function __construct($controller, $name, $fields = null, $actions = null,
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 @@ protected function logInUserAndRedirect($data) {
}
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");
}
View
46 security/PermissionCheckboxSetField.php
@@ -96,9 +96,9 @@ function Field($properties = array()) {
$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 @@ function Field($properties = array()) {
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 @@ function Field($properties = array()) {
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 @@ function Field($properties = array()) {
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)
);
}
}
View
14 security/Security.php
@@ -494,10 +494,10 @@ public function passwordsent($request) {
$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' =>
"<p>" .
- 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)) .
"</p>",
'Email' => $email
));
@@ -571,12 +571,10 @@ public function changepassword() {
if(isset($_REQUEST['h'])) {
$customisedController = $controller->customise(
array('Content' =>
- sprintf(
- _t('Security.NOTERESETLINKINVALID',
- '<p>The password reset link is invalid or expired.</p><p>You can request a new one <a href="%s">here</a> or change your password after you <a href="%s">logged in</a>.</p>'
- ),
- $this->Link('lostpassword'),
- $this->link('login')
+ _t(
+ 'Security.NOTERESETLINKINVALID',
+ '<p>The password reset link is invalid or expired.</p><p>You can request a new one <a href="{link1}">here</a> or change your password after you <a href="{link2}">logged in</a>.</p>',
+ array('link1' => $this->Link('lostpassword'), 'link2' => $this->link('login'))
)
)
);
View
26 tests/filesystem/UploadTest.php
@@ -333,28 +333,22 @@ public function validate() {
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;
}
View
21 tests/i18n/i18nTest.php
@@ -251,7 +251,8 @@ function testNewTMethodSignature() {
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 @@ function testNewTMethodSignature() {
$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);
}

0 comments on commit 151abde

Please sign in to comment.
Something went wrong with that request. Please try again.