Index: includes/locale.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/locale.inc,v
retrieving revision 1.214
diff -u -p -r1.214 locale.inc
--- includes/locale.inc	27 Apr 2009 20:19:35 -0000	1.214
+++ includes/locale.inc	3 May 2009 21:44:13 -0000
@@ -2266,7 +2266,7 @@ function _locale_translate_seek() {
   foreach ($strings as $lid => $string) {
     $rows[] = array(
       $groups[$string['group']],
-      array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
+      array('data' => check_plain(drupal_truncate_chars($value['source'], 150, array('append' => TRUE))) . '<br /><small>' . $string['location'] . '</small>'),
       array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'),
       array('data' => l(t('edit'), "admin/international/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => 'nowrap'),
       array('data' => l(t('delete'), "admin/international/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => 'nowrap'),
Index: includes/theme.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/theme.inc,v
retrieving revision 1.481
diff -u -p -r1.481 theme.inc
--- includes/theme.inc	3 May 2009 08:12:12 -0000	1.481
+++ includes/theme.inc	3 May 2009 21:44:13 -0000
@@ -1629,12 +1629,7 @@ function theme_username($object) {
 
   if ($object->uid && $object->name) {
     // Shorten the name when it is too long or it will break many tables.
-    if (drupal_strlen($object->name) > 20) {
-      $name = drupal_substr($object->name, 0, 15) . '...';
-    }
-    else {
-      $name = $object->name;
-    }
+    $name = drupal_truncate_chars($object->name, 20, array('append' => TRUE));
 
     if (user_access('access user profiles')) {
       $output = l($name, 'user/' . $object->uid, array('attributes' => array('title' => t('View user profile.'))));
Index: includes/unicode.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/unicode.inc,v
retrieving revision 1.37
diff -u -p -r1.37 unicode.inc
--- includes/unicode.inc	2 Jan 2009 22:09:53 -0000	1.37
+++ includes/unicode.inc	3 May 2009 21:44:13 -0000
@@ -190,8 +190,8 @@ function drupal_convert_to_utf8($data, $
  *
  * Use this function whenever you want to chop off a string at an unsure
  * location. On the other hand, if you're sure that you're splitting on a
- * character boundary (e.g. after using strpos() or similar), you can safely use
- * substr() instead.
+ * character boundary (e.g. after using drupal_strlen() or similar), you can
+ * safely use substr() instead.
  *
  * @param $string
  *   The string to truncate.
@@ -218,24 +218,36 @@ function drupal_truncate_bytes($string, 
  *   The string to truncate.
  * @param $len
  *   An upper limit on the returned string length.
- * @param $wordsafe
- *   Flag to truncate at last space within the upper limit. Defaults to FALSE.
- * @param $dots
- *   Flag to add trailing dots. Defaults to FALSE.
+ * @param $options
+ *   An associative array of additional options, with the following keys:
+ *     - 'wordsave'
+ *       Flag to truncate at last space within the upper limit. Defaults to
+ *       FALSE.
+ *     - 'append'
+ *       Flag to add trailing string. Defaults to FALSE.
+ *     - 'dots'
+ *       If 'append' is TRUE, append this string to the end of $string. Defaults
+ *       to "...".
  * @return
  *   The truncated string.
  */
-function truncate_utf8($string, $len, $wordsafe = FALSE, $dots = FALSE) {
+function drupal_truncate_chars($string, $len, $options = array()) {
+  // Merge in defaults.
+  $options += array(
+      'wordsave' => FALSE,
+      'append' => FALSE,
+      'dots' => '...',
+    );
 
   if (drupal_strlen($string) <= $len) {
     return $string;
   }
 
-  if ($dots) {
-    $len -= 4;
-  }
-
-  if ($wordsafe) {
+  if ($options['wordsafe']) {
+    if ($options['append']) {
+      // Make room for dots, if needed.
+      $len -= drupal_strlen($options['dots']) + 1;
+    }
     $string = drupal_substr($string, 0, $len + 1); // leave one more character
     if ($last_space = strrpos($string, ' ')) { // space exists AND is not on position 0
       $string = substr($string, 0, $last_space);
@@ -243,15 +255,18 @@ function truncate_utf8($string, $len, $w
     else {
       $string = drupal_substr($string, 0, $len);
     }
+    if ($options['append']) {
+      $string .= ' ' . $options['dots'];
+    }
+  }
+  elseif ($options['append']) {
+    $len -= drupal_strlen($options['dots']); // Make room for dots.
+    $string = drupal_substr($string, 0, $len) . $options['dots'];
   }
   else {
     $string = drupal_substr($string, 0, $len);
   }
 
-  if ($dots) {
-    $string .= ' ...';
-  }
-
   return $string;
 }
 
@@ -265,8 +280,8 @@ function truncate_utf8($string, $len, $w
  *
  * Notes:
  * - Only encode strings that contain non-ASCII characters.
- * - We progressively cut-off a chunk with truncate_utf8(). This is to ensure
- *   each chunk starts and ends on a character boundary.
+ * - We progressively cut-off a chunk with drupal_truncate_bytes(). This is to
+ *   ensure each chunk starts and ends on a byte boundary.
  * - Using \n as the chunk separator may cause problems on some systems and may
  *   have to be changed to \r\n or \r.
  */
Index: includes/database/mysql/schema.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/database/mysql/schema.inc,v
retrieving revision 1.18
diff -u -p -r1.18 schema.inc
--- includes/database/mysql/schema.inc	25 Apr 2009 16:57:19 -0000	1.18
+++ includes/database/mysql/schema.inc	3 May 2009 21:44:13 -0000
@@ -358,7 +358,7 @@ class DatabaseSchema_mysql extends Datab
     // Truncate comment to maximum comment length.
     if (isset($length)) {
       // Add table prefixes before truncating.
-      $comment = truncate_utf8($this->connection->prefixTables($comment), $length, TRUE, TRUE);
+      $comment = drupal_truncate_chars($this->connection->prefixTables($comment), $length, array('wordsafe' => TRUE, 'append' => TRUE));
     }
 
     return $this->connection->quote($comment);
Index: modules/aggregator/aggregator.parser.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.parser.inc,v
retrieving revision 1.1
diff -u -p -r1.1 aggregator.parser.inc
--- modules/aggregator/aggregator.parser.inc	22 Dec 2008 19:38:31 -0000	1.1
+++ modules/aggregator/aggregator.parser.inc	3 May 2009 21:44:13 -0000
@@ -123,7 +123,7 @@ function aggregator_parse_feed(&$data, $
       $item['TITLE'] = $item['TITLE'];
     }
     elseif (!empty($item['DESCRIPTION'])) {
-      $item['TITLE'] = preg_replace('/^(.*)[^\w;&].*?$/', "\\1", truncate_utf8($item['DESCRIPTION'], 40));
+      $item['TITLE'] = preg_replace('/^(.*)[^\w;&].*?$/', "\\1", drupal_truncate_chars($item['DESCRIPTION'], 40));
     }
     else {
       $item['TITLE'] = '';
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.492
diff -u -p -r1.492 book.module
--- modules/book/book.module	3 May 2009 10:11:34 -0000	1.492
+++ modules/book/book.module	3 May 2009 21:44:13 -0000
@@ -934,7 +934,7 @@ function _book_toc_recurse($tree, $inden
     }
 
     if (!in_array($data['link']['mlid'], $exclude)) {
-      $toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
+      $toc[$data['link']['mlid']] = $indent . ' ' . drupal_truncate_chars($data['link']['title'], 30, array('wordsafe' => TRUE, 'append' => TRUE));
       if ($data['below']) {
         _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
       }
Index: modules/comment/comment.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.admin.inc,v
retrieving revision 1.18
diff -u -p -r1.18 comment.admin.inc
--- modules/comment/comment.admin.inc	26 Apr 2009 19:44:38 -0000	1.18
+++ modules/comment/comment.admin.inc	3 May 2009 21:44:13 -0000
@@ -87,7 +87,7 @@ function comment_admin_overview($type = 
 
   foreach ($result as $comment) {
     $options[$comment->cid] = array(
-      'subject' => l($comment->subject, 'node/' . $comment->nid, array('attributes' => array('title' => truncate_utf8($comment->comment, 128)), 'fragment' => 'comment-' . $comment->cid)),
+      'subject' => l($comment->subject, 'node/' . $comment->nid, array('attributes' => array('title' => drupal_truncate_chars($comment->comment, 128)), 'fragment' => 'comment-' . $comment->cid)),
       'author' => theme('username', $comment),
       'posted_in' => l($comment->node_title, 'node/' . $comment->nid),
       'time' => format_date($comment->timestamp, 'small'),
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.708
diff -u -p -r1.708 comment.module
--- modules/comment/comment.module	3 May 2009 10:11:34 -0000	1.708
+++ modules/comment/comment.module	3 May 2009 21:44:13 -0000
@@ -1747,7 +1747,7 @@ function _comment_form_submit(&$comment_
     // 2) Strip out all HTML tags
     // 3) Convert entities back to plain-text.
     // Note: format is checked by check_markup().
-    $comment_values['subject'] = truncate_utf8(trim(decode_entities(strip_tags(check_markup($comment_values['comment'], $comment_values['comment_format'])))), 29, TRUE);
+    $comment_values['subject'] = drupal_truncate_chars(trim(decode_entities(strip_tags(check_markup($comment_values['comment'], $comment_values['comment_format'])))), 29, array('wordsafe' => TRUE));
     // Edge cases where the comment body is populated only by HTML tags will
     // require a default subject.
     if ($comment_values['subject'] == '') {
Index: modules/dblog/dblog.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/dblog/dblog.admin.inc,v
retrieving revision 1.16
diff -u -p -r1.16 dblog.admin.inc
--- modules/dblog/dblog.admin.inc	26 Apr 2009 19:44:38 -0000	1.16
+++ modules/dblog/dblog.admin.inc	3 May 2009 21:44:14 -0000
@@ -89,7 +89,7 @@ function dblog_overview() {
         $icons[$dblog->severity],
         t($dblog->type),
         format_date($dblog->timestamp, 'small'),
-        l(truncate_utf8(_dblog_format_message($dblog), 56, TRUE, TRUE), 'admin/reports/event/' . $dblog->wid, array('html' => TRUE)),
+        l(drupal_truncate_chars(_dblog_format_message($dblog), 56, array('wordsafe' => TRUE, 'append' => TRUE)), 'admin/reports/event/' . $dblog->wid, array('html' => TRUE)),
         theme('username', $dblog),
         $dblog->link,
       ),
@@ -137,7 +137,7 @@ function dblog_top($type) {
 
   $rows = array();
   foreach ($result as $dblog) {
-    $rows[] = array($dblog->count, truncate_utf8(_dblog_format_message($dblog), 56, TRUE, TRUE));
+    $rows[] = array($dblog->count, drupal_truncate_chars(_dblog_format_message($dblog), 56, array('wordsafe' => TRUE, 'append' => TRUE)));
   }
 
   if (empty($rows)) {
Index: modules/filter/filter.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/filter/filter.module,v
retrieving revision 1.248
diff -u -p -r1.248 filter.module
--- modules/filter/filter.module	25 Apr 2009 18:01:10 -0000	1.248
+++ modules/filter/filter.module	3 May 2009 21:44:14 -0000
@@ -846,12 +846,7 @@ function _filter_url_trim($text, $length
     $_length = $length;
   }
 
-  // Use +3 for '...' string length.
-  if (strlen($text) > $_length + 3) {
-    $text = substr($text, 0, $_length) . '...';
-  }
-
-  return $text;
+  return drupal_truncate_chars($text, $_length, array('append' => TRUE));
 }
 
 /**
Index: modules/menu/menu.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/menu/menu.module,v
retrieving revision 1.184
diff -u -p -r1.184 menu.module
--- modules/menu/menu.module	11 Apr 2009 22:19:45 -0000	1.184
+++ modules/menu/menu.module	3 May 2009 21:44:14 -0000
@@ -235,7 +235,7 @@ function _menu_parents_recurse($tree, $m
       break;
     }
     if ($data['link']['mlid'] != $exclude && $data['link']['hidden'] >= 0) {
-      $title = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, FALSE);
+      $title = $indent . ' ' . drupal_truncate_chars($data['link']['title'], 30, array('wordsafe' => TRUE));
       if ($data['link']['hidden']) {
         $title .= ' (' . t('disabled') . ')';
       }
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1044
diff -u -p -r1.1044 node.module
--- modules/node/node.module	3 May 2009 10:11:34 -0000	1.1044
+++ modules/node/node.module	3 May 2009 21:44:14 -0000
@@ -417,7 +417,7 @@ function node_teaser($body, $format = NU
   // sentence boundaries.
 
   // The teaser may not be longer than maximum length specified. Initial slice.
-  $teaser = truncate_utf8($body, $size);
+  $teaser = drupal_truncate_chars($body, $size);
 
   // Store the actual length of the UTF8 string -- which might not be the same
   // as $size.
Index: modules/search/search.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/search/search.module,v
retrieving revision 1.288
diff -u -p -r1.288 search.module
--- modules/search/search.module	20 Apr 2009 21:28:15 -0000	1.288
+++ modules/search/search.module	3 May 2009 21:44:14 -0000
@@ -401,7 +401,7 @@ function search_index_split($text) {
  * Helper function for array_walk in search_index_split.
  */
 function _search_index_truncate(&$text) {
-  $text = truncate_utf8($text, 50);
+  $text = drupal_truncate_chars($text, 50);
 }
 
 /**
@@ -1271,7 +1271,7 @@ function search_excerpt($keys, $text) {
 
   // If we didn't find anything, return the beginning.
   if (count($ranges) == 0) {
-    return truncate_utf8($text, 256) . ' ...';
+    return drupal_truncate_chars($text, 260, array('wordsafe' => TRUE, 'append' => TRUE));
   }
 
   // Sort the text ranges by starting position.
Index: modules/statistics/statistics.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.module,v
retrieving revision 1.303
diff -u -p -r1.303 statistics.module
--- modules/statistics/statistics.module	3 May 2009 10:11:35 -0000	1.303
+++ modules/statistics/statistics.module	3 May 2009 21:44:15 -0000
@@ -351,7 +351,7 @@ function statistics_block_view($delta = 
  */
 function _statistics_link($path, $width = 35) {
   $title = drupal_get_path_alias($path);
-  $title = truncate_utf8($title, $width, FALSE, TRUE);
+  $title = drupal_truncate_chars($title, $width, array('append' => TRUE));
   return l($title, $path);
 }
 
