diff --git a/include/class.i18n.php b/include/class.i18n.php
index c023566d90f0fc7ad154c91d004f113c232c7c51..160de51a84045aebe1d22f15f1b007b868c1f52b 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -362,9 +362,9 @@ class Internationalization {
             return TextDomain::lookup()->getTranslation($locale)
                 ->translate($msgid);
         }
-        function _NL($msgid, $plural, $count, $locale) {
+        function _NL($msgid, $plural, $n, $locale) {
             return TextDomain::lookup()->getTranslation($locale)
-                ->ngettext($msgid);
+                ->ngettext($msgid, $plural, $n);
         }
     }
 }
diff --git a/include/class.plugin.php b/include/class.plugin.php
index 8d09c7713f425446cd389fefd775e3c20103630d..5a29ce483be7aa0797165d8430ac70a25d33fb89 100644
--- a/include/class.plugin.php
+++ b/include/class.plugin.php
@@ -333,10 +333,11 @@ abstract class Plugin {
     }
 
     function getId() { return $this->id; }
-    function getName() { return $this->info['name']; }
+    function getName() { return $this->__($this->info['name']); }
     function isActive() { return $this->ht['isactive']; }
     function isPhar() { return $this->ht['isphar']; }
     function getInstallDate() { return $this->ht['installed']; }
+    function getInstallPath() { return $this->ht['install_path']; }
 
     function getIncludePath() {
         return realpath(INCLUDE_DIR . $this->info['install_path'] . '/'
@@ -446,7 +447,8 @@ abstract class Plugin {
      * `7afc8bf80b0555bed88823306744258d6030f0d9 `, which will resolve to a
      * string like the following:
      * ```
-     * "v=1; i=storage:s3; s=MEUCIFw6A489eX4Oq17BflxCZ8+MH6miNjtcpScUoKDjmblsAiEAjiBo9FzYtV3WQtW6sbhPlJXcoPpDfYyQB+BFVBMps4c=; V=0.1;"
+     * "v=1; i=storage:s3; s=MEUCIFw6A489eX4Oq17BflxCZ8+MH6miNjtcpScUoKDjmb
+     * lsAiEAjiBo9FzYtV3WQtW6sbhPlJXcoPpDfYyQB+BFVBMps4c=; V=0.1;"
      * ```
      * Which is a simple semicolon separated key-value pair string with the
      * following keys
@@ -554,6 +556,98 @@ abstract class Plugin {
 <?php       break;
         }
     }
+
+    /**
+     * Function: __
+     *
+     * Translate a single string (without plural alternatives) from the
+     * langauge pack installed in this plugin. The domain is auto-configured
+     * and detected from the plugin install path.
+     */
+    function __($msgid) {
+        if (!isset($this->translation)) {
+            // Detect the domain from the plugin install-path
+            $groups = array();
+            preg_match('`plugins/(\w+)(?:.phar)?`', $this->getInstallPath(), $groups);
+
+            $domain = $groups[1];
+            if (!$domain)
+                return $msgid;
+
+            $this->translation = self::translate($domain);
+        }
+        list($__, $_N) = $this->translation;
+        return $__($msgid);
+    }
+
+    // Domain-specific translations (plugins)
+    /**
+     * Function: translate
+     *
+     * Convenience function to setup translation functions for other
+     * domains. This is of greatest benefit for plugins. This will return
+     * two functions to perform the translations. The first will translate a
+     * single string, the second will translate a plural string.
+     *
+     * Parameters:
+     * $domain - (string) text domain. The location of the MO.php file
+     *      will be (path)/LC_MESSAGES/(locale)/(domain).mo.php. The (path)
+     *      can be set via the $options parameter
+     * $options - (array<string:mixed>) Extra options for the setup
+     *      "path" - (string) path to the folder containing the LC_MESSAGES
+     *          folder. The (locale) setting is set externally respective to
+     *          the user. If this is not set, the directory of the caller is
+     *          assumed, plus '/i18n'.  This is geared for plugins to be
+     *          built with i18n content inside the '/i18n/' folder.
+     *
+     * Returns:
+     * Translation utility functions which mimic the __() and _N()
+     * functions. Note that two functions are returned. Capture them with a
+     * PHP list() construct.
+     *
+     * Caveats:
+     * When desiging plugins which might be installed in versions of
+     * osTicket which don't provide this function, use this compatibility
+     * interface:
+     *
+     * // Provide compatibility function for versions of osTicket prior to
+     * // translation support (v1.9.4)
+     * function translate($domain) {
+     *     if (!method_exists('Plugin', 'translate')) {
+     *         return array(
+     *             function($x) { return $x; },
+     *             function($x, $y, $n) { return $n != 1 ? $y : $x; },
+     *         );
+     *     }
+     *     return Plugin::translate($domain);
+     * }
+     */
+    static function translate($domain, $options=array()) {
+
+        // Configure the path for the domain. If no
+        $path = @$options['path'];
+        if (!$path) {
+            # Fetch the working path of the caller
+            $bt = debug_backtrace(false);
+            $path = dirname($bt[0]["file"]) . '/i18n';
+        }
+        $path = rtrim($path, '/') . '/';
+
+        $D = TextDomain::lookup($domain);
+        $D->setPath($path);
+        $trans = $D->getTranslation();
+
+        return array(
+            // __()
+            function($msgid) use ($trans) {
+                return $trans->translate($msgid);
+            },
+            // _N()
+            function($singular, $plural, $n) use ($trans) {
+                return $trans->ngettext($singular, $plural, $n);
+            },
+        );
+    }
 }
 
 ?>
diff --git a/include/class.translation.php b/include/class.translation.php
index 9a093dcd17e66d955a27dbae951f2cd18336bb53..06d25b05f839ecfa1682a29a4d894c872d84c6cc 100644
--- a/include/class.translation.php
+++ b/include/class.translation.php
@@ -573,7 +573,7 @@ class Translation extends gettext_reader {
         return Format::encode($string, 'utf-8', $this->charset);
     }
 
-    static function buildHashFile($mofile, $outfile=false) {
+    static function buildHashFile($mofile, $outfile=false, $return=false) {
         if (!$outfile) {
             $stream = fopen('php://stdout', 'w');
         }
@@ -638,7 +638,11 @@ class Translation extends gettext_reader {
         );
 
         // Serialize the PHP array and write to output
-        fwrite($stream, sprintf('<?php return %s;', var_export($table, true)));
+        $contents = sprintf('<?php return %s;', var_export($table, true));
+        if ($return)
+            return $contents;
+        else
+            fwrite($stream, $contents);
     }
 }
 
@@ -694,10 +698,12 @@ class TextDomain {
             $locale_names = self::get_list_of_locales($locale);
             $input = null;
             foreach ($locale_names as $T) {
-                $phar_path = 'phar://' . $bound_path . $T . ".phar/" . $subpath;
-                if (file_exists($phar_path)) {
-                    $input = $phar_path;
-                    break;
+                if (substr($bound_path, 7) != 'phar://') {
+                    $phar_path = 'phar://' . $bound_path . $T . ".phar/" . $subpath;
+                    if (file_exists($phar_path)) {
+                        $input = $phar_path;
+                        break;
+                    }
                 }
                 $full_path = $bound_path . $T . "/" . $subpath;
                 if (file_exists($full_path)) {
diff --git a/include/class.upgrader.php b/include/class.upgrader.php
index 3dad00b6a34e5ccc1a4bb27026f105aedf31b9ed..6246e542d0b524b3dec26364297a506d9aab18ce 100644
--- a/include/class.upgrader.php
+++ b/include/class.upgrader.php
@@ -406,7 +406,7 @@ class StreamUpgrader extends SetupWizard {
             $shash = substr($phash, 9, 8);
 
             //Log the patch info
-            $logMsg = sprintf(_S("Patch %s applied successfully "), $phash);
+            $logMsg = sprintf(_S("Patch %s applied successfully"), $phash);
             if(($info = $this->readPatchInfo($patch)) && $info['version'])
                 $logMsg.= ' ('.$info['version'].') ';
 
diff --git a/include/client/register.inc.php b/include/client/register.inc.php
index 44f9fa22b5c3508228b74cad971e411c8c90aa0c..78b42ac9474fc251c3e937f5989677cbc2a70a3e 100644
--- a/include/client/register.inc.php
+++ b/include/client/register.inc.php
@@ -79,7 +79,7 @@ $info = Format::htmlchars(($errors && $_POST)?$_POST:$info);
         <input type="hidden" name="username" value="<?php echo $info['username']; ?>"/>
 <?php foreach (UserAuthenticationBackend::allRegistered() as $bk) {
     if ($bk::$id == $info['backend']) {
-        echo $bk::$name;
+        echo $bk->getName();
         break;
     }
 } ?>
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 8b73bb557a8d401b200a4e47b78cd237afe6bbb6..1dd7d105ada2a28c76b917221460563b6604c9f7 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -147,7 +147,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <option value="<?php echo $ab::$id; ?>" <?php
                     if ($info['backend'] == $ab::$id)
                         echo 'selected="selected"'; ?>><?php
-                    echo $ab::$name; ?></option>
+                    echo $ab->getName(); ?></option>
             <?php } ?>
             </select>
             </td>
diff --git a/include/staff/templates/user-register.tmpl.php b/include/staff/templates/user-register.tmpl.php
index 1af7e4af89c8cb686894367b4274847f66c31475..5834256d61ed7b53d94838b56918ee964609513c 100644
--- a/include/staff/templates/user-register.tmpl.php
+++ b/include/staff/templates/user-register.tmpl.php
@@ -65,7 +65,7 @@ echo sprintf(__(
                 <option value="<?php echo $ab::$id; ?>" <?php
                     if ($info['backend'] == $ab::$id)
                         echo 'selected="selected"'; ?>><?php
-                    echo $ab::$name; ?></option>
+                    echo $ab->getName(); ?></option>
             <?php } ?>
             </select>
                 </td>
diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php
index a863e5349c05f3d83f88a0e9da9a35eef61a902f..1adab5370afa5db6bacc16435d069c650de91f37 100644
--- a/setup/cli/modules/i18n.php
+++ b/setup/cli/modules/i18n.php
@@ -30,9 +30,15 @@ class i18n_Compiler extends Module {
             'help' => "Language pack to be signed"),
         'pkey' => array('-P', '--pkey', 'metavar'=>'key-file',
             'help' => 'Private key for signing'),
+        'root' => array('-R', '--root', 'matavar'=>'path',
+            'help' => 'Specify a root folder for `make-pot`'),
+        'domain' => array('-D', '--domain', 'metavar'=>'name',
+            'default' => '',
+            'help' => 'Add a domain to the path/context of PO strings'),
     );
 
-    static $crowdin_api_url = 'http://i18n.osticket.com/api/project/osticket-official/{command}';
+    static $project = 'osticket-official';
+    static $crowdin_api_url = 'http://i18n.osticket.com/api/project/{project}/{command}';
 
     function _http_get($url) {
         #curl post
@@ -51,7 +57,9 @@ class i18n_Compiler extends Module {
 
     function _request($command, $args=array()) {
 
-        $url = str_replace('{command}', $command, self::$crowdin_api_url);
+        $url = str_replace(array('{command}', '{project}'),
+            array($command, self::$project),
+            self::$crowdin_api_url);
 
         $args += array('key' => $this->key);
         foreach ($args as &$a)
@@ -84,7 +92,7 @@ class i18n_Compiler extends Module {
             $this->_build($options['lang']);
             break;
         case 'make-pot':
-            $this->_make_pot();
+            $this->_make_pot($options);
             break;
         case 'sign':
             if (!$options['file'] || !file_exists($options['file']))
@@ -196,17 +204,13 @@ class i18n_Compiler extends Module {
             if (is_resource($msgfmt)) {
                 fwrite($pipes[0], $po_file);
                 fclose($pipes[0]);
-                $mo_output = fopen('php://temp', 'r+b');
                 $mo_input = fopen('php://temp', 'r+b');
                 fwrite($mo_input, stream_get_contents($pipes[1]));
                 rewind($mo_input);
                 require_once INCLUDE_DIR . 'class.translation.php';
-                Translation::buildHashFile($mo_input, $mo_output);
-                rewind($mo_output);
-                $mo = stream_get_contents($mo_output);
+                $mo = Translation::buildHashFile($mo_input, false, true);
                 $phar->addFromString('LC_MESSAGES/messages.mo.php', $mo);
                 fclose($mo_input);
-                fclose($mo_output);
             }
         }
 
@@ -379,8 +383,9 @@ class i18n_Compiler extends Module {
         while (list(,$T) = each($tokens)) {
             switch ($T[0]) {
             case T_STRING:
+            case T_VARIABLE:
                 if ($funcdef)
-                    break;;
+                    break;
                 if ($T[1] == 'sprintf') {
                     foreach ($this->__find_strings($tokens, $funcs) as $i=>$f) {
                         // Only the first on gets the php-format flag
@@ -430,11 +435,11 @@ class i18n_Compiler extends Module {
         // Unescape single quote (') and escape unescaped double quotes (")
         $string = preg_replace(array("`\\\(['$])`", '`(?<!\\\)"`'), array("$1", '\"'), $string);
         // Preserve embedded newlines
-        $string = str_replace("\n", "\\n\n", $string);
+        $string = preg_replace("`\n\s*`", "\\n\n", $string);
         // Word-wrap long lines
         $string = rtrim(preg_replace('/(?=[\s\p{Ps}])(.{1,76})(\s|$|(\p{Ps}))/uS',
             "$1$2\n", $string), "\n");
-        $strings = explode("\n", $string);
+        $strings = array_filter(explode("\n", $string));
 
         if (count($strings) > 1)
             array_unshift($strings, "");
@@ -494,29 +499,33 @@ class i18n_Compiler extends Module {
         }
     }
 
-    function _make_pot() {
+    function _make_pot($options) {
         error_reporting(E_ALL);
         $funcs = array(
             '__'    => array('forms'=>1),
+            '$__'   => array('forms'=>1),
             '_S'    => array('forms'=>1),
             '_N'    => array('forms'=>2),
+            '$_N'   => array('forms'=>2),
             '_NS'   => array('forms'=>2),
             '_P'    => array('context'=>1, 'forms'=>1),
             '_NP'   => array('context'=>1, 'forms'=>2),
             // This is an error
             '_'     => array('forms'=>0),
         );
-        $files = Test::getAllScripts();
+        $root = realpath($options['root'] ?: ROOT_DIR);
+        $domain = $options['domain'] ? '('.$options['domain'].')/' : '';
+        $files = Test::getAllScripts(true, $root);
         $strings = array();
         foreach ($files as $f) {
-            $F = str_replace(ROOT_DIR, '', $f);
+            $F = str_replace($root.'/', $domain, $f);
             $this->stderr->write("$F\n");
             $tokens = new ArrayObject(token_get_all(fread(fopen($f, 'r'), filesize($f))));
             foreach ($this->__find_strings($tokens, $funcs, 1) as $call) {
                 self::__addString($strings, $call, $F);
             }
         }
-        $strings = array_merge($strings, $this->__getAllJsPhrases());
+        $strings = array_merge($strings, $this->__getAllJsPhrases($root));
         $this->__write_pot($strings);
     }
 
@@ -544,12 +553,12 @@ class i18n_Compiler extends Module {
             $E['context'] = $call['context'];
     }
 
-    function __getAllJsPhrases() {
+    function __getAllJsPhrases($root=ROOT_DIR) {
         $strings = array();
         $funcs = array('__'=>array('forms'=>1));
-        foreach (glob_recursive(ROOT_DIR . "*.js") as $s) {
+        foreach (glob_recursive($root . "*.js") as $s) {
             $script = file_get_contents($s);
-            $s = str_replace(ROOT_DIR, '', $s);
+            $s = str_replace($root, '', $s);
             $this->stderr->write($s."\n");
             $calls = array();
             preg_match_all('/__\(\s*[^\'"]*(([\'"])(?:(?<!\\\\)\2|.)+\2)\s*[^)]*\)/',
diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php
index 317de6fee8bc14132d331256094b9ca19d971167..5dd6097dcb127a1a3391d36361872ed0e6b5dec2 100644
--- a/setup/test/tests/class.test.php
+++ b/setup/test/tests/class.test.php
@@ -15,6 +15,9 @@ class Test {
         '/include/plugins/',
         '/include/h2o/',
         '/include/mpdf/',
+
+        # Includes in the core-plugins project
+        '/lib/',
     );
 
     function __construct() {
@@ -28,8 +31,8 @@ class Test {
     function teardown() {
     }
 
-    static function getAllScripts($excludes=true) {
-        $root = get_osticket_root_path();
+    static function getAllScripts($excludes=true, $root=false) {
+        $root = $root ?: get_osticket_root_path();
         $scripts = array();
         foreach (glob_recursive("$root/*.php") as $s) {
             $found = false;