diff --git a/grunttasks/build.js b/grunttasks/build.js index d0cb59a07d..dbe9b6481e 100644 --- a/grunttasks/build.js +++ b/grunttasks/build.js @@ -9,6 +9,8 @@ module.exports = function (grunt) { // Clean everything 'clean', 'selectconfig:dist', + 'l10n-generate-tos-pp:dist', + 'l10n-create-json', // l10n-generate-pages needs to be run before useminPrepare to seed // the list of resources to minimize. Generated pages are placed into // `server/templates/pages/dist` where they will be post-processed @@ -17,8 +19,6 @@ module.exports = function (grunt) { // use error pages from en_US as the static error pages 'copy:error_pages', 'useminPrepare', - 'l10n-create-json', - 'l10n-generate-tos-pp:dist', 'requirejs', 'css', 'concurrent:dist', diff --git a/grunttasks/l10n-generate-pages.js b/grunttasks/l10n-generate-pages.js index 92c0ff2648..8a875bf7dc 100644 --- a/grunttasks/l10n-generate-pages.js +++ b/grunttasks/l10n-generate-pages.js @@ -11,9 +11,15 @@ module.exports = function (grunt) { var path = require('path'); var Handlebars = require('handlebars'); + var Promise = require('bluebird'); + var legalTemplates = require('../server/lib/legal-templates'); + var defaultLang; var templateSrc; var templateDest; + var templates = { + 'db-LB': {} + }; // Make the 'gettext' function available in the templates. Handlebars.registerHelper('t', function (string) { @@ -28,17 +34,40 @@ module.exports = function (grunt) { grunt.registerTask('l10n-generate-pages', 'Generate localized versions of the static pages', function () { + var done = this.async(); + var i18n = require('../server/lib/i18n')(grunt.config.get('server.i18n')); // server config is set in the selectconfig task var supportedLanguages = grunt.config.get('server.i18n.supportedLanguages'); + defaultLang = grunt.config.get('server.i18n.defaultLang'); templateSrc = grunt.config.get('yeoman.page_template_src'); templateDest = grunt.config.get('yeoman.page_template_dist'); - supportedLanguages.forEach(function (lang) { - generatePagesForLanguage(i18n, lang); - }); + var getTemplate = legalTemplates(i18n, templateDest); + + // Create a cache of the templates so we can reference them synchronously later + Promise.settle(supportedLanguages.map(function (lang) { + + return Promise.all([ + getTemplate('terms', lang, defaultLang), + getTemplate('privacy', lang, defaultLang) + ]) + .then(function (temps) { + templates[lang] = { + terms: temps[0], + privacy: temps[1] + }; + }); + + })).then(function () { + supportedLanguages.forEach(function (lang) { + generatePagesForLanguage(i18n, lang); + }); + done(); + }).then(null, done); + }); @@ -61,13 +90,17 @@ module.exports = function (grunt) { grunt.file.copy(srcPath, destPath, { process: function (contents, path) { + var terms = templates[context.lang].terms || templates[defaultLang].terms; + var privacy = templates[context.lang].privacy || templates[defaultLang].privacy; var template = Handlebars.compile(contents); var out = template({ l10n: context, locale: context.locale, lang: context.lang, lang_dir: context.lang_dir, - fontSupportDisabled: context.fontSupportDisabled + fontSupportDisabled: context.fontSupportDisabled, + terms: terms, + privacy: privacy }); return out; } diff --git a/server/lib/legal-templates.js b/server/lib/legal-templates.js new file mode 100644 index 0000000000..463eff9d0a --- /dev/null +++ b/server/lib/legal-templates.js @@ -0,0 +1,69 @@ +/* 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/. */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var Promise = require('bluebird'); +var logger = require('intel').getLogger('legal-templates'); + +module.exports = function (i18n, root) { + + var TOS_ROOT_PATH = path.join(root, 'terms'); + var PP_ROOT_PATH = path.join(root, 'privacy'); + + function getRoot(type) { + return type === 'terms' ? TOS_ROOT_PATH : PP_ROOT_PATH; + } + + var templateCache = {}; + function getTemplate(type, lang, defaultLang) { + var DEFAULT_LOCALE = i18n.localeFrom(defaultLang); + + // Filenames are normalized to locale, not language. + var locale = i18n.localeFrom(lang); + var templatePath = path.join(getRoot(type), locale + '.html'); + var resolver = Promise.defer(); + + // cache the promises to avoid multiple concurrent checks for + // the same template due to async calls to the file system. + if (templateCache[templatePath]) { + resolver.resolve(templateCache[templatePath]); + return resolver.promise; + } + + fs.exists(templatePath, function (exists) { + if (! exists) { + var bestLang = i18n.bestLanguage(i18n.parseAcceptLanguage(lang)); + + if (locale === DEFAULT_LOCALE) { + var err = new Error(type + ' missing `' + DEFAULT_LOCALE + '` template: ' + templatePath); + return resolver.reject(err); + } else if (lang !== bestLang) { + logger.warn('`%s` does not exist, trying next best `%s`', lang, bestLang); + return resolver.resolve(getTemplate(type, bestLang, defaultLang)); + } + + + templateCache[templatePath] = null; + return resolver.resolve(null); + } + + fs.readFile(templatePath, 'utf8', function(err, data) { + if (err) { + return resolver.reject(err); + } + + templateCache[templatePath] = data; + resolver.resolve(data); + }); + }); + + return resolver.promise; + } + + return getTemplate; + +}; diff --git a/server/lib/routes/get-terms-privacy.js b/server/lib/routes/get-terms-privacy.js index 76f95365e6..bc702e7808 100644 --- a/server/lib/routes/get-terms-privacy.js +++ b/server/lib/routes/get-terms-privacy.js @@ -25,14 +25,13 @@ var Promise = require('bluebird'); var config = require('../configuration'); var PAGE_TEMPLATE_DIRECTORY = path.join(config.get('page_template_root'), 'dist'); -var TOS_ROOT_PATH = path.join(PAGE_TEMPLATE_DIRECTORY, 'terms'); -var PP_ROOT_PATH = path.join(PAGE_TEMPLATE_DIRECTORY, 'privacy'); +var templates = require('../legal-templates'); module.exports = function verRoute (i18n) { - var DEFAULT_LANG = config.get('i18n.defaultLang'); - var DEFAULT_LOCALE = i18n.localeFrom(DEFAULT_LANG); + + var getTemplate = templates(i18n, PAGE_TEMPLATE_DIRECTORY); var route = {}; route.method = 'get'; @@ -44,54 +43,6 @@ module.exports = function verRoute (i18n) { // * //legal/privacy route.path = /^\/(?:([a-zA-Z-\_]*)\/)?legal\/(terms|privacy)(?:\/)?$/; - function getRoot(type) { - return type === 'terms' ? TOS_ROOT_PATH : PP_ROOT_PATH; - } - - var templateCache = {}; - function getTemplate(type, lang) { - // Filenames are normalized to locale, not language. - var locale = i18n.localeFrom(lang); - var templatePath = path.join(getRoot(type), locale + '.html'); - var resolver = Promise.defer(); - - // cache the promises to avoid multiple concurrent checks for - // the same template due to async calls to the file system. - if (templateCache[templatePath]) { - resolver.resolve(templateCache[templatePath]); - return resolver.promise; - } - - fs.exists(templatePath, function (exists) { - if (! exists) { - var bestLang = i18n.bestLanguage(i18n.parseAcceptLanguage(lang)); - - if (locale === DEFAULT_LOCALE) { - var err = new Error(type + ' missing `' + DEFAULT_LOCALE + '` template: ' + templatePath); - return resolver.reject(err); - } else if (lang !== bestLang) { - logger.warn('`%s` does not exist, trying next best `%s`', lang, bestLang); - return resolver.resolve(getTemplate(type, bestLang)); - } - - - templateCache[templatePath] = null; - return resolver.resolve(null); - } - - fs.readFile(templatePath, 'utf8', function(err, data) { - if (err) { - return resolver.reject(err); - } - - templateCache[templatePath] = data; - resolver.resolve(data); - }); - }); - - return resolver.promise; - } - route.process = function (req, res) { var lang = req.params[0]; var page = req.params[1]; @@ -101,7 +52,7 @@ module.exports = function verRoute (i18n) { return res.redirect(getRedirectURL(req.lang || DEFAULT_LANG, page)); } - getTemplate(page, lang) + getTemplate(page, lang, DEFAULT_LANG) .then(function (template) { if (! template) { logger.warn('%s->`%s` does not exist, redirecting to `%s`', @@ -114,9 +65,12 @@ module.exports = function verRoute (i18n) { res.send(template); }, 'text/html': function () { + var context = {}; + context[page] = template; + // the HTML page removes the header to allow embedding. res.removeHeader('X-FRAME-OPTIONS'); - res.render(page, { body: template }); + res.render(page, context); } }); }, function(err) { diff --git a/server/templates/pages/src/privacy.html b/server/templates/pages/src/privacy.html index e99dbf7abd..62789b7b17 100644 --- a/server/templates/pages/src/privacy.html +++ b/server/templates/pages/src/privacy.html @@ -34,7 +34,7 @@

{{#t}}Privacy Notice{{/t}}

diff --git a/server/templates/pages/src/terms.html b/server/templates/pages/src/terms.html index ee6a0b4d93..3b5526b308 100644 --- a/server/templates/pages/src/terms.html +++ b/server/templates/pages/src/terms.html @@ -34,7 +34,7 @@

{{#t}}Terms of Service{{/t}}

diff --git a/tests/functional/legal.js b/tests/functional/legal.js index 7fdf051b2f..54b8417580 100644 --- a/tests/functional/legal.js +++ b/tests/functional/legal.js @@ -49,6 +49,36 @@ define([ // success is going back to the legal screen. .waitForElementById('fxa-legal-header') .end(); + }, + + 'start at terms page': function () { + + return this.get('remote') + .get(require.toUrl(url + '/terms')) + .waitForVisibleByCssSelector('#legal-copy') + + .elementById('legal-copy') + .text() + .then(function (resultText) { + // the legal text shouldn't be empty + assert.ok(resultText.trim().length); + }) + .end(); + }, + + 'start at privacy page': function () { + + return this.get('remote') + .get(require.toUrl(url + '/privacy')) + .waitForVisibleByCssSelector('#legal-copy') + + .elementById('legal-copy') + .text() + .then(function (resultText) { + // the legal text shouldn't be empty + assert.ok(resultText.trim().length); + }) + .end(); } }); });