/* Copyright (c) 2011-2012 Fabien Cazenave, Mozilla. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ 'use strict'; (function(window) { var gL10nData = {}; var gTextData = ''; var gLanguage = ''; // parser function evalString(text) { return text.replace(/\\\\/g, '\\') .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\t/g, '\t') .replace(/\\b/g, '\b') .replace(/\\f/g, '\f') .replace(/\\{/g, '{') .replace(/\\}/g, '}') .replace(/\\"/g, '"') .replace(/\\'/g, "'"); } function parseProperties(text, lang) { var reBlank = /^\s*|\s*$/; var reComment = /^\s*#|^\s*$/; var reSection = /^\s*\[(.*)\]\s*$/; var reImport = /^\s*@import\s+url\((.*)\)\s*$/i; // parse the *.properties file into an associative array var currentLang = '*'; var supportedLang = []; var skipLang = false; var data = []; var match = ''; var entries = text.replace(reBlank, '').split(/[\r\n]+/); for (var i = 0; i < entries.length; i++) { var line = entries[i]; // comment or blank line? if (reComment.test(line)) continue; // section start? if (reSection.test(line)) { match = reSection.exec(line); currentLang = match[1]; skipLang = (currentLang != lang) && (currentLang != '*'); continue; } else if (skipLang) { continue; } // @import rule? if (reImport.test(line)) { match = reImport.exec(line); } // key-value pair var tmp = line.split('='); if (tmp.length > 1) data[tmp[0]] = evalString(tmp[1]); } // find the attribute descriptions, if any for (var key in data) { var id, prop, index = key.lastIndexOf('.'); if (index > 0) { // attribute id = key.substring(0, index); prop = key.substr(index + 1); } else { // textContent, could be innerHTML as well id = key; prop = 'textContent'; } if (!gL10nData[id]) gL10nData[id] = {}; gL10nData[id][prop] = data[key]; } } function parse(text, lang) { gTextData += text; // we only support *.properties files at the moment return parseProperties(text, lang); } // load and parse the specified resource file function loadResource(href, lang, onSuccess, onFailure) { var xhr = new XMLHttpRequest(); xhr.open('GET', href, true); xhr.overrideMimeType('text/plain; charset=utf-8'); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200 || xhr.status == 0) { parse(xhr.responseText, lang); if (onSuccess) onSuccess(); } else { if (onFailure) onFailure(); } } }; xhr.send(null); } // load and parse all resources for the specified locale function loadLocale(lang, callback) { clear(); // check all nodes // and load the resource files var langLinks = document.querySelectorAll('link[type="application/l10n"]'); var langLinksCount = langLinks.length; var langScripts = document.querySelectorAll('script[type="application/l10n"]'); var langScriptCount = langScripts.length; var langCount = langLinksCount + langScriptCount; // start the callback when all resources are loaded var onResourceLoaded = null; var gResourceCount = 0; onResourceLoaded = function() { gResourceCount++; if (gResourceCount >= langCount) { // execute the [optional] callback if (callback) callback(); // fire a 'localized' DOM event var evtObject = document.createEvent('Event'); evtObject.initEvent('localized', false, false); evtObject.language = lang; window.dispatchEvent(evtObject); } } // load all resource files function l10nResourceLink(link) { var href = link.href; var type = link.type; this.load = function(lang, callback) { var applied = lang; loadResource(href, lang, callback, function() { console.warn(href + ' not found.'); applied = ''; }); return applied; // return lang if found, an empty string if not found }; } gLanguage = lang; for (var i = 0; i < langLinksCount; i++) { var resource = new l10nResourceLink(langLinks[i]); var rv = resource.load(lang, onResourceLoaded); if (rv != lang) // lang not found, used default resource instead gLanguage = ''; } for (var i = 0; i < langScriptCount; i++) { var scriptText = langScripts[i].text; parse(scriptText, lang); onResourceLoaded(); } } // fetch an l10n object, warn if not found function getL10nData(key) { var data = gL10nData[key]; if (!data) console.warn('[l10n] #' + key + ' missing for [' + gLanguage + ']'); return data; } // replace {{arguments}} with their values function substArguments(str, args) { var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/; var match = reArgs.exec(str); while (match) { if (!match || match.length < 2) return str; // argument key not found var arg = match[1]; var sub = ''; if (arg in args) { sub = args[arg]; } else if (arg in gL10nData) { sub = gL10nData[arg].textContent; } else { console.warn('[l10n] could not find argument {{' + arg + '}}'); return str; } str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length); match = reArgs.exec(str); } return str; } // translate a string function translateString(key, args) { var data = getL10nData(key); if (!data) return '{{' + key + '}}'; return substArguments(data.textContent, args); } // translate an HTML element function translateElement(element) { if (!element || !element.dataset) return; // get the related l10n object var key = element.dataset.l10nId; var data = getL10nData(key); if (!data) return; // get arguments (if any) // TODO: more flexible parser? var args; if (element.dataset.l10nArgs) try { args = JSON.parse(element.dataset.l10nArgs); } catch (e) { console.warn('[l10n] could not parse arguments for #' + key + ''); } // translate element // TODO: security check? for (var k in data) element[k] = substArguments(data[k], args); } // translate an HTML subtree function translateFragment(element) { element = element || document.querySelector('html'); // check all translatable children (= w/ a `data-l10n-id' attribute) var children = element.querySelectorAll('*[data-l10n-id]'); var elementCount = children.length; for (var i = 0; i < elementCount; i++) translateElement(children[i]); // translate element itself if necessary if (element.dataset.l10nId) translateElement(element); } // clear all l10n data function clear() { gL10nData = {}; gTextData = ''; gLanguage = ''; } // load the default locale on startup window.addEventListener('DOMContentLoaded', function() { var lang = navigator.language; if (navigator.mozSettings) { var req = navigator.mozSettings.getLock().get('language.current'); req.onsuccess = function() { loadLocale(req.result['language.current'] || lang, translateFragment); }; req.onerror = function() { loadLocale(lang, translateFragment); }; } else { loadLocale(lang, translateFragment); } }); // Public API document.mozL10n = { // get a localized string get: translateString, // get|set the document language and direction get language() { return { // get|set the document language (ISO-639-1) get code() { return gLanguage; }, set code(lang) { loadLocale(lang, translateFragment); }, // get the direction (ltr|rtl) of the current language get direction() { // http://www.w3.org/International/questions/qa-scripts // Arabic, Hebrew, Farsi, Pashto, Urdu var rtlList = ['ar', 'he', 'fa', 'ps', 'ur']; return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr'; } }; } }; })(this);