diff --git a/core/Common.php b/core/Common.php
index 7b49f0b9fbd805f9937cb051aad07019fb89992a..a75e151269112fe4b8bc0fd644525baca5b5a57e 100644
--- a/core/Common.php
+++ b/core/Common.php
@@ -553,6 +553,81 @@ class Piwik_Common
 		return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $filename));
 	}
 
+/*
+ * String operations
+ */
+
+	/**
+	 * byte-oriented substr() - ASCII
+	 *
+	 * @param string $string
+	 * @param int $start
+	 * @param int $length optional length
+	 * @return string
+	 */
+	static public function substr($string, $start)
+	{
+		// in case mbstring overloads substr function
+		$substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr';
+
+		$length = func_num_args() > 2
+			? func_get_arg(2)
+			: self::strlen($string);
+
+		return $substr($string, $start, $length);
+	}
+
+	/**
+	 * byte-oriented strlen() - ASCII
+	 *
+	 * @param string $string
+	 * @return int
+	 */
+	static public function strlen($string)
+	{
+		// in case mbstring overloads strlen function
+		$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
+		return $strlen($string);
+	}
+
+	/**
+	 * multi-byte substr() - UTF-8
+	 *
+	 * @param string $string
+	 * @param int $start
+	 * @param int $length optional length
+	 * @return string
+	 */
+	static public function mb_substr($string, $start)
+	{
+		$length = func_num_args() > 2
+			? func_get_arg(2)
+			: self::mb_strlen($string);
+
+		if (function_exists('mb_substr'))
+		{
+			return mb_substr($string, $start, $length, 'UTF-8');
+		}
+
+		return substr($string, $start, $length);
+	}
+
+	/**
+	 * multi-byte strlen() - UTF-8
+	 *
+	 * @param string $string
+	 * @return int
+	 */
+	static public function mb_strlen($string)
+	{
+		if (function_exists('mb_strlen'))
+		{
+			return mb_strlen($string, 'UTF-8');
+		}
+
+		return strlen($string);
+	}
+
 /*
  * Escaping input
  */
diff --git a/core/Http.php b/core/Http.php
index 90db47b3f99cb565f7c75367767e39b8f1282b46..6cfaf372af823e6454877f2aac8adb44c150bf3b 100644
--- a/core/Http.php
+++ b/core/Http.php
@@ -92,7 +92,6 @@ class Piwik_Http
 			throw new Exception('Too many redirects ('.$followDepth.')');
 		}
 
-		$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
 		$contentLength = 0;
 		$fileLength = 0;
 
@@ -285,7 +284,7 @@ class Piwik_Http
 					throw new Exception('Timed out waiting for server response');
 				}
 
-				$fileLength += $strlen($line);
+				$fileLength += Piwik_Common::strlen($line);
 
 				if(is_resource($file))
 				{
@@ -345,7 +344,7 @@ class Piwik_Http
 				while(!feof($handle))
 				{
 					$response = fread($handle, 8192);
-					$fileLength += $strlen($response);
+					$fileLength += Piwik_Common::strlen($response);
 					fwrite($file, $response);
 				}
 				fclose($handle);
@@ -353,7 +352,7 @@ class Piwik_Http
 			else
 			{
 				$response = @file_get_contents($aUrl, 0, $ctx);
-				$fileLength = $strlen($response);
+				$fileLength = Piwik_Common::strlen($response);
 			}
 
 			// restore the socket_timeout value
@@ -445,7 +444,7 @@ class Piwik_Http
 			}
 
 			$contentLength = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
-			$fileLength = is_resource($file) ? curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : $strlen($response);
+			$fileLength = is_resource($file) ? curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : Piwik_Common::strlen($response);
 
 			@curl_close($ch);
 			unset($ch);
diff --git a/core/IP.php b/core/IP.php
index 4629c8c8a35cb9dae13fab972a5e57e73ede3b27..08dae1ff23f6d32f8c1d2f1d12b5f331c8ca1f49 100644
--- a/core/IP.php
+++ b/core/IP.php
@@ -111,9 +111,6 @@ class Piwik_IP
 	 */
 	static public function sanitizeIpRange($ipRangeString)
 	{
-		// in case mbstring overloads strlen function
-		$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
-
 		$ipRangeString = trim($ipRangeString);
 		if(empty($ipRangeString))
 		{
@@ -143,7 +140,7 @@ class Piwik_IP
 		if(($ip = @_inet_pton($ipRangeString)) === false)
 			return false;
 
-		$maxbits = $strlen($ip) * 8;
+		$maxbits = Piwik_Common::strlen($ip) * 8;
 		if(!isset($bits))
 			$bits = $maxbits;
 
@@ -207,24 +204,20 @@ class Piwik_IP
 	 */
 	static public function long2ip($ip)
 	{
-		// in case mbstring overloads strlen and substr functions
-		$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
-		$substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr';
-
 		// IPv4
-		if($strlen($ip) == 4)
+		if(Piwik_Common::strlen($ip) == 4)
 		{
 			return self::N2P($ip);
 		}
 
 		// IPv6 - transitional address?
-		if($strlen($ip) == 16)
+		if(Piwik_Common::strlen($ip) == 16)
 		{
 			if(substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0
 				|| substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0)
 			{
 				// remap 128-bit IPv4-mapped and IPv4-compat addresses
-				return self::N2P($substr($ip, 12));
+				return self::N2P(Piwik_Common::substr($ip, 12));
 			}
 		}
 
@@ -239,9 +232,6 @@ class Piwik_IP
 	 */
 	static public function getIpsForRange($ipRange)
 	{
-		// in case mbstring overloads strlen function
-		$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
-
 		if(strpos($ipRange, '/') === false)
 		{
 			$ipRange = self::sanitizeIpRange($ipRange);
@@ -256,7 +246,7 @@ class Piwik_IP
 			return false;
 		}
 
-		$lowLen = $strlen($low);
+		$lowLen = Piwik_Common::strlen($low);
 		$i = $lowLen - 1;
 		$bits = $lowLen * 8 - $bits;
 
@@ -287,10 +277,7 @@ class Piwik_IP
 	 */
 	static public function isIpInRange($ip, $ipRanges)
 	{
-		// in case mbstring overloads strlen function
-		$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
-
-		$ipLen = $strlen($ip);
+		$ipLen = Piwik_Common::strlen($ip);
 		if(empty($ip) || empty($ipRanges) || ($ipLen != 4 && $ipLen != 16))
 		{
 			return false;
@@ -316,7 +303,7 @@ class Piwik_IP
 
 			$low = $range[0];
 			$high = $range[1];
-			if($strlen($low) != $ipLen)
+			if(Piwik_Common::strlen($low) != $ipLen)
 			{
 				continue;
 			}
@@ -461,12 +448,9 @@ class Piwik_IP
  */
 function php_compat_inet_ntop($in_addr)
 {
-	// in case mbstring overloads strlen function
-	$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
-
 	$r = bin2hex($in_addr);
 
-	switch ($strlen($in_addr))
+	switch (Piwik_Common::strlen($in_addr))
 	{
 		case 4:
 			// IPv4 address
diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php
index debd77d707d06cff93ac4c74fd11ac3cee9c4b68..0053672271442b27990d459744cf81bd470f88d7 100644
--- a/core/Tracker/Visit.php
+++ b/core/Tracker/Visit.php
@@ -1251,8 +1251,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface
 	protected function getConfigHash( $os, $browserName, $browserVersion, $resolution, $plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, $ip, $browserLang)
 	{
 		$hash = md5( $os . $browserName . $browserVersion . $plugin_Flash . $plugin_Java . $plugin_Director . $plugin_Quicktime . $plugin_RealPlayer . $plugin_PDF . $plugin_WindowsMedia . $plugin_Gears . $plugin_Silverlight . $plugin_Cookie . $ip . $browserLang, $raw_output = true );
-		$substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr';
-		return $substr( $hash, 0, Piwik_Tracker::LENGTH_BINARY_ID );
+		return Piwik_Common::substr( $hash, 0, Piwik_Tracker::LENGTH_BINARY_ID );
 	}
 
 	/**
diff --git a/core/Visualization/Cloud.php b/core/Visualization/Cloud.php
index 152c7a9b8086b89d205ba51c27772d9fc6142823..ee1f6c2df727dceb0e199e1a8747689300068d3a 100644
--- a/core/Visualization/Cloud.php
+++ b/core/Visualization/Cloud.php
@@ -43,9 +43,6 @@ class Piwik_Visualization_Cloud implements Piwik_View_Interface
 
 	public function render()
 	{
-		$strlen = function_exists('mb_strlen') ? 'mb_strlen' : 'strlen';
-		$substr = function_exists('mb_substr') ? 'mb_substr' : 'substr';
-
 		$this->shuffleCloud();
 		$return = array();
 		if(empty($this->wordsArray)) {
@@ -55,9 +52,9 @@ class Piwik_Visualization_Cloud implements Piwik_View_Interface
 		foreach ($this->wordsArray as $word => $popularity)
 		{
 			$wordTruncated = $word;
-			if($strlen($word) > $this->truncatingLimit)
+			if(Piwik_Common::mb_strlen($word) > $this->truncatingLimit)
 			{
-				$wordTruncated = $substr($word, 0, $this->truncatingLimit - 3).'...';
+				$wordTruncated = Piwik_Common::mb_substr($word, 0, $this->truncatingLimit - 3).'...';
 			}
 			
 			// case hideFutureHoursWhenToday=1 shows hours with no visits
diff --git a/libs/Smarty/plugins/modifier.truncate.php b/libs/Smarty/plugins/modifier.truncate.php
index 35c89690a1556cc6dfa943bde00f40abb43dca15..ca789774779fee54fbaf14088e2c08047880571a 100644
--- a/libs/Smarty/plugins/modifier.truncate.php
+++ b/libs/Smarty/plugins/modifier.truncate.php
@@ -30,6 +30,21 @@ function smarty_modifier_truncate($string, $length = 80, $etc = '...',
     if ($length == 0)
         return '';
 
+    if (function_exists('mb_strlen') && function_exists('mb_substr'))
+    {
+        if (mb_strlen($string, 'UTF-8') > $length) {
+            $length -= min($length, mb_strlen($etc, 'UTF-8'));
+            if (!$break_words && !$middle) {
+                $string = preg_replace('/\s+?(\S+)?$/', '', mb_substr($string, 0, $length+1, 'UTF-8'));
+            }
+            if(!$middle) {
+                return mb_substr($string, 0, $length, 'UTF-8') . $etc;
+            }
+            return mb_substr($string, 0, $length/2, 'UTF-8') . $etc . mb_substr($string, -$length/2, $length/2, 'UTF-8');
+        }
+        return $string;
+    }
+
     if (strlen($string) > $length) {
         $length -= min($length, strlen($etc));
         if (!$break_words && !$middle) {
@@ -37,12 +52,10 @@ function smarty_modifier_truncate($string, $length = 80, $etc = '...',
         }
         if(!$middle) {
             return substr($string, 0, $length) . $etc;
-        } else {
-            return substr($string, 0, $length/2) . $etc . substr($string, -$length/2);
         }
-    } else {
-        return $string;
+        return substr($string, 0, $length/2) . $etc . substr($string, -$length/2);
     }
+    return $string;
 }
 
 /* vim: set expandtab: */
diff --git a/plugins/AnonymizeIP/AnonymizeIP.php b/plugins/AnonymizeIP/AnonymizeIP.php
index bb6220a10c7393e14dd3ce92f34fffeaea0d9d0a..cfee9f309dee2b9ba0365e7c3341a871a698a600 100644
--- a/plugins/AnonymizeIP/AnonymizeIP.php
+++ b/plugins/AnonymizeIP/AnonymizeIP.php
@@ -50,9 +50,7 @@ class Piwik_AnonymizeIP extends Piwik_Plugin
 	 */
 	static public function applyIPMask($ip, $maskLength)
 	{
-		// in case mbstring overloads strlen function
-		$strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen';
-		$i = $strlen($ip);
+		$i = Piwik_Common::strlen($ip);
 		if($maskLength > $i)
 		{
 			$maskLength = $i;
diff --git a/plugins/Proxy/Controller.php b/plugins/Proxy/Controller.php
index b1af775d802b666d8f07bfc7da95ed6646b8fc4a..9f88b1748629f799298c89ca09849f52c6f2a6b1 100644
--- a/plugins/Proxy/Controller.php
+++ b/plugins/Proxy/Controller.php
@@ -57,9 +57,8 @@ class Piwik_Proxy_Controller extends Piwik_Controller
 		$data = base64_decode($rawData);
 		if($data !== false)
 		{
-			$substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr';
 			// check for PNG header
-			if($substr($data, 0, 8) === "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a")
+			if(Piwik_Common::substr($data, 0, 8) === "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a")
 			{
 				header('Content-Type: image/png');