From d5ab2577dd755a16cd7bcd28b15bae42910cb1b1 Mon Sep 17 00:00:00 2001 From: Fabien Cazenave Date: Sun, 23 Dec 2012 18:55:36 +0100 Subject: [PATCH] bug 815852: inline l10n resources to avoid flickering on all localized apps + don't zip */locales/* if l10n resources are inlined + ensure we don't have to clobber the profile to rebuild Gaia r=ochameau, a=21 --- Makefile | 3 ++ build/webapp-l10n.js | 92 ++++++++++++++++++++++++++++++++++++-------- build/webapp-zip.js | 13 +++++-- shared/js/l10n.js | 33 +++++++++++++--- 4 files changed, 116 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 65bdb0df5f6d..3dafb0f869d3 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,7 @@ GAIA_LOCALES_PATH?=locales LOCALES_FILE?=shared/resources/languages.json GAIA_LOCALE_SRCDIRS=shared $(GAIA_APP_SRCDIRS) GAIA_DEFAULT_LOCALE?=en-US +GAIA_INLINE_LOCALES?=1 ############################################################################### # The above rules generate the profile/ folder and all its content. # @@ -316,10 +317,12 @@ define run-js-command const HOMESCREEN = "$(HOMESCREEN)"; const GAIA_PORT = "$(GAIA_PORT)"; \ const GAIA_APP_SRCDIRS = "$(GAIA_APP_SRCDIRS)"; \ const GAIA_LOCALES_PATH = "$(GAIA_LOCALES_PATH)"; \ + const LOCALES_FILE = "$(LOCALES_FILE)"; \ const BUILD_APP_NAME = "$(BUILD_APP_NAME)"; \ const PRODUCTION = "$(PRODUCTION)"; \ const OFFICIAL = "$(MOZILLA_OFFICIAL)"; \ const GAIA_DEFAULT_LOCALE = "$(GAIA_DEFAULT_LOCALE)"; \ + const GAIA_INLINE_LOCALES = "$(GAIA_INLINE_LOCALES)"; \ const GAIA_ENGINE = "xpcshell"; \ '; \ $(XULRUNNERSDK) $(XPCSHELLSDK) -e "$$JS_CONSTS" -f build/utils.js "build/$(strip $1).js" diff --git a/build/webapp-l10n.js b/build/webapp-l10n.js index 8516be8bc8b1..95c4e17bc747 100644 --- a/build/webapp-l10n.js +++ b/build/webapp-l10n.js @@ -5,12 +5,25 @@ function debug(str) { /** - * Expose a global `l10nTarget' object and load `l10n.js' in it + * Expose a global `l10nTarget' object and load `l10n.js' in it -- + * note: the `?reload' trick ensures we don't load a cached `l10njs' library. */ var l10nTarget = { navigator: {} }; -Services.scriptloader. - loadSubScript('file:///' + GAIA_DIR + '/shared/js/l10n.js', l10nTarget); +Services.scriptloader.loadSubScript('file:///' + GAIA_DIR + + '/shared/js/l10n.js?reload=' + new Date().getTime(), l10nTarget); + + +/** + * Locale list -- by default, only the default one + */ + +var l10nLocales = [GAIA_DEFAULT_LOCALE]; +var l10nDictionary = { + locales: {}, + default_locale: GAIA_DEFAULT_LOCALE +}; +l10nDictionary.locales[GAIA_DEFAULT_LOCALE] = {}; /** @@ -47,7 +60,22 @@ function l10n_getFileContent(webapp, htmlFile, relativePath) { } } -function l10n_serializeHTMLDocument(file, doc) { +function l10n_embedExternalResources(doc, dictionary) { + // remove all external l10n resource nodes + var resources = doc.querySelectorAll('link[type="application/l10n"]'); + for (let i = 0; i < resources.length; i++) { + let res = resources[i].outerHTML; + resources[i].outerHTML = ''; + } + + // put the current dictionary in an inline JSON script + let script = doc.createElement('script'); + script.type = 'application/l10n'; + script.innerHTML = '\n ' + JSON.stringify(dictionary) + '\n'; + doc.documentElement.appendChild(script); +} + +function l10n_serializeHTMLDocument(doc, file) { debug('saving: ' + file.path); // the doctype string should always be '' but just in case... @@ -72,7 +100,7 @@ function l10n_serializeHTMLDocument(file, doc) { htmlStr += ' ' + attrs[i].nodeName.toLowerCase() + '="' + attrs[i].nodeValue + '"'; } - let innerHTML = docElt.innerHTML.replace(/ \n\n\n<\/body>$/, ' '); + let innerHTML = docElt.innerHTML.replace(/ \n*<\/body>\n*/, ' \n'); htmlStr += '>\n ' + innerHTML + '\n\n'; writeContent(file, doctypeStr + htmlStr); @@ -81,6 +109,9 @@ function l10n_serializeHTMLDocument(file, doc) { function l10n_compile(webapp, file) { let mozL10n = l10nTarget.navigator.mozL10n; + let processedLocales = 0; + let dictionary = l10nDictionary; + // catch console.[log|warn|info] calls and redirect them to `dump()' // XXX for some reason, this won't work if gDEBUG >= 2 in l10n.js function l10n_dump(str) { @@ -111,17 +142,28 @@ function l10n_compile(webapp, file) { // catch the `localized' event dispatched by `fireL10nReadyEvent()' l10nTarget.dispatchEvent = function() { - debug('fireL10nReadyEvent'); - let docElt = l10nTarget.document.documentElement; - - // set the lang/dir attributes of the current document - docElt.dir = mozL10n.language.direction; - docElt.lang = mozL10n.language.code; + processedLocales++; + debug('fireL10nReadyEvent - ' + + processedLocales + '/' + l10nLocales.length); - // save localized document - let newPath = file.path + '.' + docElt.lang; - let newFile = new FileUtils.File(newPath); - l10n_serializeHTMLDocument(newFile, l10nTarget.document); + let docElt = l10nTarget.document.documentElement; + dictionary.locales[mozL10n.language.code] = mozL10n.dictionary; + + if (processedLocales < l10nLocales.length) { + // load next locale + mozL10n.language.code = l10nLocales[processedLocales]; + } else { + // we expect the last locale to be the default one: + // set the lang/dir attributes of the current document + docElt.dir = mozL10n.language.direction; + docElt.lang = mozL10n.language.code; + + // save localized document + let newPath = file.path + '.' + GAIA_DEFAULT_LOCALE; + let newFile = new FileUtils.File(newPath); + l10n_embedExternalResources(l10nTarget.document, dictionary); + l10n_serializeHTMLDocument(l10nTarget.document, newFile); + } }; // load and parse the HTML document @@ -133,7 +175,7 @@ function l10n_compile(webapp, file) { // selecting a language triggers `XMLHttpRequest' and `dispatchEvent' above if (l10nTarget.document.querySelector('script[src$="l10n.js"]')) { debug('localizing: ' + file.path); - mozL10n.language.code = GAIA_DEFAULT_LOCALE; + mozL10n.language.code = l10nLocales[processedLocales]; } } @@ -144,6 +186,24 @@ function l10n_compile(webapp, file) { debug('Begin'); +if (GAIA_INLINE_LOCALES) { + l10nLocales = []; + l10nDictionary.locales = {}; + + let file = getFile(GAIA_DIR + '/' + LOCALES_FILE); + let locales = JSON.parse(getFileContent(file)); + + // we keep the default locale order for `l10nDictionary.locales', + // but we ensure the default locale comes last in `l10nLocales'. + for (let lang in locales) { + if (lang != GAIA_DEFAULT_LOCALE) { + l10nLocales.push(lang); + } + l10nDictionary.locales[lang] = {}; + } + l10nLocales.push(GAIA_DEFAULT_LOCALE); +} + Gaia.webapps.forEach(function(webapp) { // if BUILD_APP_NAME isn't `*`, we only accept one webapp if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME) diff --git a/build/webapp-zip.js b/build/webapp-zip.js index 6da916c0e793..cd2a303f3ba3 100644 --- a/build/webapp-zip.js +++ b/build/webapp-zip.js @@ -65,6 +65,7 @@ function addToZip(zip, pathInZip, file) { // Case 2/ Directory else if (file.isDirectory()) { debug(' +directory to zip ' + pathInZip); + if (!zip.hasEntry(pathInZip)) zip.addEntryDirectory(pathInZip, file.lastModifiedTime, false); @@ -150,6 +151,10 @@ Gaia.webapps.forEach(function(webapp) { debug('# Create zip for: ' + webapp.domain); let files = ls(webapp.sourceDirectoryFile); files.forEach(function(file) { + // Ignore l10n files if they have been inlined + if (GAIA_INLINE_LOCALES && + (file.leafName === 'locales' || file.leafName === 'locales.ini')) + return; // Ignore files from /shared directory (these files were created by // Makefile code). Also ignore files in the /test directory. if (file.leafName !== 'shared' && file.leafName !== 'test') @@ -190,9 +195,11 @@ Gaia.webapps.forEach(function(webapp) { used.js.push(path); break; case 'locales': - let localeName = path.substr(0, path.lastIndexOf('.')); - if (used.locales.indexOf(localeName) == -1) { - used.locales.push(localeName); + if (!GAIA_INLINE_LOCALES) { + let localeName = path.substr(0, path.lastIndexOf('.')); + if (used.locales.indexOf(localeName) == -1) { + used.locales.push(localeName); + } } break; case 'resources': diff --git a/shared/js/l10n.js b/shared/js/l10n.js index 7d3a60b830f2..8d9517a0d0b7 100644 --- a/shared/js/l10n.js +++ b/shared/js/l10n.js @@ -65,6 +65,12 @@ return document.querySelectorAll('link[type="application/l10n"]'); } + function getL10nDictionary() { + var script = document.querySelector('script[type="application/l10n"]'); + // TODO: support multiple and external JSON dictionaries + return script ? JSON.parse(script.innerHTML) : null; + } + function getTranslatableChildren(element) { return element ? element.querySelectorAll('*[data-l10n-id]') : []; } @@ -262,6 +268,8 @@ // load and parse all resources for the specified locale function loadLocale(lang, callback) { + callback = callback || function _callback() {}; + clear(); gLanguage = lang; @@ -270,7 +278,16 @@ var langLinks = getL10nResourceLinks(); var langCount = langLinks.length; if (langCount == 0) { - consoleLog('no resource to load, early way out'); + // we might have a pre-compiled dictionary instead + var dict = getL10nDictionary(); + if (dict && dict.locales && dict.default_locale) { + consoleLog('using the embedded JSON directory, early way out'); + gL10nData = dict.locales[lang] || dict.locales[dict.default_locale]; + callback(); + } else { + consoleLog('no resource to load, early way out'); + } + // early way out fireL10nReadyEvent(lang); gReadyState = 'complete'; return; @@ -282,9 +299,7 @@ onResourceLoaded = function() { gResourceCount++; if (gResourceCount >= langCount) { - if (callback) { // execute the [optional] callback - callback(); - } + callback(); fireL10nReadyEvent(lang); gReadyState = 'complete'; } @@ -910,6 +925,9 @@ /** * Startup & Public API + * + * This section is quite specific to the B2G project: old browsers are not + * supported and the API is slightly different from the standard webl10n one. */ // load the default locale on startup @@ -943,7 +961,7 @@ }); } - // Public API + // public API navigator.mozL10n = { // get a localized string get: function l10n_get(key, args, fallback) { @@ -974,7 +992,10 @@ // translate an element or document fragment translate: translateFragment, - // this can be used to avoid race conditions + // get (a clone of) the dictionary for the current locale + get dictionary() { return JSON.parse(JSON.stringify(gL10nData)); }, + + // this can be used to prevent race conditions get readyState() { return gReadyState; } };