diff --git a/bin/browserid b/bin/browserid
index 225526ab27a818edbc3f7545e0f216b088c5573f..f3399bb57408478b01731f1f6993375150f544b3 100755
--- a/bin/browserid
+++ b/bin/browserid
@@ -61,7 +61,7 @@ app.use(i18n.abide({
   supported_languages: config.get('supported_languages'),
   default_lang: config.get('default_lang'),
   debug_lang: config.get('debug_lang'),
-  locale_directory: config.get('locale_directory'),
+  translation_directory: config.get('translation_directory'),
   disable_locale_check: config.get('disable_locale_check')
 }));
 
diff --git a/bin/static b/bin/static
index 47af95441d6858387a0bfa09918fef5c124bdc52..d9f3894084f03958960848d9ffcc60ec085f09ef 100755
--- a/bin/static
+++ b/bin/static
@@ -53,7 +53,7 @@ app.use(i18n.abide({
   supported_languages: config.get('supported_languages'),
   default_lang: config.get('default_lang'),
   debug_lang: config.get('debug_lang'),
-  locale_directory: config.get('locale_directory'),
+  translation_directory: config.get('translation_directory'),
   disable_locale_check: config.get('disable_locale_check')
 }));
 
diff --git a/bin/verifier b/bin/verifier
index 5596047ee2b6b86f6edc65a65361a4ec08f3685f..b891002b1d9f13d062fc495212b8adc37035124d 100755
--- a/bin/verifier
+++ b/bin/verifier
@@ -104,9 +104,11 @@ function doVerification(req, resp, next) {
     else if (!r || !r.success) err = "no response returned from child process";
 
     if (err) {
+      statsd.increment("assertion_failure");
       resp.json({"status":"failure", reason: err});  //Could be 500 or 200 OK if invalid cert
       metrics.report('verify', {
         result: 'failure',
+        reason: err,
         rp: audience
       });
     } else {
diff --git a/config/aws.json b/config/aws.json
index c944c2e28d64c4d0f930b044c504fc6e711b6806..336f5ed2ab338b43791738d08bb8ac660e7944cf 100644
--- a/config/aws.json
+++ b/config/aws.json
@@ -10,9 +10,9 @@
   // are inverted and reversed), and en-US.
   // This set can be overridden by adding more to config.json on the VM.
   "supported_languages": [
-    "en-US"
+    "en-US", "it-CH"
   ],
-  "locale_directory": "/home/app/code/locale",
+  "debug_lang": "it-CH",
   "var_path": "/home/app/var",
 
   "http_proxy": {
diff --git a/config/production.json b/config/production.json
index 61ca34a2a499aa4d2bd10256951c9e2092498665..024fab21e8d347502287b96b617d488d2b8989dc 100644
--- a/config/production.json
+++ b/config/production.json
@@ -33,7 +33,6 @@
   ],
   "debug_lang": "it-CH",
   // locale directory should be overridden
-  "locale_directory": "/home/app/code/locale",
   "express_log_format": "default_bid",
   "email_to_console": false,
   // var path should be overridded
diff --git a/docs/TESTING.md b/docs/TESTING.md
index e052624a3fdda0df31ef32317101ebcb242ff0a8..5f0c74624c8b3d151c3d36223ac30825b5b05c72 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -35,6 +35,19 @@ create tables.  You can then run the mysql suite with, e.g.,
 NODE_ENV=test_mysql MYSQL_USER=browserid MYSQL_PASSWORD=browserid npm test
 ```
 
+#### Initial MySQL setup
+
+The following will create a database user with enough privileges:
+
+    $ mysql -uroot -p
+    > CREATE USER 'browserid'@'localhost' IDENTIFIED BY 'browserid';
+    > GRANT ALL ON *.* TO 'browserid'@'localhost';
+    > FLUSH PRIVILEGES;
+
+If you need to reset the MySQL root password on a Debian system, you'll need to do something like:
+
+    $ sudo dpkg-reconfigure -plow mysql-server-5.5
+
 ### Test Suites
 
 There are two test suites:
diff --git a/lib/configuration.js b/lib/configuration.js
index ef4d97f42e2609563a792783337bbc04f2697663..5d82f914db91f8b44aa7fc2cc4a543cec2de0a1d 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -185,14 +185,18 @@ var conf = module.exports = convict({
   debug_lang: 'string = "it-CH"',
   supported_languages: {
     doc: "List of languages this deployment should detect and display localized strings.",
-    format: 'array { string }* = [ "en-US" ]',
+    format: 'array { string }* = [ "en-US", "it-CH" ]',
     env: 'SUPPORTED_LANGUAGES'
   },
   disable_locale_check: {
     doc: "Skip checking for gettext .mo files for supported locales",
     format: 'boolean = false'
   },
-  locale_directory: 'string = "locale"',
+  translation_directory: {
+    doc: "The directory where per-locale .json files containing translations reside",
+    format: 'string = "resources/static/i18n/"',
+    env: "TRANSLATION_DIR"
+  },
   express_log_format: 'string [ "default_bid", "dev_bid", "default", "dev", "short", "tiny" ] = "default"',
   keysigner_url: {
     format: 'string?',
@@ -223,6 +227,11 @@ var conf = module.exports = convict({
   enable_development_menu: {
     doc: "Whether or not the development menu can be accessed",
     format: 'boolean = false'
+  },
+  proxy_idps: {
+    doc: "A mapping of domain names to urls, which maps popular email services to shimmed IDP deployments.",
+    format: 'object { } *?',
+    env: 'PROXY_IDPS' // JSON text, i.e. {"yahoo.com":"yahoo.login.persona.org"}
   }
 });
 
@@ -256,6 +265,11 @@ if (process.env['CONFIG_FILES']) {
   });
 }
 
+// allow supported langauges to be specified in the env as a CSV string
+if (process.env['SUPPORTED_LANGUAGES']) {
+  conf.set('supported_languages', process.env['SUPPORTED_LANGUAGES'].split(','));
+}
+
 // special handling of HTTP_PROXY env var
 if (process.env['HTTP_PROXY']) {
   var p = process.env['HTTP_PROXY'].split(':');
@@ -263,6 +277,11 @@ if (process.env['HTTP_PROXY']) {
   conf.set('http_proxy.port', p[1]);
 }
 
+// special handling of PROXY_IDPS env var
+if (process.env['PROXY_IDPS']) {
+  conf.set('proxy_idps', JSON.parse(process.env['PROXY_IDPS']));
+}
+
 // set the 'scheme' of the server based on the public_url (which is needed for
 // things like
 conf.set('scheme', urlparse(conf.get('public_url')).scheme);
diff --git a/lib/i18n.js b/lib/i18n.js
index 145365aea0bdfae53db1bc839e17fb568e39bf97..7989c58936869bacb9536ce6a67473c9b3a2735d 100644
--- a/lib/i18n.js
+++ b/lib/i18n.js
@@ -18,7 +18,8 @@
 var logger = require('./logging.js').logger,
     path = require('path'),
     util = require('util'),
-    fs = require('fs');
+    fs = require('fs'),
+    gobbledygook = require('gobbledygook');
 
 // existsSync moved from path in 0.6.x to fs in 0.8.x
 if (typeof fs.existsSync === 'function') {
@@ -27,7 +28,7 @@ if (typeof fs.existsSync === 'function') {
   var existsSync = path.existsSync;
 }
 
-const BIDI_RTL_LANGS = ['ar', 'db-LB', 'fa', 'he'];
+const BIDI_RTL_LANGS = ['ar', 'fa', 'he'];
 
 var translations = {};
 
@@ -38,33 +39,29 @@ var translations = {};
   app.use(i18n.abide({
     supported_languages: ['en-US', 'fr', 'pl'],
     default_lang: 'en-US',
-    locale_directory: 'locale'
   }));
  *
  * Other valid options: gettext_alias, ngettext_alias
  */
 exports.abide = function (options) {
-
-  if (! options.gettext_alias)        options.gettext_alias = 'gettext';
-  if (! options.ngettext_alias)       options.ngettext_alias = 'ngettext';
-  if (! options.supported_languages)  options.supported_languages = ['en-US'];
-  if (! options.default_lang)         options.default_lang = 'en-US';
-  if (! options.debug_lang)           options.debug_lang = 'it-CH';
-  if (! options.disable_locale_check) options.disable_locale_check = false;
-  if (! options.locale_directory)     options.locale_directory = 'locale';
-  if (! options.i18n_json_dir)        options.i18n_json_dir = 'resources/static/i18n/';
+  if (! options.gettext_alias)          options.gettext_alias = 'gettext';
+  if (! options.supported_languages)    options.supported_languages = ['en-US'];
+  if (! options.default_lang)           options.default_lang = 'en-US';
+  if (! options.debug_lang)             options.debug_lang = 'it-CH';
+  if (! options.disable_locale_check)   options.disable_locale_check = false;
+  if (! options.translation_directory)  options.i18n_json_dir = 'l10n/';
 
   var json_dir = path.resolve(
           path.join(__dirname, '..'),
-          path.join(options.i18n_json_dir));
+          path.join(options.translation_directory));
 
   var debug_locale = localeFrom(options.debug_lang);
 
   options.supported_languages.forEach(function (lang, i) {
-    var l = (lang === options.debug_lang ? 'db_LB' : localeFrom(lang));
+    // ignore .json files for default and debug languages
+    if (options.default_lang == lang || options.debug_lang == lang) return;
 
-    // ignore .json files for en-US
-    if (lang == 'en-US') return;
+    var l = localeFrom(lang);
 
     try {
       // populate the in-memory translation cache with client.json, which contains
@@ -101,14 +98,10 @@ exports.abide = function (options) {
         debug_lang = options.debug_lang.toLowerCase(),
         locale;
 
-    if (lang && lang.toLowerCase && lang.toLowerCase() == debug_lang) {
-        lang = 'db-LB'; // What? http://www.youtube.com/watch?v=rJLnGjhPT1Q
-    }
-
     resp.local('lang', lang);
 
     // BIDI support, which direction does text flow?
-    lang_dir = BIDI_RTL_LANGS.indexOf(lang) >= 0 ? 'rtl' : 'ltr';
+    lang_dir = ((BIDI_RTL_LANGS.indexOf(lang) >= 0) || debug_lang == lang.toLowerCase()) ? 'rtl' : 'ltr';
     resp.local('lang_dir', lang_dir);
     req.lang = lang;
 
@@ -122,9 +115,15 @@ exports.abide = function (options) {
 
     var gt;
 
-    if (translations[locale]) {
+    if (lang.toLowerCase() === debug_lang) {
+      gt = gobbledygook;
+      resp.local('lang', 'db-LB');
+    } else if (translations[locale]) {
       gt = function(sid) {
-        return (translations[locale][sid] ? translations[locale][sid][1] : sid);
+        if (translations[locale][sid] && translations[locale][sid][1].length) {
+          sid = translations[locale][sid][1];
+        }
+        return sid;
       };
     } else {
       gt = function(a) { return a; }
diff --git a/lib/primary.js b/lib/primary.js
index 37c3fc809ff4aeafc3cbba888b8d548c8ba538c4..f89ba0111737cc46a4e43a0a3577b7c1ae3a8d38 100644
--- a/lib/primary.js
+++ b/lib/primary.js
@@ -104,8 +104,6 @@ function parseWellKnownBody(body, domain, delegates, cb) {
   });
 }
 
-
-
 // Support "shimmed primaries" for local development.  That is an environment variable that is any number of
 // CSV values of the form:
 //  <domain>|<origin>|<path to .well-known/browserid>,
@@ -131,14 +129,32 @@ if (process.env['SHIMMED_PRIMARIES']) {
 }
 
 var getWellKnown = function (domain, delegates, cb) {
+  // called when we fail to fetch well-known.  Looks in configuration for proxyidp
+  // configuration, if that exists, it's as if a delegation of authority existed.
+  function handleProxyIDP() {
+    if (config.has('proxy_idps')) {
+      var proxyIDPs = config.get('proxy_idps');
+      if (proxyIDPs.hasOwnProperty(domain)) {
+        var generatedBody = JSON.stringify({
+          authority: proxyIDPs[domain]
+        });
+        cb(null, generatedBody, domain, delegates);
+      } else {
+        cb(null, false, null);
+      }
+    } else {
+      cb(null, false, null);
+    }
+  }
+
   function handleResponse(res) {
     if (res.statusCode !== 200) {
       logger.debug(domain + ' is not a browserid primary - non-200 response code to ' + WELL_KNOWN_URL);
-      return cb(null, false, null);
+      return handleProxyIDP();
     }
     if (res.headers['content-type'].indexOf('application/json') !== 0) {
       logger.debug(domain + ' is not a browserid primary - non "application/json" response to ' + WELL_KNOWN_URL);
-      return cb(null, false, null);
+      return handleProxyIDP();
     }
 
     var body = "";
@@ -178,7 +194,7 @@ var getWellKnown = function (domain, delegates, cb) {
 
   req.on('error', function(e) {
     logger.debug(domain + ' is not a browserid primary: ' + e.toString());
-    cb(null, false, null);
+    handleProxyIDP();
   });
 };
 
@@ -198,7 +214,6 @@ exports.checkSupport = function(domain, cb, delegates) {
     return process.nextTick(function() { cb("invalid domain"); });
   }
 
-
   getWellKnown(domain, delegates, function (err, body, domain, cbdelegates) {
     if (err) {
       logger.debug(err);
diff --git a/lib/static/views.js b/lib/static/views.js
index 65fe37fa4b2007b9fa2ff2d3fbcebac9a2f129eb..e3779900b728bb6c04f5f1d66f3e0e4851fa9855 100644
--- a/lib/static/views.js
+++ b/lib/static/views.js
@@ -18,6 +18,9 @@ version = require('../version');
 
 require("jwcrypto/lib/algs/rs");
 
+// the underbar decorator to allow getext to extract strings
+function _(str) { return str; }
+
 // all templated content, redirects, and renames are handled here.
 // anything that is not an api, and not static
 const
@@ -97,7 +100,7 @@ exports.setup = function(app) {
   app.get('/sign_in', function(req, res, next ) {
     metrics.userEntry(req);
     renderCachableView(req, res, 'dialog.ejs', {
-      title: 'A Better Way to Sign In',
+      title: _('A Better Way to Sign In'),
       layout: 'dialog_layout.ejs',
       useJavascript: true,
       production: config.get('use_minified_resources')
@@ -113,11 +116,19 @@ exports.setup = function(app) {
   });
 
   app.get("/unsupported_dialog", function(req,res) {
-    renderCachableView(req, res, 'unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
+    renderCachableView(req, res, 'unsupported_dialog.ejs', {
+      title: _('Unsupported Browser'),
+      layout: 'dialog_layout.ejs',
+      useJavascript: false
+    });
   });
 
   app.get("/cookies_disabled", function(req,res) {
-    renderCachableView(req, res, 'cookies_disabled.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
+    renderCachableView(req, res, 'cookies_disabled.ejs', {
+      title: _('Cookies Are Disabled'),
+      layout: 'dialog_layout.ejs',
+      useJavascript: false
+    });
   });
 
   // Used for a relay page for communication.
@@ -133,16 +144,16 @@ exports.setup = function(app) {
   });
 
   app.get('/', function(req,res) {
-    renderCachableView(req, res, 'index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
+    renderCachableView(req, res, 'index.ejs', {title: _('A Better Way to Sign In'), fullpage: true});
   });
 
   app.get("/signup", function(req, res) {
-    renderCachableView(req, res, 'signup.ejs', {title: 'Sign Up', fullpage: false});
+    renderCachableView(req, res, 'signup.ejs', {title: _('Sign Up'), fullpage: false});
   });
 
   app.get("/idp_auth_complete", function(req, res) {
     renderCachableView(req, res, 'idp_auth_complete.ejs', {
-      title: 'Sign In Complete',
+      title: _('Sign In Complete'),
       fullpage: false
     });
   });
@@ -150,32 +161,32 @@ exports.setup = function(app) {
   app.get("/forgot", function(req, res) {
     res.local('util', util);
     renderCachableView(req, res, 'forgot.ejs', {
-      title: 'Forgot Password',
+      title: _('Forgot Password'),
       fullpage: false,
       enable_development_menu: config.get('enable_development_menu')
     });
   });
 
   app.get("/signin", function(req, res) {
-    renderCachableView(req, res, 'signin.ejs', {title: 'Sign In', fullpage: false});
+    renderCachableView(req, res, 'signin.ejs', {title: _('Sign In'), fullpage: false});
   });
 
   app.get("/about", function(req, res) {
-    renderCachableView(req, res, 'about.ejs', {title: 'About', fullpage: false});
+    renderCachableView(req, res, 'about.ejs', {title: _('About'), fullpage: false});
   });
 
   app.get("/tos", function(req, res) {
-    renderCachableView(req, res, 'tos.ejs', {title: 'Terms of Service', fullpage: false});
+    renderCachableView(req, res, 'tos.ejs', {title: _('Terms of Service'), fullpage: false});
   });
 
   app.get("/privacy", function(req, res) {
-    renderCachableView(req, res, 'privacy.ejs', {title: 'Privacy Policy', fullpage: false});
+    renderCachableView(req, res, 'privacy.ejs', {title: _('Privacy Policy'), fullpage: false});
   });
 
   app.get("/verify_email_address", function(req, res) {
     res.local('util', util);
     renderCachableView(req, res, 'verify_email_address.ejs', {
-      title: 'Complete Registration',
+      title: _('Complete Registration'),
       fullpage: true,
       enable_development_menu: config.get('enable_development_menu')
     });
@@ -184,16 +195,16 @@ exports.setup = function(app) {
   // This page can be removed a couple weeks after this code ships into production,
   // we're leaving it here to not break outstanding emails
   app.get("/add_email_address", function(req,res) {
-    renderCachableView(req, res, 'confirm.ejs', {title: 'Verify Email Address', fullpage: false});
+    renderCachableView(req, res, 'confirm.ejs', {title: _('Verify Email Address'), fullpage: false});
   });
 
 
   app.get("/reset_password", function(req,res) {
-    renderCachableView(req, res, 'confirm.ejs', {title: 'Reset Password'});
+    renderCachableView(req, res, 'confirm.ejs', {title: _('Reset Password')});
   });
 
   app.get("/confirm", function(req,res) {
-    renderCachableView(req, res, 'confirm.ejs', {title: 'Confirm Email'});
+    renderCachableView(req, res, 'confirm.ejs', {title: _('Confirm Email')});
   });
 
 
@@ -204,6 +215,15 @@ exports.setup = function(app) {
     app.get(/^\/test\/(?:index.html)?$/, function (req, res) {
       res.render('test.ejs', {title: 'Mozilla Persona QUnit Test', layout: false});
     });
+
+    // l10n test template
+    var testPath = path.join(__dirname, '..', '..', 'tests', 'i18n_test_templates');
+    app.get('/i18n_test', function(req, res) {
+      renderCachableView(req, res, path.join(testPath, 'i18n_test.ejs'), { layout: false, title: 'l10n testing title' });
+    });
+    app.get('/i18n_fallback_test', function(req, res) {
+      renderCachableView(req, res, path.join(testPath, 'i18n_fallback_test.ejs'), { layout: false, title: 'l10n testing title' });
+    });
   } else {
     // this is stage or production, explicitly disable all resources under /test
     app.get(/^\/test/, function(req, res) {
diff --git a/lib/static_resources.js b/lib/static_resources.js
index 31cf4e08e158a5cbb870a13daeec2bb4138b2b2e..87575a1375eea7c4f0ab9aace005ce4f8df9f019 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -23,9 +23,9 @@ var common_js = [
   '/common/js/lib/ejs.js',
   '/common/js/lib/micrajax.js',
   '/common/js/lib/urlparse.js',
+  '/common/js/lib/gobbledygook.js',
   '/common/js/javascript-extensions.js',
   '/i18n/:locale/client.json',
-  '/common/js/gettext.js',
   '/common/js/browserid.js',
   '/common/js/lib/hub.js',
   '/common/js/lib/dom-jquery.js',
@@ -39,6 +39,7 @@ var common_js = [
   '/common/js/validation.js',
   '/common/js/helpers.js',
   '/common/js/dom-helpers.js',
+  '/common/js/gettext.js',
   '/common/js/screens.js',
   '/common/js/browser-support.js',
   '/common/js/enable_cookies_url.js',
diff --git a/lib/validate.js b/lib/validate.js
index dd2e55b6fefb0a8c36668f404d93958dcbc9cd99..c95d787f46a802cee6082a0bd773c62d47049de4 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -38,9 +38,24 @@ var types = {
     JSON.parse(x);
   },
   origin: function(x) {
-    // allow single hostnames, e.g. localhost
-    if (typeof x !== 'string' || !x.match(/^https?:\/\/[a-z\d_-]+(\.[a-z\d_-]+)*(:\d+)?$/i)) {
-      throw "not a valid origin";
+    /* origin regex
+    /^                          // beginning
+    https?:\/\/                 // starts with http:// or https://
+    (?=.{1,254}(?::|$))         // hostname must be within 1-254 characters
+    (?:                         // match hostname part (<part>.<part>...)
+      (?!-)                     // cannot start with a dash (allow it to start with a digit re issue #2042)
+      (?![a-z0-9\-]{1,62}-      // part cannot end with a dash
+        (?:\.|:|$))             // (end of part will be '.', ':', or end of str)
+      [a-z0-9\-]{1,63}\b        // part will be 1-63 letters, numbers, or dashes
+        (?!\.$)                 // final part cannot end with a '.'
+        \.?                     // part followed by '.' unless final part
+    )+                          // one or more hostname parts
+    (:\d+)?                     // optional port
+    $/i;                        // end; case-insensitive
+    */
+    var regex = /^https?:\/\/(?=.{1,254}(?::|$))(?:(?!-)(?![a-z0-9\-]{1,62}-(?:\.|:|$))[a-z0-9\-]{1,63}\b(?!\.$)\.?)+(:\d+)?$/i;
+    if (typeof x !== 'string' || !x.match(regex)) {
+      throw new Error("not a valid origin");
     }
   }
 };
diff --git a/lib/wsapi.js b/lib/wsapi.js
index cabedf038c27df25dc4dcb4f6b0eb765b8f8e8a7..7cee435e2afba35e628923bb3a1c2583447eca40 100644
--- a/lib/wsapi.js
+++ b/lib/wsapi.js
@@ -34,7 +34,7 @@ i18n = require('./i18n');
 var abide = i18n.abide({
   supported_languages: config.get('supported_languages'),
   default_lang: config.get('default_lang'),
-  locale_directory: config.get('locale_directory'),
+  translation_directory: config.get('translation_directory'),
   disable_locale_check: config.get('disable_locale_check')
 });
 
diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js
index a9ae6733af46cbdbc1754019303459d3f7219198..8196a77e40a0b709e9e10fb2f376f0e8519fb3c5 100644
--- a/lib/wsapi/address_info.js
+++ b/lib/wsapi/address_info.js
@@ -41,8 +41,8 @@ exports.process = function(req, res) {
     }
     done = true;
     if (err) {
-      logger.warn('error checking "' + m[1] + '" for primary support: ' + err);
-      return httputils.serverError(res, "can't check email address");
+      logger.info('"' + m[1] + '" primary support is misconfigured, falling back to secondary: ' + err);
+      // primary check failed, fall back to secondary email verification
     }
 
     if (urls) {
diff --git a/package.json b/package.json
index 2d1810ee6348b898996d666ebd354ee317e9d2cd..1e45de6881495042a873d00681c892f3c587fa08 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
         "ejs": "0.4.3",
         "etagify": "0.0.2",
         "express": "2.5.0",
+        "gobbledygook": "0.0.3", 
         "mustache": "0.3.1-dev",
         "jwcrypto": "0.3.2",
         "mysql": "0.9.5",
@@ -31,7 +32,7 @@
         "underscore": "1.3.1",
         "urlparse": "0.0.1",
         "validator": "0.4.9",
-        "winston": "0.5.6"
+        "winston": "0.6.2"
     },
     "devDependencies": {
         "vows": "0.5.13",
diff --git a/resources/static/common/js/browserid.js b/resources/static/common/js/browserid.js
index 5f662387697380a5bbeb9639d4d2260842fa1be3..cf4e9f167e4bfcf838ef80abcf7a775e91b7a453 100644
--- a/resources/static/common/js/browserid.js
+++ b/resources/static/common/js/browserid.js
@@ -14,6 +14,10 @@
     // no sense since no component of this is 128 bits
     // so making this 160 as per DSA 1024/160
     // EXCEPT, for backwards compatibility this is still 128 for now
-    KEY_LENGTH: 128
+    KEY_LENGTH: 128,
+
+    PASSWORD_MIN_LENGTH: 8,
+    PASSWORD_MAX_LENGTH: 80
+
   });
 }());
diff --git a/resources/static/common/js/gettext.js b/resources/static/common/js/gettext.js
index a5d3413949a8c464599e7114cad10564d8144a19..a8794883efadfdd829b5e79df8a6c2d29c0d88da 100644
--- a/resources/static/common/js/gettext.js
+++ b/resources/static/common/js/gettext.js
@@ -6,17 +6,25 @@
 (function() {
   "use strict";
 
+  var bid = BrowserID,
+      dom = bid.DOM;
+
   function Gettext() {
       return {
         gettext: function (msgid) {
+          if (window.Gobbledygook &&
+              dom.getAttr('html', 'lang') === 'db-LB') {
+            return window.Gobbledygook(msgid);
+          }
+              
           if (window.json_locale_data && json_locale_data["client"]) {
-          var dict = json_locale_data["client"];
+            var dict = json_locale_data["client"];
             if (dict[msgid] && dict[msgid].length >= 2 &&
                 dict[msgid][1].trim() != "") {
               return dict[msgid][1];
             }
-        }
-        return msgid;
+          }
+          return msgid;
         },
         // See lib/i18n.js format docs
         format: function (fmt, obj, named) {
diff --git a/resources/static/common/js/lib/ejs.js b/resources/static/common/js/lib/ejs.js
index 31a9df53fb1b958f4059a2d33c1fa300d61e4cab..d2396e0bd9beb2a29ff3a773f1c8ebd2ffb2b6f1 100644
--- a/resources/static/common/js/lib/ejs.js
+++ b/resources/static/common/js/lib/ejs.js
@@ -152,9 +152,16 @@ EJS.Scanner = function(source, left, right) {
          double_left: 		left+'%%',
          double_right:  	'%%'+right,
          left_equal: 		left+'%=',
+         // set - Persona addition. The backend understands <%-, which acts
+         // identical to the frontend's <%=.  <%= on the backend escapes
+         // characters to their HTML code equivalents.  For unit testing, we
+         // write backend templates on the front end, so we have to be able to
+         // process <%-.  Creating an alias here.  Using it wherever
+         // left_equal is found.
+         left_dash: 		left+'%-',
          left_comment: 	left+'%#'})
 
-	this.SplitRegexp = left=='[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('('+this.double_left+')|(%%'+this.double_right+')|('+this.left_equal+')|('+this.left_comment+')|('+this.left_delimiter+')|('+this.right_delimiter+'\n)|('+this.right_delimiter+')|(\n)') ;
+	this.SplitRegexp = left=='[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('('+this.double_left+')|(%%'+this.double_right+')|('+this.left_equal+')|('+this.left_dash+')|('+this.left_comment+')|('+this.left_delimiter+')|('+this.right_delimiter+'\n)|('+this.right_delimiter+')|(\n)') ;
 
 	this.source = source;
 	this.stag = null;
@@ -297,6 +304,7 @@ EJS.Compiler.prototype = {
 					break;
 				case scanner.left_delimiter:
 				case scanner.left_equal:
+				case scanner.left_dash:
 				case scanner.left_comment:
 					scanner.stag = token;
 					if (content.length > 0)
@@ -328,6 +336,7 @@ EJS.Compiler.prototype = {
 								buff.push(content);
 							}
 							break;
+            case scanner.left_dash:
 						case scanner.left_equal:
 							buff.push(insert_cmd + "(EJS.Scanner.to_text(" + content + ")))");
 							break;
diff --git a/resources/static/common/js/lib/gobbledygook.js b/resources/static/common/js/lib/gobbledygook.js
new file mode 120000
index 0000000000000000000000000000000000000000..1403b642aef4fd46ccca2f5868826d9027e9f02f
--- /dev/null
+++ b/resources/static/common/js/lib/gobbledygook.js
@@ -0,0 +1 @@
+../../../../../node_modules/gobbledygook/gobbledygook.js
\ No newline at end of file
diff --git a/resources/static/common/js/modules/interaction_data.js b/resources/static/common/js/modules/interaction_data.js
index 11379e4df87faddcb1909f8e6bab23b66614c06e..fef84018e484fd4d7efb34f11a480925cec57b9a 100644
--- a/resources/static/common/js/modules/interaction_data.js
+++ b/resources/static/common/js/modules/interaction_data.js
@@ -79,7 +79,11 @@ BrowserID.Modules.InteractionData = (function() {
     user_confirmed: "user.user_confirmed",
     email_staged: "user.email_staged",
     email_confirmed: "user.email_confrimed",
-    notme: "user.logout"
+    notme: "user.logout",
+    enter_password: "authenticate.enter_password",
+    password_submit: "authenticate.password_submitted",
+    authentication_success: "authenticate.password_success",
+    authentication_fail: "authenticate.password_fail"
   };
 
   function getKPIName(msg, data) {
@@ -204,7 +208,7 @@ BrowserID.Modules.InteractionData = (function() {
     self.initialEventStream = null;
 
     self.samplesBeingStored = true;
-    
+
   }
 
   function indexOfEvent(eventStream, eventName) {
diff --git a/resources/static/common/js/network.js b/resources/static/common/js/network.js
index e68780d920d925e08818777139536f32f629b6de..190b3396df94d5d4be4e61abeff2789576075b88 100644
--- a/resources/static/common/js/network.js
+++ b/resources/static/common/js/network.js
@@ -714,14 +714,19 @@ BrowserID.Network = (function() {
       withContext(function() {
         var enabled;
         try {
-          // set a test cookie with a duration of 1 second.
           // NOTE - The Android 3.3 and 4.0 default browsers will still pass
           // this check.  This causes the Android browsers to only display the
           // cookies diabled error screen only after the user has entered and
           // submitted input.
-          // http://stackoverflow.com/questions/8509387/android-browser-not-respecting-cookies-disabled/9264996#9264996
-          document.cookie = "test=true; max-age=1";
-          enabled = document.cookie.indexOf("test") > -1;
+          // http://stackoverflow.com/questions/8509387/android-browser-not-respecting-cookies-disabled
+
+          document.cookie = "__cookiesEnabledCheck=1";
+          enabled = document.cookie.indexOf("__cookiesEnabledCheck") > -1;
+
+          // expire the cookie NOW by setting its expires date to yesterday.
+          var expires = new Date();
+          expires.setDate(expires.getDate() - 1);
+          document.cookie = "__cookiesEnabledCheck=; expires=" + expires.toGMTString();
         } catch(e) {
           enabled = false;
         }
diff --git a/resources/static/common/js/user.js b/resources/static/common/js/user.js
index 5438da17921fa3b5a5889d8a37a58614b51af0a0..babc592290d3ce01c4e62e3c36aad20757d842eb 100644
--- a/resources/static/common/js/user.js
+++ b/resources/static/common/js/user.js
@@ -836,6 +836,14 @@ BrowserID.User = (function() {
      * @param {function} [onFailure] - Called on error.
      */
     authenticate: function(email, password, onComplete, onFailure) {
+      // password is out of length range.  Don't even send the request
+      // and waste backend cycles. See issue #2032.
+      if (password.length < bid.PASSWORD_MIN_LENGTH
+       || password.length > bid.PASSWORD_MAX_LENGTH) {
+        complete(onComplete, false);
+        return;
+      }
+
       network.authenticate(email, password, function(authenticated) {
         setAuthenticationStatus(authenticated);
 
diff --git a/resources/static/dialog/js/misc/helpers.js b/resources/static/dialog/js/misc/helpers.js
index ea2157153159bbc700989f3e730b1c07ab0c21e9..4c3f150a0706a3d028c443aeec2e664465a875dd 100644
--- a/resources/static/dialog/js/misc/helpers.js
+++ b/resources/static/dialog/js/misc/helpers.js
@@ -62,9 +62,14 @@
 
   function authenticateUser(email, pass, callback) {
     var self=this;
+    self.publish("password_submit");
     user.authenticate(email, pass,
       function (authenticated) {
-        if (!authenticated) {
+        if (authenticated) {
+          self.publish("authentication_success");
+        }
+        else {
+          self.publish("authentication_fail");
           tooltip.showTooltip("#cannot_authenticate");
         }
         complete(callback, authenticated);
diff --git a/resources/static/pages/js/manage_account.js b/resources/static/pages/js/manage_account.js
index 5862120c74dffa36a2266c6e00f3304a8187634f..2e650f6a16e9297d2b7fa08a9a681eb45487ac92 100644
--- a/resources/static/pages/js/manage_account.js
+++ b/resources/static/pages/js/manage_account.js
@@ -49,7 +49,8 @@ BrowserID.manageAccount = (function() {
         displayStoredEmails(oncomplete);
       }
       else if (_.size(emails) > 1) {
-        if (confirmAction("Remove " + email + " from your BrowserID?")) {
+        if (confirmAction(format(gettext("Remove %(email) from your BrowserID?"),
+                                 { email: email }))) {
           user.removeEmail(email, function() {
             displayStoredEmails(oncomplete);
           }, pageHelpers.getFailure(errors.removeEmail, oncomplete));
@@ -59,7 +60,7 @@ BrowserID.manageAccount = (function() {
         }
       }
       else {
-        if (confirmAction("Removing the last address will cancel your BrowserID account.\nAre you sure you want to continue?")) {
+        if (confirmAction(gettext("Removing the last address will cancel your BrowserID account.\nAre you sure you want to continue?"))) {
           user.cancelUser(function() {
             doc.location="/";
             complete();
@@ -92,7 +93,7 @@ BrowserID.manageAccount = (function() {
   }
 
   function cancelAccount(oncomplete) {
-    if (confirmAction("Are you sure you want to cancel your BrowserID account?")) {
+    if (confirmAction(gettext("Are you sure you want to cancel your BrowserID account?"))) {
       user.cancelUser(function() {
         doc.location="/";
         oncomplete && oncomplete();
diff --git a/resources/static/test/cases/common/js/user.js b/resources/static/test/cases/common/js/user.js
index 1911f69a256b1623c871483d2c706c4345c1c3fd..f39ed418c20b87b8fd73f881e1f965cfcfb44aeb 100644
--- a/resources/static/test/cases/common/js/user.js
+++ b/resources/static/test/cases/common/js/user.js
@@ -630,6 +630,22 @@
   });
 
 
+  asyncTest("authenticate with too short a password - user not authenticated", function() {
+    var password = testHelpers.generateString(bid.PASSWORD_MIN_LENGTH - 1);
+    lib.authenticate(TEST_EMAIL, password, function onComplete(authenticated) {
+      equal(false, authenticated, "invalid authentication.");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("authenticate with too long a password - user not authenticated", function() {
+    var password = testHelpers.generateString(bid.PASSWORD_MAX_LENGTH + 1);
+    lib.authenticate(TEST_EMAIL, password, function onComplete(authenticated) {
+      equal(false, authenticated, "invalid authentication.");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
   asyncTest("authenticate with invalid credentials", function() {
     xhr.useResult("invalid");
     lib.authenticate(TEST_EMAIL, "testuser", function onComplete(authenticated) {
diff --git a/resources/static/test/cases/dialog/js/misc/helpers.js b/resources/static/test/cases/dialog/js/misc/helpers.js
index edc1fa98a8a5417630559085b2218cc2ee9a66fc..5e1a5638d7276f88beca53442d84c81072622457 100644
--- a/resources/static/test/cases/dialog/js/misc/helpers.js
+++ b/resources/static/test/cases/dialog/js/misc/helpers.js
@@ -18,6 +18,8 @@
       mediator = bid.Mediator,
       errorCB,
       expectedError = testHelpers.expectedXHRFailure,
+      expectedMessage = testHelpers.expectedMessage,
+      unexpectedMessage = testHelpers.unexpectedMessage,
       badError = testHelpers.unexpectedXHRFailure;
 
   var controllerMock = {
@@ -29,22 +31,7 @@
     }
   };
 
-  function expectedMessage(message, expectedFields) {
-    mediator.subscribe(message, function(m, info) {
-      equal(m, message, "correct message: " + message);
-
-      testHelpers.testObjectValuesEqual(info, expectedFields);
-    });
-  }
-
-
-  function unexpectedMessage(message) {
-    mediator.subscribe(message, function(m, info) {
-      ok(false, "close should have never been called");
-    });
-  }
-
-  module("resources/helpers", {
+  module("dialog/js/misc/helpers", {
     setup: function() {
       testHelpers.setup();
       errorCB = null;
@@ -78,10 +65,12 @@
 
     xhr.useResult("ajaxError");
     storage.addEmail("registered@testuser.com", {});
-    dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess);
+    dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", testHelpers.expectedFailure);
   });
 
   asyncTest("authenticateUser happy case", function() {
+    expectedMessage("password_submit");
+    expectedMessage("authentication_success");
     dialogHelpers.authenticateUser.call(controllerMock, "testuser@testuser.com", "password", function(authenticated) {
       equal(authenticated, true, "user is authenticated");
       start();
@@ -90,6 +79,8 @@
 
   asyncTest("authenticateUser invalid credentials", function() {
     xhr.useResult("invalid");
+    expectedMessage("password_submit");
+    expectedMessage("authentication_fail");
     dialogHelpers.authenticateUser.call(controllerMock, "testuser@testuser.com", "password", function(authenticated) {
       equal(authenticated, false, "user is not authenticated");
       start();
@@ -100,6 +91,7 @@
     errorCB = expectedError;
 
     xhr.useResult("ajaxError");
+    expectedMessage("password_submit");
     dialogHelpers.authenticateUser.call(controllerMock, "testuser@testuser.com", "password", testHelpers.unexpectedSuccess);
   });
 
@@ -209,7 +201,7 @@
   });
 
   asyncTest("resetPassword happy case", function() {
-    expectedMessage("password_reset", {
+    expectedMessage("reset_password_staged", {
       email: "registered@testuser.com"
     });
 
diff --git a/resources/static/test/cases/pages/js/verify_secondary_address.js b/resources/static/test/cases/pages/js/verify_secondary_address.js
index 8afe1e7f1030a16f0a3db3e181b2e3713e32038e..c9227af84e2ef6c0c9315349c4178142ebc26aee 100644
--- a/resources/static/test/cases/pages/js/verify_secondary_address.js
+++ b/resources/static/test/cases/pages/js/verify_secondary_address.js
@@ -84,7 +84,7 @@
     createController(config, function() {
       testVisible("#congrats");
       testHasClass("body", "complete");
-      equal($(".website").text(), returnTo, "website is updated");
+      equal($(".website").eq(0).text(), returnTo, "website is updated");
       equal(doc.location.href, returnTo, "redirection occurred to correct URL");
       equal(storage.getLoggedIn("https://test.domain"), "testuser@testuser.com", "logged in status set");
       start();
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 66868076a8c8cd477b25b2db0e5e1e6023595037..c9f9329442d808d31b1e158ccb23c079519d1bd1 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -27,7 +27,7 @@ BrowserID.TestHelpers = (function() {
       if(calls[msg]) {
         throw msg + " triggered more than once";
       }
-      calls[msg] = true;
+      calls[msg] = info || true;
 
       cb && cb.apply(null, arguments);
     }));
@@ -112,13 +112,43 @@ BrowserID.TestHelpers = (function() {
 
     register: register,
     isTriggered: function(message) {
-      return calls[message];
+      return message in calls;
     },
 
-    testTriggered: function(message) {
-      equal(calls[message], true, message + " was triggered");
+    testTriggered: function(message, expectedFields) {
+      ok(message in calls, message + " was triggered");
+      if (expectedFields) this.testObjectValuesEqual(calls[message], expectedFields);
     },
 
+    expectedMessage: function(message, expectedFields) {
+    // keep track of the original start function.  When the start function is
+    // called, call the proxy start function and then the original start
+    // function.  This allows proxy start functions to be chained and multiple
+    // expectedMessages to be called.
+    start = function(origStart) {
+      TestHelpers.testTriggered(message, expectedFields);
+      start = origStart;
+      start();
+    }.bind(null, start);
+
+    register(message);
+  },
+
+  unexpectedMessage: function(message) {
+    // keep track of the original start function.  When the start function is
+    // called, call the proxy start function and then the original start
+    // function.  This allows proxy start functions to be chained and multiple
+    // expectedMessages to be called.
+    start = function(origStart) {
+      equal(TestHelpers.isTriggered(message), false, message + " was not triggered");
+      start = origStart;
+      start();
+
+    }.bind(null, start);
+    register(message);
+  },
+
+
     errorVisible: function() {
       return screens.error.visible;
     },
diff --git a/resources/views/about.ejs b/resources/views/about.ejs
index 3c41de497f35575cacf7ae8c847da2991dde5b22..43da5ce79b07edc8eae6cfcfa747172642429571 100644
--- a/resources/views/about.ejs
+++ b/resources/views/about.ejs
@@ -5,44 +5,44 @@
 <div id="content" class="display_always">
     <div class="about">
         <section class="simple-signon">
-            <h2 class="title">Simplified sign-on.</h2>
+            <h2 class="title"><%- gettext('Simplified sign-on.') %></h2>
             <article class="blurb">
                 <div class="info first">
-                    <h1>Persona replaces multiple passwords</h1>
-                    <p>Sites such as <a href="http://crossword.thetimes.co.uk/" target="_blank">The Times Crossword</a>, <a href="http://current.openphoto.me/" target="_blank">OpenPhoto</a> and <a href="https://www.voo.st/" target="_blank">Voost</a> use Persona instead of usernames to sign you in.</p><p>This means you only need one password to sign in to many sites.</p>
+                    <h1><%- gettext('Persona replaces multiple passwords') %></h1>
+                    <p><%- gettext('Sites such as <a href="http://crossword.thetimes.co.uk/" target="_blank">The Times Crossword</a>, <a href="http://current.openphoto.me/" target="_blank">OpenPhoto</a> and <a href="https://www.voo.st/" target="_blank">Voost</a> use Persona instead of usernames to sign you in.') %></p><p><%- gettext('This means you only need one password to sign in to many sites.') %></p>
                 </div>
 
                 <div class="graphic">
-                    <img src="<%- cachify('/pages/i/one-password-graphic.png') %>" alt="One password to rule them all.">
+                    <img src="<%- cachify('/pages/i/one-password-graphic.png') %>" alt="<%- gettext('One password to rule them all.') %>">
                 </div>
             </article>
 
             <article class="blurb flexible">
                 <div class="graphic first">
-                    <img src="<%- cachify('/pages/i/flexible-graphic.png') %>" alt="Use multiple email addresses">
+                    <img src="<%- cachify('/pages/i/flexible-graphic.png') %>" alt="<%- gettext('Use multiple email addresses') %>">
                 </div>
 
                 <div class="info">
-                    <h1>Persona is flexible</h1>
-                    <p>Within Persona, your identity is your email address. You can use as many email addresses as you want, but you still only need one password.</p>
+                    <h1><%- gettext('Persona is flexible') %></h1>
+                    <p><%- gettext('Within Persona, your identity is your email address. You can use as many email addresses as you want, but you still only need one password.') %></p>
                 </div>
             </article>
         </section>
 
         <section class="privacy">
-            <h2 class="title">Real privacy.</h2>
+            <h2 class="title"><%- gettext('Real privacy.') %></h2>
 
             <article class="blurb half first" style="min-height: 195px; ">
-                <h1>Persona is proudly non-profit for you</h1>
-                <p>Persona is developed by Mozilla, a not-for-profit company trusted throughout the Web community. Our goal is to create technologies that balance an open Web platform with people’s privacy.</p>
+                <h1><%- gettext('Persona is proudly non-profit for you') %></h1>
+                <p><%- gettext('Persona is developed by Mozilla, a not-for-profit company trusted throughout the Web community. Our goal is to create technologies that balance an open Web platform with people\'s privacy.') %></p>
             </article>
             <article class="blurb half">
-                <h1>Persona preserves your privacy</h1>
-                <p>Persona does not track your activity around the Web. It creates a wall between signing you in and what you do once you’re there. The history of what sites you visit is stored only on your own computer.</p>
+                <h1><%- gettext('Persona preserves your privacy') %></h1>
+                <p><%- gettext('Persona does not track your activity around the Web. It creates a wall between signing you in and what you do once you\'re there. The history of what sites you visit is stored only on your own computer.') %></p>
             </article>
         </section>
 
-        <a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" class="developers" target="_blank"><img src="<%- cachify('/pages/i/developers-link.png') %>" alt="Persona for developers"><span>Implement Persona on your site </span>Developer guides and API documentation</a>
+        <a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" class="developers" target="_blank"><img src="<%- cachify('/pages/i/developers-link.png') %>" alt="<%- gettext('Persona for developers') %>"><span><%- gettext('Implement Persona on your site') %> </span><%- gettext('Developer guides and API documentation') %></a>
     </div><!-- #dashboard -->
 </div>
 
diff --git a/resources/views/authenticate_with_primary.ejs b/resources/views/authenticate_with_primary.ejs
index 57ef4cc40d5e7743e8eeea1de8153a4480d49aae..dbea3d96f352cd0c3f5b119609e1bfab089cdae1 100644
--- a/resources/views/authenticate_with_primary.ejs
+++ b/resources/views/authenticate_with_primary.ejs
@@ -3,7 +3,7 @@
 <html>
 <head>
   <meta charset="utf-8">
-  <title>Browser ID</title>
+  <title>Persona</title>
   <%- cachify_js('/production/authenticate_with_primary.js') %>
 </head>
 </html>
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index 5700e6d410d166daf2f68ef74521930085db8bc3..f596ca155a3ac86a9d263c357eeabfd3b5c6f87d 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -13,7 +13,10 @@
   <!--[if lt IE 9]>
     <%- cachify_css('/production/ie8_dialog.css') %>
   <![endif]-->
-  <title><%= gettext('Mozilla Persona') %></title>
+  <% /* the title comes from the server when the page is loaded.
+         It still needs translated, so wrap it in its own gettext
+     */ %>
+  <title><%= format(gettext("Mozilla Persona: %s"), [gettext(title)]) %></title>
 </head>
   <body class="waiting">
       <header id="header">
diff --git a/resources/views/forgot.ejs b/resources/views/forgot.ejs
index 705d14ead5e6cb0f4c5c666d1db39106c74d08f8..59a0c5d591a27cfb5768b6e1ac5ccb0ed1d1ec93 100644
--- a/resources/views/forgot.ejs
+++ b/resources/views/forgot.ejs
@@ -6,70 +6,70 @@
     <div id="vAlign">
         <!-- XXX this form submits to nowhere -->
         <form id="signUpForm" class="cf authform" novalidate>
-            <h1>Forgot Password</h1>
+            <h1><%- gettext('Forgot Password') %></h1>
             <div class="notifications">
                 <div class="notification emailsent">
-                  <h2>Confirm your email address</h2>
+                  <h2><%- gettext('Confirm your email address') %></h2>
 
                   <p>
-                    Check your email at <strong id="sentToEmail"></strong>.
+                    <%- gettext('Check your email at <strong id="sentToEmail"></strong>.') %>
                   </p>
 
                   <p>
-                    Click the link in the confirmation email. Your password will then be reset.
+                    <%- gettext('Click the link in the confirmation email. Your password will then be reset.') %>
                   </p>
                 </div>
             </div>
 
             <ul class="inputs forminputs">
                 <li>
-                    <label for="email">Email Address</label>
-                    <input id="email" autofocus required placeholder="Your Email" type="email" autocapitalize="off" autocorrect="off" maxlength="254" />
+                    <label for="email"><%- gettext('Email Address') %></label>
+                    <input id="email" autofocus required placeholder="<%- gettext('Your Email') %>" type="email" autocapitalize="off" autocorrect="off" maxlength="254" />
 
                     <div id="email_format" class="tooltip" for="email">
-                      This field must be an email address.
+                      <%- gettext('This field must be an email address.') %>
                     </div>
 
                     <div id="email_required" class="tooltip" for="email">
-                      The email field is required.
+                      <%- gettext('The email field is required.') %>
                     </div>
 
                     <div id="could_not_add" class="tooltip" for="email">
-                      We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                      <%- gettext('We just sent an email to that address! If you really want to send another, wait a minute or two and try again.') %>
                     </div>
 
                     <div id="not_registered" class="tooltip" for="email">
-                      Non existent user!
+                      <%- gettext('Non existent user!') %>
                     </div>
                 </li>
 
                 <li>
-                    <label for="password">Password</label>
-                    <input id="password" placeholder="Password" type="password" maxlength="80">
+                    <label for="password"><%- gettext('Password') %></label>
+                    <input id="password" placeholder="<%- gettext('Password') %>" type="password" maxlength="80">
 
                     <div id="password_required" class="tooltip" for="password">
-                        Password is required.
+                        <%- gettext('Password is required.') %>
                     </div>
 
                     <div class="tooltip" id="password_length" for="password">
-                        Password must be between 8 and 80 characters long.
+                        <%- gettext('Password must be between 8 and 80 characters long.') %>
                     </div>
 
                     <div id="could_not_add" class="tooltip" for="password">
-                        We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                        <%- gettext('We just sent an email to that address! If you really want to send another, wait a minute or two and try again.') %>
                     </div>
                 </li>
 
                 <li>
-                    <label for="vpassword">Verify Password</label>
-                    <input id="vpassword" placeholder="Verify Password" type="password" maxlength="80">
+                    <label for="vpassword"><%- gettext('Verify Password') %></label>
+                    <input id="vpassword" placeholder="<%- gettext('Verify Password') %>" type="password" maxlength="80">
 
                     <div id="password_required" class="tooltip" for="vpassword">
-                      Verification password is required.
+                      <%- gettext('Verification password is required.') %>
                     </div>
 
                     <div class="tooltip" id="passwords_no_match" for="vpassword">
-                      These passwords don't match!
+                      <%- gettext('These passwords don\'t match!') %>
                     </div>
 
                 </li>
@@ -77,9 +77,9 @@
             </ul>
 
             <div class="submit cf forminputs">
-                <button>Reset Password</button>
+                <button><%- gettext('Reset Password') %></button>
                 <div class="remember cf">
-                    <a class="action" href="/signin">Know your password? Sign in.</a>
+                    <a class="action" href="/signin"><%- gettext('Know your password? Sign in.') %></a>
                 </div>
             </div>
         </form>
diff --git a/resources/views/idp_auth_complete.ejs b/resources/views/idp_auth_complete.ejs
index f7e5686359c037c2f3e54c7969aaa447e21bef66..538a99157639073bb3adfe23342645bd2be3d72d 100644
--- a/resources/views/idp_auth_complete.ejs
+++ b/resources/views/idp_auth_complete.ejs
@@ -3,7 +3,7 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
   <div id="vAlign" class="disply_always">
-    This window will now close and account creation will continue.
+    <%- gettext('This window will now close and account creation will continue.') %>
   </div>
 
   <script type="text/javascript">
diff --git a/resources/views/index.ejs b/resources/views/index.ejs
index 7eb8e2c35b8f6c9e22322a1062e428ce90671ffc..98c47d935442bea0271471800a9297be03b1ab45 100644
--- a/resources/views/index.ejs
+++ b/resources/views/index.ejs
@@ -7,11 +7,13 @@
           <div id="signUp">
               <div id="card"><img src="<%- cachify('/pages/i/slit.png') %>"></div>
 
-              <h1 class="white headline-main">Connect with Mozilla Persona, the safest &amp; easiest way to sign in.</h1>
+              <h1 class="white headline-main"><%- gettext('Connect with Mozilla Persona, the safest &amp; easiest way to sign in.') %></h1>
               <p class="tour white">
-                <a href="/about">Take the tour</a>
-                or
-                <a href="/signup" class="button create">Sign up &rarr;</a>
+                <%- format(gettext('<a %(aboutLink)>Take the tour</a> or <a %(signupButton)>Sign Up &rarr;</a>'),
+                      {
+                        aboutLink: 'href="/about"',
+                        signupButton: 'href="/signup" class="button create"',
+                      }) %>
               </p>
           </div>
       </div>
@@ -20,25 +22,25 @@
   <script type="text/html" id="templateUser">
     <li class="identity cf" id="{{ email.replace('@', '_').replace('.', '_') }}">
       <div class="email">{{ email }}</div>
-      <button class="delete">remove</button>
+      <button class="delete"><%- gettext('remove') %></button>
     </li>
   </script>
 
   <script type="text/html" id="templateManage">
     <div id="content">
         <div class="newsbanner" id="newuser">
-          New to Mozilla Persona? <a href="/about">Learn more</a>
+          <%- gettext('New to Mozilla Persona? <a href="/about">Learn more</a>') %>
         </div>
 
         <div id="manage">
-            <h1>Account Manager</h1>
+            <h1><%- gettext('Account Manager') %></h1>
 
             <section>
               <header class="buttonrow cf">
-                  <h2>Your Email Addresses</h2>
+                  <h2><%- gettext('Your Email Addresses') %></h2>
 
-                  <button class="edit">edit</button>
-                  <button class="done">done</button>
+                  <button class="edit"><%- gettext('edit') %></button>
+                  <button class="done"><%- gettext('done') %></button>
               </header>
 
               <ul id="emailList">
@@ -47,32 +49,32 @@
 
             <section id="edit_password">
               <header class="buttonrow cf">
-                <h2>Password</h2>
+                <h2><%- gettext('Password') %></h2>
 
-                <button class="edit">edit</button>
-                <button class="done">cancel</button>
+                <button class="edit"><%- gettext('edit') %></button>
+                <button class="done"><%- gettext('cancel') %></button>
               </header>
 
               <div class="showedit">
-                <label for="old_password">Old Password</label>
-                <label for="new_password">New Password</label>
+                <label for="old_password"><%- gettext('Old Password') %></label>
+                <label for="new_password"><%- gettext('New Password') %></label>
               </div>
 
               <form id="edit_password_form" class="showedit">
                 <input type="password" id="old_password" name="old_password" maxlength="80"/>
                 <input type="password" id="new_password" name="new_password" maxlength="80"/>
-                <button id="changePassword">done</button>
+                <button id="changePassword"><%- gettext('done') %></button>
 
-                <div class="tooltip" for="old_password" id="tooltipOldRequired">Old password is required.</div>
-                <div class="tooltip" for="old_password" id="tooltipInvalidPassword">Incorrect old password, password not updated.</div>
-                <div class="tooltip" for="new_password" id="tooltipNewRequired">New password is required.</div>
-                <div class="tooltip" for="new_password" id="tooltipPasswordsSame">Old and new passwords are the same.</div>
-                <div class="tooltip" for="new_password" id="tooltipPasswordLength">Password must be between 8 and 80 characters long.</div>
+                <div class="tooltip" for="old_password" id="tooltipOldRequired"><%- gettext('Old password is required.') %></div>
+                <div class="tooltip" for="old_password" id="tooltipInvalidPassword"><%- gettext('Incorrect old password, password not updated.') %></div>
+                <div class="tooltip" for="new_password" id="tooltipNewRequired"><%- gettext('New password is required.') %></div>
+                <div class="tooltip" for="new_password" id="tooltipPasswordsSame"><%- gettext('Old and new passwords are the same.') %></div>
+                <div class="tooltip" for="new_password" id="tooltipPasswordLength"><%- gettext('Password must be between 8 and 80 characters long.') %></div>
               </form>
             </section>
 
 
-            <p id="disclaimer">You may, at any time, <a href="#" id="cancelAccount" class="action">cancel your account</a></p>
+            <p id="disclaimer"><%- gettext('You may, at any time, <a href="#" id="cancelAccount" class="action">cancel your account</a>') %></p>
         </div>
     </div>
 
diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs
index bea90bcae84c2dad6a30d0d250760a08b687b485..3ebad475ee2b3f45c290b1d8c41ec650ac699d19 100644
--- a/resources/views/layout.ejs
+++ b/resources/views/layout.ejs
@@ -1,6 +1,6 @@
 <!DOCTYPE html>
 <%- partial('partial/license_with_code_ver') %>
-<html>
+<html LANG="<%= lang %>" dir="<%= lang_dir %>">
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, width=device-width" />
@@ -13,7 +13,10 @@
     <%- cachify_css('/production/ie8_main.css') %>
   <![endif]-->
   <%- cachify_js(util.format('/production/%s/browserid.js', locale)) %>
-  <title><%= format(gettext("Mozilla Persona: %s"), [title]) %></title>
+  <% /* the title comes from the server when the page is loaded.
+         It still needs translated, so wrap it in its own gettext
+     */ %>
+  <title><%= format(gettext("Mozilla Persona: %s"), [gettext(title)]) %></title>
 </head>
 <body class="loading">
 <% if (enable_development_menu) { %>
@@ -49,7 +52,7 @@
                        [" href='http://identity.mozilla.com' target='_blank'", " href='https://mozilla.org' target='_blank'"]) %></li>
         <li><a href="/privacy"><%= gettext('Privacy &rarr;') %></a></li>
         <li><a href="/tos"><%= gettext('TOS &rarr;') %></a></li>
-        <li class="help"><a href="https://support.mozilla.com/en-US/kb/what-browserid-and-how-does-it-work" target="_blank"><%= gettext('Need Help? &rarr;') %></a></li>
+        <li class="help"><a href="https://support.mozilla.com/kb/what-browserid-and-how-does-it-work" target="_blank"><%= gettext('Need Help? &rarr;') %></a></li>
     </ul>
 </footer>
 
diff --git a/resources/views/relay.ejs b/resources/views/relay.ejs
index a530006e70b4b6633a0b3e3ce4f2efe4daebe059..993bb7c1f1066beb14574d7b1640eaf63f994372 100644
--- a/resources/views/relay.ejs
+++ b/resources/views/relay.ejs
@@ -3,7 +3,7 @@
 <html>
 <head>
   <meta charset="utf-8">
-  <title>Browser ID</title>
+  <title>Persona</title>
   <%- cachify_js('/production/relay.js') %>
 </head>
 </html>
diff --git a/resources/views/signin.ejs b/resources/views/signin.ejs
index d09e2a56e3ad317ced53ac1e9547dcf606433900..683af56d65753361e8a228c85fec202bd3111e51 100644
--- a/resources/views/signin.ejs
+++ b/resources/views/signin.ejs
@@ -6,64 +6,62 @@
     <div id="vAlign">
         <!-- XXX this form submits to nowhere -->
         <form id="signUpForm" class="cf authform" novalidate>
-            <h1>Sign In</h1>
+            <h1><%- gettext('Sign In') %></h1>
 
             <ul class="notifications">
                 <li class="notification" id="unknown_secondary">
-                  <strong id="unknown_email">Email</strong> is not registered.
-                  Would you like to <a class="action" href="/signup">sign up</a> instead?
+                  <%- gettext('<strong id="unknown_email">Email</strong> is not registered. Would you like to <a class="action" href="/signup">sign up</a> instead?') %>
                 </li>
 
             </ul>
 
             <ul class="inputs">
                 <li>
-                    <label for="email">Email Address</label>
-                    <input id="email" autofocus placeholder="Your Email" type="email" autocapitalize="off" autocorrect="off" tabindex="1" maxlength="254" />
+                    <label for="email"><%- gettext('Email Address') %></label>
+                    <input id="email" autofocus placeholder="<%- gettext('Your Email') %>" type="email" autocapitalize="off" autocorrect="off" tabindex="1" maxlength="254" />
 
                     <div id="email_format" class="tooltip" for="email">
-                      This field must be an email address.
+                      <%- gettext('This field must be an email address.') %>
                     </div>
 
                     <div id="email_required" class="tooltip" for="email">
-                      The email field is required.
+                      <%- gettext('The email field is required.') %>
                     </div>
                 </li>
               </ul>
 
               <ul class="inputs password_entry">
                 <li class="password_section">
-                    <a class="forgot right" href="/forgot" tabindex="4">forgot your password?</a>
-                    <label for="password">Password</label>
-                    <input id="password" placeholder="Your Password" type="password" tabindex="2" maxlength="80">
+                    <a class="forgot right" href="/forgot" tabindex="4"><%- gettext('forgot your password?') %></a>
+                    <label for="password"><%- gettext('Password') %></label>
+                    <input id="password" placeholder="<%- gettext('Your Password') %>" type="password" tabindex="2" maxlength="80">
 
                     <div id="password_required" class="tooltip" for="password">
-                      The password field is required.
+                      <%- gettext('The password field is required.') %>
                     </div>
 
                     <div id="cannot_authenticate" class="tooltip" for="password">
-                      This email address and password do not match.
+                      <%- gettext('This email address and password do not match.') %>
                     </div>
 
                 </li>
             </ul>
 
             <div class="submit cf forminputs">
-                <button tabindex="5">Sign In</button>
+                <button tabindex="5"><%- gettext('Sign In') %></button>
                 <div class="remember cf">
-                    <a class="action" href="/signup">New to Persona? Sign up today.</a>
+                    <a class="action" href="/signup"><%- gettext('New to Persona? Sign up today.') %></a>
                 </div>
             </div>
 
             <ul class="notifications">
                 <li class="notification" id="verify_primary">
                   <p>
-                    To verify that you own <strong id="primary_email">address</strong>, you must
-                    sign in with your provider.  A new window will be opened.
+                    <%- gettext('To verify that you own <strong id="primary_email">address</strong>, you must sign in with your provider.  A new window will be opened.') %>
                   </p>
 
                   <p>
-                    <button id="authWithPrimary">Verify</button>
+                    <button id="authWithPrimary"><%- gettext('Verify') %></button>
                   </p>
                 </li>
             </ul>
@@ -72,6 +70,6 @@
 </div>
 
 <noscript>
-  We're sorry, Persona requires that Javascript is enabled.
+  <%- gettext('We\'re sorry, Persona requires that Javascript is enabled.') %>
 </noscript>
 
diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs
index 8440816f1aa7113fd49c490e01818ccdd7c69d54..14e823d4f5c38012dd5a14151699bbefc597962f 100644
--- a/resources/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -6,23 +6,22 @@
     <div id="vAlign">
         <!-- XXX this form submits to nowhere -->
         <form id="signUpForm" class="cf authform" novalidate>
-            <h1>Create Account</h1>
+            <h1><%- gettext('Create Account') %></h1>
 
             <ul class="notifications">
                 <li class="notification alreadyRegistered">
-                  <strong id="registeredEmail"></strong> is already registered.
-                  Would you like to <a class="action" href="/signin">sign in</a> instead?
+                  <%- gettext('<strong id="registeredEmail"></strong> is already registered. Would you like to <a class="action" href="/signin">sign in</a> instead?') %>
                 </li>
 
                 <li class="notification emailsent">
-                  <h2>Confirm your email address</h2>
+                  <h2><%- gettext('Confirm your email address') %></h2>
 
                   <p>
-                    Check your email at <strong id="sentToEmail"></strong>.
+                    <%- gettext('Check your email at <strong id="sentToEmail"></strong>.') %>
                   </p>
 
                   <p>
-                    Click the link in the confirmation email. You'll then immediately be signed into Persona.
+                    <%- gettext('Click the link in the confirmation email. You\'ll then immediately be signed into Persona.') %>
                   </p>
                 </li>
 
@@ -30,49 +29,45 @@
 
             <ul class="inputs forminputs">
                 <li>
-                    <label for="email">Email Address</label>
-                    <input id="email" autofocus placeholder="Your Email" type="email" autocapitalize="off" autocorrect="off" maxlength="254" />
+                    <label for="email"><%- gettext('Email Address') %></label>
+                    <input id="email" autofocus placeholder="<%- gettext('Your Email') %>" type="email" autocapitalize="off" autocorrect="off" maxlength="254" />
 
                     <div id="email_format" class="tooltip" for="email">
-                      This field must be an email address.
+                      <%- gettext('This field must be an email address.') %>
                     </div>
 
                     <div id="email_required" class="tooltip" for="email">
-                      The email field is required.
+                      <%- gettext('The email field is required.') %>
                     </div>
 
                     <div id="could_not_add" class="tooltip" for="email">
-                      We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                      <%- gettext('We just sent an email to that address! If you really want to send another, wait a minute or two and try again.') %>
                     </div>
                 </li>
 
                 <li class="password_entry">
-                    <label for="password">Password</label>
-                    <input id="password" placeholder="Password" type="password" maxlength="80">
+                    <label for="password"><%- gettext('Password') %></label>
+                    <input id="password" placeholder="<%- gettext('Password') %>" type="password" maxlength="80">
 
                     <div id="password_required" class="tooltip" for="password">
-                        Password is required.
+                        <%- gettext('Password is required.') %>
                     </div>
 
                     <div class="tooltip" id="password_length" for="password">
-                        Password must be between 8 and 80 characters long.
-                    </div>
-
-                    <div id="could_not_add" class="tooltip" for="password">
-                        We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                        <%- gettext('Password must be between 8 and 80 characters long.') %>
                     </div>
                 </li>
 
                 <li class="password_entry">
-                    <label for="vpassword">Verify Password</label>
-                    <input id="vpassword" placeholder="Verify Password" type="password" maxlength="80">
+                    <label for="vpassword"><%- gettext('Verify Password') %></label>
+                    <input id="vpassword" placeholder="<%- gettext('Verify Password') %>" type="password" maxlength="80">
 
                     <div id="password_required" class="tooltip" for="vpassword">
-                      Verification password is required.
+                      <%- gettext('Verification password is required.') %>
                     </div>
 
                     <div class="tooltip" id="passwords_no_match" for="vpassword">
-                      These passwords don't match!
+                      <%- gettext('These passwords don\'t match!') %>
                     </div>
 
                 </li>
@@ -80,17 +75,17 @@
 
             <div class="submit cf forminputs">
                 <p class="cf">
-                  <button>Verify Email</button>
-                  <a class="action remember" href="/signin" tabindex="2">Existing account? Sign in.</a>
+                  <button><%- gettext('Verify Email') %></button>
+                  <a class="action remember" href="/signin" tabindex="2"><%- gettext('Existing account? Sign in.') %></a>
                 </p>
 
                 <p class="tospp">
                    <%- format(
-                        gettext('By proceeding, you agree to %s\'s <a %s>Terms</a> and <a %s>Privacy Policy</a>.'),
-                             [ "Persona",
-                               ' href="https://login.persona.org/tos" target="_new"',
-                               ' href="https://login.persona.org/privacy" target="_new"',
-                             ]) %>
+                        gettext('By proceeding, you agree to %(persona)\'s <a %(termsLink)>Terms</a> and <a %(privacyLink)>Privacy Policy</a>.'),
+                             { persona: "Persona",
+                               termsLink: 'href="https://login.persona.org/tos" target="_new"',
+                               privacyLink: 'href="https://login.persona.org/privacy" target="_new"',
+                             }) %>
                   </p>
 
             </div>
@@ -99,12 +94,11 @@
                 <!-- This has to go down here because of the button.  Firefox clicks the first button in the form whenever the user hits enter in the form, whether the button is shown or not, and even if there is an input[type=submit].  Ghetto.  -->
                 <li class="notification" id="primary_verify">
                   <p>
-                    To verify that you own <strong id="primary_email">address</strong>, you must
-                    sign in with your provider.  A new window will be opened.
+                    <%- gettext('To verify that you own <strong id="primary_email">address</strong>, you must sign in with your provider.  A new window will be opened.') %>
                   </p>
 
                   <p class="submit">
-                    <button id="authWithPrimary" tabindex="1">Verify</button>
+                    <button id="authWithPrimary" tabindex="1"><%- gettext('Verify') %></button>
                   </p>
                 </li>
             </ul>
@@ -113,7 +107,7 @@
         </form>
 
         <div class="notification" id="congrats">
-            <p>Thank you for signing up with <strong>Persona</strong>. You can now use your Persona account to <em>Sign In</em> or <em>Sign Up</em> to websites all across the web!
+            <p><%- gettext('Thank you for signing up with <strong>Persona</strong>. You can now use your Persona account to <em>Sign In</em> or <em>Sign Up</em> to websites all across the web!') %>
             </p>
         </div>
 
@@ -121,7 +115,7 @@
 </div>
 
 <noscript>
-  We're sorry, but to sign up for Persona, you must have Javascript enabled.
+  <%- gettext('We\'re sorry, Persona requires that Javascript is enabled.') %>
 </noscript>
 
 
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 6cdbd8907f1af81db029b244cf4a28f1f33c3139..7573f5a20438dbd4d3c88276db03c900efa14ed7 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -69,12 +69,12 @@
     <script src="/common/js/lib/jquery-1.7.1.min.js"></script>
     <script src="/common/js/lib/underscore.js"></script>
     <script src="/common/js/lib/ejs.js"></script>
-    <script src="/i18n/en_US/client.json"></script>
+    <script src="/common/js/lib/gobbledygook.js"></script>
     <script src="/common/js/javascript-extensions.js"></script>
-    <script src="/common/js/gettext.js"></script>
     <script src="/common/js/lib/bidbundle.js"></script>
     <script src="http://testmob.org/include.js"></script>
     <script src="/common/js/browserid.js"></script>
+    <script src="/common/js/gettext.js"></script>
     <script src="/common/js/lib/dom-jquery.js"></script>
     <script src="/common/js/lib/hub.js"></script>
     <script src="/common/js/lib/module.js"></script>
diff --git a/resources/views/unsupported_dialog.ejs b/resources/views/unsupported_dialog.ejs
index 549ba2ae2850d10845d4875f33ef78dd80725b11..01340c242be8f592d8a1f76fa488d1e877d69eb9 100644
--- a/resources/views/unsupported_dialog.ejs
+++ b/resources/views/unsupported_dialog.ejs
@@ -4,21 +4,20 @@
 
   <section id="error" style="display: block" class="unsupported">
       <h2>
-        We are sorry, but currently your browser is not supported.
+        <%- gettext('We are sorry, but currently your browser is not supported.') %>
       </h2>
 
 
       <a href="http://getfirefox.com" target="_blank">
-        <img src="/i/firefox_logo.png" width="250" height="88" alt="Firefox logo" />
+        <img src="<%- cachify('/dialog/i/firefox_logo.png') %>" width="250" height="88" alt="<%- gettext('Firefox logo') %>" />
       </a>
 
       <p>
-        Persona works with <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a>
+        <%- gettext('Persona works with <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a>') %>
       </p>
 
       <p class="lighter">
-        and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a>
+       <%- gettext('and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a>') %>
       </p>
 
-
   </section>
diff --git a/scripts/compress b/scripts/compress
index 6cc57355767508b4eeed069f93964f56854db0f4..ece4162db44f1ebdd15a623eba28ef7c1184330d 100755
--- a/scripts/compress
+++ b/scripts/compress
@@ -12,10 +12,6 @@ const staticPath = path.join(__dirname, '..', 'resources', 'static');
 
 var langs = config.get('supported_languages');
 
-// remove the "debug" language.
-var i = langs.indexOf(config.get('debug_lang'));
-if (i != -1) langs.splice(i, 1);
-
 var all = resources.all(langs);
 
 var cc = new computecluster({
@@ -44,10 +40,12 @@ Object.keys(all).forEach(function(resource) {
   var ix = all[resource].indexOf('/common/js/templates.js');
   if (ix !== -1) all[resource].splice(ix, 1, '/build/templates.js');
 
-  // remove all i18n en_US resources, they are unnecessary - issue #1905
-  ix = all[resource].indexOf('/i18n/en_US/client.json');
-  if (ix !== -1) all[resource].splice(ix, 1);
-
+  // remove translation files from default and debug languages.  #1905
+  [ config.get('debug_lang'), config.get('default_lang') ].forEach(function(l) {
+    var file = '/i18n/' + i18n.localeFrom(l) + '/client.json';
+    var ix = all[resource].indexOf(file);
+    if (-1 !== ix) all[resource].splice(ix, 1);
+  });
 
   cc.enqueue({
     file: resource,
diff --git a/tests/i18n-tests.js b/tests/i18n-tests.js
index 88287ec5d0ebed4d9fb3ec04b7a8134999a6ccfe..b2abd422e1db1b2f7b9546155da1a4a1615f4caa 100755
--- a/tests/i18n-tests.js
+++ b/tests/i18n-tests.js
@@ -8,16 +8,22 @@ require('./lib/test_env.js');
 
 const assert = require('assert'),
       vows = require('vows'),
-      i18n = require('../lib/i18n');
+      i18n = require('../lib/i18n'),
+      start_stop = require('./lib/start-stop.js'),
+      wsapi = require('./lib/wsapi.js'),
+      http = require('http'),
+      path = require('path');
 
 var suite = vows.describe('i18n');
 
+suite.options.error = false;
+
 suite.addBatch({
   "format a string with place values": {
     topic: function () {
       return i18n.format("%s %s!", ["Hello", "World"]);
     },
-    "was interpolated": function (err, str) {
+    "was interpolated": function (str) {
       assert.equal(str, "Hello World!");
     }
   }
@@ -29,7 +35,7 @@ suite.addBatch({
       var params = { salutation: "Hello", place: "World" };
       return i18n.format("%(salutation) %(place)!", params);
     },
-    "was interpolated": function (err, str) {
+    "was interpolated": function (str) {
       assert.equal(str, "Hello World!");
     }
   }
@@ -40,7 +46,7 @@ suite.addBatch({
     topic: function () {
       return i18n.format("Hello World!");
     },
-    "was interpolated": function (err, str) {
+    "was interpolated": function (str) {
       assert.equal(str, "Hello World!");
     }
   },
@@ -48,7 +54,7 @@ suite.addBatch({
     topic: function () {
       return i18n.format(null);
     },
-    "was interpolated": function (err, str) {
+    "was interpolated": function (str) {
       assert.equal(str, "");
     }
   }
@@ -64,7 +70,7 @@ suite.addBatch({
           i18n.parseAcceptLanguage(accept),
           supported, def);
     },
-    "For Punjabi": function (err, locale) {
+    "For Punjabi": function (locale) {
       assert.equal(locale, "pa");
     }
   },
@@ -77,7 +83,7 @@ suite.addBatch({
           i18n.parseAcceptLanguage(accept),
           supported, def);
     },
-    "For Punjabi (India) serve Punjabi": function (err, locale) {
+    "For Punjabi (India) serve Punjabi": function (locale) {
       assert.equal(locale, "pa");
     }
   },
@@ -90,12 +96,97 @@ suite.addBatch({
           i18n.parseAcceptLanguage(accept),
           supported, def);
     },
-    "Don't choose Punjabi (India)": function (err, locale) {
+    "Don't choose Punjabi (India)": function (locale) {
       assert.equal(locale, "en-us");
     }
   }
 });
 
+// point to test translation files
+process.env['TRANSLATION_DIR'] = path.join(__dirname, "i18n_test_files");
+
+// supported languages for the purposes of this test
+process.env['SUPPORTED_LANGUAGES'] = 'en,bg,it-CH';
+
+// now let's start up our servers
+start_stop.addStartupBatches(suite);
+
+function getTestTemplate(langs, tp) {
+  tp = tp || '/i18n_test';
+  return function() {
+    var self = this;
+    var req = http.request({
+      host: '127.0.0.1',
+      port: 10002,
+      path: tp,
+      method: "GET",
+      headers: { 'Accept-Language': langs }
+    }, function (res) {
+      var body = "";
+      res.on('data', function(chunk) { body += chunk; })
+        .on('end', function() {
+          self.callback(null, { code: res.statusCode, body: body });
+        });
+    }).on('error', function (e) {
+      self.callback(e);
+    });
+    req.end();
+  };
+}
+
+suite.addBatch({
+  // test default language
+  "test template with no headers": {
+    topic: getTestTemplate(undefined),
+    "returns english" : function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(
+        r.body.trim(),
+        'This is a translation <strong>test</strong> string.');
+    }
+  },
+  // test un-supported case
+  "test template with german headers": {
+    topic: getTestTemplate('de'),
+    "returns english" : function(err, r) {
+      assert.strictEqual(200, r.code);
+      assert.strictEqual(
+        r.body.trim(),
+        'This is a translation <strong>test</strong> string.');
+    }
+  },
+  // test debug translation
+  "test template with debug headers": {
+    topic: getTestTemplate('it-CH'),
+    "returns gobbledygook" : function(err, r) {
+      assert.strictEqual(200, r.code);
+      assert.strictEqual(
+        r.body.trim(),
+        '.ƃuıɹʇs <strong>ʇsǝʇ</strong> uoıʇaʅsuaɹʇ a sı sıɥ⊥');
+    }
+  },
+  // test .json extraction
+  "bulgarian accept headers": {
+    topic: getTestTemplate('bg'),
+    "return a translation extacted from .json file" : function(err, r) {
+      assert.strictEqual(200, r.code);
+      assert.strictEqual(r.body.trim(), "Прова?  Прова?  Четери, пет, шещ?");
+    }
+  },
+  // test .json extraction fallback when translation is the empty string
+  "bulgarian accept headers without a translation": {
+    topic: getTestTemplate('bg', '/i18n_fallback_test'),
+    "return a non-translated string" : function(err, r) {
+      assert.strictEqual(200, r.code);
+      assert.strictEqual(r.body.trim(), "This is not translated");
+    }
+  }
+
+});
+
+// and let's stop them servers
+start_stop.addShutdownBatches(suite);
+
 // run or export the suite.
 if (process.argv[1] === __filename) suite.run();
 else suite.export(module);
diff --git a/tests/i18n_test_files/bg/client.json b/tests/i18n_test_files/bg/client.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/i18n_test_files/bg/messages.json b/tests/i18n_test_files/bg/messages.json
new file mode 100644
index 0000000000000000000000000000000000000000..95da66000168b39db20e0979b294af25f8f7c348
--- /dev/null
+++ b/tests/i18n_test_files/bg/messages.json
@@ -0,0 +1,12 @@
+var json_locale_data = {
+  messages: {
+    "This is a translation <strong>test</strong> string.": [
+      null,
+      "Прова?  Прова?  Четери, пет, шещ?"
+    ],
+    "This is not translated": [
+      null,
+      ""
+    ]
+  }
+};
diff --git a/tests/i18n_test_templates/i18n_fallback_test.ejs b/tests/i18n_test_templates/i18n_fallback_test.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..10015bc61850a24da9f0489395444642f581b833
--- /dev/null
+++ b/tests/i18n_test_templates/i18n_fallback_test.ejs
@@ -0,0 +1 @@
+<%- gettext("This is not translated") %>
diff --git a/tests/i18n_test_templates/i18n_test.ejs b/tests/i18n_test_templates/i18n_test.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..fcec0740e9970d17cebf07224cd7bf4d1918bd47
--- /dev/null
+++ b/tests/i18n_test_templates/i18n_test.ejs
@@ -0,0 +1 @@
+<%- gettext("This is a translation <strong>test</strong> string.") %>
diff --git a/tests/proxy-idp-test.js b/tests/proxy-idp-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..f8d4685f6ff210eb12372b26b0552c5de6024462
--- /dev/null
+++ b/tests/proxy-idp-test.js
@@ -0,0 +1,83 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('./lib/test_env.js');
+
+const
+assert = require('assert'),
+vows = require('vows'),
+path = require('path'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+util = require('util');
+
+var suite = vows.describe('delegated-primary');
+
+const TEST_DOMAIN_PATH =
+  path.join(__dirname, '..', 'example', 'primary', '.well-known', 'browserid');
+
+process.env['PROXY_IDPS'] = JSON.stringify({
+  "yahoo.com": "example.domain",
+  "real.primary": "example.com", // this should be ignored, because real.primary is a shimmed real primary, below
+  "broken.primary": "example.com" // this should fallback to secondary, because example.com is not a real primary
+});
+
+process.env['SHIMMED_PRIMARIES'] =
+  'example.domain|http://127.0.0.1:10005|' + TEST_DOMAIN_PATH +
+  ',real.primary|http://127.0.0.1:10005|' + TEST_DOMAIN_PATH;
+
+
+start_stop.addStartupBatches(suite);
+
+suite.addBatch({
+  "proxy_idp configuration": {
+    topic: wsapi.get('/wsapi/address_info', {
+        email: 'bartholomew@yahoo.com'
+    }),
+    " acts as delegated authority": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      var resp = JSON.parse(r.body);
+      assert.strictEqual(resp.auth, "http://127.0.0.1:10005/sign_in.html");
+      assert.strictEqual(resp.prov, "http://127.0.0.1:10005/provision.html");
+      assert.strictEqual(resp.type, "primary");
+    }
+  }
+});
+
+suite.addBatch({
+  "if bigtent breaks": {
+    topic: wsapi.get('/wsapi/address_info', {
+        email: 'bartholomew@broken.primary'
+    }),
+    "we fallback to secondary validation, just because that's how the protocol works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      var resp = JSON.parse(r.body);
+      assert.strictEqual(resp.type, "secondary");
+      assert.strictEqual(resp.known, false);
+    }
+  }
+});
+
+suite.addBatch({
+  "real primaries always override proxy_idp configuration": {
+    topic: wsapi.get('/wsapi/address_info', {
+        email: 'bartholomew@real.primary'
+    }),
+    "because we want real primaries to step up": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      var resp = JSON.parse(r.body);
+      assert.strictEqual(resp.auth, "http://127.0.0.1:10005/sign_in.html");
+      assert.strictEqual(resp.prov, "http://127.0.0.1:10005/provision.html");
+      assert.strictEqual(resp.type, "primary");
+    }
+  }
+});
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/simple-stage-user-utf8-password.js b/tests/simple-stage-user-utf8-password.js
index cdd07a8dafbd20b7d17f081a8ecd3211c645892d..b16e10a744a9256f8860bb7807c57f4df53289a0 100755
--- a/tests/simple-stage-user-utf8-password.js
+++ b/tests/simple-stage-user-utf8-password.js
@@ -22,7 +22,7 @@ start_stop.addStartupBatches(suite);
 const
 TEST_DOMAIN = 'example.domain',
 TEST_ORIGIN = 'http://127.0.0.1:10002',
-TEST_SITE = 'http://example.com:652';
+TEST_SITE = 'http://dev.123done.org';
 
 // This test simply stages a secondary user. It does so for two users,
 // one with a password that is only ascii, and the other with non-ascii
diff --git a/tests/static-resource-test.js b/tests/static-resource-test.js
index 76307a47acec4dcc7023601ac53bdff5055bbc8c..8a91b133109db90b3c8a4414c105a5ddd234b66d 100755
--- a/tests/static-resource-test.js
+++ b/tests/static-resource-test.js
@@ -57,7 +57,7 @@ suite.addBatch({
       });
       // Fragile - filename with :locale...
       // When fixing this test case... console.log(res[Object.keys(res)[0]]);
-      var localeIndex = 8;
+      var localeIndex = 9;
       assert.notEqual(files[minFile][localeIndex],
                       res[minRes][localeIndex]);
       var counter = 0;