diff --git a/.gitignore b/.gitignore
index 3bdc4e066866d0433aafc3341fa4b43b0b420b58..6e7535b0b92c9fd5d8c741d3c95ba4fe7c19630e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ Vagrantfile
 
 # Staging directory used for packaging script
 stage
+
+# Ignore packaged plugins and language packs
+*.phar
diff --git a/ajax.php b/ajax.php
index 57b6274ccee12f4292f97aa7df5b4f9c9cd938ce..3374776c7a54ef4cbaa1c0bf7a8139d2179be2dc 100644
--- a/ajax.php
+++ b/ajax.php
@@ -39,5 +39,6 @@ $dispatcher = patterns('',
         url_get('^help-topic/(?P<id>\d+)$', 'getClientFormsForHelpTopic')
     ))
 );
+Signal::send('ajax.client', $dispatcher);
 print $dispatcher->resolve($ost->get_path_info());
 ?>
diff --git a/bootstrap.php b/bootstrap.php
index d726e8c0452c7ee2141a02d6065e01b36f308285..a82796ebd2d7ecba1eb453884502f10a193ded53 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -177,7 +177,7 @@ class Bootstrap {
         require(INCLUDE_DIR.'class.log.php');
         require(INCLUDE_DIR.'class.crypto.php');
         require(INCLUDE_DIR.'class.timezone.php');
-        require(INCLUDE_DIR.'class.signal.php');
+        require_once(INCLUDE_DIR.'class.signal.php');
         require(INCLUDE_DIR.'class.nav.php');
         require(INCLUDE_DIR.'class.page.php');
         require_once(INCLUDE_DIR.'class.format.php'); //format helpers
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index e6c0fdb36a4aa6268f89feb437c53e713d2ce350..f52a4341205f6c0e66e12ffff07ebff15d14767f 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -103,12 +103,10 @@ class TicketsAjaxAPI extends AjaxController {
         global $thisstaff, $cfg;
 
         $result=array();
-        $select = 'SELECT DISTINCT ticket.ticket_id';
+        $select = 'SELECT ticket.ticket_id';
         $from = ' FROM '.TICKET_TABLE.' ticket ';
-        $where = ' WHERE 1 ';
-
         //Access control.
-        $where.=' AND ( ticket.staff_id='.db_input($thisstaff->getId());
+        $where = ' WHERE ( ticket.staff_id='.db_input($thisstaff->getId());
 
         if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
             $where.=' OR ticket.team_id IN('.implode(',', db_input(array_filter($teams))).')';
@@ -179,57 +177,65 @@ class TicketsAjaxAPI extends AjaxController {
             $where.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')';
 
         //Query
+        $joins = array();
         if($req['query']) {
             $queryterm=db_real_escape($req['query'], false);
 
-            $from.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )'
-                .' LEFT JOIN '.FORM_ENTRY_TABLE.' tentry ON (tentry.object_id = ticket.ticket_id
-                   AND tentry.object_type="T")
-                   LEFT JOIN '.FORM_ANSWER_TABLE.' tans ON (tans.entry_id = tentry.id
-                   AND tans.value_id IS NULL)
-                   LEFT JOIN '.FORM_ENTRY_TABLE.' uentry ON (uentry.object_id = ticket.user_id
+            // Setup sets of joins and queries
+            $joins[] = array(
+                'from' =>
+                    'LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )',
+                'where' => "thread.title LIKE '%$queryterm%' OR thread.body LIKE '%$queryterm%'"
+            );
+            $joins[] = array(
+                'from' =>
+                    'LEFT JOIN '.FORM_ENTRY_TABLE.' tentry ON (tentry.object_id = ticket.ticket_id AND tentry.object_type="T")
+                    LEFT JOIN '.FORM_ANSWER_TABLE.' tans ON (tans.entry_id = tentry.id AND tans.value_id IS NULL)',
+                'where' => "tans.value LIKE '%$queryterm%'"
+            );
+            $joins[] = array(
+                'from' =>
+                   'LEFT JOIN '.FORM_ENTRY_TABLE.' uentry ON (uentry.object_id = ticket.user_id
                    AND uentry.object_type="U")
                    LEFT JOIN '.FORM_ANSWER_TABLE.' uans ON (uans.entry_id = uentry.id
                    AND uans.value_id IS NULL)
                    LEFT JOIN '.USER_TABLE.' user ON (ticket.user_id = user.id)
-                   LEFT JOIN '.USER_EMAIL_TABLE.' uemail ON (user.id = uemail.user_id)';
-
-            $where.=" AND (  uemail.address LIKE '%$queryterm%'"
-                       ." OR user.name LIKE '%$queryterm%'"
-                       ." OR tans.value LIKE '%$queryterm%'"
-                       ." OR uans.value LIKE '%$queryterm%'"
-                       ." OR thread.title LIKE '%$queryterm%'"
-                       ." OR thread.body LIKE '%$queryterm%'"
-                       .' )';
+                   LEFT JOIN '.USER_EMAIL_TABLE.' uemail ON (user.id = uemail.user_id)',
+                'where' =>
+                    "uemail.address LIKE '%$queryterm%' OR user.name LIKE '%$queryterm%' OR uans.value LIKE '%$queryterm%'",
+            );
         }
 
         // Dynamic fields
-        $dynfields='(SELECT entry.object_id, %s '.
-             'FROM '.FORM_ANSWER_TABLE.' ans '.
-             'JOIN '.FORM_ENTRY_TABLE.' entry ON entry.id=ans.entry_id '.
-             'JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id '.
-             'WHERE entry.object_type="T" GROUP BY entry.object_id)';
-        $vals = array();
+        $cdata_search = false;
         foreach (TicketForm::getInstance()->getFields() as $f) {
             if (isset($req[$f->getFormName()])
                     && ($val = $req[$f->getFormName()])) {
-                $id = $f->get('id');
-                $vals[] = "MAX(IF(field.id = '$id', ans.value_id, NULL)) as `f_{$id}_id`";
-                $vals[] = "MAX(IF(field.id = '$id', ans.value, NULL)) as `f_$id`";
-                $where .= " AND (dyn.`f_{$id}_id` = ".db_input($val)
-                    . " OR dyn.`f_$id` LIKE '%".db_real_escape($val)."%')";
+                $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id');
+                $cwhere = "cdata.`$name` LIKE '%".db_real_escape($val)."%'";
+                if ($f->getImpl()->hasIdValue() && is_numeric($val))
+                    $cwhere .= " OR cdata.`{$name}_id` = ".db_input($val);
+                $where .= ' AND ('.$cwhere.')';
+                $cdata_search = true;
             }
         }
-        if ($vals)
-            $from .= ' LEFT JOIN '.sprintf($dynfields, implode(',', $vals))
-                ." dyn ON (dyn.object_id = ticket.ticket_id)";
+        if ($cdata_search)
+            $from .= 'LEFT JOIN '.TABLE_PREFIX.'ticket__cdata '
+                    ." cdata ON (cdata.ticket_id = ticket.ticket_id)";
+
+        $sections = array();
+        foreach ($joins as $j) {
+            $sections[] = "$select $from {$j['from']} $where AND ({$j['where']})";
+        }
+        if (!$joins)
+            $sections[] = "$select $from $where";
 
-        $sql="$select $from $where";
+        $sql=implode(' union ', $sections);
         $res = db_query($sql);
 
         $tickets = array();
-        while (list($tickets[]) = db_fetch_row($res));
-        $tickets = array_filter($tickets);
+        while ($row = db_fetch_row($res))
+            $tickets[] = $row[0];
 
         return $tickets;
     }
diff --git a/include/ajax.tips.php b/include/ajax.tips.php
index ed560403e945933600ad3390dea1aa508530ea8b..e81d1301b53622478520050bcd6e75528b28e36e 100644
--- a/include/ajax.tips.php
+++ b/include/ajax.tips.php
@@ -20,8 +20,13 @@ if(!defined('INCLUDE_DIR')) die('!');
 require_once(INCLUDE_DIR.'class.i18n.php');
 
 class HelpTipAjaxAPI extends AjaxController {
-    function getTipsJson($namespace, $lang='en_US') {
-        global $ost;
+    function getTipsJson($namespace, $lang=false) {
+        global $ost, $thisstaff;
+
+        if (!$lang)
+            $lang = ($thisstaff)
+                ? $thisstaff->getLanguage()
+                : Internationalization::getDefaultLanguage();
 
         $i18n = new Internationalization($lang);
         $tips = $i18n->getTemplate("help/tips/$namespace.yaml");
diff --git a/include/class.config.php b/include/class.config.php
index 06607fdc52393e084f8fdc9dde376b4c8108a1e1..f5c7077d7dcbe07cddca31d10e83d1e63a7cb32e 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -148,6 +148,7 @@ class OsticketConfig extends Config {
         'allow_online_attachments_onlogin' => false,
         'name_format' =>        'full', # First Last
         'auto_claim_tickets'=>  true,
+        'system_language' =>    'en_US',
     );
 
     function OsticketConfig($section=null) {
@@ -709,6 +710,10 @@ class OsticketConfig extends Config {
         return ($this->allowAttachments() && $this->get('allow_email_attachments'));
     }
 
+    function getSystemLanguage() {
+        return $this->get('system_language');
+    }
+
     //TODO: change db field to allow_api_attachments - which will include  email/json/xml attachments
     //       terminology changed on the UI
     function allowAPIAttachments() {
diff --git a/include/class.dispatcher.php b/include/class.dispatcher.php
index 0448f5024eb3e9244f1bf1bbe1b8b462d171117a..d586fd0ba77e010549d72196bcaec0a5f94c44d2 100644
--- a/include/class.dispatcher.php
+++ b/include/class.dispatcher.php
@@ -153,7 +153,10 @@ class UrlMatcher {
     function apply_prefix() {
         if (is_array($this->func)) { list($class, $func) = $this->func; }
         else { $func = $this->func; $class = ""; }
-        $class = $this->prefix . $class;
+        if (is_object($class))
+            return array(false, $this->func);
+        if ($this->prefix)
+            $class = $this->prefix . $class;
 
         if (strpos($class, ":")) {
             list($file, $class) = explode(":", $class, 2);
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 52e3109356db760e6a9d412c7782b5697b9891ec..f164138994cc1846eec67baccc16b99f1fe49c97 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -18,6 +18,7 @@
 **********************************************************************/
 require_once(INCLUDE_DIR . 'class.orm.php');
 require_once(INCLUDE_DIR . 'class.forms.php');
+require_once(INCLUDE_DIR . 'class.signal.php');
 
 /**
  * Form template, used for designing the custom form and for entering custom
@@ -130,6 +131,7 @@ class DynamicForm extends VerySimpleModel {
             foreach ($ht['fields'] as $f) {
                 $f = DynamicFormField::create($f);
                 $f->form_id = $inst->id;
+                $f->setForm($inst);
                 $f->save();
             }
         }
@@ -196,6 +198,89 @@ class TicketForm extends DynamicForm {
         static::$instance = $o[0]->instanciate();
         return static::$instance;
     }
+
+    // Materialized View for Ticket custom data (MySQL FlexViews would be
+    // nice)
+    //
+    // @see http://code.google.com/p/flexviews/
+    static function getDynamicDataViewFields() {
+        $fields = array();
+        foreach (self::getInstance()->getFields() as $f) {
+            $impl = $f->getImpl();
+            if (!$impl->hasData() || $impl->isPresentationOnly())
+                continue;
+
+            $name = ($f->get('name')) ? $f->get('name')
+                : 'field_'.$f->get('id');
+
+            $fields[] = sprintf(
+                'MAX(IF(field.name=\'%1$s\',ans.value,NULL)) as `%1$s`',
+                $name);
+            if ($impl->hasIdValue()) {
+                $fields[] = sprintf(
+                    'MAX(IF(field.name=\'%1$s\',ans.value_id,NULL)) as `%1$s_id`',
+                    $name);
+            }
+        }
+        return $fields;
+    }
+
+    static function ensureDynamicDataView() {
+        $sql = 'SHOW TABLES LIKE \''.TABLE_PREFIX.'ticket__cdata\'';
+        if (!db_num_rows(db_query($sql)))
+            return static::buildDynamicDataView();
+    }
+
+    static function buildDynamicDataView() {
+        // create  table __cdata (primary key (ticket_id)) as select
+        // entry.object_id as ticket_id, MAX(IF(field.name = 'subject',
+        // ans.value, NULL)) as `subject`,MAX(IF(field.name = 'priority',
+        // ans.value, NULL)) as `priority_desc`,MAX(IF(field.name =
+        // 'priority', ans.value_id, NULL)) as `priority_id`
+        // FROM ost_form_entry entry LEFT JOIN ost_form_entry_values ans ON
+        // ans.entry_id = entry.id LEFT JOIN ost_form_field field ON
+        // field.id=ans.field_id
+        // where entry.object_type='T' group by entry.object_id;
+        $fields = static::getDynamicDataViewFields();
+        $sql = 'CREATE TABLE `'.TABLE_PREFIX.'ticket__cdata` (PRIMARY KEY (ticket_id)) AS
+            SELECT entry.`object_id` AS ticket_id, '.implode(',', $fields)
+         .' FROM ost_form_entry entry
+            JOIN ost_form_entry_values ans ON ans.entry_id = entry.id
+            JOIN ost_form_field field ON field.id=ans.field_id
+            WHERE entry.object_type=\'T\' GROUP BY entry.object_id';
+        db_query($sql);
+    }
+
+    static function dropDynamicDataView() {
+        db_query('DROP TABLE IF EXISTS `'.TABLE_PREFIX.'ticket__cdata`');
+    }
+
+    static function updateDynamicDataView($answer, $data) {
+        // TODO: Detect $data['dirty'] for value and value_id
+        // We're chiefly concerned with Ticket form answers
+        if (!($e = $answer->getEntry()) || $e->get('object_type') != 'T')
+            return;
+
+        // If the `name` column is in the dirty list, we would be renaming a
+        // column. Delete the view instead.
+        if (isset($data['dirty']) && isset($data['dirty']['name']))
+            return self::dropDynamicDataView();
+
+        // $record = array();
+        // $record[$f] = $answer->value'
+        // TicketFormData::objects()->filter(array('ticket_id'=>$a))
+        //      ->merge($record);
+        $f = $answer->getField();
+        $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id');
+        $ids = $f->hasIdValue();
+        $fields = sprintf('`%s`=', $name) . db_input($answer->get('value'));
+        if ($f->hasIdValue())
+            $fields .= sprintf(',`%s_id`=', $name) . db_input($answer->getIdValue());
+        $sql = 'INSERT INTO `'.TABLE_PREFIX.'ticket__cdata` SET '.$fields
+            .', `ticket_id`='.db_input($answer->getEntry()->get('object_id'))
+            .' ON DUPLICATE KEY UPDATE '.$fields;
+        db_query($sql);
+    }
 }
 // Add fields from the standard ticket form to the ticket filterable fields
 Filter::addSupportedMatches('Custom Fields', function() {
@@ -207,6 +292,23 @@ Filter::addSupportedMatches('Custom Fields', function() {
     }
     return $matches;
 });
+// Manage materialized view on custom data updates
+Signal::connect('model.created',
+    array('TicketForm', 'updateDynamicDataView'),
+    'DynamicFormEntryAnswer');
+Signal::connect('model.updated',
+    array('TicketForm', 'updateDynamicDataView'),
+    'DynamicFormEntryAnswer');
+// Recreate the dynamic view after new or removed fields to the ticket
+// details form
+Signal::connect('model.created',
+    array('TicketForm', 'dropDynamicDataView'),
+    'DynamicFormField',
+    function($o) { return $o->getForm()->get('type') == 'T'; });
+Signal::connect('model.deleted',
+    array('TicketForm', 'dropDynamicDataView'),
+    'DynamicFormField',
+    function($o) { return $o->getForm()->get('type') == 'T'; });
 
 require_once(INCLUDE_DIR . "class.json.php");
 
@@ -555,6 +657,7 @@ class DynamicFormEntry extends VerySimpleModel {
                 array('field_id'=>$f->get('id')));
             $a->field = $f;
             $a->field->setAnswer($a);
+            $a->entry = $inst;
             $inst->_values[] = $a;
         }
         return $inst;
diff --git a/include/class.forms.php b/include/class.forms.php
index b5a7802408378eac68b0d41efc302b7af944ef75..9d0bdc87e460396ee901a708b6e1ff31948fb767 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -440,6 +440,14 @@ class FormField {
         return $this->presentation_only;
     }
 
+    /**
+     * Indicates if the field places data in the `value_id` column. This
+     * is currently used by the materialized view system
+     */
+    function hasIdValue() {
+        return false;
+    }
+
     function getConfigurationForm() {
         if (!$this->_cform) {
             $type = static::getFieldType($this->get('type'));
@@ -806,6 +814,10 @@ class PriorityField extends ChoiceField {
         return $widget;
     }
 
+    function hasIdValue() {
+        return true;
+    }
+
     function getChoices() {
         $this->ht['default'] = 0;
 
@@ -1140,6 +1152,7 @@ class ThreadEntryWidget extends Widget {
         <input type="file" class="multifile" name="attachments[]" id="attachments" size="30" value="" />
         </div>
         <font class="error">&nbsp;<?php echo $errors['attachments']; ?></font>
+        </div>
         <hr/>
         <?php
         }
diff --git a/include/class.http.php b/include/class.http.php
index 3aea32b6e93029e541bf0d0c9fd28bef15b20dc3..ef917845d76a1f943e022167304eb7a2b0d131c5 100644
--- a/include/class.http.php
+++ b/include/class.http.php
@@ -43,9 +43,14 @@ class Http {
         exit;
     }
 
-	function redirect($url,$delay=0,$msg='') {
+    function redirect($url,$delay=0,$msg='') {
 
-        if(strstr($_SERVER['SERVER_SOFTWARE'], 'IIS')){
+        $iis = strpos($_SERVER['SERVER_SOFTWARE'], 'IIS') !== false;
+        @list($name, $version) = explode('/', $_SERVER['SERVER_SOFTWARE']);
+        // Legacy code for older versions of IIS that would not emit the
+        // correct HTTP status and headers when using the `Location`
+        // header alone
+        if ($iis && version_compare($version, '7.0', '<')) {
             header("Refresh: $delay; URL=$url");
         }else{
             header("Location: $url");
diff --git a/include/class.i18n.php b/include/class.i18n.php
index 7891d7ef937ecacf2bc148ae4a4e441801a8e003..b4b2564b7ec96e5983b5fafbf245140ab771fff4 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -16,7 +16,6 @@
 **********************************************************************/
 require_once INCLUDE_DIR.'class.error.php';
 require_once INCLUDE_DIR.'class.yaml.php';
-require_once INCLUDE_DIR.'class.config.php';
 
 class Internationalization {
 
@@ -43,7 +42,6 @@ class Internationalization {
     function loadDefaultData() {
         # notrans -- do not translate the contents of this array
         $models = array(
-            'email_template_group.yaml' => 'EmailTemplateGroup',
             'department.yaml' =>    'Dept',
             'sla.yaml' =>           'SLA',
             'form.yaml' =>          'DynamicForm',
@@ -77,6 +75,7 @@ class Internationalization {
         }
 
         // Configuration
+        require_once INCLUDE_DIR.'class.config.php';
         if (($tpl = $this->getTemplate('config.yaml'))
                 && ($data = $tpl->getData())) {
             foreach ($data as $section=>$items) {
@@ -101,6 +100,8 @@ class Internationalization {
             if (db_query($sql) && ($id = db_insert_id()))
                 $_config->set("{$type}_page_id", $id);
         }
+        // Default Language
+        $_config->set('system_language', $this->langs[0]);
 
         // Canned response examples
         if (($tpl = $this->getTemplate('templates/premade.yaml'))
@@ -118,6 +119,13 @@ class Internationalization {
 
         // Email templates
         // TODO: Lookup tpl_id
+        if ($objects = $this->getTemplate('email_template_group.yaml')->getData()) {
+            foreach ($objects as $o) {
+                $o['lang_id'] = $this->langs[0];
+                $tpl = EmailTemplateGroup::create($o, $errors);
+            }
+        }
+        // This shouldn't be necessary
         $tpl = EmailTemplateGroup::lookup(1);
         foreach ($tpl->all_names as $name=>$info) {
             if (($tp = $this->getTemplate("templates/email/$name.yaml"))
@@ -131,6 +139,122 @@ class Internationalization {
             }
         }
     }
+
+    static function availableLanguages($base=I18N_DIR) {
+        $langs = (include I18N_DIR . 'langs.php');
+
+        // Consider all subdirectories and .phar files in the base dir
+        $dirs = glob(I18N_DIR . '*', GLOB_ONLYDIR | GLOB_NOSORT);
+        $phars = glob(I18N_DIR . '*.phar', GLOB_NOSORT);
+
+        $installed = array();
+        foreach (array_merge($dirs, $phars) as $f) {
+            $base = basename($f, '.phar');
+            @list($code, $locale) = explode('_', $base);
+            if (isset($langs[$code])) {
+                $installed[strtolower($base)] =
+                    $langs[$code] + array(
+                    'lang' => $code,
+                    'locale' => $locale,
+                    'path' => $f,
+                    'code' => $base,
+                    'desc' => sprintf("%s%s (%s)",
+                        $langs[$code]['nativeName'],
+                        $locale ? sprintf(' - %s', $locale) : '',
+                        $langs[$code]['name']),
+                );
+            }
+        }
+        usort($installed, function($a, $b) { return strcasecmp($a['code'], $b['code']); });
+
+        return $installed;
+    }
+
+    // TODO: Move this to the REQUEST class or some middleware when that
+    // exists.
+    // Algorithm borrowed from Drupal 7 (locale.inc)
+    static function getDefaultLanguage() {
+        global $cfg;
+
+        if (empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
+            return $cfg->getSystemLanguage();
+
+        $languages = self::availableLanguages();
+
+        // The Accept-Language header contains information about the
+        // language preferences configured in the user's browser / operating
+        // system. RFC 2616 (section 14.4) defines the Accept-Language
+        // header as follows:
+        //   Accept-Language = "Accept-Language" ":"
+        //                  1#( language-range [ ";" "q" "=" qvalue ] )
+        //   language-range  = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
+        // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
+        $browser_langcodes = array();
+        $matches = array();
+        if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@',
+            trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
+          foreach ($matches as $match) {
+            // We can safely use strtolower() here, tags are ASCII.
+            // RFC2616 mandates that the decimal part is no more than three
+            // digits, so we multiply the qvalue by 1000 to avoid floating
+            // point comparisons.
+            $langcode = strtolower($match[1]);
+            $qvalue = isset($match[2]) ? (float) $match[2] : 1;
+            $browser_langcodes[$langcode] = (int) ($qvalue * 1000);
+          }
+        }
+
+        // We should take pristine values from the HTTP headers, but
+        // Internet Explorer from version 7 sends only specific language
+        // tags (eg. fr-CA) without the corresponding generic tag (fr)
+        // unless explicitly configured. In that case, we assume that the
+        // lowest value of the specific tags is the value of the generic
+        // language to be as close to the HTTP 1.1 spec as possible.
+        //
+        // References:
+        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
+        // http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
+        asort($browser_langcodes);
+        foreach ($browser_langcodes as $langcode => $qvalue) {
+          $generic_tag = strtok($langcode, '-');
+          if (!isset($browser_langcodes[$generic_tag])) {
+            $browser_langcodes[$generic_tag] = $qvalue;
+          }
+        }
+
+        // Find the enabled language with the greatest qvalue, following the rules
+        // of RFC 2616 (section 14.4). If several languages have the same qvalue,
+        // prefer the one with the greatest weight.
+        $best_match_langcode = FALSE;
+        $max_qvalue = 0;
+        foreach ($languages as $langcode => $language) {
+          // Language tags are case insensitive (RFC2616, sec 3.10).
+          // We use _ as the location separator
+          $langcode = str_replace('_','-',strtolower($langcode));
+
+          // If nothing matches below, the default qvalue is the one of the wildcard
+          // language, if set, or is 0 (which will never match).
+          $qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0;
+
+          // Find the longest possible prefix of the browser-supplied language
+          // ('the language-range') that matches this site language ('the language tag').
+          $prefix = $langcode;
+          do {
+            if (isset($browser_langcodes[$prefix])) {
+              $qvalue = $browser_langcodes[$prefix];
+              break;
+            }
+          } while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));
+
+          // Find the best match.
+          if ($qvalue > $max_qvalue) {
+            $best_match_langcode = $language['code'];
+            $max_qvalue = $qvalue;
+          }
+        }
+
+        return $best_match_langcode;
+    }
 }
 
 class DataTemplate {
@@ -153,6 +277,12 @@ class DataTemplate {
                 $this->filepath = realpath("{$this->base}/$l/$path");
                 break;
             }
+            elseif (Phar::isValidPharFilename("{$this->base}/$l.phar")
+                    && file_exists("phar://{$this->base}/$l.phar/$path")) {
+                $this->lang = $l;
+                $this->filepath = "phar://{$this->base}/$l.phar/$path";
+                break;
+            }
         }
     }
 
diff --git a/include/class.mailer.php b/include/class.mailer.php
index a57f8d7be1ac5b7e15f79fd3d8430cbfd2d58819..21a7d157d68a28191b88e798dade0633d3c15725 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -139,12 +139,11 @@ class Mailer {
 
         $mime = new Mail_mime();
 
+        // If the message is not explicitly declared to be a text message,
+        // then assume that it needs html processing to create a valid text
+        // body
         $isHtml = true;
-        // Ensure that the 'text' option / hint is not set to true and that
-        // the message appears to be HTML -- that is, the first
-        // non-whitespace char is a '<' character
-        if (!(isset($options['text']) && $options['text'])
-                && (!$cfg || $cfg->isHtmlThreadEnabled())) {
+        if (!(isset($options['text']) && $options['text'])) {
             // Make sure nothing unsafe has creeped into the message
             $message = Format::safe_html($message); //XXX??
             $mime->setTXTBody(Format::html2text($message, 90, false));
diff --git a/include/class.orm.php b/include/class.orm.php
index 38d3482daf79c91fddeb9ef0f9b81240d4a4ed42..dee287cea77adc73d323db573e79a83151a998de 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -147,7 +147,10 @@ class VerySimpleModel {
         foreach ($pk as $p)
             $filter[] = $p.' = '.db_input($this->get($p));
         $sql .= ' WHERE '.implode(' AND ', $filter).' LIMIT 1';
-        return db_query($sql) && db_affected_rows() == 1;
+        if (!db_query($sql) || db_affected_rows() != 1)
+            throw new Exception(db_error());
+        Signal::send('model.deleted', $this);
+        return true;
     }
 
     function save($refetch=false) {
@@ -183,6 +186,11 @@ class VerySimpleModel {
             $this->__new__ = false;
             // Setup lists again
             $this->__setupForeignLists();
+            Signal::send('model.created', $this);
+        }
+        else {
+            $data = array('dirty' => $this->dirty);
+            Signal::send('model.updated', $this, $data);
         }
         # Refetch row from database
         # XXX: Too much voodoo
diff --git a/include/class.plugin.php b/include/class.plugin.php
index 107ff4f46d8b451c038ae4b850f4731a6ada8b7e..29dddf0f982c9549e5300e40a462e6906de83e08 100644
--- a/include/class.plugin.php
+++ b/include/class.plugin.php
@@ -114,25 +114,39 @@ class PluginManager {
         if (!($res = db_query($sql)))
             return static::$plugin_list;
 
-        $infos = static::allInfos();
         while ($ht = db_fetch_array($res)) {
             // XXX: Only read active plugins here. allInfos() will
             //      read all plugins
-            if (isset($infos[$ht['install_path']])) {
-                $info = $infos[$ht['install_path']];
-                if ($ht['isactive']) {
-                    list($path, $class) = explode(':', $info['plugin']);
-                    if (!$class)
-                        $class = $path;
-                    else
-                        require_once(INCLUDE_DIR . $ht['install_path']
-                            . '/' . $path);
-                    static::$plugin_list[$ht['install_path']]
-                        = new $class($ht['id']);
-                }
-                else {
-                    static::$plugin_list[$ht['install_path']] = $ht;
-                }
+            $info = static::getInfoForPath(
+                INCLUDE_DIR . $ht['install_path'], $ht['isphar']);
+            list($path, $class) = explode(':', $info['plugin']);
+            if (!$class)
+                $class = $path;
+            elseif ($ht['isphar'])
+                require_once('phar://' . INCLUDE_DIR . $ht['install_path']
+                    . '/' . $path);
+            else
+                require_once(INCLUDE_DIR . $ht['install_path']
+                    . '/' . $path);
+            if ($ht['isactive']) {
+                static::$plugin_list[$ht['install_path']]
+                    = new $class($ht['id']);
+            }
+            else {
+                // Get instance without calling the constructor. Thanks
+                // http://stackoverflow.com/a/2556089
+                $a = unserialize(
+                    sprintf(
+                        'O:%d:"%s":0:{}',
+                        strlen($class), $class
+                    )
+                );
+                // Simulate __construct() and load()
+                $a->id = $ht['id'];
+                $a->ht = $ht;
+                $a->info = $info;
+                static::$plugin_list[$ht['install_path']] = &$a;
+                unset($a);
             }
         }
         return static::$plugin_list;
@@ -146,6 +160,10 @@ class PluginManager {
         return $plugins;
     }
 
+    function throwException($errno, $errstr) {
+        throw new RuntimeException($errstr);
+    }
+
     /**
      * allInfos
      *
@@ -158,34 +176,62 @@ class PluginManager {
      * queried to determine if the plugin is installed
      */
     static function allInfos() {
-        static $defaults = array(
-            'include' => 'include/',
-            'stream' => false,
-        );
+        foreach (glob(INCLUDE_DIR . 'plugins/*',
+                GLOB_NOSORT|GLOB_BRACE) as $p) {
+            $is_phar = false;
+            if (substr($p, strlen($p) - 5) == '.phar'
+                    && Phar::isValidPharFilename($p)) {
+                try {
+                // When public key is invalid, openssl throws a
+                // 'supplied key param cannot be coerced into a public key' warning
+                // and phar ignores sig verification.
+                // We need to protect from that by catching the warning
+                // Thanks, https://github.com/koto/phar-util
+                set_error_handler(array('self', 'throwException'));
+                $ph = new Phar($p);
+                restore_error_handler();
+                // Verify the signature
+                $ph->getSignature();
+                $p = 'phar://' . $p;
+                $is_phar = true;
+                } catch (UnexpectedValueException $e) {
+                    // Cannot find signature file
+                } catch (RuntimeException $e) {
+                    // Invalid signature file
+                }
 
-        if (static::$plugin_info)
-            return static::$plugin_info;
+            }
 
-        foreach (glob(INCLUDE_DIR . 'plugins/*', GLOB_ONLYDIR) as $p) {
             if (!is_file($p . '/plugin.php'))
                 // Invalid plugin -- must define "/plugin.php"
                 continue;
-            // plugin.php is require to return an array of informaiton about
-            // the plugin.
-            $info = array_merge($defaults, (include $p . '/plugin.php'));
-            $info['install_path'] = str_replace(INCLUDE_DIR, '', $p);
 
-            // XXX: Ensure 'id' key isset
-            static::$plugin_info[$info['install_path']] = $info;
+            // Cache the info into static::$plugin_info
+            static::getInfoForPath($p, $is_phar);
         }
         return static::$plugin_info;
     }
 
-    static function getInfoForPath($path) {
-        $infos = static::allInfos();
-        if (isset($infos[$path]))
-            return $infos[$path];
-        return null;
+    static function getInfoForPath($path, $is_phar=false) {
+        static $defaults = array(
+            'include' => 'include/',
+            'stream' => false,
+        );
+
+        $install_path = str_replace(INCLUDE_DIR, '', $path);
+        $install_path = str_replace('phar://', '', $install_path);
+        if ($is_phar && substr($path, 0, 7) != 'phar://')
+            $path = 'phar://' . $path;
+        if (!isset(static::$plugin_info[$install_path])) {
+            // plugin.php is require to return an array of informaiton about
+            // the plugin.
+            $info = array_merge($defaults, (include $path . '/plugin.php'));
+            $info['install_path'] = $install_path;
+
+            // XXX: Ensure 'id' key isset
+            static::$plugin_info[$install_path] = $info;
+        }
+        return static::$plugin_info[$install_path];
     }
 
     function getInstance($path) {
@@ -215,17 +261,22 @@ class PluginManager {
      * registered in the plugin registry -- the %plugin table.
      */
     function install($path) {
-        if (!($info = $this->getInfoForPath($path)))
+        $is_phar = substr($path, strlen($path) - 5) == '.phar';
+        if (!($info = $this->getInfoForPath(INCLUDE_DIR . $path, $is_phar)))
             return false;
 
         $sql='INSERT INTO '.PLUGIN_TABLE.' SET installed=NOW() '
             .', install_path='.db_input($path)
-            .', name='.db_input($info['name']);
+            .', name='.db_input($info['name'])
+            .', isphar='.db_input($is_phar);
         if (!db_query($sql) || !db_affected_rows())
             return false;
+        static::clearCache();
+        return true;
+    }
 
+    static function clearCache() {
         static::$plugin_list = array();
-        return true;
     }
 }
 
@@ -255,7 +306,8 @@ class Plugin {
             `id`='.db_input($this->id);
         if (($res = db_query($sql)) && ($ht=db_fetch_array($res)))
             $this->ht = $ht;
-        $this->info = PluginManager::getInfoForPath($this->ht['install_path']);
+        $this->info = PluginManager::getInfoForPath($this->ht['install_path'],
+            $this->isPhar());
     }
 
     function getId() { return $this->id; }
@@ -283,6 +335,7 @@ class Plugin {
 
         $sql = 'DELETE FROM '.PLUGIN_TABLE
             .' WHERE id='.db_input($this->getId());
+        PluginManager::clearCache();
         if (db_query($sql) && db_affected_rows())
             return $this->getConfig()->purge();
         return false;
@@ -302,12 +355,14 @@ class Plugin {
     function enable() {
         $sql = 'UPDATE '.PLUGIN_TABLE
             .' SET isactive=1 WHERE id='.db_input($this->getId());
+        PluginManager::clearCache();
         return (db_query($sql) && db_affected_rows());
     }
 
     function disable() {
         $sql = 'UPDATE '.PLUGIN_TABLE
             .' SET isactive=0 WHERE id='.db_input($this->getId());
+        PluginManager::clearCache();
         return (db_query($sql) && db_affected_rows());
     }
 
diff --git a/include/class.signal.php b/include/class.signal.php
index f931acebc50c8beaf020859279c2ba0cba56f1de..928c15c4d2392ae767a0c465b6730d859f3dc85f 100644
--- a/include/class.signal.php
+++ b/include/class.signal.php
@@ -93,7 +93,7 @@ class Signal {
             list($s, $callable, $check) = $sub;
             if ($s && !is_a($object, $s))
                 continue;
-            elseif ($check && !call_user_func($check, $data))
+            elseif ($check && !call_user_func($check, $object, $data))
                 continue;
             call_user_func($callable, $object, $data);
         }
diff --git a/include/class.staff.php b/include/class.staff.php
index 3883ab3636b0d32dfb4b4b0108edb7e685cda6ce..efd4f341c03f40cb3fa79756cc038533b7c86c7f 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -65,6 +65,7 @@ class Staff extends AuthenticatedUser {
         $this->teams = $this->ht['teams'] = array();
         $this->group = $this->dept = null;
         $this->departments = $this->stats = array();
+        $this->config = new Config('staff.'.$this->id);
 
         //WE have to patch info here to support upgrading from old versions.
         if(($time=strtotime($this->ht['passwdreset']?$this->ht['passwdreset']:$this->ht['added'])))
@@ -91,7 +92,7 @@ class Staff extends AuthenticatedUser {
     }
 
     function getInfo() {
-        return $this->getHastable();
+        return $this->config->getInfo() + $this->getHastable();
     }
 
     /*compares user password*/
@@ -255,6 +256,17 @@ class Staff extends AuthenticatedUser {
         return $this->dept;
     }
 
+    function getLanguage() {
+        static $cached = false;
+        if (!$cached) $cached = &$_SESSION['staff:lang'];
+
+        if (!$cached) {
+            $cached = $this->config->get('lang');
+            if (!$cached)
+                $cached = Internationalization::getDefaultLanguage();
+        }
+        return $cached;
+    }
 
     function isManager() {
         return (($dept=$this->getDept()) && $dept->getManagerId()==$this->getId());
@@ -466,6 +478,9 @@ class Staff extends AuthenticatedUser {
 
         if($errors) return false;
 
+        $this->config->set('lang', $vars['lang']);
+        $_SESSION['staff:lang'] = null;
+
         $sql='UPDATE '.STAFF_TABLE.' SET updated=NOW() '
             .' ,firstname='.db_input($vars['firstname'])
             .' ,lastname='.db_input($vars['lastname'])
diff --git a/include/class.template.php b/include/class.template.php
index e135034cd4943d45190d70b710e9868f4df6a43f..70af7fce0d960ce494638398869ecfd34b858add 100644
--- a/include/class.template.php
+++ b/include/class.template.php
@@ -118,7 +118,7 @@ class EmailTemplateGroup {
     }
 
     function getLanguage() {
-        return 'en_US';
+        return $this->ht['lang'];
     }
 
     function isInUse(){
@@ -320,6 +320,10 @@ class EmailTemplateGroup {
             .' ,isactive='.db_input($vars['isactive'])
             .' ,notes='.db_input(Format::sanitize($vars['notes']));
 
+        if ($vars['lang_id'])
+            // TODO: Validation of lang_id
+            $sql .= ',lang='.db_input($vars['lang_id']);
+
         if($id) {
             $sql='UPDATE '.EMAIL_TEMPLATE_GRP_TABLE.' SET '.$sql.' WHERE tpl_id='.db_input($id);
             if(db_query($sql))
diff --git a/include/class.thread.php b/include/class.thread.php
index 3ebdc83a6729e9130134c256040d246b589512bb..5e6c88ea7a9c9ccdb6a4074d0c550729136662f0 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -746,7 +746,7 @@ Class ThreadEntry {
         $subject = $mailinfo['subject'];
         $match = array();
         if ($subject && $mailinfo['email']
-                && preg_match("/\[#([0-9]{1,10})\]/", $subject, $match)
+                && preg_match("/#[\p{L}-]+?([0-9]{1,10})/u", $subject, $match)
                 && ($tid = Ticket::getIdByExtId((int)$match[1], $mailinfo['email']))
                 )
             // Return last message for the thread
diff --git a/include/class.ticket.php b/include/class.ticket.php
index bb07895ae17cb779e64ccad3a0c71aa8452350ca..97d83f293029e7c916afafabe510b414431fe5c7 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -1486,7 +1486,6 @@ class Ticket {
                 && ($tpl = $dept->getTemplate())
                 && ($msg = $tpl->getNewMessageAlertMsgTemplate())) {
 
-            $attachments = $message->getAttachments();
             $msg = $this->replaceVars($msg->asArray(), $variables);
 
             //Build list of recipients and fire the alerts.
@@ -1508,7 +1507,7 @@ class Ticket {
             foreach( $recipients as $k=>$staff) {
                 if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], $attachments, $options);
+                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
         }
@@ -1557,8 +1556,6 @@ class Ticket {
             else
                 $signature='';
 
-            $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array();
-
             $msg = $this->replaceVars($msg->asArray(),
                 array('response' => $response, 'signature' => $signature));
 
@@ -1718,8 +1715,6 @@ class Ticket {
                 && ($tpl = $dept->getTemplate())
                 && ($msg=$tpl->getNoteAlertMsgTemplate())) {
 
-            $attachments = $note->getAttachments();
-
             $msg = $this->replaceVars($msg->asArray(),
                 array('note' => $note));
 
@@ -1738,7 +1733,6 @@ class Ticket {
             if($cfg->alertDeptManagerONNewNote() && $dept && $dept->getManagerId())
                 $recipients[]=$dept->getManager();
 
-            $attachments = $note->getAttachments();
             $options = array(
                 'inreplyto'=>$note->getEmailMessageId(),
                 'references'=>$note->getEmailReferences());
@@ -1750,7 +1744,7 @@ class Ticket {
                         || $note->getStaffId() == $staff->getId())  //No need to alert the poster!
                     continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
-                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], $attachments, $options);
+                $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
         }
diff --git a/include/client/header.inc.php b/include/client/header.inc.php
index 7266060fbf2f50160e54b1a275f3b4e90fafc87e..9f20098b4b1b4fcfc2d891973717c42876296fde 100644
--- a/include/client/header.inc.php
+++ b/include/client/header.inc.php
@@ -3,6 +3,7 @@ $title=($cfg && is_object($cfg) && $cfg->getTitle())?$cfg->getTitle():'osTicket
 header("Content-Type: text/html; charset=UTF-8\r\n");
 ?>
 <!DOCTYPE html>
+<html>
 <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
diff --git a/include/i18n/en_US/help/tips/install.yaml b/include/i18n/en_US/help/tips/install.yaml
index 2cb86b36dfeee5b7a282c2e797f4fd992fa06234..b65340da0096ba4152e4660bbc639b058a3b340f 100644
--- a/include/i18n/en_US/help/tips/install.yaml
+++ b/include/i18n/en_US/help/tips/install.yaml
@@ -20,6 +20,16 @@ system_email:
         <p>Default email address e.g support@yourcompany.com - you can add
         more later!</p>
 
+default_lang:
+    title: Default System Language
+    content: |
+        <p>Initial data for this language will be installed into the
+        database. For instance email templates and default system pages will
+        be installed for this language.</p>
+    links:
+      - title: osTicket Language Packs
+        href: http://osticket.com/download?product=langs
+
 first_name:
     title: First Name
     content: |
diff --git a/include/i18n/langs.php b/include/i18n/langs.php
new file mode 100644
index 0000000000000000000000000000000000000000..09957c42be514963c8d7a40f99051b996e728b42
--- /dev/null
+++ b/include/i18n/langs.php
@@ -0,0 +1,735 @@
+<?php
+/**
+ * @author Phil Teare
+ * using wikipedia data
+ */
+return array(
+    "ab" => array(
+        "name" => "Abkhaz",
+        "nativeName" => "аҧсуа"
+    ),
+    "aa" => array(
+        "name" => "Afar",
+        "nativeName" => "Afaraf"
+    ),
+    "af" => array(
+        "name" => "Afrikaans",
+        "nativeName" => "Afrikaans"
+    ),
+    "ak" => array(
+        "name" => "Akan",
+        "nativeName" => "Akan"
+    ),
+    "sq" => array(
+        "name" => "Albanian",
+        "nativeName" => "Shqip"
+    ),
+    "am" => array(
+        "name" => "Amharic",
+        "nativeName" => "አማርኛ"
+    ),
+    "ar" => array(
+        "name" => "Arabic",
+        "nativeName" => "العربية"
+    ),
+    "an" => array(
+        "name" => "Aragonese",
+        "nativeName" => "Aragonés"
+    ),
+    "hy" => array(
+        "name" => "Armenian",
+        "nativeName" => "Հայերեն"
+    ),
+    "as" => array(
+        "name" => "Assamese",
+        "nativeName" => "অসমীয়া"
+    ),
+    "av" => array(
+        "name" => "Avaric",
+        "nativeName" => "авар мацӀ, магӀарул мацӀ"
+    ),
+    "ae" => array(
+        "name" => "Avestan",
+        "nativeName" => "avesta"
+    ),
+    "ay" => array(
+        "name" => "Aymara",
+        "nativeName" => "aymar aru"
+    ),
+    "az" => array(
+        "name" => "Azerbaijani",
+        "nativeName" => "azərbaycan dili"
+    ),
+    "bm" => array(
+        "name" => "Bambara",
+        "nativeName" => "bamanankan"
+    ),
+    "ba" => array(
+        "name" => "Bashkir",
+        "nativeName" => "башҡорт теле"
+    ),
+    "eu" => array(
+        "name" => "Basque",
+        "nativeName" => "euskara, euskera"
+    ),
+    "be" => array(
+        "name" => "Belarusian",
+        "nativeName" => "Беларуская"
+    ),
+    "bn" => array(
+        "name" => "Bengali",
+        "nativeName" => "বাংলা"
+    ),
+    "bh" => array(
+        "name" => "Bihari",
+        "nativeName" => "भोजपुरी"
+    ),
+    "bi" => array(
+        "name" => "Bislama",
+        "nativeName" => "Bislama"
+    ),
+    "bs" => array(
+        "name" => "Bosnian",
+        "nativeName" => "bosanski jezik"
+    ),
+    "br" => array(
+        "name" => "Breton",
+        "nativeName" => "brezhoneg"
+    ),
+    "bg" => array(
+        "name" => "Bulgarian",
+        "nativeName" => "български език"
+    ),
+    "my" => array(
+        "name" => "Burmese",
+        "nativeName" => "ဗမာစာ"
+    ),
+    "ca" => array(
+        "name" => "Catalan; Valencian",
+        "nativeName" => "Català"
+    ),
+    "ch" => array(
+        "name" => "Chamorro",
+        "nativeName" => "Chamoru"
+    ),
+    "ce" => array(
+        "name" => "Chechen",
+        "nativeName" => "нохчийн мотт"
+    ),
+    "ny" => array(
+        "name" => "Chichewa; Chewa; Nyanja",
+        "nativeName" => "chiCheŵa, chinyanja"
+    ),
+    "zh" => array(
+        "name" => "Chinese",
+        "nativeName" => "中文 (Zhōngwén), 汉语, 漢語"
+    ),
+    "cv" => array(
+        "name" => "Chuvash",
+        "nativeName" => "чӑваш чӗлхи"
+    ),
+    "kw" => array(
+        "name" => "Cornish",
+        "nativeName" => "Kernewek"
+    ),
+    "co" => array(
+        "name" => "Corsican",
+        "nativeName" => "corsu, lingua corsa"
+    ),
+    "cr" => array(
+        "name" => "Cree",
+        "nativeName" => "ᓀᐦᐃᔭᐍᐏᐣ"
+    ),
+    "hr" => array(
+        "name" => "Croatian",
+        "nativeName" => "hrvatski"
+    ),
+    "cs" => array(
+        "name" => "Czech",
+        "nativeName" => "česky, čeština"
+    ),
+    "da" => array(
+        "name" => "Danish",
+        "nativeName" => "dansk"
+    ),
+    "dv" => array(
+        "name" => "Divehi; Dhivehi; Maldivian;",
+        "nativeName" => "ދިވެހި"
+    ),
+    "nl" => array(
+        "name" => "Dutch",
+        "nativeName" => "Nederlands, Vlaams"
+    ),
+    "en" => array(
+        "name" => "English",
+        "nativeName" => "English"
+    ),
+    "eo" => array(
+        "name" => "Esperanto",
+        "nativeName" => "Esperanto"
+    ),
+    "et" => array(
+        "name" => "Estonian",
+        "nativeName" => "eesti, eesti keel"
+    ),
+    "ee" => array(
+        "name" => "Ewe",
+        "nativeName" => "Eʋegbe"
+    ),
+    "fo" => array(
+        "name" => "Faroese",
+        "nativeName" => "føroyskt"
+    ),
+    "fj" => array(
+        "name" => "Fijian",
+        "nativeName" => "vosa Vakaviti"
+    ),
+    "fi" => array(
+        "name" => "Finnish",
+        "nativeName" => "suomi, suomen kieli"
+    ),
+    "fr" => array(
+        "name" => "French",
+        "nativeName" => "français, langue française"
+    ),
+    "ff" => array(
+        "name" => "Fula; Fulah; Pulaar; Pular",
+        "nativeName" => "Fulfulde, Pulaar, Pular"
+    ),
+    "gl" => array(
+        "name" => "Galician",
+        "nativeName" => "Galego"
+    ),
+    "ka" => array(
+        "name" => "Georgian",
+        "nativeName" => "ქართული"
+    ),
+    "de" => array(
+        "name" => "German",
+        "nativeName" => "Deutsch"
+    ),
+    "el" => array(
+        "name" => "Greek, Modern",
+        "nativeName" => "Ελληνικά"
+    ),
+    "gn" => array(
+        "name" => "Guaraní",
+        "nativeName" => "Avañeẽ"
+    ),
+    "gu" => array(
+        "name" => "Gujarati",
+        "nativeName" => "ગુજરાતી"
+    ),
+    "ht" => array(
+        "name" => "Haitian; Haitian Creole",
+        "nativeName" => "Kreyòl ayisyen"
+    ),
+    "ha" => array(
+        "name" => "Hausa",
+        "nativeName" => "Hausa, هَوُسَ"
+    ),
+    "he" => array(
+        "name" => "Hebrew (modern)",
+        "nativeName" => "עברית"
+    ),
+    "hz" => array(
+        "name" => "Herero",
+        "nativeName" => "Otjiherero"
+    ),
+    "hi" => array(
+        "name" => "Hindi",
+        "nativeName" => "हिन्दी, हिंदी"
+    ),
+    "ho" => array(
+        "name" => "Hiri Motu",
+        "nativeName" => "Hiri Motu"
+    ),
+    "hu" => array(
+        "name" => "Hungarian",
+        "nativeName" => "Magyar"
+    ),
+    "ia" => array(
+        "name" => "Interlingua",
+        "nativeName" => "Interlingua"
+    ),
+    "id" => array(
+        "name" => "Indonesian",
+        "nativeName" => "Bahasa Indonesia"
+    ),
+    "ie" => array(
+        "name" => "Interlingue",
+        "nativeName" => "Originally called Occidental; then Interlingue after WWII"
+    ),
+    "ga" => array(
+        "name" => "Irish",
+        "nativeName" => "Gaeilge"
+    ),
+    "ig" => array(
+        "name" => "Igbo",
+        "nativeName" => "Asụsụ Igbo"
+    ),
+    "ik" => array(
+        "name" => "Inupiaq",
+        "nativeName" => "Iñupiaq, Iñupiatun"
+    ),
+    "io" => array(
+        "name" => "Ido",
+        "nativeName" => "Ido"
+    ),
+    "is" => array(
+        "name" => "Icelandic",
+        "nativeName" => "Íslenska"
+    ),
+    "it" => array(
+        "name" => "Italian",
+        "nativeName" => "Italiano"
+    ),
+    "iu" => array(
+        "name" => "Inuktitut",
+        "nativeName" => "ᐃᓄᒃᑎᑐᑦ"
+    ),
+    "ja" => array(
+        "name" => "Japanese",
+        "nativeName" => "日本語 (にほんご/にっぽんご)"
+    ),
+    "jv" => array(
+        "name" => "Javanese",
+        "nativeName" => "basa Jawa"
+    ),
+    "kl" => array(
+        "name" => "Kalaallisut, Greenlandic",
+        "nativeName" => "kalaallisut, kalaallit oqaasii"
+    ),
+    "kn" => array(
+        "name" => "Kannada",
+        "nativeName" => "ಕನ್ನಡ"
+    ),
+    "kr" => array(
+        "name" => "Kanuri",
+        "nativeName" => "Kanuri"
+    ),
+    "ks" => array(
+        "name" => "Kashmiri",
+        "nativeName" => "कश्मीरी, كشميري‎"
+    ),
+    "kk" => array(
+        "name" => "Kazakh",
+        "nativeName" => "Қазақ тілі"
+    ),
+    "km" => array(
+        "name" => "Khmer",
+        "nativeName" => "ភាសាខ្មែរ"
+    ),
+    "ki" => array(
+        "name" => "Kikuyu, Gikuyu",
+        "nativeName" => "Gĩkũyũ"
+    ),
+    "rw" => array(
+        "name" => "Kinyarwanda",
+        "nativeName" => "Ikinyarwanda"
+    ),
+    "ky" => array(
+        "name" => "Kirghiz, Kyrgyz",
+        "nativeName" => "кыргыз тили"
+    ),
+    "kv" => array(
+        "name" => "Komi",
+        "nativeName" => "коми кыв"
+    ),
+    "kg" => array(
+        "name" => "Kongo",
+        "nativeName" => "KiKongo"
+    ),
+    "ko" => array(
+        "name" => "Korean",
+        "nativeName" => "한국어 (韓國語), 조선말 (朝鮮語)"
+    ),
+    "ku" => array(
+        "name" => "Kurdish",
+        "nativeName" => "Kurdî, كوردی‎"
+    ),
+    "kj" => array(
+        "name" => "Kwanyama, Kuanyama",
+        "nativeName" => "Kuanyama"
+    ),
+    "la" => array(
+        "name" => "Latin",
+        "nativeName" => "latine, lingua latina"
+    ),
+    "lb" => array(
+        "name" => "Luxembourgish, Letzeburgesch",
+        "nativeName" => "Lëtzebuergesch"
+    ),
+    "lg" => array(
+        "name" => "Luganda",
+        "nativeName" => "Luganda"
+    ),
+    "li" => array(
+        "name" => "Limburgish, Limburgan, Limburger",
+        "nativeName" => "Limburgs"
+    ),
+    "ln" => array(
+        "name" => "Lingala",
+        "nativeName" => "Lingála"
+    ),
+    "lo" => array(
+        "name" => "Lao",
+        "nativeName" => "ພາສາລາວ"
+    ),
+    "lt" => array(
+        "name" => "Lithuanian",
+        "nativeName" => "lietuvių kalba"
+    ),
+    "lu" => array(
+        "name" => "Luba-Katanga",
+        "nativeName" => ""
+    ),
+    "lv" => array(
+        "name" => "Latvian",
+        "nativeName" => "latviešu valoda"
+    ),
+    "gv" => array(
+        "name" => "Manx",
+        "nativeName" => "Gaelg, Gailck"
+    ),
+    "mk" => array(
+        "name" => "Macedonian",
+        "nativeName" => "македонски јазик"
+    ),
+    "mg" => array(
+        "name" => "Malagasy",
+        "nativeName" => "Malagasy fiteny"
+    ),
+    "ms" => array(
+        "name" => "Malay",
+        "nativeName" => "bahasa Melayu, بهاس ملايو‎"
+    ),
+    "ml" => array(
+        "name" => "Malayalam",
+        "nativeName" => "മലയാളം"
+    ),
+    "mt" => array(
+        "name" => "Maltese",
+        "nativeName" => "Malti"
+    ),
+    "mi" => array(
+        "name" => "Māori",
+        "nativeName" => "te reo Māori"
+    ),
+    "mr" => array(
+        "name" => "Marathi (Marāṭhī)",
+        "nativeName" => "मराठी"
+    ),
+    "mh" => array(
+        "name" => "Marshallese",
+        "nativeName" => "Kajin M̧ajeļ"
+    ),
+    "mn" => array(
+        "name" => "Mongolian",
+        "nativeName" => "монгол"
+    ),
+    "na" => array(
+        "name" => "Nauru",
+        "nativeName" => "Ekakairũ Naoero"
+    ),
+    "nv" => array(
+        "name" => "Navajo, Navaho",
+        "nativeName" => "Diné bizaad, Dinékʼehǰí"
+    ),
+    "nb" => array(
+        "name" => "Norwegian Bokmål",
+        "nativeName" => "Norsk bokmål"
+    ),
+    "nd" => array(
+        "name" => "North Ndebele",
+        "nativeName" => "isiNdebele"
+    ),
+    "ne" => array(
+        "name" => "Nepali",
+        "nativeName" => "नेपाली"
+    ),
+    "ng" => array(
+        "name" => "Ndonga",
+        "nativeName" => "Owambo"
+    ),
+    "nn" => array(
+        "name" => "Norwegian Nynorsk",
+        "nativeName" => "Norsk nynorsk"
+    ),
+    "no" => array(
+        "name" => "Norwegian",
+        "nativeName" => "Norsk"
+    ),
+    "ii" => array(
+        "name" => "Nuosu",
+        "nativeName" => "ꆈꌠ꒿ Nuosuhxop"
+    ),
+    "nr" => array(
+        "name" => "South Ndebele",
+        "nativeName" => "isiNdebele"
+    ),
+    "oc" => array(
+        "name" => "Occitan",
+        "nativeName" => "Occitan"
+    ),
+    "oj" => array(
+        "name" => "Ojibwe, Ojibwa",
+        "nativeName" => "ᐊᓂᔑᓈᐯᒧᐎᓐ"
+    ),
+    "cu" => array(
+        "name" => "Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic",
+        "nativeName" => "ѩзыкъ словѣньскъ"
+    ),
+    "om" => array(
+        "name" => "Oromo",
+        "nativeName" => "Afaan Oromoo"
+    ),
+    "or" => array(
+        "name" => "Oriya",
+        "nativeName" => "ଓଡ଼ିଆ"
+    ),
+    "os" => array(
+        "name" => "Ossetian, Ossetic",
+        "nativeName" => "ирон æвзаг"
+    ),
+    "pa" => array(
+        "name" => "Panjabi, Punjabi",
+        "nativeName" => "ਪੰਜਾਬੀ, پنجابی‎"
+    ),
+    "pi" => array(
+        "name" => "Pāli",
+        "nativeName" => "पाऴि"
+    ),
+    "fa" => array(
+        "name" => "Persian",
+        "nativeName" => "فارسی"
+    ),
+    "pl" => array(
+        "name" => "Polish",
+        "nativeName" => "polski"
+    ),
+    "ps" => array(
+        "name" => "Pashto, Pushto",
+        "nativeName" => "پښتو"
+    ),
+    "pt" => array(
+        "name" => "Portuguese",
+        "nativeName" => "Português"
+    ),
+    "qu" => array(
+        "name" => "Quechua",
+        "nativeName" => "Runa Simi, Kichwa"
+    ),
+    "rm" => array(
+        "name" => "Romansh",
+        "nativeName" => "rumantsch grischun"
+    ),
+    "rn" => array(
+        "name" => "Kirundi",
+        "nativeName" => "kiRundi"
+    ),
+    "ro" => array(
+        "name" => "Romanian, Moldavian, Moldovan",
+        "nativeName" => "română"
+    ),
+    "ru" => array(
+        "name" => "Russian",
+        "nativeName" => "русский язык"
+    ),
+    "sa" => array(
+        "name" => "Sanskrit (Saṁskṛta)",
+        "nativeName" => "संस्कृतम्"
+    ),
+    "sc" => array(
+        "name" => "Sardinian",
+        "nativeName" => "sardu"
+    ),
+    "sd" => array(
+        "name" => "Sindhi",
+        "nativeName" => "सिन्धी, سنڌي، سندھی‎"
+    ),
+    "se" => array(
+        "name" => "Northern Sami",
+        "nativeName" => "Davvisámegiella"
+    ),
+    "sm" => array(
+        "name" => "Samoan",
+        "nativeName" => "gagana faa Samoa"
+    ),
+    "sg" => array(
+        "name" => "Sango",
+        "nativeName" => "yângâ tî sängö"
+    ),
+    "sr" => array(
+        "name" => "Serbian",
+        "nativeName" => "српски језик"
+    ),
+    "gd" => array(
+        "name" => "Scottish Gaelic; Gaelic",
+        "nativeName" => "Gàidhlig"
+    ),
+    "sn" => array(
+        "name" => "Shona",
+        "nativeName" => "chiShona"
+    ),
+    "si" => array(
+        "name" => "Sinhala, Sinhalese",
+        "nativeName" => "සිංහල"
+    ),
+    "sk" => array(
+        "name" => "Slovak",
+        "nativeName" => "slovenčina"
+    ),
+    "sl" => array(
+        "name" => "Slovene",
+        "nativeName" => "slovenščina"
+    ),
+    "so" => array(
+        "name" => "Somali",
+        "nativeName" => "Soomaaliga, af Soomaali"
+    ),
+    "st" => array(
+        "name" => "Southern Sotho",
+        "nativeName" => "Sesotho"
+    ),
+    "es" => array(
+        "name" => "Spanish; Castilian",
+        "nativeName" => "español, castellano"
+    ),
+    "su" => array(
+        "name" => "Sundanese",
+        "nativeName" => "Basa Sunda"
+    ),
+    "sw" => array(
+        "name" => "Swahili",
+        "nativeName" => "Kiswahili"
+    ),
+    "ss" => array(
+        "name" => "Swati",
+        "nativeName" => "SiSwati"
+    ),
+    "sv" => array(
+        "name" => "Swedish",
+        "nativeName" => "svenska"
+    ),
+    "ta" => array(
+        "name" => "Tamil",
+        "nativeName" => "தமிழ்"
+    ),
+    "te" => array(
+        "name" => "Telugu",
+        "nativeName" => "తెలుగు"
+    ),
+    "tg" => array(
+        "name" => "Tajik",
+        "nativeName" => "тоҷикӣ, toğikī, تاجیکی‎"
+    ),
+    "th" => array(
+        "name" => "Thai",
+        "nativeName" => "ไทย"
+    ),
+    "ti" => array(
+        "name" => "Tigrinya",
+        "nativeName" => "ትግርኛ"
+    ),
+    "bo" => array(
+        "name" => "Tibetan Standard, Tibetan, Central",
+        "nativeName" => "བོད་ཡིག"
+    ),
+    "tk" => array(
+        "name" => "Turkmen",
+        "nativeName" => "Türkmen, Түркмен"
+    ),
+    "tl" => array(
+        "name" => "Tagalog",
+        "nativeName" => "Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔"
+    ),
+    "tn" => array(
+        "name" => "Tswana",
+        "nativeName" => "Setswana"
+    ),
+    "to" => array(
+        "name" => "Tonga (Tonga Islands)",
+        "nativeName" => "faka Tonga"
+    ),
+    "tr" => array(
+        "name" => "Turkish",
+        "nativeName" => "Türkçe"
+    ),
+    "ts" => array(
+        "name" => "Tsonga",
+        "nativeName" => "Xitsonga"
+    ),
+    "tt" => array(
+        "name" => "Tatar",
+        "nativeName" => "татарча, tatarça, تاتارچا‎"
+    ),
+    "tw" => array(
+        "name" => "Twi",
+        "nativeName" => "Twi"
+    ),
+    "ty" => array(
+        "name" => "Tahitian",
+        "nativeName" => "Reo Tahiti"
+    ),
+    "ug" => array(
+        "name" => "Uighur, Uyghur",
+        "nativeName" => "Uyƣurqə, ئۇيغۇرچە‎"
+    ),
+    "uk" => array(
+        "name" => "Ukrainian",
+        "nativeName" => "українська"
+    ),
+    "ur" => array(
+        "name" => "Urdu",
+        "nativeName" => "اردو"
+    ),
+    "uz" => array(
+        "name" => "Uzbek",
+        "nativeName" => "zbek, Ўзбек, أۇزبېك‎"
+    ),
+    "ve" => array(
+        "name" => "Venda",
+        "nativeName" => "Tshivenḓa"
+    ),
+    "vi" => array(
+        "name" => "Vietnamese",
+        "nativeName" => "Tiếng Việt"
+    ),
+    "vo" => array(
+        "name" => "Volapük",
+        "nativeName" => "Volapük"
+    ),
+    "wa" => array(
+        "name" => "Walloon",
+        "nativeName" => "Walon"
+    ),
+    "cy" => array(
+        "name" => "Welsh",
+        "nativeName" => "Cymraeg"
+    ),
+    "wo" => array(
+        "name" => "Wolof",
+        "nativeName" => "Wollof"
+    ),
+    "fy" => array(
+        "name" => "Western Frisian",
+        "nativeName" => "Frysk"
+    ),
+    "xh" => array(
+        "name" => "Xhosa",
+        "nativeName" => "isiXhosa"
+    ),
+    "yi" => array(
+        "name" => "Yiddish",
+        "nativeName" => "ייִדיש"
+    ),
+    "yo" => array(
+        "name" => "Yoruba",
+        "nativeName" => "Yorùbá"
+    ),
+    "za" => array(
+        "name" => "Zhuang, Chuang",
+        "nativeName" => "Saɯ cueŋƅ, Saw cuengh"
+    )
+);
diff --git a/include/mysqli.php b/include/mysqli.php
index 34fa57479b64ebcda7e1ac381f4c70a6069773aa..7245d9ec20eeab75197500a8b9f5d8f06af8e567 100644
--- a/include/mysqli.php
+++ b/include/mysqli.php
@@ -39,18 +39,24 @@ function db_connect($host, $user, $passwd, $options = array()) {
         return NULL;
 
     $port = ini_get("mysqli.default_port");
+    $socket = ini_get("mysqli.default_socket");
     if (strpos($host, ':') !== false) {
-        list($host, $port) = explode(':', $host);
+        list($host, $portspec) = explode(':', $host);
         // PHP may not honor the port number if connecting to 'localhost'
-        if (!strcasecmp($host, 'localhost'))
-            // XXX: Looks like PHP gethostbyname() is IPv4 only
-            $host = gethostbyname($host);
-        $port = (int) $port;
+        if ($portspec && is_numeric($portspec)) {
+            if (!strcasecmp($host, 'localhost'))
+                // XXX: Looks like PHP gethostbyname() is IPv4 only
+                $host = gethostbyname($host);
+            $port = (int) $portspec;
+        }
+        elseif ($portspec) {
+            $socket = $portspec;
+        }
     }
 
     // Connect
     $start = microtime(true);
-    if (!@$__db->real_connect($host, $user, $passwd, null, $port))
+    if (!@$__db->real_connect($host, $user, $passwd, null, $port, $socket))
         return NULL;
 
     //Select the database, if any.
diff --git a/include/pear/Net/DNS2/Packet.php b/include/pear/Net/DNS2/Packet.php
index ccd633eb7d2dbbbde819a02c40ae369f95b0f33a..12c67063c3f741d0283d30c776b43c4027e54935 100644
--- a/include/pear/Net/DNS2/Packet.php
+++ b/include/pear/Net/DNS2/Packet.php
@@ -190,7 +190,7 @@ class Net_DNS2_Packet
     /**
      * applies a standard DNS name compression on the given name/offset
      *
-     * This logic was based on the Net::DNS::Packet::dn_comp() function 
+     * This logic was based on the Net::DNS::Packet::dn_comp() function (nolint)
      * by Michanel Fuhr
      *
      * @param string  $name    the name to be compressed
@@ -250,7 +250,7 @@ class Net_DNS2_Packet
     /**
      * applies a standard DNS name compression on the given name/offset
      *
-     * This logic was based on the Net::DNS::Packet::dn_comp() function 
+     * This logic was based on the Net::DNS::Packet::dn_comp() function (nolint)
      * by Michanel Fuhr
      *
      * @param string $name the name to be compressed
@@ -283,7 +283,7 @@ class Net_DNS2_Packet
     /**
      * expands the domain name stored at a given offset in a DNS Packet
      *
-     * This logic was based on the Net::DNS::Packet::dn_expand() function
+     * This logic was based on the Net::DNS::Packet::dn_expand() function (nolint)
      * by Michanel Fuhr
      *
      * @param Net_DNS2_Packet &$packet the DNS packet to look in for the domain name
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index ded810d8996c70fed985b75c690abdb5f9e8974b..d2a3e43b82277bb13eb38718e06bfd84340457f9 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -2,6 +2,7 @@
 <html>
 <head>
     <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
     <meta http-equiv="cache-control" content="no-cache" />
     <meta http-equiv="pragma" content="no-cache" />
     <title><?php echo ($ost && ($title=$ost->getPageTitle()))?$title:'osTicket :: Staff Control Panel'; ?></title>
diff --git a/include/staff/plugins.inc.php b/include/staff/plugins.inc.php
index eab4a796ee6a3240cd30d35fac1e20f2071b17f0..4b9c5bccc3d83a0474fc4e953b8dfb5c161d0ef9 100644
--- a/include/staff/plugins.inc.php
+++ b/include/staff/plugins.inc.php
@@ -35,19 +35,11 @@ foreach ($ost->plugins->allInstalled() as $p) {
                 <?php echo $sel?'checked="checked"':''; ?>></td>
         <td><a href="plugins.php?id=<?php echo $p->getId(); ?>"
             ><?php echo $p->getName(); ?></a></td>
-        <td>Enabled</td>
+        <td><?php echo ($p->isActive())
+            ? 'Enabled' : '<strong>Disabled</strong>'; ?></td>
         <td><?php echo Format::db_datetime($p->getInstallDate()); ?></td>
     </tr>
-    <?php } else {
-        $p = $ost->plugins->getInfoForPath($p['install_path']); ?>
-    <tr>
-        <td><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p['install_path']; ?>"
-                <?php echo $sel?'checked="checked"':''; ?>></td>
-        <td><?php echo $p['name']; ?></td>
-        <td><strong>Disabled</strong></td>
-        <td></td>
-    </tr>
-    <?php } ?>
+    <?php } else {} ?>
 <?php } ?>
     </tbody>
     <tfoot>
diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php
index 8ceaca328452056ad3829a00ae69f64c85dae52e..116a39eab0617425965850687298f47999bfb77d 100644
--- a/include/staff/profile.inc.php
+++ b/include/staff/profile.inc.php
@@ -100,6 +100,24 @@ $info['id']=$staff->getId();
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['timezone_id']; ?></span>
             </td>
         </tr>
+        <tr>
+            <td width="180">
+                Preferred Language:
+            </td>
+            <td>
+        <?php
+        $langs = Internationalization::availableLanguages(); ?>
+                <select name="lang">
+                    <option value="">&mdash; Use Browser Preference &mdash;</option>
+<?php foreach($langs as $l) {
+    $selected = ($info['lang'] == $l['code']) ? 'selected="selected"' : ''; ?>
+                    <option value="<?php echo $l['code']; ?>" <?php echo $selected;
+                        ?>><?php echo $l['desc']; ?></option>
+<?php } ?>
+                </select>
+                <span class="error">&nbsp;<?php echo $errors['lang']; ?></span>
+            </td>
+        </tr>
         <tr>
             <td width="180">
                Daylight Saving:
diff --git a/include/staff/template.inc.php b/include/staff/template.inc.php
index 8fee6a2ce6a8bae63c9273b99eafa178c48d0214..2d05235eecb07d2a20c5d389fc58b1aaf763a022 100644
--- a/include/staff/template.inc.php
+++ b/include/staff/template.inc.php
@@ -15,6 +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();
     $qstr.='&a='.urlencode($_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
@@ -54,19 +55,22 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['isactive']; ?></span>
             </td>
         </tr>
+        <?php
+        if($template){ ?>
         <tr>
             <td width="180" class="required">
                 Language:
             </td>
             <td>
-                <select name="lang_id">
-                    <option value="en" selected="selected">English (US)</option>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['lang_id']; ?></span>
+                <?php
+            $langs = Internationalization::availableLanguages();
+            $lang = strtolower($info['lang']);
+            if (isset($langs[$lang]))
+                echo $langs[$lang]['desc'];
+            else
+                echo $info['lang']; ?>
             </td>
         </tr>
-        <?php
-        if($template){ ?>
         <tr>
             <th colspan="2">
                 <em><strong>Template Messages</strong>: Click on the message to edit.&nbsp;
@@ -100,6 +104,23 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             }
         }
         }else{ ?>
+        <tr>
+            <td width="180" class="required">
+                Language:
+            </td>
+            <td>
+        <?php
+        $langs = Internationalization::availableLanguages(); ?>
+                <select name="lang_id">
+<?php foreach($langs as $l) {
+    $selected = ($info['lang_id'] == $l['code']) ? 'selected="selected"' : ''; ?>
+                    <option value="<?php echo $l['code']; ?>" <?php echo $selected;
+                        ?>><?php echo $l['desc']; ?></option>
+<?php } ?>
+                </select>
+                &nbsp;<span class="error">*&nbsp;<?php echo $errors['lang_id']; ?></span>
+            </td>
+        </tr>
         <tr>
             <td width="180" class="required">
                 Template To Clone:
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index e463e515d88a0afa7c7e013f57adf9f411d0dc9a..1415ce76d059cb8617c26f7f66456e51ba9bf4b9 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -129,6 +129,9 @@ if($search):
             if (count($tickets))
                 $qwhere .= ' AND ticket.ticket_id IN ('.
                     implode(',',db_input($tickets)).')';
+            else
+                // No hits -- there should be an empty list of results
+                $qwhere .= ' AND false';
         }
    }
 
@@ -192,10 +195,7 @@ $$x=' class="'.strtolower($order).'" ';
 if($_GET['limit'])
     $qstr.='&limit='.urlencode($_GET['limit']);
 
-$qselect ='SELECT DISTINCT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id '
-    .',MAX(IF(field.name = \'subject\', ans.value, NULL)) as `subject`'
-    .',MAX(IF(field.name = \'priority\', ans.value, NULL)) as `priority_desc`'
-    .',MAX(IF(field.name = \'priority\', ans.value_id, NULL)) as `priority_id`'
+$qselect ='SELECT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id '
     .' ,user.name'
     .' ,email.address as email, dept_name '
          .' ,ticket.status,ticket.source,isoverdue,isanswered,ticket.created ';
@@ -203,18 +203,13 @@ $qselect ='SELECT DISTINCT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,tick
 $qfrom=' FROM '.TICKET_TABLE.' ticket '.
        ' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'.
        ' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'.
-       ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '.
-       ' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.object_type=\'T\'
-             and entry.object_id=ticket.ticket_id'.
-       ' LEFT JOIN '.FORM_ANSWER_TABLE.' ans ON ans.entry_id = entry.id'.
-       ' LEFT JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id';
+       ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id ';
 
 $sjoin='';
 if($search && $deep_search) {
     $sjoin=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )';
 }
 
-$qgroup=' GROUP BY ticket.ticket_id';
 //get ticket count based on the query so far..
 $total=db_count("SELECT count(DISTINCT ticket.ticket_id) $qfrom $sjoin $qwhere");
 //pagenate
@@ -224,23 +219,23 @@ $pageNav=new Pagenate($total,$page,$pagelimit);
 $pageNav->setURL('tickets.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
 
 //ADD attachment,priorities, lock and other crap
-$qselect.=' ,count(attach.attach_id) as attachments '
-         .' ,count(DISTINCT thread.id) as thread_count '
-         .' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)), ticket.duedate) as duedate '
+$qselect.=' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)), ticket.duedate) as duedate '
          .' ,CAST(GREATEST(IFNULL(ticket.lastmessage, 0), IFNULL(ticket.reopened, 0), ticket.created) as datetime) as effective_date '
          .' ,CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team '
          .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned '
-         .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic ';
+         .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '
+         .' ,cdata.priority_id, cdata.subject';
 
 $qfrom.=' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()
                AND tlock.staff_id!='.db_input($thisstaff->getId()).') '
-       .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) '
-       .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) '
        .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) '
        .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) '
        .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
        .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) '
-       .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) ';
+       .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) '
+       .' LEFT JOIN '.TABLE_PREFIX.'ticket__cdata cdata ON (cdata.ticket_id = ticket.ticket_id) ';
+
+TicketForm::ensureDynamicDataView();
 
 // Fetch priority information
 $res = db_query('select * from '.PRIORITY_TABLE);
@@ -248,7 +243,7 @@ $prios = array();
 while ($row = db_fetch_array($res))
     $prios[$row['priority_id']] = $row;
 
-$query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
+$query="$qselect $qfrom $qwhere ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 //echo $query;
 $hash = md5($query);
 $_SESSION['search_'.$hash] = $query;
@@ -262,6 +257,27 @@ if($search)
 
 $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting..
 
+// Fetch the results
+$results = array();
+while ($row = db_fetch_array($res)) {
+    $results[$row['ticket_id']] = $row;
+}
+
+// Fetch attachment and thread entry counts
+if ($results) {
+    $counts_sql = 'SELECT ticket.ticket_id, count(attach.attach_id) as attachments,
+        count(DISTINCT thread.id) as thread_count
+        FROM '.TICKET_TABLE.' ticket
+        LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) '
+     .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) '
+     .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).')
+        GROUP BY ticket.ticket_id';
+    $ids_res = db_query($counts_sql);
+    while ($row = db_fetch_array($ids_res)) {
+        $results[$row['ticket_id']] += $row;
+    }
+}
+
 //YOU BREAK IT YOU FIX IT.
 ?>
 <!-- SEARCH FORM START -->
@@ -345,9 +361,9 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting..
         <?php
         $class = "row1";
         $total=0;
-        if($res && ($num=db_num_rows($res))):
+        if($res && ($num=count($results))):
             $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null;
-            while ($row = db_fetch_array($res)) {
+            foreach ($results as $row) {
                 $tag=$row['staff_id']?'assigned':'openticket';
                 $flag=null;
                 if($row['lock_id'])
diff --git a/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql b/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql
index e565227249787174009fddfb6e7f06ed82f01d52..97c2bf97b45d6e5fa1368e8011d05e07d48c545e 100644
--- a/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql
+++ b/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql
@@ -224,7 +224,7 @@ CREATE TABLE `%TABLE_PREFIX%email_filter_rule` (
 
 -- SYSTEM BAN LIST was the first filter created, with ID of '1'
 INSERT INTO `%TABLE_PREFIX%email_filter_rule` (`filter_id`, `what`, `how`, `val`)
-    SELECT LAST_INSERT_ID(), 'email', 'equals', email FROM `%TABLE_PREFIX%email_banlist`;
+    SELECT LAST_INSERT_ID(), 'email', 'equal', email FROM `%TABLE_PREFIX%email_banlist`;
 
 -- Create table session
 DROP TABLE IF EXISTS `%TABLE_PREFIX%session`;
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index 0fed815da1831b9425a22d6f2702766e1ca81362..337d525a5d1cbc5a85601313cf2e941b242d00d4 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -140,7 +140,12 @@ $(function() {
         var el = $(el),
             options = {
                 'air': el.hasClass('no-bar'),
-                'airButtons': ['formatting', '|', 'bold', 'italic', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'image'],
+                'airButtons': ['formatting', '|', 'bold', 'italic', 'underline', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'image'],
+                'buttons': ['html', '|', 'formatting', '|', 'bold',
+                    'italic', 'underline', 'deleted', '|', 'unorderedlist',
+                    'orderedlist', 'outdent', 'indent', '|', 'image', 'video',
+                    'file', 'table', 'link', '|', 'alignment', '|',
+                    'horizontalrule'],
                 'autoresize': !el.hasClass('no-bar'),
                 'minHeight': el.hasClass('small') ? 75 : 150,
                 'focus': false,
diff --git a/scp/ajax.php b/scp/ajax.php
index 01b41867dd53bc052d5a7b9c8765327b140e3739..7e990934b8abe59b1b9a1aecfd3ccd91cfb06271 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -101,8 +101,8 @@ $dispatcher = patterns('',
     )),
     url_post('^/upgrader', array('ajax.upgrader.php:UpgraderAjaxAPI', 'upgrade')),
     url('^/help/', patterns('ajax.tips.php:HelpTipAjaxAPI',
-        url_get('tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'),
-        url_get('(?P<lang>\w{2}_\w{2})?/tips/(?P<namespace>[\w_.]+)$', 'getTipsForLangJson')
+        url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'),
+        url_get('^(?P<lang>[\w_]+)?/tips/(?P<namespace>[\w_.]+)$', 'getTipsJsonForLang')
     ))
 );
 
diff --git a/scp/forms.php b/scp/forms.php
index 5af2dd327781d18adca740b46ad654eb272929db..6f14be6c1a2bec06aea87c6f1c4c97d9db177efd 100644
--- a/scp/forms.php
+++ b/scp/forms.php
@@ -99,6 +99,7 @@ if($_POST) {
                 'private'=>$_POST["private-new-$i"] == 'on' ? 1 : 0,
                 'required'=>$_POST["required-new-$i"] == 'on' ? 1 : 0
             ));
+            $field->setForm($form);
             if ($field->isValid())
                 $field->save();
             else
diff --git a/scp/plugins.php b/scp/plugins.php
index c5e7e8918027296970b717289626fb4c5982d6ac..44dda73b80897b668caafe38638c163321a31e64 100644
--- a/scp/plugins.php
+++ b/scp/plugins.php
@@ -19,8 +19,8 @@ if($_POST) {
             $count = count($_POST['ids']);
             switch(strtolower($_POST['a'])) {
             case 'enable':
-                foreach ($_POST['ids'] as $path) {
-                    if ($p = $ost->plugins->getInstance($path)) {
+                foreach ($_POST['ids'] as $id) {
+                    if ($p = Plugin::lookup($id)) {
                         $p->enable();
                     }
                 }
@@ -35,7 +35,6 @@ if($_POST) {
             case 'delete':
                 foreach ($_POST['ids'] as $id) {
                     if ($p = Plugin::lookup($id)) {
-                        var_dump($p);
                         $p->uninstall();
                     }
                 }
diff --git a/setup/ajax.php b/setup/ajax.php
index 9c2c7b282a325f135f3dde4e8e80491860666b31..97e45daddc88ece9cd65e24ea71dfb9f0d7ac8b9 100644
--- a/setup/ajax.php
+++ b/setup/ajax.php
@@ -23,8 +23,8 @@ require_once INCLUDE_DIR.'/class.ajax.php';
 
 $dispatcher = patterns('',
     url('^/help/', patterns('ajax.tips.php:HelpTipAjaxAPI',
-        url_get('tips/(?P<namespace>[\w_]+)$', 'getTipsJson'),
-        url_get('(?P<lang>\w{2}_\w{2})?/tips/(?P<namespace>[\w_]+)$', 'getTipsForLangJson')
+        url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'),
+        url_get('^(?P<lang>[\w_]+)?/tips/(?P<namespace>[\w_.]+)$', 'getTipsJsonForLang')
     ))
 );
 print $dispatcher->resolve(Osticket::get_path_info());
diff --git a/setup/cli/cli.inc.php b/setup/cli/cli.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..31bdbfe8993cdadadbc4896c1e9257b3909b8cc0
--- /dev/null
+++ b/setup/cli/cli.inc.php
@@ -0,0 +1,31 @@
+<?php
+/*********************************************************************
+    cli.inc.php
+
+    Master include file which must be included at the start of every file.
+    This is a modification of main.inc.php to support running cli scripts.
+
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+#Disable direct access.
+if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!');
+
+define('ROOT_PATH', '/');
+define('INC_DIR',dirname(__file__).'/../inc/'); //local include dir!
+
+require_once(dirname(__file__).'/../../bootstrap.php');
+
+Bootstrap::loadConfig();
+Bootstrap::defineTables(TABLE_PREFIX);
+Bootstrap::loadCode();
+Bootstrap::i18n_prep();
+
+?>
diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php
index 437f87c609eb6b63f6799978ce324f437a456ca5..421e49bd11a22f115af0e9d3c288099f07859e3f 100644
--- a/setup/cli/modules/class.module.php
+++ b/setup/cli/modules/class.module.php
@@ -227,6 +227,11 @@ class Module {
     function run($args, $options) {
     }
 
+    function fail($message) {
+        $this->stderr->write($message . "\n");
+        die();
+    }
+
     /* static */
     function register($action, $class) {
         global $registered_modules;
diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php
new file mode 100644
index 0000000000000000000000000000000000000000..a9a4117c4b15f59d6914dbd11edf741b1c5544b7
--- /dev/null
+++ b/setup/cli/modules/i18n.php
@@ -0,0 +1,130 @@
+<?php
+
+require_once dirname(__file__) . "/class.module.php";
+require_once dirname(__file__) . "/../cli.inc.php";
+require_once INCLUDE_DIR . 'class.format.php';
+
+class i18n_Compiler extends Module {
+
+    var $prologue = "Manages translation files from Crowdin";
+
+    var $arguments = array(
+        "command" => "Action to be performed.
+            list    - Show list of available translations"
+    );
+
+    var $options = array(
+        "key" => array('-k','--key','metavar'=>'API-KEY',
+            'help'=>'Crowdin project API key. This can be omitted if
+            CROWDIN_API_KEY is defined in the ost-config.php file'),
+        "lang" => array('-L', '--lang', 'metavar'=>'code',
+            'help'=>'Language code (used for building)'),
+    );
+
+    static $crowdin_api_url = 'http://i18n.osticket.com/api/project/osticket-official/{command}';
+
+    function _http_get($url) {
+        #curl post
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_USERAGENT, 'osTicket/'.THIS_VERSION);
+        curl_setopt($ch, CURLOPT_HEADER, FALSE);
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+        $result=curl_exec($ch);
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+
+        return array($code, $result);
+    }
+
+    function _request($command, $args=array()) {
+
+        $url = str_replace('{command}', $command, self::$crowdin_api_url);
+
+        $args += array('key' => $this->key);
+        foreach ($args as &$a)
+            $a = urlencode($a);
+        unset($a);
+        $url .= '?' . Format::array_implode('=', '&', $args);
+
+        return $this->_http_get($url);
+    }
+
+    function run($args, $options) {
+        $this->key = $options['key'];
+        if (!$this->key && defined('CROWDIN_API_KEY'))
+            $this->key = CROWDIN_API_KEY;
+
+        switch (strtolower($args['command'])) {
+        case 'list':
+            if (!$this->key)
+                $this->fail('API key is required');
+            $this->_list();
+            break;
+        case 'build':
+            if (!$this->key)
+                $this->fail('API key is required');
+            if (!$options['lang'])
+                $this->fail('Language code is required. See `list`');
+            $this->_build($options['lang']);
+            break;
+        }
+    }
+
+    function _list() {
+        error_reporting(E_ALL);
+        list($code, $body) = $this->_request('status');
+        $d = new DOMDocument();
+        $d->loadXML($body);
+
+        $xp = new DOMXpath($d);
+        foreach ($xp->query('//language') as $c) {
+            $name = $code = '';
+            foreach ($c->childNodes as $n) {
+                switch (strtolower($n->nodeName)) {
+                case 'name':
+                    $name = $n->textContent;
+                    break;
+                case 'code':
+                    $code = $n->textContent;
+                    break;
+                }
+            }
+            if (!$code)
+                continue;
+            $this->stdout->write(sprintf("%s (%s)\n", $code, $name));
+        }
+    }
+
+    function _build($lang) {
+        list($code, $zip) = $this->_request("download/$lang.zip");
+
+        if ($code !== 200)
+            $this->fail('Language is not available'."\n");
+
+        $temp = tempnam('/tmp', 'osticket-cli');
+        $f = fopen($temp, 'w');
+        fwrite($f, $zip);
+        fclose($f);
+        $zip = new ZipArchive();
+        $zip->open($temp);
+        unlink($temp);
+
+        $lang = str_replace('-','_',$lang);
+        @unlink(I18N_DIR."$lang.phar");
+        $phar = new Phar(I18N_DIR."$lang.phar");
+
+        for ($i=0; $i<$zip->numFiles; $i++) {
+            $info = $zip->statIndex($i);
+            $phar->addFromString($info['name'], $zip->getFromIndex($i));
+        }
+
+        // TODO: Add i18n extras (like fonts)
+
+        // TODO: Sign files
+    }
+}
+
+Module::register('i18n', 'i18n_Compiler');
+?>
diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php
index d57fa0b449095e9bb88a1cb3b9004dd0a1325c5a..3912a3a99a8d66049adb9de64b51b49e837c248d 100644
--- a/setup/inc/class.installer.php
+++ b/setup/inc/class.installer.php
@@ -15,6 +15,7 @@
 **********************************************************************/
 require_once INCLUDE_DIR.'class.migrater.php';
 require_once INCLUDE_DIR.'class.setup.php';
+require_once INCLUDE_DIR.'class.i18n.php';
 
 class Installer extends SetupWizard {
 
@@ -81,9 +82,7 @@ class Installer extends SetupWizard {
 
         // Support port number specified in the hostname with a colon (:)
         list($host, $port) = explode(':', $vars['dbhost']);
-        if ($port && (!is_numeric($port) || !((int)$port)))
-            $this->errors['db'] = 'Database port number must be a number';
-        elseif ($port && ($port < 1 || $port > 65535))
+        if ($port && is_numeric($port) && ($port < 1 || $port > 65535))
             $this->errors['db'] = 'Invalid database port number';
 
         //MYSQL: Connect to the DB and check the version & database (create database if it doesn't exist!)
@@ -149,10 +148,9 @@ class Installer extends SetupWizard {
         }
 
         if(!$this->errors) {
-            // TODO: Use language selected from install worksheet
-            require_once INCLUDE_DIR.'class.i18n.php';
 
-            $i18n = new Internationalization('en_US');
+            // TODO: Use language selected from install worksheet
+            $i18n = new Internationalization($vars['lang_id']);
             $i18n->loadDefaultData();
 
             $sql='SELECT `id` FROM '.PREFIX.'sla ORDER BY `id` LIMIT 1';
diff --git a/setup/inc/install.inc.php b/setup/inc/install.inc.php
index e2d6a5a0f3b87b28f7466a91780b2d5d932956fb..f58943a2fc450022077fd2d1ba59f752b636c66f 100644
--- a/setup/inc/install.inc.php
+++ b/setup/inc/install.inc.php
@@ -1,8 +1,8 @@
-<?php 
+<?php
 if(!defined('SETUPINC')) die('Kwaheri!');
-$info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbhost'=>'localhost');
+$info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbhost'=>'localhost','lang_id'=>'en_US');
 ?>
-<div id="main" class="step2">        
+<div id="main" class="step2">
     <h1>osTicket Basic Installation</h1>
             <p>Please fill out the information below to continue your osTicket installation. All fields are required.</p>
             <font class="error"><strong><?php echo $errors['err']; ?></strong></font>
@@ -26,6 +26,19 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbho
                     <a class="tip" href="#system_email"><i class="icon-question-sign help-tip"></i></a>
                     <font class="error"><?php echo $errors['email']; ?></font>
                 </div>
+                <div class="row">
+                    <label>Default Language:</label>
+<?php $langs = Internationalization::availableLanguages(); ?>
+                <select name="lang_id">
+<?php foreach($langs as $l) {
+    $selected = ($info['lang_id'] == $l['code']) ? 'selected="selected"' : ''; ?>
+                    <option value="<?php echo $l['code']; ?>" <?php echo $selected;
+                        ?>><?php echo $l['desc']; ?></option>
+<?php } ?>
+                </select>
+                <a class="tip" href="#default_lang"><i class="icon-question-sign help-tip"></i></a>
+                <font class="error">&nbsp;<?php echo $errors['lang_id']; ?></font>
+                </div>
 
                 <h4 class="head admin">Admin User</h4>
                 <span class="subhead">Your primary administrator account - you can add more users later.</span>
diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php
index 591700ec50f0ef88f208b141c0347e170ae662c9..a26014a11ad13e4ddad120d22d263c32d092ea91 100644
--- a/setup/test/tests/stubs.php
+++ b/setup/test/tests/stubs.php
@@ -45,6 +45,7 @@ class DomElement {
 
 class DomDocument {
     function loadHTML() {}
+    function loadXML() {}
 }
 
 class Exception {
@@ -83,4 +84,12 @@ class DateTimeZone {
     static function listIdentifiers() {}
 }
 
+class Phar {
+    static function isValidPharFilename() {}
+}
+
+class ZipArchive {
+    function statIndex() {}
+    function getFromIndex() {}
+}
 ?>