diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php
index a9a4117c4b15f59d6914dbd11edf741b1c5544b7..3c044660a670800a7e49f4f23e7804463d00059b 100644
--- a/setup/cli/modules/i18n.php
+++ b/setup/cli/modules/i18n.php
@@ -10,7 +10,8 @@ class i18n_Compiler extends Module {
 
     var $arguments = array(
         "command" => "Action to be performed.
-            list    - Show list of available translations"
+            list    - Show list of available translations
+            make-pot - Build the PO file for gettext translations"
     );
 
     var $options = array(
@@ -69,6 +70,9 @@ class i18n_Compiler extends Module {
                 $this->fail('Language code is required. See `list`');
             $this->_build($options['lang']);
             break;
+        case 'make-pot':
+            $this->_make_pot();
+            break;
         }
     }
 
@@ -124,6 +128,227 @@ class i18n_Compiler extends Module {
 
         // TODO: Sign files
     }
+
+    function __read_next_string($tokens) {
+        $string = array();
+
+        while (list(,$T) = each($tokens)) {
+            switch ($T[0]) {
+                case T_CONSTANT_ENCAPSED_STRING:
+                    // String leading and trailing ' and " chars
+                    $string['form'] = preg_replace(array("`^{$T[1][0]}`","`{$T[1][0]}$`"),array("",""), $T[1]);
+                    $string['line'] = $T[2];
+                    break;
+                case T_DOC_COMMENT:
+                case T_COMMENT:
+                    switch ($T[1][0]) {
+                    case '/':
+                        if ($T[1][1] == '*')
+                            $text = trim($T[1], '/* ');
+                        else
+                            $text = ltrim($T[1], '/ ');
+                        break;
+                    case '#':
+                        $text = ltrim($T[1], '# ');
+                    }
+                    $string['comment'] = $text;
+                    break;
+                case T_WHITESPACE:
+                    // noop
+                    continue;
+                case T_STRING_VARNAME:
+                case T_NUM_STRING:
+                case T_ENCAPSED_AND_WHITESPACE:
+                case '.':
+                    $string['constant'] = false;
+                    break;
+                default:
+                    return array($string, $T);
+            }
+        }
+    }
+    function __read_args($tokens, $constants=1) {
+        $args = array('forms'=>array());
+        $arg = null;
+
+        while (list($string,$T) = $this->__read_next_string($tokens)) {
+            if (count($args['forms']) < $constants && $string) {
+                if (isset($string['constant']) && !$string['constant']) {
+                    throw new Exception($string['form'] . ': Untranslatable string');
+                }
+                $args['forms'][] = $string['form'];
+                $args['line'] = $string['line'];
+                if (isset($string['comment']))
+                    $args['comments'][] = $string['comment'];
+            }
+
+            switch ($T[0]) {
+            case ')':
+                return $args;
+            }
+        }
+    }
+
+    function __get_func_args($tokens, $args) {
+        while (list(,$T) = each($tokens)) {
+            switch ($T[0]) {
+            case T_WHITESPACE:
+                continue;
+            case '(':
+                return $this->__read_args($tokens, $args);
+            default:
+                // Not a function call
+                return false;
+            }
+        }
+    }
+    function __find_strings($tokens, $funcs, $parens=0) {
+        $T_funcs = array();
+        $funcdef = false;
+        while (list(,$T) = each($tokens)) {
+            switch ($T[0]) {
+            case T_STRING:
+                if ($funcdef)
+                    break;;
+                if ($T[1] == 'sprintf') {
+                    foreach ($this->__find_strings($tokens, $funcs) as $i=>$f) {
+                        // Only the first on gets the php-format flag
+                        if ($i == 0)
+                            $f['flags'] = array('php-format');
+                        $T_funcs[] = $f;
+                    }
+                    break;
+                }
+                if (!isset($funcs[$T[1]]))
+                    continue;
+                $constants = $funcs[$T[1]];
+                if ($info = $this->__get_func_args($tokens, $constants))
+                    $T_funcs[] = $info;
+                break;
+            case T_COMMENT:
+            case T_DOC_COMMENT:
+                if (strpos($T[1], '* trans *') !== false) {
+                    // Find the next textual token
+                    list($S, $T) = $this->__read_next_string($tokens);
+                    $string = array('forms'=>array($S['form']), 'line'=>$S['line']);
+                    if (isset($S['comment']))
+                        $string['comments'][] = $S['comment'];
+                    $T_funcs[] = $string;
+                }
+                break;
+            // Track function definitions of the gettext functions
+            case T_FUNCTION:
+                $funcdef = true;
+                break;
+            case '{';
+                $funcdef = false;
+            case '(':
+                $parens++;
+                break;
+            case ')':
+                // End of scope?
+                if (--$parens == 0)
+                    return $T_funcs;
+            }
+        }
+        return $T_funcs;
+    }
+
+    function __write_string($string) {
+        // 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);
+        // 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);
+
+        if (count($strings) > 1)
+            array_unshift($strings, "");
+        foreach ($strings as $line) {
+            print "\"{$line}\"\n";
+        }
+    }
+    function __write_pot_header() {
+        $lines = array(
+            'msgid ""',
+            'msgstr ""',
+            '"Project-Id-Version: osTicket '.trim(`git describe`).'\n"',
+            '"POT-Create-Date: '.date('Y-m-d H:i O').'\n"',
+            '"Report-Msgid-Bugs-To: support@osticket.com\n"',
+            '"Language: en_US\n"',
+            '"MIME-Version: 1.0\n"',
+            '"Content-Type: text/plain; charset=UTF-8\n"',
+            '"Content-Transfer-Encoding: 8bit\n"',
+            '"X-Generator: osTicket i18n CLI\n"',
+        );
+        print implode("\n", $lines);
+        print "\n";
+    }
+    function __write_pot($strings) {
+        $this->__write_pot_header();
+        foreach ($strings as $S) {
+            print "\n";
+            if ($c = @$S['comments']) {
+                foreach ($c as $comment) {
+                    foreach (explode("\n", $comment) as $line) {
+                        $line = trim($line);
+                        print "#. {$line}\n";
+                    }
+                }
+            }
+            foreach ($S['usage'] as $ref) {
+                print "#: ".$ref."\n";
+            }
+            if ($f = @$S['flags']) {
+                print "#, ".implode(', ', $f)."\n";
+            }
+            print "msgid ";
+            $this->__write_string($S['forms'][0]);
+            if (count($S['forms']) == 2) {
+                print "msgid_plural ";
+                $this->__write_string($S['forms'][1]);
+                print 'msgstr[0] ""'."\n";
+                print 'msgstr[1] ""'."\n";
+            }
+            else {
+                print 'msgstr ""'."\n";
+            }
+        }
+    }
+
+    function _make_pot() {
+        error_reporting(E_ALL);
+        $funcs = array('__'=>1, '_N'=>2);
+        function get_osticket_root_path() { return ROOT_DIR; }
+        require_once(ROOT_DIR.'setup/test/tests/class.test.php');
+        $files = Test::getAllScripts();
+        $strings = array();
+        foreach ($files as $f) {
+            $F = str_replace(ROOT_DIR, '', $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 $calls) {
+                if (!($forms = $calls['forms']))
+                    // Transation of non-constant
+                    continue;
+                $primary = $forms[0];
+                if (!isset($strings[$primary])) {
+                    $strings[$primary] = array('forms' => $forms);
+                }
+                $E = &$strings[$primary];
+
+                if (isset($calls['line']))
+                    $E['usage'][] = "{$F}:{$calls['line']}";
+                if (isset($calls['flags']))
+                    $E['flags'] = array_unique(array_merge(@$E['flags'] ?: array(), $calls['flags']));
+                if (isset($calls['comments']))
+                    $E['comments'] = array_merge(@$E['comments'] ?: array(), $calls['comments']);
+            }
+        }
+        $this->__write_pot($strings);
+    }
 }
 
 Module::register('i18n', 'i18n_Compiler');
diff --git a/setup/test/run-tests.php b/setup/test/run-tests.php
index 326f8be18f9ba349b2d908eab241be1f60d817d4..1b641a8926686bf30b33ffb1b1f3edbc384271db 100644
--- a/setup/test/run-tests.php
+++ b/setup/test/run-tests.php
@@ -7,34 +7,11 @@ $selected_test = (isset($argv[1])) ? $argv[1] : false;
 
 require_once "tests/class.test.php";
 
-if (!function_exists('get_osticket_root_path')) {
-    function get_osticket_root_path() {
-        # Hop up to the root folder
-        $start = dirname(__file__);
-        for (;;) {
-            if (file_exists($start . '/main.inc.php')) break;
-            $start .= '/..';
-        }
-        return realpath($start);
-    }
-}
 $root = get_osticket_root_path();
 define('INCLUDE_DIR', "$root/include/");
 define('PEAR_DIR', INCLUDE_DIR."pear/");
 ini_set('include_path', './'.PATH_SEPARATOR.INCLUDE_DIR.PATH_SEPARATOR.PEAR_DIR);
 
-if (!function_exists('glob_recursive')) {
-    # Check PHP syntax across all php files
-    function glob_recursive($pattern, $flags = 0) {
-        $files = glob($pattern, $flags);
-        foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
-            $files = array_merge($files,
-                glob_recursive($dir.'/'.basename($pattern), $flags));
-        }
-        return $files;
-    }
-}
-
 $fails = array();
 
 function show_fails() {
diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php
index a7f6c2fa044da245b2912c15e395755059d351c9..317de6fee8bc14132d331256094b9ca19d971167 100644
--- a/setup/test/tests/class.test.php
+++ b/setup/test/tests/class.test.php
@@ -5,7 +5,7 @@ class Test {
     var $warnings = array();
     var $name = "";
 
-    var $third_party_paths = array(
+    static $third_party_paths = array(
         '/include/JSON.php',
         '/include/htmLawed.php',
         '/include/PasswordHash.php',
@@ -17,9 +17,6 @@ class Test {
         '/include/mpdf/',
     );
 
-    function Test() {
-        call_user_func_array(array($this, '__construct'), func_get_args());
-    }
     function __construct() {
         assert_options(ASSERT_CALLBACK, array($this, 'fail'));
         error_reporting(E_ALL & ~E_WARNING);
@@ -31,14 +28,13 @@ class Test {
     function teardown() {
     }
 
-    /*static*/
-    function getAllScripts($excludes=true) {
+    static function getAllScripts($excludes=true) {
         $root = get_osticket_root_path();
         $scripts = array();
         foreach (glob_recursive("$root/*.php") as $s) {
             $found = false;
             if ($excludes) {
-                foreach ($this->third_party_paths as $p) {
+                foreach (self::$third_party_paths as $p) {
                     if (strpos($s, $p) !== false) {
                         $found = true;
                         break;
@@ -105,4 +101,28 @@ class Test {
         return $line;
     }
 }
+
+if (!function_exists('glob_recursive')) {
+    # Check PHP syntax across all php files
+    function glob_recursive($pattern, $flags = 0) {
+        $files = glob($pattern, $flags);
+        foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+            $files = array_merge($files,
+                glob_recursive($dir.'/'.basename($pattern), $flags));
+        }
+        return $files;
+    }
+}
+
+if (!function_exists('get_osticket_root_path')) {
+    function get_osticket_root_path() {
+        # Hop up to the root folder
+        $start = dirname(__file__);
+        for (;;) {
+            if (file_exists($start . '/main.inc.php')) break;
+            $start .= '/..';
+        }
+        return realpath($start);
+    }
+}
 ?>