diff --git a/include/class.client.php b/include/class.client.php
index cfa492bfc2474af087b2e69406019908847d636a..84b69f2570a15f436caca77cbf407a340596809c 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -64,9 +64,12 @@ abstract class TicketUser {
             'user' => $this,
             'recipient' => $this);
 
+        $lang = false;
+        if (is_callable(array($this, 'getLanguage')))
+            $lang = $this->getLanguage(UserAccount::LANG_MAILOUTS);
         $msg = $ost->replaceTemplateVariables(array(
-            'subj' => $content->getName(),
-            'body' => $content->getBody(),
+            'subj' => $content->getLocalName($lang),
+            'body' => $content->getLocalBody($lang),
         ), $vars);
 
         $email->send($this->getEmail(), Format::striptags($msg['subj']),
@@ -269,16 +272,9 @@ class  EndUser extends AuthenticatedUser {
         return $this->_account;
     }
 
-    function getLanguage() {
-        static $cached = false;
-
-        if (!$cached) {
-            if ($acct = $this->getAccount())
-                $cached = $acct->getLanguage();
-            if (!$cached)
-                $cached = Internationalization::getDefaultLanguage();
-        }
-        return $cached;
+    function getLanguage($flags=false) {
+        if ($acct = $this->getAccount())
+            return $acct->getLanguage($flags);
     }
 
     private function getStats() {
@@ -360,32 +356,12 @@ class ClientAccount extends UserAccount {
         unset($_SESSION['_client']['reset-token']);
     }
 
-    function getExtraAttr($attr=false) {
-        if (!isset($this->_extra))
-            $this->_extra = JsonDataParser::decode($this->ht['extra']);
-
-        return $attr ? $this->_extra[$attr] : $this->_extra;
-    }
-
-    function setExtraAttr($attr, $value) {
-        $this->getExtraAttr();
-        $this->_extra[$attr] = $value;
-    }
-
     function onLogin($bk) {
         $this->setExtraAttr('browser_lang',
             Internationalization::getCurrentLanguage());
         $this->save();
     }
 
-    function save($refetch=false) {
-        // Serialize the extra column on demand
-        if (isset($this->_extra)) {
-            $this->extra = JsonDataEncoder::encode($this->_extra);
-        }
-        return parent::save($refetch);
-    }
-
     function update($vars, &$errors) {
         global $cfg;
 
diff --git a/include/class.page.php b/include/class.page.php
index 617190f3a97cf5a40c981ef3c2824351915030c2..d39c39ab8f23406024e22ba619fb8ebee81eda84 100644
--- a/include/class.page.php
+++ b/include/class.page.php
@@ -66,19 +66,41 @@ class Page {
     function getName() {
         return $this->ht['name'];
     }
+    function getLocalName($lang=false) {
+        return $this->_getLocal('name', $lang);
+    }
 
     function getBody() {
         return $this->ht['body'];
     }
-    function getLocalBody() {
-        $tag = $this->getTranslateTag('body');
-        $T = CustomDataTranslation::translateArticle($tag);
-        return $T != $tag ? $T : $this->getBody();
+    function getLocalBody($lang=false) {
+        return $this->_getLocal('body', $lang);
     }
     function getBodyWithImages() {
         return Format::viewableImages($this->getLocalBody(), ROOT_PATH.'image.php');
     }
 
+    function _fetchTranslation($lang=false) {
+        if (!isset($this->_local) || $lang) {
+            $tags = array(
+                $this->getTranslateTag('body'),
+                $this->getTranslateTag('article'),
+            );
+            if (!$lang)
+                $lang = Internationalization::getCurrentLanguage();
+            $this->_local = CustomDataTranslation::allTranslations($tags, 'article', $lang);
+        }
+        return $this->_local;
+    }
+    function _getLocal($what, $lang=false) {
+        $tag = $this->getTranslateTag($what);
+        foreach ($this->_fetchTranslation($lang) as $t)
+            if ($tag == $t->object_hash)
+                return $t->text;
+
+        return $this->ht[$what];
+    }
+
     function getNotes() {
         return $this->ht['notes'];
     }
diff --git a/include/class.staff.php b/include/class.staff.php
index 9738abf2842551defd0d0cf71915e0e498cfbfed..e20e3c03cd805be8dcf5d8c91578d4168274d938 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -277,17 +277,7 @@ class Staff extends AuthenticatedUser {
     }
 
     function getLanguage() {
-        // XXX: This should just return the language preference. Caching
-        //      should be done elsewhere
-        static $cached = false;
-        if (!$cached)
-            $cached = &$_SESSION['staff:lang'];
-
-        if (!$cached) {
-            $cached = $this->ht['lang']
-                ?: Internationalization::getDefaultLanguage();
-        }
-        return $cached;
+        return $this->ht['lang'];
     }
 
     function isManager() {
@@ -435,11 +425,11 @@ class Staff extends AuthenticatedUser {
         return ($stats=$this->getTicketsStats())?$stats['closed']:0;
     }
 
-    function getExtraAttr($attr=false) {
-        if (!isset($this->extra))
-            $this->extra = JsonDataParser::decode($this->ht['extra']);
+    function getExtraAttr($attr=false, $default=null) {
+        if (!isset($this->_extra))
+            $this->_extra = JsonDataParser::decode($this->ht['extra']);
 
-        return $attr ? $this->extra[$attr] : $this->extra;
+        return $attr ? (@$this->_extra[$attr] ?: $default) : $this->_extra;
     }
 
     function setExtraAttr($attr, $value, $commit=true) {
@@ -735,9 +725,10 @@ class Staff extends AuthenticatedUser {
                 $email->getEmail()
             ), false);
 
+        $lang = $this->ht['lang'] ?: $this->getExtraAttr('browser_lang');
         $msg = $ost->replaceTemplateVariables(array(
-            'subj' => $content->getName(),
-            'body' => $content->getBody(),
+            'subj' => $content->getLocalName($lang),
+            'body' => $content->getLocalBody($lang),
         ), $vars);
 
         $_config = new Config('pwreset');
diff --git a/include/class.translation.php b/include/class.translation.php
index b01d8bfc8cffa16148aece8bfd9bb18fc3d50bfb..8951857b2ae5eb8be42fb91fc5d7f8ecc933fecf 100644
--- a/include/class.translation.php
+++ b/include/class.translation.php
@@ -934,7 +934,7 @@ class CustomDataTranslation extends VerySimpleModel {
         return static::translate($msgid, $locale, false, 'article');
     }
 
-    static function allTranslations($msgid, $type='phrase') {
+    static function allTranslations($msgid, $type='phrase', $lang=false) {
         $criteria = array('type' => $type);
 
         if (is_array($msgid))
@@ -942,6 +942,9 @@ class CustomDataTranslation extends VerySimpleModel {
         else
             $criteria['object_hash'] = $msgid;
 
+        if ($lang)
+            $criteria['lang'] = $lang;
+
         return static::objects()->filter($criteria)->all();
     }
 
diff --git a/include/class.user.php b/include/class.user.php
index 4fec3df6275ebbed48f6758be6cb9e5944435304..f94b585cc2f92db7ee58243677b7dfca871ed6c8 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -868,13 +868,60 @@ class UserAccountModel extends VerySimpleModel {
         return $this->user;
     }
 
-    function getLanguage() {
-        return $this->get('lang');
+    function getExtraAttr($attr=false, $default=null) {
+        if (!isset($this->_extra))
+            $this->_extra = JsonDataParser::decode($this->extra);
+
+        return $attr ? (@$this->_extra[$attr] ?: $default) : $this->_extra;
+    }
+
+    function setExtraAttr($attr, $value) {
+        $this->getExtraAttr();
+        $this->_extra[$attr] = $value;
+    }
+
+    /**
+     * Function: getLanguage
+     *
+     * Returns the language preference for the user or false if no
+     * preference is defined. False indicates the browser indicated
+     * preference should be used. For requests apart from browser requests,
+     * the last language preference of the browser is set in the
+     * 'browser_lang' extra attribute upon logins. Send the LANG_MAILOUTS
+     * flag to also consider this saved value. Such is useful when sending
+     * the user a message (such as an email), and the user's browser
+     * preference is not available in the HTTP request.
+     *
+     * Parameters:
+     * $flags - (int) Send UserAccount::LANG_MAILOUTS if the user's
+     *      last-known browser preference should be considered. Normally
+     *      only the user's saved language preference is considered.
+     *
+     * Returns:
+     * Current or last-known language preference or false if no language
+     * preference is currently set or known.
+     */
+    function getLanguage($flags=false) {
+        $lang = $this->get('lang', false);
+        if (!$lang && ($flags & UserAccount::LANG_MAILOUTS))
+            $lang = $this->getExtraAttr('browser_lang', false);
+
+        return $lang;
+    }
+
+    function save($refetch=false) {
+        // Serialize the extra column on demand
+        if (isset($this->_extra)) {
+            $this->extra = JsonDataEncoder::encode($this->_extra);
+        }
+        return parent::save($refetch);
     }
 }
 
 class UserAccount extends UserAccountModel {
 
+    const LANG_MAILOUTS = 1;            // Language preference for mailouts
+
     function hasPassword() {
         return (bool) $this->get('passwd');
     }
@@ -914,9 +961,10 @@ class UserAccount extends UserAccountModel {
         $info = array('email' => $email, 'vars' => &$vars, 'log'=>true);
         Signal::send('auth.pwreset.email', $this->getUser(), $info);
 
+        $lang = $this->getLanguage(UserAccount::LANG_MAILOUTS);
         $msg = $ost->replaceTemplateVariables(array(
-            'subj' => $content->getName(),
-            'body' => $content->getBody(),
+            'subj' => $content->getLocalName($lang),
+            'body' => $content->getLocalBody($lang),
         ), $vars);
 
         $_config = new Config('pwreset');