diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index b67139e149dca97683341a7891b4d2aa1fa09bbb..758eb94c6883fb630eb65f6d8dfb49ccfc16bb40 100644
--- a/includes/bootstrap.inc
+++ b/includes/bootstrap.inc
@@ -1431,15 +1431,7 @@ function drupal_unpack($obj, $field = 'data') {
  *   A string containing the English string to translate.
  * @param $args
  *   An associative array of replacements to make after translation.
- *   Occurrences in $string of any key in $args are replaced with the
- *   corresponding value, after sanitization. The sanitization function depends
- *   on the first character of the key:
- *   - !variable: Inserted as is. Use this for text that has already been
- *     sanitized.
- *   - @variable: Escaped to HTML using check_plain(). Use this for anything
- *     displayed on a page on the site.
- *   - %variable: Escaped as a placeholder for user-submitted content using
- *     drupal_placeholder(), which shows up as <em>emphasized</em> text.
+ *   See format_string().
  * @param $options
  *   An associative array of additional options, with the following elements:
  *   - 'langcode' (defaults to the current language): The language code to
@@ -1485,26 +1477,50 @@ function t($string, array $args = array(), array $options = array()) {
     return $string;
   }
   else {
-    // Transform arguments before inserting them.
-    foreach ($args as $key => $value) {
-      switch ($key[0]) {
-        case '@':
-          // Escaped only.
-          $args[$key] = check_plain($value);
-          break;
+    return format_string($string, $args);
+  }
+}
 
-        case '%':
-        default:
-          // Escaped and placeholder.
-          $args[$key] = drupal_placeholder($value);
-          break;
+/**
+ * Replace placeholders with sanitized values in a string.
+ *
+ * @param $string
+ *   A string containing placeholders.
+ * @param $args
+ *   An associative array of replacements to make. Occurrences in $string of
+ *   any key in $args are replaced with the corresponding value, after
+ *   sanitization. The sanitization function depends on the first character of
+ *   the key:
+ *   - !variable: Inserted as is. Use this for text that has already been
+ *     sanitized.
+ *   - @variable: Escaped to HTML using check_plain(). Use this for anything
+ *     displayed on a page on the site.
+ *   - %variable: Escaped as a placeholder for user-submitted content using
+ *     drupal_placeholder(), which shows up as <em>emphasized</em> text.
+ *
+ * @see t()
+ * @ingroup sanitization
+ */
+function format_string($string, array $args = array()) {
+  // Transform arguments before inserting them.
+  foreach ($args as $key => $value) {
+    switch ($key[0]) {
+      case '@':
+        // Escaped only.
+        $args[$key] = check_plain($value);
+        break;
 
-        case '!':
-          // Pass-through.
-      }
+      case '%':
+      default:
+        // Escaped and placeholder.
+        $args[$key] = drupal_placeholder($value);
+        break;
+
+      case '!':
+        // Pass-through.
     }
-    return strtr($string, $args);
   }
+  return strtr($string, $args);
 }
 
 /**
diff --git a/misc/drupal.js b/misc/drupal.js
index 3cebbd28458dedcc7c9c37f39aa72b23ef1bb661..7e2cc4d7b425b6b8ed751b51c78a09fcf28f65d5 100644
--- a/misc/drupal.js
+++ b/misc/drupal.js
@@ -111,6 +111,8 @@ Drupal.detachBehaviors = function (context, settings, trigger) {
 
 /**
  * Encode special characters in a plain-text string for display as HTML.
+ *
+ * @ingroup sanitization
  */
 Drupal.checkPlain = function (str) {
   var character, regex,
@@ -125,6 +127,45 @@ Drupal.checkPlain = function (str) {
   return str;
 };
 
+/**
+ * Replace placeholders with sanitized values in a string.
+ *
+ * @param str
+ *   A string with placeholders.
+ * @param args
+ *   An object of replacements pairs to make. Incidences of any key in this
+ *   array are replaced with the corresponding value. Based on the first
+ *   character of the key, the value is escaped and/or themed:
+ *    - !variable: inserted as is
+ *    - @variable: escape plain text to HTML (Drupal.checkPlain)
+ *    - %variable: escape text and theme as a placeholder for user-submitted
+ *      content (checkPlain + Drupal.theme('placeholder'))
+ *
+ * @see Drupal.t()
+ * @ingroup sanitization
+ */
+Drupal.formatString = function(str, args) {
+  // Transform arguments before inserting them.
+  for (var key in args) {
+    switch (key.charAt(0)) {
+      // Escaped only.
+      case '@':
+        args[key] = Drupal.checkPlain(args[key]);
+      break;
+      // Pass-through.
+      case '!':
+        break;
+      // Escaped and placeholder.
+      case '%':
+      default:
+        args[key] = Drupal.theme('placeholder', args[key]);
+        break;
+    }
+    str = str.replace(key, args[key]);
+  }
+  return str;
+}
+
 /**
  * Translate strings to the page language or a given language.
  *
@@ -135,11 +176,7 @@ Drupal.checkPlain = function (str) {
  * @param args
  *   An object of replacements pairs to make after translation. Incidences
  *   of any key in this array are replaced with the corresponding value.
- *   Based on the first character of the key, the value is escaped and/or themed:
- *    - !variable: inserted as is
- *    - @variable: escape plain text to HTML (Drupal.checkPlain)
- *    - %variable: escape text and theme as a placeholder for user-submitted
- *      content (checkPlain + Drupal.theme('placeholder'))
+ *   See Drupal.formatString().
  * @return
  *   The translated string.
  */
@@ -150,24 +187,7 @@ Drupal.t = function (str, args) {
   }
 
   if (args) {
-    // Transform arguments before inserting them.
-    for (var key in args) {
-      switch (key.charAt(0)) {
-        // Escaped only.
-        case '@':
-          args[key] = Drupal.checkPlain(args[key]);
-        break;
-        // Pass-through.
-        case '!':
-          break;
-        // Escaped and placeholder.
-        case '%':
-        default:
-          args[key] = Drupal.theme('placeholder', args[key]);
-          break;
-      }
-      str = str.replace(key, args[key]);
-    }
+    str = Drupal.formatString(str, args);
   }
   return str;
 };
@@ -193,11 +213,7 @@ Drupal.t = function (str, args) {
  * @param args
  *   An object of replacements pairs to make after translation. Incidences
  *   of any key in this array are replaced with the corresponding value.
- *   Based on the first character of the key, the value is escaped and/or themed:
- *    - !variable: inserted as is
- *    - @variable: escape plain text to HTML (Drupal.checkPlain)
- *    - %variable: escape text and theme as a placeholder for user-submitted
- *      content (checkPlain + Drupal.theme('placeholder'))
+ *   See Drupal.formatString().
  *   Note that you do not need to include @count in this array.
  *   This replacement is done automatically for the plural case.
  * @return
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index 177e457338fbd9d9fc55c55989fed1545bc36966..5f696733ebc4fd113288686a8618feeb86033218 100644
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -345,14 +345,14 @@ class CommonURLUnitTest extends DrupalWebTestCase {
 }
 
 /**
- * Tests for the check_plain() and filter_xss() functions.
+ * Tests for the check_plain(), filter_xss() and format_string() functions.
  */
 class CommonXssUnitTest extends DrupalUnitTestCase {
 
   public static function getInfo() {
     return array(
       'name' => 'String filtering tests',
-      'description' => 'Confirm that check_plain(), filter_xss(), and check_url() work correctly, including invalid multi-byte sequences.',
+      'description' => 'Confirm that check_plain(), filter_xss(), format_string() and check_url() work correctly, including invalid multi-byte sequences.',
       'group' => 'System',
     );
   }
@@ -385,6 +385,22 @@ class CommonXssUnitTest extends DrupalUnitTestCase {
      $this->assertEqual($text, '&lt;&gt;&amp;&quot;&#039;', 'check_plain() escapes reserved HTML characters.');
   }
 
+  /**
+   * Test t() and format_string() replacement functionality.
+   */
+  function testFormatStringAndT() {
+    foreach (array('format_string', 't') as $function) {
+      $text = $function('Simple text');
+      $this->assertEqual($text, 'Simple text', $function . ' leaves simple text alone.');
+      $text = $function('Escaped text: @value', array('@value' => '<script>'));
+      $this->assertEqual($text, 'Escaped text: &lt;script&gt;', $function . ' replaces and escapes string.');
+      $text = $function('Placeholder text: %value', array('%value' => '<script>'));
+      $this->assertEqual($text, 'Placeholder text: <em class="placeholder">&lt;script&gt;</em>', $function . ' replaces, escapes and themes string.');
+      $text = $function('Verbatim text: !value', array('!value' => '<script>'));
+      $this->assertEqual($text, 'Verbatim text: <script>', $function . ' replaces verbatim string as-is.');
+    }
+  }
+
   /**
    * Check that harmful protocols are stripped.
    */