From 073a686f0b56e52467441e4f27c518dc26d1bf43 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 9 Jul 2014 17:53:27 -0400 Subject: [PATCH] Update l10n.js, fix error in firetext.js --- scripts/firetext.js | 8 +- scripts/lib/l10n.js | 456 +++++++++++++++++++++++--------------------- 2 files changed, 245 insertions(+), 219 deletions(-) diff --git a/scripts/firetext.js b/scripts/firetext.js index 3fe0c8c..d4143f2 100644 --- a/scripts/firetext.js +++ b/scripts/firetext.js @@ -51,6 +51,9 @@ firetext.init = function () { getURLs(function(){ }); + + // Initialize Settings + firetext.settings.init(); // Initialize language handler firetext.language(firetext.settings.get('language')); @@ -105,10 +108,7 @@ firetext.init = function () { regions.nav('welcome'); // Initialize the editor - initEditor(function() { - // Initialize Settings - firetext.settings.init(); - + initEditor(function() { // Init extIcon extIcon(); diff --git a/scripts/lib/l10n.js b/scripts/lib/l10n.js index a9cd22c..1cf44a4 100644 --- a/scripts/lib/l10n.js +++ b/scripts/lib/l10n.js @@ -2,6 +2,16 @@ 'use strict'; /* jshint validthis:true */ + function L10nError(message, id, loc) { + this.name = 'L10nError'; + this.message = message; + this.id = id; + this.loc = loc; + } + L10nError.prototype = Object.create(Error.prototype); + L10nError.prototype.constructor = L10nError; + + /* jshint browser:true */ var io = { @@ -18,7 +28,7 @@ if (e.target.status === 200 || e.target.status === 0) { callback(null, e.target.responseText); } else { - callback(new Error('Not found: ' + url)); + callback(new L10nError('Not found: ' + url)); } }); xhr.addEventListener('error', callback); @@ -28,7 +38,7 @@ try { xhr.send(null); } catch (e) { - callback(new Error('Not found: ' + url)); + callback(new L10nError('Not found: ' + url)); } }, @@ -46,7 +56,7 @@ if (e.target.status === 200 || e.target.status === 0) { callback(null, e.target.response); } else { - callback(new Error('Not found: ' + url)); + callback(new L10nError('Not found: ' + url)); } }); xhr.addEventListener('error', callback); @@ -56,7 +66,7 @@ try { xhr.send(null); } catch (e) { - callback(new Error('Not found: ' + url)); + callback(new L10nError('Not found: ' + url)); } } }; @@ -284,7 +294,7 @@ return list.indexOf(n) !== -1; } function isBetween(n, start, end) { - return start <= n && n <= end; + return typeof n === typeof start && start <= n && n <= end; } // list of all plural rules methods: @@ -559,155 +569,135 @@ - var nestedProps = ['style', 'dataset']; - var parsePatterns; + function PropertiesParser() { + var parsePatterns = { + comment: /^\s*#|^\s*$/, + entity: /^([^=\s]+)\s*=\s*(.+)$/, + multiline: /[^\\]\\$/, + macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i, + unicode: /\\u([0-9a-fA-F]{1,4})/g, + entries: /[\r\n]+/, + controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g + }; - function parse(ctx, source) { - var ast = {}; + this.parse = function (ctx, source) { + var ast = Object.create(null); - if (!parsePatterns) { - parsePatterns = { - comment: /^\s*#|^\s*$/, - entity: /^([^=\s]+)\s*=\s*(.+)$/, - multiline: /[^\\]\\$/, - macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i, - unicode: /\\u([0-9a-fA-F]{1,4})/g, - entries: /[\r\n]+/, - controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g - }; - } - - var entries = source.split(parsePatterns.entries); - for (var i = 0; i < entries.length; i++) { - var line = entries[i]; + var entries = source.split(parsePatterns.entries); + for (var i = 0; i < entries.length; i++) { + var line = entries[i]; - if (parsePatterns.comment.test(line)) { - continue; - } + if (parsePatterns.comment.test(line)) { + continue; + } - while (parsePatterns.multiline.test(line) && i < entries.length) { - line = line.slice(0, -1) + entries[++i].trim(); - } + while (parsePatterns.multiline.test(line) && i < entries.length) { + line = line.slice(0, -1) + entries[++i].trim(); + } - var entityMatch = line.match(parsePatterns.entity); - if (entityMatch) { - try { - parseEntity(entityMatch[1], entityMatch[2], ast); - } catch (e) { - if (ctx) { - ctx._emitter.emit('error', e); - } else { - throw e; + var entityMatch = line.match(parsePatterns.entity); + if (entityMatch) { + try { + parseEntity(entityMatch[1], entityMatch[2], ast); + } catch (e) { + if (ctx) { + ctx._emitter.emit('error', e); + } else { + throw e; + } } } } - } - return ast; - } + return ast; + }; - function setEntityValue(id, attr, key, value, ast) { - var obj = ast; - var prop = id; + function setEntityValue(id, attr, key, value, ast) { + var obj = ast; + var prop = id; - if (attr) { - if (!(id in obj)) { - obj[id] = {}; + if (attr) { + if (!(id in obj)) { + obj[id] = {}; + } + if (typeof(obj[id]) === 'string') { + obj[id] = {'_': obj[id]}; + } + obj = obj[id]; + prop = attr; } - if (typeof(obj[id]) === 'string') { - obj[id] = {'_': obj[id]}; + + if (!key) { + obj[prop] = value; + return; } - obj = obj[id]; - prop = attr; - } - if (!key) { - obj[prop] = value; - return; + if (!(prop in obj)) { + obj[prop] = {'_': {}}; + } else if (typeof(obj[prop]) === 'string') { + obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}}; + } + obj[prop]._[key] = value; } - if (!(prop in obj)) { - obj[prop] = {'_': {}}; - } else if (typeof(obj[prop]) === 'string') { - obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}}; - } - obj[prop]._[key] = value; - } + function parseEntity(id, value, ast) { + var name, key; - function parseEntity(id, value, ast) { - var name, key; + var pos = id.indexOf('['); + if (pos !== -1) { + name = id.substr(0, pos); + key = id.substring(pos + 1, id.length - 1); + } else { + name = id; + key = null; + } - var pos = id.indexOf('['); - if (pos !== -1) { - name = id.substr(0, pos); - key = id.substring(pos + 1, id.length - 1); - } else { - name = id; - key = null; - } + var nameElements = name.split('.'); - var nameElements = name.split('.'); + if (nameElements.length > 2) { + throw new Error('Error in ID: "' + name + '".' + + ' Nested attributes are not supported.'); + } - var attr; - if (nameElements.length > 1) { - var attrElements = []; - attrElements.push(nameElements.pop()); + var attr; if (nameElements.length > 1) { - // Usually the last dot separates an attribute from an id - // - // In case when there are more than one dot in the id - // and the second to last item is "style" or "dataset" then the last two - // items are becoming the attribute. - // - // ex. - // id.style.color = foo => - // - // id: - // style.color: foo - // - // id.other.color = foo => - // - // id.other: - // color: foo - if (nestedProps.indexOf(nameElements[nameElements.length - 1]) !== -1) { - attrElements.push(nameElements.pop()); - } + name = nameElements[0]; + attr = nameElements[1]; + } else { + attr = null; } - name = nameElements.join('.'); - attr = attrElements.reverse().join('.'); - } else { - attr = null; - } - setEntityValue(name, attr, key, unescapeString(value), ast); - } + setEntityValue(name, attr, key, unescapeString(value), ast); + } - function unescapeControlCharacters(str) { - return str.replace(parsePatterns.controlChars, '$1'); - } + function unescapeControlCharacters(str) { + return str.replace(parsePatterns.controlChars, '$1'); + } - function unescapeUnicode(str) { - return str.replace(parsePatterns.unicode, function(match, token) { - return unescape('%u' + '0000'.slice(token.length) + token); - }); - } + function unescapeUnicode(str) { + return str.replace(parsePatterns.unicode, function(match, token) { + return unescape('%u' + '0000'.slice(token.length) + token); + }); + } - function unescapeString(str) { - if (str.lastIndexOf('\\') !== -1) { - str = unescapeControlCharacters(str); + function unescapeString(str) { + if (str.lastIndexOf('\\') !== -1) { + str = unescapeControlCharacters(str); + } + return unescapeUnicode(str); } - return unescapeUnicode(str); - } - function parseMacro(str) { - var match = str.match(parsePatterns.macro); - if (!match) { - throw new Error('Malformed macro'); + function parseMacro(str) { + var match = str.match(parsePatterns.macro); + if (!match) { + throw new L10nError('Malformed macro'); + } + return [match[1], match[2]]; } - return [match[1], match[2]]; } + var MAX_PLACEABLE_LENGTH = 2500; var MAX_PLACEABLES = 100; var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g; @@ -723,9 +713,9 @@ } else { // it's either a hash or it has attrs, or both for (var key in node) { - if (node.hasOwnProperty(key) && key[0] !== '_') { + if (key[0] !== '_') { if (!this.attributes) { - this.attributes = {}; + this.attributes = Object.create(null); } this.attributes[key] = new Entity(this.id + '.' + key, node[key], env); @@ -768,13 +758,12 @@ var entity = { value: this.toString(ctxdata), - attributes: {} + attributes: Object.create(null) }; for (var key in this.attributes) { - if (this.attributes.hasOwnProperty(key)) { - entity.attributes[key] = this.attributes[key].toString(ctxdata); - } + /* jshint -W089 */ + entity.attributes[key] = this.attributes[key].toString(ctxdata); } return entity; @@ -787,7 +776,9 @@ return ctxdata[id]; } - if (env.hasOwnProperty(id)) { + // XXX: special case for Node.js where still: + // '__proto__' in Object.create(null) => true + if (id in env && id !== '__proto__') { if (!(env[id] instanceof Entity)) { env[id] = new Entity(id, env[id], env); } @@ -795,8 +786,9 @@ if (typeof value === 'string') { // prevent Billion Laughs attacks if (value.length >= MAX_PLACEABLE_LENGTH) { - throw new Error('Too many characters in placeable (' + value.length + - ', max allowed is ' + MAX_PLACEABLE_LENGTH + ')'); + throw new L10nError('Too many characters in placeable (' + + value.length + ', max allowed is ' + + MAX_PLACEABLE_LENGTH + ')'); } return value; } @@ -809,8 +801,8 @@ var value = str.replace(rePlaceables, function(match, id) { // prevent Quadratic Blowup attacks if (placeablesCount++ >= MAX_PLACEABLES) { - throw new Error('Too many placeables (' + placeablesCount + - ', max allowed is ' + MAX_PLACEABLES + ')'); + throw new L10nError('Too many placeables (' + placeablesCount + + ', max allowed is ' + MAX_PLACEABLES + ')'); } return subPlaceable(ctxdata, env, match, id); }); @@ -860,24 +852,24 @@ } function compile(env, ast) { - env = env || {}; + /* jshint -W089 */ + env = env || Object.create(null); for (var id in ast) { - if (ast.hasOwnProperty(id)) { - env[id] = new Entity(id, ast[id], env); - } + env[id] = new Entity(id, ast[id], env); } return env; } + var propertiesParser = null; + function Locale(id, ctx) { this.id = id; this.ctx = ctx; this.isReady = false; - this.entries = { - __plural: getPluralRule(id) - }; + this.entries = Object.create(null); + this.entries.__plural = getPluralRule(id); } Locale.prototype.getEntry = function L_getEntry(id) { @@ -885,7 +877,7 @@ var entries = this.entries; - if (!entries.hasOwnProperty(id)) { + if (!(id in entries)) { return undefined; } @@ -929,7 +921,10 @@ function onPropLoaded(err, source) { if (!err && source) { - var ast = parse(ctx, source); + if (!propertiesParser) { + propertiesParser = new PropertiesParser(); + } + var ast = propertiesParser.parse(ctx, source); self.addAST(ast); } onL10nLoaded(err); @@ -952,10 +947,9 @@ }; Locale.prototype.addAST = function(ast) { + /* jshint -W089 */ for (var id in ast) { - if (ast.hasOwnProperty(id)) { - this.entries[id] = ast[id]; - } + this.entries[id] = ast[id]; } }; @@ -989,7 +983,7 @@ /* jshint -W084 */ if (!this.isReady) { - throw new ContextError('Context not ready'); + throw new L10nError('Context not ready'); } var cur = 0; @@ -1004,14 +998,14 @@ var entry = locale.getEntry(id); if (entry === undefined) { cur++; - warning.call(this, new ContextError(id + ' not found in ' + loc, id, - loc)); + warning.call(this, new L10nError(id + ' not found in ' + loc, id, + loc)); continue; } return entry; } - error.call(this, new ContextError(id + ' not found', id)); + error.call(this, new L10nError(id + ' not found', id)); return null; } @@ -1076,7 +1070,7 @@ this.requestLocales = function requestLocales() { if (this.isLoading && !this.isReady) { - throw new ContextError('Context not ready'); + throw new L10nError('Context not ready'); } this.isLoading = true; @@ -1132,23 +1126,20 @@ } } - Context.Error = ContextError; - - function ContextError(message, id, loc) { - this.name = 'ContextError'; - this.message = message; - this.id = id; - this.loc = loc; - } - ContextError.prototype = Object.create(Error.prototype); - ContextError.prototype.constructor = ContextError; - - /* jshint -W104 */ var DEBUG = false; var isPretranslated = false; var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur']; + var nodeObserver = false; + + var moConfig = { + attributes: true, + characterData: false, + childList: true, + subtree: true, + attributeFilter: ['data-l10n-id', 'data-l10n-args'] + }; // Public API @@ -1160,9 +1151,14 @@ localize: function localize(element, id, args) { return localizeElement.call(navigator.mozL10n, element, id, args); }, - translate: function translate(element) { - return translateFragment.call(navigator.mozL10n, element); + translate: function () { + // XXX: Remove after removing obsolete calls. Bugs 992473 and 1020136 + }, + translateFragment: function (fragment) { + return translateFragment.call(navigator.mozL10n, fragment); }, + setAttributes: setL10nAttributes, + getAttributes: getL10nAttributes, ready: function ready(callback) { return navigator.mozL10n.ctx.ready(callback); }, @@ -1185,16 +1181,17 @@ }, _getInternalAPI: function() { return { + Error: L10nError, Context: Context, Locale: Locale, Entity: Entity, getPluralRule: getPluralRule, rePlaceables: rePlaceables, getTranslatableChildren: getTranslatableChildren, - getL10nAttributes: getL10nAttributes, + translateDocument: translateDocument, loadINI: loadINI, fireLocalizedEvent: fireLocalizedEvent, - parse: parse, + PropertiesParser: PropertiesParser, compile: compile }; } @@ -1288,7 +1285,8 @@ direction: getDirection(locale.id) } }; - translateFragment.call(l10n); + translateDocument.call(l10n); + // the visible DOM is now pretranslated isPretranslated = true; return true; @@ -1298,9 +1296,8 @@ var resLinks = document.head .querySelectorAll('link[type="application/l10n"]'); var iniLinks = []; - var i; - for (i = 0; i < resLinks.length; i++) { + for (var i = 0; i < resLinks.length; i++) { var link = resLinks[i]; var url = link.getAttribute('href'); var type = url.substr(url.lastIndexOf('.') + 1); @@ -1332,20 +1329,57 @@ function initLocale() { this.ctx.requestLocales(navigator.language); - // mozSettings won't be required here when https://bugzil.la/780953 lands - if (navigator.mozSettings) { - navigator.mozSettings.addObserver('language.current', function(event) { - navigator.mozL10n.language.code = event.settingValue; - }); + window.addEventListener('languagechange', function l10n_langchange() { + navigator.mozL10n.language.code = navigator.language; + }); + } + + function localizeMutations(mutations) { + var mutation; + + for (var i = 0; i < mutations.length; i++) { + mutation = mutations[i]; + if (mutation.type === 'childList') { + var addedNode; + + for (var j = 0; j < mutation.addedNodes.length; j++) { + addedNode = mutation.addedNodes[j]; + + if (addedNode.nodeType !== Node.ELEMENT_NODE) { + continue; + } + + if (addedNode.childElementCount) { + translateFragment.call(this, addedNode); + } else if (addedNode.hasAttribute('data-l10n-id')) { + translateElement.call(this, addedNode); + } + } + } + + if (mutation.type === 'attributes') { + translateElement.call(this, mutation.target); + } } } + function onMutations(mutations, self) { + self.disconnect(); + localizeMutations.call(this, mutations); + self.observe(document, moConfig); + } + function onReady() { if (!isPretranslated) { - this.translate(); + translateDocument.call(this); } isPretranslated = false; + if (!nodeObserver) { + nodeObserver = new MutationObserver(onMutations.bind(this)); + nodeObserver.observe(document, moConfig); + } + fireLocalizedEvent.call(this); } @@ -1439,13 +1473,16 @@ /* jshint -W104 */ + function translateDocument() { + document.documentElement.lang = this.language.code; + document.documentElement.dir = this.language.direction; + translateFragment.call(this, document.documentElement); + } + function translateFragment(element) { - if (!element) { - element = document.documentElement; - document.documentElement.lang = this.language.code; - document.documentElement.dir = this.language.direction; + if (element.hasAttribute('data-l10n-id')) { + translateElement.call(this, element); } - translateElement.call(this, element); var nodes = getTranslatableChildren(element); for (var i = 0; i < nodes.length; i++ ) { @@ -1453,15 +1490,25 @@ } } + function setL10nAttributes(element, id, args) { + element.setAttribute('data-l10n-id', id); + if (args) { + element.setAttribute('data-l10n-args', JSON.stringify(args)); + } + } + + function getL10nAttributes(element) { + return { + id: element.getAttribute('data-l10n-id'), + args: JSON.parse(element.getAttribute('data-l10n-args')) + }; + } + function getTranslatableChildren(element) { return element ? element.querySelectorAll('*[data-l10n-id]') : []; } function localizeElement(element, id, args) { - if (!element) { - return; - } - if (!id) { element.removeAttribute('data-l10n-id'); element.removeAttribute('data-l10n-args'); @@ -1475,38 +1522,19 @@ } else { element.removeAttribute('data-l10n-args'); } - - if (this.ctx.isReady) { - translateElement.call(this, element); - } - } - - function getL10nAttributes(element) { - if (!element) { - return {}; - } - - var l10nId = element.getAttribute('data-l10n-id'); - var l10nArgs = element.getAttribute('data-l10n-args'); - - var args = l10nArgs ? JSON.parse(l10nArgs) : null; - - return {id: l10nId, args: args}; } - - function translateElement(element) { var l10n = getL10nAttributes(element); if (!l10n.id) { - return; + return false; } var entity = this.ctx.getEntity(l10n.id, l10n.args); if (!entity) { - return; + return false; } if (typeof entity === 'string') { @@ -1519,16 +1547,14 @@ } for (var key in entity.attributes) { - if (entity.attributes.hasOwnProperty(key)) { - var attr = entity.attributes[key]; - var pos = key.indexOf('.'); - if (pos !== -1) { - element[key.substr(0, pos)][key.substr(pos + 1)] = attr; - } else if (key === 'ariaLabel') { - element.setAttribute('aria-label', attr); - } else { - element[key] = attr; - } + var attr = entity.attributes[key]; + if (key === 'ariaLabel') { + element.setAttribute('aria-label', attr); + } else if (key === 'innerHTML') { + // XXX: to be removed once bug 994357 lands + element.innerHTML = attr; + } else { + element.setAttribute(key, attr); } }