diff --git a/bootstrap.php b/bootstrap.php
index 41cd7cf4f5d700772919a697182d4eb50f882f1f..d6f75d0f7e8d30d4fe5ac78751a174ee7beaf3c4 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -180,6 +180,7 @@ class Bootstrap {
     function loadCode() {
         #include required files
         require_once INCLUDE_DIR.'class.util.php';
+        require INCLUDE_DIR.'class.translation.php';
         require(INCLUDE_DIR.'class.signal.php');
         require(INCLUDE_DIR.'class.user.php');
         require(INCLUDE_DIR.'class.auth.php');
diff --git a/include/ajax.config.php b/include/ajax.config.php
index d2547d98c4abd967837d9a49073ff29b64bc5d32..ab1ebecc07ae23856d22c36bbb27624a053992ac 100644
--- a/include/ajax.config.php
+++ b/include/ajax.config.php
@@ -39,6 +39,8 @@ class ConfigAjaxAPI extends AjaxController {
               'lang'            => $lang,
               'short_lang'      => $sl,
               'has_rtl'         => $rtl,
+              'primary_language' => $cfg->getPrimaryLanguage(),
+              'secondary_languages' => $cfg->getSecondaryLanguages(),
         );
         return $this->json_encode($config);
     }
@@ -60,6 +62,8 @@ class ConfigAjaxAPI extends AjaxController {
             'lang'            => $lang,
             'short_lang'      => $sl,
             'has_rtl'         => $rtl,
+            'primary_language' => $cfg->getPrimaryLanguage(),
+            'secondary_languages' => $cfg->getSecondaryLanguages(),
         );
 
         $config = $this->json_encode($config);
diff --git a/include/ajax.i18n.php b/include/ajax.i18n.php
index 3aeaefa135b8448db40e97fc8fb2b0ce1a13ac6e..2f5ef9ebe7cbc319b3099f5538e16c120fa3eb51 100644
--- a/include/ajax.i18n.php
+++ b/include/ajax.i18n.php
@@ -37,5 +37,80 @@ class i18nAjaxAPI extends AjaxController {
         Http::cacheable(md5($data), $cfg->lastModified());
         echo $data;
     }
+
+    function getTranslations($tag) {
+        $t = CustomDataTranslation::allTranslations($tag);
+        $phrases = array();
+        $lm = 0;
+        foreach ($t as $translation) {
+            $phrases[$translation->lang] = $translation->text;
+            $lm = max($lm, strtotime($translation->updated));
+        }
+        $json = JsonDataEncoder::encode($phrases) ?: '{}';
+        //Http::cacheable(md5($json), $lm);
+
+        return $json;
+    }
+
+    function updateTranslations($tag) {
+        global $thisstaff, $cfg;
+
+        if (!$thisstaff)
+            Http::response(403, "Agent login required");
+        if (!$_POST)
+            Http::response(422, "No translations found to update");
+
+        $t = CustomDataTranslation::allTranslations($tag);
+        $phrases = array();
+        foreach ($t as $translation) {
+            $phrases[$translation->lang] = $translation;
+        }
+        foreach ($_POST as $lang => $phrase) {
+            if (isset($phrases[$lang])) {
+                if (!$phrase) {
+                    $p->delete();
+                }
+                else {
+                    $p = $phrases[$lang];
+                    // Avoid XSS injection
+                    $p->text = trim(Format::striptags($phrase));
+                    $p->agent_id = $thisstaff->getId();
+                }
+            }
+            elseif (in_array($lang, $cfg->getSecondaryLanguages())) {
+                if (!$phrase)
+                    continue;
+                $phrases[$lang] = CustomDataTranslation::create(array(
+                    'lang'          => $lang,
+                    'text'          => $phrase,
+                    'object_hash'   => $tag,
+                    'type'          => 'phrase',
+                    'agent_id'      => $thisstaff->getId(),
+                    'updated'       => new SqlFunction('NOW'),
+                ));
+            }
+            else {
+                Http::response(400,
+                    sprintf("%s: Must be a secondary language", $lang));
+            }
+        }
+        // Commit.
+        foreach ($phrases as $p)
+            if (!$p->save())
+                Http::response(500, sprintf("%s: Unable to commit language"));
+    }
+
+    function getSecondaryLanguages() {
+        global $cfg;
+
+        $langs = array();
+        foreach (array('de', 'ja', 'zh_CN') as $l) {
+            $langs[$l] = Internationalization::getLanguageDescription($l);
+        }
+        $json = JsonDataEncoder::encode($langs);
+        Http::cacheable(md5($json), $cfg->lastModified());
+
+        return $json;
+    }
 }
 ?>
diff --git a/include/class.config.php b/include/class.config.php
index 04a7cea4546bdf65430034564fd3d2812a5fc50a..9685fb55d40ae45e8006920d2683841605469deb 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -797,10 +797,14 @@ class OsticketConfig extends Config {
         return ($this->get('allow_attachments'));
     }
 
-    function getSystemLanguage() {
+    function getPrimaryLanguage() {
         return $this->get('system_language');
     }
 
+    function getSecondaryLanguages() {
+        return array('de', 'ja', 'zh_CN');
+    }
+
     /* Needed by upgrader on 1.6 and older releases upgrade - not not remove */
     function getUploadDir() {
         return $this->get('upload_dir');
diff --git a/include/class.dept.php b/include/class.dept.php
index 7abb372d1e9c16f75deb80b8ed60ee31a6a87c1b..d12216f5611f26a26ac2c22c814b1f6e82cfb029 100644
--- a/include/class.dept.php
+++ b/include/class.dept.php
@@ -14,7 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Dept {
+class Dept implements Translatable {
+
     var $id;
 
     var $email;
@@ -77,6 +78,12 @@ class Dept {
         return $this->ht['name'];
     }
 
+    function getLocalName($locale=false) {
+        return CustomDataTranslation::translate($this->getTranslationTag(), $locale);
+    }
+    function getTranslationTag() {
+        return _H('dept.name.' . $this->getId());
+    }
 
     function getEmailId() {
         return $this->ht['email_id'];
@@ -391,6 +398,12 @@ class Dept {
                 $depts[$id] = $name;
         }
 
+        // Fetch local names
+        foreach (CustomDataTranslation::getDepartmentNames(array_keys($depts)) as $id=>$name) {
+            // Translate the department
+            $depts[$id] = $name;
+        }
+
         return $depts;
     }
 
diff --git a/include/class.i18n.php b/include/class.i18n.php
index 4a0d138f295bec3586369127a16d255d3d51e61a..8db307de912384cc1874f3487b71755c2aeccac8 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -26,7 +26,7 @@ class Internationalization {
     function Internationalization($language=false) {
         global $cfg;
 
-        if ($cfg && ($lang = $cfg->getSystemLanguage()))
+        if ($cfg && ($lang = $cfg->getPrimaryLanguage()))
             array_unshift($this->langs, $language);
 
         // Detect language filesystem path, case insensitively
@@ -252,7 +252,7 @@ class Internationalization {
         global $cfg;
 
         if (empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
-            return $cfg->getSystemLanguage();
+            return $cfg->getPrimaryLanguage();
 
         $languages = self::availableLanguages();
 
@@ -337,6 +337,7 @@ class Internationalization {
         $user = $user ?: $thisstaff ?: $thisclient;
         if ($user && method_exists($user, 'getLanguage'))
             return $user->getLanguage();
+        // Support the flag buttons for guests
         if (isset($_SESSION['client:lang']))
             return $_SESSION['client:lang'];
         return self::getDefaultLanguage();
diff --git a/include/class.translation.php b/include/class.translation.php
index cc21848a31e9153f8940c939a48d624bed36d461..4900bae82dc1d98c8721024785a0d3fe3884e58f 100644
--- a/include/class.translation.php
+++ b/include/class.translation.php
@@ -856,6 +856,116 @@ class TextDomain {
     }
 }
 
+require_once INCLUDE_DIR . 'class.orm.php';
+class CustomDataTranslation extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => 'ost_translation',
+        'pk' => array('id')
+    );
+
+    const FLAG_FUZZY        = 0x01;     // Source string has been changed
+    const FLAG_UNAPPROVED   = 0x02;     // String has been reviewed by an authority
+    const FLAG_CURRENT      = 0x04;     // If more than one version exist, this is current
+
+    static function lookup($msgid, $flags=0) {
+        if (!is_string($msgid))
+            return parent::lookup($msgid);
+
+        // Hash is 16 char of md5
+        $hash = substr(md5($msgid), -16);
+
+        $criteria = array('object_hash'=>$hash);
+
+        if ($flags)
+            $criteria += array('flags__hasbit'=>$flags);
+
+        return parent::lookup($criteria);
+    }
+
+    static function getTranslation($locale, $cache=true) {
+        static $_cache = array();
+
+        if ($cache && isset($_cache[$locale]))
+            return $_cache[$locale];
+
+        $criteria = array(
+            'lang' => $locale,
+            'type' => 'phrase',
+        );
+
+        $mo = array();
+        foreach (static::objects()->filter($criteria) as $t) {
+            $mo[$t->object_hash] = $t;
+        }
+
+        return $_cache[$locale] = $mo;
+    }
+
+    static function translate($msgid, $locale=false, $cache=true) {
+        global $thisstaff, $thisclient;
+
+        if (!$locale
+                && ($user = $thisstaff ?: $thisclient)
+                && method_exists($user, 'getLanguage'))
+            $locale = $user->getLanguage();
+
+        // Support sending a User as the locale
+        elseif (is_object($locale) && method_exists($locale, 'getLanguage'))
+            $locale = $locale->getLanguage();
+
+        if ($locale) {
+            if ($cache) {
+                $mo = static::getTranslation($locale);
+                if (isset($mo[$msgid]))
+                    $msgid = $mo[$msgid]->text;
+            }
+            elseif ($p = static::lookup(array(
+                    'type' => 'phrase',
+                    'lang' => $locale,
+                    'object_hash' => $msgid
+            ))) {
+                $msgid = $p->text;
+            }
+        }
+        return $msgid;
+    }
+
+    static function allTranslations($msgid) {
+        return static::objects()->filter(array(
+            'type' => 'phrase',
+            'object_hash' => $msgid
+        ))->all();
+    }
+
+    static function getDepartmentNames($ids) {
+        global $cfg;
+
+        $tags = array();
+        $names = array();
+        foreach ($ids as $i)
+            $tags[_H('dept.name.'.$i)] = $i;
+
+        if (($lang = Internationalization::getCurrentLanguage())
+            && $lang != $cfg->getPrimaryLanguage()
+        ) {
+            foreach (CustomDataTranslation::objects()->filter(array(
+                'object_hash__in'=>array_keys($tags),
+                'lang'=>$lang
+                )) as $translation
+            ) {
+                $names[$tags[$translation->object_hash]] = $translation->text;
+            }
+        }
+        return $names;
+    }
+
+}
+
+class CustomTextDomain {
+
+}
+
 // Functions for gettext library. Since the gettext extension for PHP is not
 // used as a fallback, there is no detection and compat funciton
 // installation for the gettext library function calls.
@@ -911,6 +1021,18 @@ function _dcnpgettext($domain, $context, $singular, $plural, $category, $n) {
         ->npgettext($context, $singular, $plural, $n);
 }
 
+// Custom data translations
+function _H($tag) {
+    return substr(md5($tag), -16);
+}
+function _C(Translatable $object) {
+    return $objet->getLocalName();
+}
+
+interface Translatable {
+    function getTranslationTag();
+    function getLocalName($user=false);
+}
 
 do {
   if (PHP_SAPI != 'cli') break;
diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php
index 53d30908c9b324be7e0a1ce3db0554cc15e0c30f..502c309208cd69490a824064585a34bb06ae20df 100644
--- a/include/staff/department.inc.php
+++ b/include/staff/department.inc.php
@@ -47,7 +47,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Name');?>:
             </td>
             <td>
-                <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>">
+                <input data-translate-tag="<?php echo $dept ? $dept->getTranslationTag() : '';
+                ?>" type="text" size="30" name="name" value="<?php echo $info['name']; ?>">
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
             </td>
         </tr>
@@ -294,3 +295,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="departments.php"'>
 </p>
 </form>
+<script type="text/javascript">
+    $('input[data-translate-tag]').translatable();
+</script>
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index 60079586dfc40c86fb3a3490a1aa8a6cdc0ce97e..3c29232e550e3c2de4490749c62d52b4605e69b7 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -29,10 +29,12 @@ if (($lang = Internationalization::getCurrentLanguage())
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-fonts.js"></script>
     <script type="text/javascript" src="./js/bootstrap-typeahead.js"></script>
+    <script type="text/javascript" src="./js/jquery.translatable.js"></script>
     <link rel="stylesheet" href="<?php echo ROOT_PATH ?>css/thread.css" media="all">
     <link rel="stylesheet" href="./css/scp.css" media="all">
     <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/redactor.css" media="screen">
     <link rel="stylesheet" href="./css/typeahead.css" media="screen">
+    <link rel="stylesheet" href="./css/jquery.translatable.css" media="screen">
     <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css"
          rel="stylesheet" media="screen" />
      <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css">
diff --git a/include/staff/template.inc.php b/include/staff/template.inc.php
index 475ac80b5d250c6ab30d57e70527822318b07e23..325a7423f60eaf50edfb685a8007dfd5537d6141 100644
--- a/include/staff/template.inc.php
+++ b/include/staff/template.inc.php
@@ -15,7 +15,7 @@ if($template && $_REQUEST['a']!='add'){
     $action='add';
     $submit_text=__('Add Template');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:0;
-    $info['lang_id'] = $cfg->getSystemLanguage();
+    $info['lang_id'] = $cfg->getPrimaryLanguage();
     $qstr.='&a='.urlencode($_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
diff --git a/scp/ajax.php b/scp/ajax.php
index 09dce15dd85103fadcc55bb0b2b25c0a7cc08aac..4ca0abf850e615d3f09479db47a904ecd511169c 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -176,8 +176,11 @@ $dispatcher = patterns('',
         url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'),
         url_get('^(?P<lang>[\w_]+)?/tips/(?P<namespace>[\w_.]+)$', 'getTipsJsonForLang')
     )),
-    url('^/i18n/(?P<lang>[\w_]+)/', patterns('ajax.i18n.php:i18nAjaxAPI',
-        url_get('(?P<tag>\w+)$', 'getLanguageFile')
+    url('^/i18n/', patterns('ajax.i18n.php:i18nAjaxAPI',
+        url_get('^langs$', 'getSecondaryLanguages'),
+        url_get('^translate/(?P<tag>\w+)$', 'getTranslations'),
+        url_post('^translate/(?P<tag>\w+)$', 'updateTranslations'),
+        url_get('^(?P<lang>[\w_]+)/(?P<tag>\w+)$', 'getLanguageFile')
     ))
 );
 
diff --git a/scp/css/jquery.translatable.css b/scp/css/jquery.translatable.css
new file mode 100644
index 0000000000000000000000000000000000000000..4f7f3898256f9510f2b33b746b65ec2c13810a62
--- /dev/null
+++ b/scp/css/jquery.translatable.css
@@ -0,0 +1,43 @@
+button.translatable {
+    background-color: #ccc;
+    border: none;
+    border-radius: 0 4px 4px 0;
+}
+
+div.add-translation {
+    margin-top: 5px;
+    padding: 5px;
+    border-top: 1px solid rgba(0, 0, 0, 0.3);
+}
+
+div.translations {
+    min-height: 20px;
+}
+
+ul.translations {
+    padding-left: 0;
+    min-width: 300px;
+    max-height: 150px;
+    overflow-y: auto;
+}
+ul.translations li {
+    list-style: none;
+    padding: 0 10px;
+    box-sizing: border-box;
+    display: block;
+}
+ul.translations li + li {
+    margin-top: 10px;
+}
+ul.translations li label.language {
+    color: #888;
+}
+ul.translations li input {
+    width: 100%;
+    box-sizing: border-box;
+}
+.language-commit {
+    text-align: right;
+    padding: 5px 10px;
+    background-color: cyan;
+}
diff --git a/scp/js/jquery.translatable.js b/scp/js/jquery.translatable.js
new file mode 100644
index 0000000000000000000000000000000000000000..f75208ac640ace77ae9642d50ee7413fb08121de
--- /dev/null
+++ b/scp/js/jquery.translatable.js
@@ -0,0 +1,186 @@
+
+!function( $ ){
+
+  "use strict";
+
+  var Translatable = function( element, options ) {
+    this.$element = $(element);
+    this.options = $.extend({}, $.fn.translatable.defaults, options);
+    this.$translations = $('<ul class="translations"></ul>');
+    this.$status = $('<li class="status"><i class="icon-spinner icon-spin"></i> Loading ...</li>')
+      .appendTo(this.$translations);
+    this.$footer = $('<div class="add-translation"></div>');
+    this.$select = $('<select name="locale"></select>');
+    this.$menu = $(this.options.menu).appendTo('body');
+    this.$button = $(this.options.button).insertAfter(this.$element);
+    //this.$menu.append('<a class="close pull-right" href=""><i class="icon-remove-circle"></i></a>')
+    //    .on('click', $.proxy(this.hide, this));
+    this.$menu.append(this.$translations).append(this.$footer);
+    this.shown = false;
+    this.populated = false;
+    this.decorate();
+  },
+  // Class-static variables
+  urlcache = {};
+
+  Translatable.prototype = {
+
+    constructor: Translatable,
+
+    fetch: function( url, data, callback ) {
+      if ( !urlcache[ url ] ) {
+        urlcache[ url ] = $.Deferred(function( defer ) {
+          $.ajax( url, { data: data, dataType: 'json' } )
+            .then( defer.resolve, defer.reject );
+        }).promise();
+      }
+      return urlcache[ url ].done( callback );
+    },
+
+    decorate: function() {
+      this.$button.on('click', $.proxy(this.toggle, this));
+      var self = this;
+      this.fetch('ajax.php/i18n/langs').then(function(json) { self.langs = json; });
+    },
+
+    buildAdd: function() {
+      var self=this;
+      this.$footer
+        .append($('<form method="post"></form>')
+          .append(this.$select)
+          .append($('<button type="button"><i class="icon-plus-sign"></i> Add</button>')
+            .on('click', $.proxy(this.define, this))
+          )
+        );
+      this.fetch('ajax.php/i18n/langs').then(function(langs) {
+        $.each(langs, function(k, v) {
+          self.$select.append($('<option>').val(k).text(v));
+        });
+      });
+    },
+
+    populate: function() {
+      var self=this;
+      if (this.populated)
+        return;
+      this.buildAdd();
+      this.fetch('ajax.php/i18n/translate/' + this.$element.data('translateTag'))
+      .then(function(json) {
+        $.each(json, function(k,v) {
+          self.add(k, v);
+        });
+        if (!Object.keys(json).length) {
+          self.$status.text('Not currently translated');
+        }
+        else
+          self.$status.remove();
+      });
+      self.populated = true;
+    },
+
+    define: function(e) {
+      this.add($('option:selected', this.$select).val());
+    },
+
+    add: function(lang, text) {
+      this.$translations.append(
+        $('<li>')
+        .append($('<label class="language">').text(this.langs[lang])
+          .append($('<input type="text" data-lang="'+lang+'">')
+            .on('change', $.proxy(this.showCommit, this))
+            .val(text)
+          )
+        )
+      );
+      $('option[value='+lang+']', this.$select).remove();
+      if (!$('option', this.$select).length)
+        this.$footer.hide();
+    },
+
+    showCommit: function(e) {
+      if (this.$commit)
+          return this.$commit.show();
+
+      return this.$commit = $('<div class="language-commit"></div>')
+        .insertAfter(this.$translations)
+        .append($('<button type="button" class="commit"><i class="fa fa-save icon-save"></i> Save</button>')
+          .on('click', $.proxy(this.commit, this))
+        );
+    },
+
+    commit: function(e) {
+      var changes = {}, self = this;
+      $('input[type=text]', this.$translations).each(function() {
+        changes[$(this).data('lang')] = $(this).val();
+      });
+      $.ajax('ajax.php/i18n/translate/' + this.$element.data('translateTag'), {
+        type: 'post',
+        data: changes,
+        success: function() {
+          self.$commit.hide();
+        }
+      });
+    },
+
+    toggle: function(e) {
+      e.stopPropagation();
+      e.preventDefault();
+
+      if (this.shown)
+        this.hide();
+      else
+        this.show();
+    },
+
+    show: function() {
+      if (this.shown)
+          return this;
+
+      var pos = $.extend({}, this.$element.offset(), {
+        height: this.$element[0].offsetHeight
+      })
+
+      this.$menu.css({
+        top: pos.top + pos.height
+      , left: pos.left
+      });
+
+      this.populate();
+
+      this.$menu.show();
+      this.shown = true;
+      return this;
+    },
+
+    hide: function() {
+      if (this.shown) {
+        this.$menu.hide();
+        this.shown = false;
+      }
+      return this;
+    }
+
+
+  };
+
+  /* PLUGIN DEFINITION
+   * =========================== */
+
+  $.fn.translatable = function ( option ) {
+    return this.each(function () {
+      var $this = $(this),
+        data = $this.data('translatable'),
+        options = typeof option == 'object' && option;
+      if (!data) $this.data('translatable', (data = new Translatable(this, options)));
+      if (typeof option == 'string') data[option]();
+    });
+  };
+
+  $.fn.translatable.defaults = {
+    menu: '<div class="translatable dropdown-menu"></div>',
+    button: '<button class="translatable"><i class="fa fa-globe icon-globe"></i></button>'
+  };
+
+  $.fn.translatable.Constructor = Translatable;
+
+}( window.jQuery );