diff --git a/doc/api/embed_parameters.md b/doc/api/embed_parameters.md
index 7f84f064..3100fff9 100644
--- a/doc/api/embed_parameters.md
+++ b/doc/api/embed_parameters.md
@@ -52,3 +52,11 @@ Default: false
* Boolean
Default: false
+
+## lang
+ * String
+
+Default: en
+
+Example: `lang=ar` (translates the interface into Arabic)
+
diff --git a/doc/index.md b/doc/index.md
index db7cefaa..5d3022be 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -1,5 +1,6 @@
@include documentation
-@include cusotm_static
+@include localization
+@include custom_static
@include api/api
@include plugins
@include database
diff --git a/doc/localization.md b/doc/localization.md
new file mode 100644
index 00000000..3f0901ca
--- /dev/null
+++ b/doc/localization.md
@@ -0,0 +1,19 @@
+# Localization
+Etherpad lite provides a multi-language user interface, that's apart from your users' content, so users from different countries can collaborate on a single document, while still having the user interface displayed in their mother tongue.
+
+## Translating
+`/src/locales` contains files for all supported languages which contain the translated strings. To add support for a new language, copy the English language file named `en.ini` and translate it.
+Translation files are simply `*.ini` files and look like this:
+
+```
+pad.modals.connected = Connecté.
+pad.modals.uderdup = Ouvrir dans une nouvelle fenêtre.
+pad.toolbar.unindent.title = Désindenter
+pad.toolbar.undo.title = Annuler (Ctrl-Z)
+timeslider.pageTitle = {{appTitle}} Curseur temporel
+```
+
+There must be only one translation per line. Each translation consists of a key (the id of the string that is to be translated), an equal sign and the translated string. Anything after the equa sign will be used as the translated string (you may put some spaces after `=` for better readability, though). Terms in curly braces must not be touched but left as they are, since they represent a dynamically changing part of the string like a variable. Imagine a message welcoming a user: `Welcome, {{userName}}!` would be translated as `Ahoy, {{userName}}!` in pirate.
+
+## Under the hood
+We use a `language` cookie to save your language settings if you change them. If you don't, we autodetect your locale using information from your browser. Now, that we know your preferred language this information is feeded into a very nice library called [webL10n](https://github.com/fabi1cazenave/webL10n), which loads the appropriate translations and applies them to our templates, providing translation params, pluralization, include rules and even a nice javascript API along the way.
\ No newline at end of file
diff --git a/src/ep.json b/src/ep.json
index b96ef037..89c8964a 100644
--- a/src/ep.json
+++ b/src/ep.json
@@ -5,6 +5,7 @@
"restartServer": "ep_etherpad-lite/node/hooks/express:restartServer"
} },
{ "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } },
+ { "name": "i18n", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/i18n:expressCreateServer" } },
{ "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } },
{ "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } },
{ "name": "padreadonly", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly:expressCreateServer" } },
diff --git a/src/locales/de.ini b/src/locales/de.ini
new file mode 100644
index 00000000..35fa61ee
--- /dev/null
+++ b/src/locales/de.ini
@@ -0,0 +1,85 @@
+[de]
+index.newPad = Neues Pad
+index.createOpenPad = Pad mit folgendem Namen öffnen
+
+pad.toolbar.bold.title = Fett (Strg-B)
+pad.toolbar.italic.title = Kursiv (Strg-I)
+pad.toolbar.underline.title = Unterstrichen (Strg-U)
+pad.toolbar.strikethrough.title = Durchgestrichen
+pad.toolbar.ol.title = Nummerierte Liste
+pad.toolbar.ul.title = Ungeordnete Liste
+pad.toolbar.indent.title = Einrücken
+pad.toolbar.unindent.title = Ausrücken
+pad.toolbar.undo.title = Rückgängig (Strg-Z)
+pad.toolbar.redo.title = Wiederholen (Strg-Y)
+pad.toolbar.clearAuthorship.title = Autorenfarben zurücksetzen
+pad.toolbar.import_export.title = Import/Export von verschiedenen Dateiformaten
+pad.toolbar.timeslider.title = Pad-Geschichte anzeigen
+pad.toolbar.savedRevision.title = Diese Revision markieren
+pad.toolbar.settings.title = Einstellungen
+pad.toolbar.embed.title = Dieses Pad teilen oder einbetten
+pad.toolbar.showusers.title = Verbundene Benutzer anzeigen
+
+pad.colorpicker.save = Speichern
+pad.colorpicker.cancel = Abbrechen
+
+pad.loading = Laden...
+pad.settings.padSettings = Pad Einstellungen
+pad.settings.myView = Eigene Ansicht
+pad.settings.stickychat = Chat immer anzeigen
+pad.settings.colorcheck = Autorenfarben anzeigen
+pad.settings.linenocheck = Zeilennummern
+pad.settings.fontType = Schriftart:
+pad.settings.fontType.normal = Normal
+pad.settings.fontType.monospaced = Monospace
+pad.settings.language = Sprache:
+pad.settings.globalView = Gemeinsame Ansicht
+
+pad.importExport.import_export = Import/Export
+pad.importExport.import = Datei oder Dokument hochladen
+pad.importExport.successful = Erfolgreich!
+
+pad.importExport.export = Dieses Pad exportieren
+pad.importExport.exporthtml = HTML
+pad.importExport.exportplain = Reiner Text
+pad.importExport.exportword = Microsoft Word
+pad.importExport.exportpdf = PDf
+pad.importExport.exportopen = ODF (Open Document Format)
+pad.importExport.exportdokuwiki = DokuWiki
+
+pad.modals.connected = Verbunden.
+pad.modals.reconnecting = Wiederherstellen der Verbindung...
+pad.modals.forcereconnect = Erneut Verbinden
+pad.modals.uderdup = In einem anderen Fenster geöffnet
+pad.modals.userdup.explanation = Dieses Pad scheint in mehr als einem Browser-Fenster auf diesem Computer geöffnet zu sein.
+pad.modals.userdup.advice = Um dieses Fenster zu benutzen, verbinden Sie bitte erneut.
+pad.modals.unauth = Nicht Authorisiert.
+pad.modals.unauth.explanation = Ihre Befugnisse auf dieses Pad zuzugreifen haben sich geädert. Versuchen Sie, erneut zu verbinden.
+pad.modals.looping = Verbindung unterbrochen.
+pad.modals.looping.explanation = Es gibt Probleme bei der Kommunikation mit dem Synchronisationsserver.
+pad.modals.looping.cause = Möglicherweise verläuft Ihre Verbindung durch eine inkompatible Firewall oder einen inkompatiblen Proxy.
+pad.modals.initsocketfail = Server nicht erreichbar.
+pad.modals.initsocketfail.explanation = Es konnte keine Verbindung zum Synchronisationsserver hergestellt werden.
+pad.modals.initsocketfail.cause = Dies könnte an Ihrem Browser oder Ihrer Internet-Verbindung liegen.
+pad.modals.slowcommit = Verbindung unterbrochen.
+pad.modals.slowcommit.explanation = Der Server reagiert nicht.
+pad.modals.slowcommit.cause = Dies könnte an Problemen mit Netzwerk-Konnektivität liegen. Möglicherweise ist der Server aber auch überlastet.
+pad.modals.deleted = Entfernt.
+pad.modals.deleted.explanation = Dieses Pad wurde entfernt.
+pad.modals.disconnected = Verbindung unterbrochen.
+pad.modals.disconnected.explanation = Die Verbindung zum Synchronisationsserver wurde unterbrochen.
+pad.modals.disconnected.cause = Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtigen Sie uns, falls dies weiterhin passiert.
+
+pad.share = Dieses Pad teilen
+pad.share.readonly = Eingeschränkter zugriff (Nur lesen)
+pad.share.link = Link
+pad.share.emebdcode = In Webseite einbetten
+
+pad.chat = Chat
+pad.chat.title = Den Chat für dieses Pad öffnen
+
+timeslider.pageTitle = {{appTitle}} Pad-Geschichte
+timeslider.toolbar.returnbutton = Zurück zum Pad
+timeslider.toolbar.authors = Autoren:
+timeslider.toolbar.authorsList = keine Autoren
+timeslider.exportCurrent = Exportiere diese Version als:
\ No newline at end of file
diff --git a/src/locales/en.ini b/src/locales/en.ini
new file mode 100644
index 00000000..95b8eadd
--- /dev/null
+++ b/src/locales/en.ini
@@ -0,0 +1,77 @@
+[en]
+index.newPad = New Pad
+index.createOpenPad = or create/open a Pad with the name:
+pad.toolbar.bold.title = Bold (Ctrl-B)
+pad.toolbar.italic.title = Italic (Ctrl-I)
+pad.toolbar.underline.title = Underline (Ctrl-U)
+pad.toolbar.strikethrough.title = Strikethrough
+pad.toolbar.ol.title = Ordered list
+pad.toolbar.ul.title = UnOrdered List
+pad.toolbar.indent.title = Indent
+pad.toolbar.unindent.title = Outdent
+pad.toolbar.undo.title = Undo (Ctrl-Z)
+pad.toolbar.redo.title = Redo (Ctrl-Y)
+pad.toolbar.clearAuthorship.title = Clear Authorship Colors
+pad.toolbar.import_export.title = Import/Export from/to different file formats
+pad.toolbar.timeslider.title = Timeslider
+pad.toolbar.savedRevision.title = Saved Revisions
+pad.toolbar.settings.title = Settings
+pad.toolbar.embed.title = Embed this pad
+pad.toolbar.showusers.title = Show the users on this pad
+pad.colorpicker.save = Save
+pad.colorpicker.cancel = Cancel
+pad.loading = Loading...
+pad.settings.padSettings = Pad Settings
+pad.settings.myView = My View
+pad.settings.stickychat = Chat always on screen
+pad.settings.colorcheck = Authorship colors
+pad.settings.linenocheck = Line numbers
+pad.settings.fontType = Font type:
+pad.settings.fontType.normal = Normal
+pad.settings.fontType.monospaced = Monospace
+pad.settings.globalView = Global View
+pad.settings.language = Language:
+pad.importExport.import_export = Import/Export
+pad.importExport.import = Upload any text file or document
+pad.importExport.successful = Successful!
+pad.importExport.export = Export current pad as
+pad.importExport.exporthtml = HTML
+pad.importExport.exportplain = Plain text
+pad.importExport.exportword = Microsoft Word
+pad.importExport.exportpdf = PDf
+pad.importExport.exportopen = ODF (Open Document Format)
+pad.importExport.exportdokuwiki = DokuWiki
+pad.modals.connected = Connected.
+pad.modals.reconnecting = Reconnecting to your pad..
+pad.modals.forcereconnect = Force reconnect
+pad.modals.uderdup = Open in another window
+pad.modals.userdup.explanation = This pad seems to be opened in more than one browser window on this computer.
+pad.modals.userdup.advice = Reconnect to use this windows instead.
+pad.modals.unauth = Not authorized
+pad.modals.unauth.explanation = Your permissions have changes while viewing this page. Try to reconnect.
+pad.modals.looping = Disconnected.
+pad.modals.looping.explanation = We're having problem communicating to the synchronization server.
+pad.modals.looping.cause = Perhaps their connection runs through an incompatible firewall or incompatible proxy.
+pad.modals.initsocketfail = Server is unreachable.
+pad.modals.initsocketfail.explanation = Couldn't connect to the synchronization server.
+pad.modals.initsocketfail.cause = This could be because of your browser or Internet connection. #sounds stupid!
+pad.modals.slowcommit = Disconnected.
+pad.modals.slowcommit.explanation = The server is not responding.
+pad.modals.slowcommit.cause = This could be due to problems with network connectivity.
+pad.modals.deleted = Deleted.
+pad.modals.deleted.explanation = This pad has been removed.
+pad.modals.disconnected = You have been disconnected.
+pad.modals.disconnected.explanation = The connection to the server was lost
+pad.modals.disconnected.cause = The server may be unavailable. Please notify us if this continues to happen.
+pad.share = Share this pad
+pad.share.readonly = Read only
+pad.share.link = Link
+pad.share.emebdcode = Embed URL
+pad.chat = Chat
+pad.chat.title = Open the chat for this pad.
+
+timeslider.pageTitle = {{appTitle}} Timeslider
+timeslider.toolbar.returnbutton = Return to pad
+timeslider.toolbar.authors = Authors:
+timeslider.toolbar.authorsList = No Authors
+timeslider.exportCurrent = Export current version as:
\ No newline at end of file
diff --git a/src/locales/fr.ini b/src/locales/fr.ini
new file mode 100644
index 00000000..5203f7fd
--- /dev/null
+++ b/src/locales/fr.ini
@@ -0,0 +1,86 @@
+[fr]
+index.newPad = Nouveau Pad
+index.createOpenPad = ou créer/ouvrir un Pad intitulé
+
+pad.toolbar.bold.title = Gras (Ctrl-B)
+pad.toolbar.italic.title = Italique (Ctrl-I)
+pad.toolbar.underline.title = Souligner (Ctrl-U)
+pad.toolbar.strikethrough.title = Barrer
+pad.toolbar.ol.title = Liste ordonnée
+pad.toolbar.ul.title = Liste non-ordonnée
+pad.toolbar.indent.title = Indenter
+pad.toolbar.unindent.title = Désindenter
+pad.toolbar.undo.title = Annuler (Ctrl-Z)
+pad.toolbar.redo.title = Rétablir (Ctrl-Y)
+pad.toolbar.clearAuthorship.title = Effacer les couleurs identifant les auteurs
+pad.toolbar.import_export.title = Importer/Exporter de/vers un format de fichier différent
+pad.toolbar.timeslider.title = Navigateur d'historique
+pad.toolbar.savedRevision.title = Versions enregistrées
+pad.toolbar.settings.title = Paramètres
+pad.toolbar.embed.title = Intégrer ce Pad
+pad.toolbar.showusers.title = Afficher les utilisateurs du Pad
+
+pad.colorpicker.save = Sauver
+pad.colorpicker.cancel = Annuler
+
+pad.loading = Chargement...
+pad.settings.padSettings = Paramètres du Pad
+pad.settings.myView = Ma vue
+pad.settings.stickychat = Messagerie toujours affichée
+pad.settings.colorcheck = Couleurs d'identification
+pad.settings.linenocheck = Numéros des lignes
+pad.settings.fontType = Type de police:
+pad.settings.fontType.normal = Normal
+pad.settings.fontType.monospaced = Monospace
+pad.settings.globalView = Vue d'ensemble
+pad.settings.language = Langue:
+
+pad.importExport.import_export = Importer/Exporter
+pad.importExport.import = Charger un texte ou un document
+pad.importExport.successful = Traitement effectué!
+
+pad.importExport.export = Exporter ce Pad vers
+pad.importExport.exporthtml = HTML
+pad.importExport.exportplain = Texte brut
+pad.importExport.exportword = Microsoft Word
+pad.importExport.exportpdf = PDf
+pad.importExport.exportopen = ODF (Open Document Format)
+pad.importExport.exportdokuwiki = DokuWiki
+
+pad.modals.connected = Connecté.
+pad.modals.reconnecting = Reconnexion vers votre Pad...
+pad.modals.forcereconnect = Forcer la reconnexion.
+pad.modals.uderdup = Ouvrir dans une nouvelle fenêtre
+pad.modals.userdup.explanation = Ce Pad semble avoir été ouvert dans plusieurs fenêtres de votre fureteur sur cet ordinateur.
+pad.modals.userdup.advice = Se reconnecter en utilisant cette fenêtre.
+pad.modals.unauth = Not authorized Non authorisé
+pad.modals.unauth.explanation = Vos permissions ont été changées lors de la visualisation de cette page. Essayer de vous reconnecter.
+pad.modals.looping = Disconnected. Déconnecté.
+pad.modals.looping.explanation = Nous éprouvons un problème de communication au serveur de synchronisation.
+pad.modals.looping.cause = Il est possible que leur connection soit protégée par un pare-feu incompatible ou un serveur proxy incompatible.
+pad.modals.initsocketfail = Le serveur est introuvable.
+pad.modals.initsocketfail.explanation = Impossible de se connecter au serveur de synchronisation.
+pad.modals.initsocketfail.cause = La cause de ce problème peut être liée à votre fureteur web.
+pad.modals.slowcommit = Disconnected. Déconnecté
+pad.modals.slowcommit.explanation = Le serveur ne répond pas.
+pad.modals.slowcommit.cause = La cause de ce problème peut être liée à une erreur de connectivité du réseau.
+pad.modals.deleted = Supprimé.
+pad.modals.deleted.explanation = Ce Pad a été supprimé.
+pad.modals.disconnected = Vous avez été déconnecté.
+pad.modals.disconnected.explanation = La connexion au serveur a échoué.
+pad.modals.disconnected.cause = Ce serveur est possiblement hors-ligne. Veuillez nous joindre si le problème persiste.
+
+pad.share = Partager ce Pad
+pad.share.readonly = Lecture seule
+pad.share.link = Lien
+pad.share.emebdcode = Lien à intégrer
+
+pad.chat = Messagerie
+pad.chat.title = Ouvrir la messagerie liée au Pad.
+
+timeslider.pageTitle = {{appTitle}} Curseur temporel
+timeslider.toolbar.returnbutton = Retour à ce Pad.
+timeslider.toolbar.authors = Auteurs:
+timeslider.toolbar.authorsList = Aucun auteurs
+timeslider.exportCurrent = Exporter version actuelle vers:
+
diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.js
new file mode 100644
index 00000000..6238f2cb
--- /dev/null
+++ b/src/node/hooks/i18n.js
@@ -0,0 +1,33 @@
+var Globalize = require('globalize')
+ , fs = require('fs')
+ , path = require('path')
+ , express = require('express')
+
+var localesPath = __dirname+"/../../locales";
+
+var localeIndex = '[*]\r\n@import url(locales/en.ini)\r\n';
+exports.availableLangs = {en: 'English'};
+
+fs.readdir(localesPath, function(er, files) {
+ files.forEach(function(locale) {
+ locale = locale.split('.')[0]
+ if(locale.toLowerCase() == 'en') return;
+
+ // build locale index
+ localeIndex += '['+locale+']\r\n@import url(locales/'+locale+'.ini)\r\n'
+
+ require('globalize/lib/cultures/globalize.culture.'+locale+'.js')
+ var culture = Globalize.cultures[locale];
+ exports.availableLangs[culture.name] = culture.nativeName;
+ })
+})
+
+exports.expressCreateServer = function(n, args) {
+
+ args.app.use('/locales', express.static(localesPath));
+
+ args.app.get('/locales.ini', function(req, res) {
+ res.send(localeIndex);
+ })
+
+}
\ No newline at end of file
diff --git a/src/package.json b/src/package.json
index ced0264d..f7438c39 100644
--- a/src/package.json
+++ b/src/package.json
@@ -35,7 +35,8 @@
"security" : "1.0.0",
"tinycon" : "0.0.1",
"underscore" : "1.3.1",
- "unorm" : "1.0.0"
+ "unorm" : "1.0.0",
+ "globalize" : "0.1.1"
},
"bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": {
diff --git a/src/static/css/pad.css b/src/static/css/pad.css
index 5ee6b3c5..64f9f0d4 100644
--- a/src/static/css/pad.css
+++ b/src/static/css/pad.css
@@ -748,6 +748,15 @@ input[type=checkbox] {
.popup p {
margin: 5px 0
}
+.popup select {
+ background: #fff;
+ padding: 2px;
+ height: 24px;
+ border-radius: 3px;
+ border: 1px solid #ccc;
+ outline: none;
+ min-width: 105px;
+}
.column {
float: left;
width: 50%;
diff --git a/src/static/js/l10n.js b/src/static/js/l10n.js
new file mode 100644
index 00000000..e2584cc4
--- /dev/null
+++ b/src/static/js/l10n.js
@@ -0,0 +1,1028 @@
+/** 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.
+ */
+/*jshint browser: true, devel: true, es5: true, globalstrict: true */
+'use strict';
+
+document.webL10n = (function(window, document, undefined) {
+ var gL10nData = {};
+ var gTextData = '';
+ var gTextProp = 'textContent';
+ var gLanguage = '';
+ var gMacros = {};
+ var gReadyState = 'loading';
+
+ // read-only setting -- we recommend to load l10n resources synchronously
+ var gAsyncResourceLoading = true;
+
+ // debug helpers
+ var gDEBUG = false;
+ function consoleLog(message) {
+ if (gDEBUG)
+ console.log('[l10n] ' + message);
+ };
+ function consoleWarn(message) {
+ if (gDEBUG)
+ console.warn('[l10n] ' + message);
+ };
+
+ /**
+ * DOM helpers for the so-called "HTML API".
+ *
+ * These functions are written for modern browsers. For old versions of IE,
+ * they're overridden in the 'startup' section at the end of this file.
+ */
+
+ function getL10nResourceLinks() {
+ return document.querySelectorAll('link[type="application/l10n"]');
+ }
+
+ function getTranslatableChildren(element) {
+ return element ? element.querySelectorAll('*[data-l10n-id]') : [];
+ }
+
+ function getL10nAttributes(element) {
+ if (!element)
+ return {};
+
+ var l10nId = element.getAttribute('data-l10n-id');
+ var l10nArgs = element.getAttribute('data-l10n-args');
+ var args = {};
+ if (l10nArgs) {
+ try {
+ args = JSON.parse(l10nArgs);
+ } catch (e) {
+ consoleWarn('could not parse arguments for #' + l10nId);
+ }
+ }
+ return { id: l10nId, args: args };
+ }
+
+ function fireL10nReadyEvent(lang) {
+ var evtObject = document.createEvent('Event');
+ evtObject.initEvent('localized', false, false);
+ evtObject.language = lang;
+ window.dispatchEvent(evtObject);
+ }
+
+
+ /**
+ * l10n resource parser:
+ * - reads (async XHR) the l10n resource matching `lang';
+ * - imports linked resources (synchronously) when specified;
+ * - parses the text data (fills `gL10nData' and `gTextData');
+ * - triggers success/failure callbacks when done.
+ *
+ * @param {string} href
+ * URL of the l10n resource to parse.
+ *
+ * @param {string} lang
+ * locale (language) to parse.
+ *
+ * @param {Function} successCallback
+ * triggered when the l10n resource has been successully parsed.
+ *
+ * @param {Function} failureCallback
+ * triggered when the an error has occured.
+ *
+ * @return {void}
+ * uses the following global variables: gL10nData, gTextData, gTextProp.
+ */
+
+ function parseResource(href, lang, successCallback, failureCallback) {
+ var baseURL = href.replace(/\/[^\/]*$/, '/');
+
+ // handle escaped characters (backslashes) in a string
+ function evalString(text) {
+ if (text.lastIndexOf('\\') < 0)
+ return 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, "'");
+ }
+
+ // parse *.properties text data into an l10n dictionary
+ function parseProperties(text) {
+ var dictionary = {};
+
+ // token expressions
+ var reBlank = /^\s*|\s*$/;
+ var reComment = /^\s*#|^\s*$/;
+ var reSection = /^\s*\[(.*)\]\s*$/;
+ var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
+ var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'
+
+ // parse the *.properties file into an associative array
+ function parseRawLines(rawText, extendedSyntax) {
+ var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
+ var currentLang = '*';
+ var genericLang = lang.replace(/-[a-z]+$/i, '');
+ var skipLang = false;
+ var match = '';
+
+ for (var i = 0; i < entries.length; i++) {
+ var line = entries[i];
+
+ // comment or blank line?
+ if (reComment.test(line))
+ continue;
+
+ // the extended syntax supports [lang] sections and @import rules
+ if (extendedSyntax) {
+ if (reSection.test(line)) { // section start?
+ match = reSection.exec(line);
+ currentLang = match[1];
+ skipLang = (currentLang !== '*') &&
+ (currentLang !== lang) && (currentLang !== genericLang);
+ continue;
+ } else if (skipLang) {
+ continue;
+ }
+ if (reImport.test(line)) { // @import rule?
+ match = reImport.exec(line);
+ loadImport(baseURL + match[1]); // load the resource synchronously
+ }
+ }
+
+ // key-value pair
+ consoleLog(tmp)
+ var tmp = line.match(reSplit);
+ if (tmp && tmp.length == 3)
+ dictionary[tmp[1]] = evalString(tmp[2]);
+ }
+ }
+
+ // import another *.properties file
+ function loadImport(url) {
+ loadResource(url, function(content) {
+ parseRawLines(content, false); // don't allow recursive imports
+ }, false, false); // load synchronously
+ }
+
+ // fill the dictionary
+ parseRawLines(text, true);
+ return dictionary;
+ }
+
+ // load the specified resource file
+ function loadResource(url, onSuccess, onFailure, asynchronous) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, asynchronous);
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType('text/plain; charset=utf-8');
+ }
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status === 0) {
+ if (onSuccess)
+ onSuccess(xhr.responseText);
+ } else {
+ if (onFailure)
+ onFailure();
+ }
+ }
+ };
+ xhr.send(null);
+ }
+
+ // load and parse l10n data (warning: global variables are used here)
+ loadResource(href, function(response) {
+ gTextData += response; // mostly for debug
+
+ // parse *.properties text data into an l10n dictionary
+ var data = parseProperties(response);
+
+ // allowed attributes
+ var attrList =
+ { "title": 1
+ , "innerHTML": 1
+ , "alt": 1
+ , "textContent": 1
+ }
+
+ // find attribute descriptions, if any
+ for (var key in data) {
+ var id, prop, index = key.lastIndexOf('.');
+ if (index > 0 && key.substr(index + 1) in attrList) { // an attribute has been specified
+ id = key.substring(0, index);
+ prop = key.substr(index + 1);
+ } else { // no attribute: assuming text content by default
+ id = key;
+ prop = gTextProp;
+ }
+ if (!gL10nData[id]) {
+ gL10nData[id] = {};
+ }
+ gL10nData[id][prop] = data[key];
+ }
+
+ // trigger callback
+ if (successCallback)
+ successCallback();
+ }, failureCallback, gAsyncResourceLoading);
+ };
+
+ // load and parse all resources for the specified locale
+ function loadLocale(lang, callback) {
+ clear();
+ gLanguage = lang;
+
+ // check all nodes
+ // and load the resource files
+ var langLinks = getL10nResourceLinks();
+ var langCount = langLinks.length;
+ if (langCount == 0) {
+ consoleLog('no resource to load, early way out');
+ fireL10nReadyEvent(lang);
+ gReadyState = 'complete';
+ return;
+ }
+
+ // start the callback when all resources are loaded
+ var onResourceLoaded = null;
+ var gResourceCount = 0;
+ onResourceLoaded = function() {
+ gResourceCount++;
+ if (gResourceCount >= langCount) {
+ if (callback) // execute the [optional] callback
+ callback();
+ fireL10nReadyEvent(lang);
+ gReadyState = 'complete';
+ }
+ };
+
+ // load all resource files
+ function l10nResourceLink(link) {
+ var href = link.href;
+ var type = link.type;
+ this.load = function(lang, callback) {
+ var applied = lang;
+ parseResource(href, lang, callback, function() {
+ consoleWarn(href + ' not found.');
+ applied = '';
+ });
+ return applied; // return lang if found, an empty string if not found
+ };
+ }
+
+ for (var i = 0; i < langCount; i++) {
+ var resource = new l10nResourceLink(langLinks[i]);
+ var rv = resource.load(lang, onResourceLoaded);
+ if (rv != lang) { // lang not found, used default resource instead
+ consoleWarn('"' + lang + '" resource not found');
+ gLanguage = '';
+ }
+ }
+ }
+
+ // clear all l10n data
+ function clear() {
+ gL10nData = {};
+ gTextData = '';
+ gLanguage = '';
+ // TODO: clear all non predefined macros.
+ // There's no such macro /yet/ but we're planning to have some...
+ }
+
+
+ /**
+ * Get rules for plural forms (shared with JetPack), see:
+ * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
+ *
+ * @param {string} lang
+ * locale (language) used.
+ *
+ * @return {Function}
+ * returns a function that gives the plural form name for a given integer:
+ * var fun = getPluralRules('en');
+ * fun(1) -> 'one'
+ * fun(0) -> 'other'
+ * fun(1000) -> 'other'.
+ */
+
+ function getPluralRules(lang) {
+ var locales2rules = {
+ 'af': 3,
+ 'ak': 4,
+ 'am': 4,
+ 'ar': 1,
+ 'asa': 3,
+ 'az': 0,
+ 'be': 11,
+ 'bem': 3,
+ 'bez': 3,
+ 'bg': 3,
+ 'bh': 4,
+ 'bm': 0,
+ 'bn': 3,
+ 'bo': 0,
+ 'br': 20,
+ 'brx': 3,
+ 'bs': 11,
+ 'ca': 3,
+ 'cgg': 3,
+ 'chr': 3,
+ 'cs': 12,
+ 'cy': 17,
+ 'da': 3,
+ 'de': 3,
+ 'dv': 3,
+ 'dz': 0,
+ 'ee': 3,
+ 'el': 3,
+ 'en': 3,
+ 'eo': 3,
+ 'es': 3,
+ 'et': 3,
+ 'eu': 3,
+ 'fa': 0,
+ 'ff': 5,
+ 'fi': 3,
+ 'fil': 4,
+ 'fo': 3,
+ 'fr': 5,
+ 'fur': 3,
+ 'fy': 3,
+ 'ga': 8,
+ 'gd': 24,
+ 'gl': 3,
+ 'gsw': 3,
+ 'gu': 3,
+ 'guw': 4,
+ 'gv': 23,
+ 'ha': 3,
+ 'haw': 3,
+ 'he': 2,
+ 'hi': 4,
+ 'hr': 11,
+ 'hu': 0,
+ 'id': 0,
+ 'ig': 0,
+ 'ii': 0,
+ 'is': 3,
+ 'it': 3,
+ 'iu': 7,
+ 'ja': 0,
+ 'jmc': 3,
+ 'jv': 0,
+ 'ka': 0,
+ 'kab': 5,
+ 'kaj': 3,
+ 'kcg': 3,
+ 'kde': 0,
+ 'kea': 0,
+ 'kk': 3,
+ 'kl': 3,
+ 'km': 0,
+ 'kn': 0,
+ 'ko': 0,
+ 'ksb': 3,
+ 'ksh': 21,
+ 'ku': 3,
+ 'kw': 7,
+ 'lag': 18,
+ 'lb': 3,
+ 'lg': 3,
+ 'ln': 4,
+ 'lo': 0,
+ 'lt': 10,
+ 'lv': 6,
+ 'mas': 3,
+ 'mg': 4,
+ 'mk': 16,
+ 'ml': 3,
+ 'mn': 3,
+ 'mo': 9,
+ 'mr': 3,
+ 'ms': 0,
+ 'mt': 15,
+ 'my': 0,
+ 'nah': 3,
+ 'naq': 7,
+ 'nb': 3,
+ 'nd': 3,
+ 'ne': 3,
+ 'nl': 3,
+ 'nn': 3,
+ 'no': 3,
+ 'nr': 3,
+ 'nso': 4,
+ 'ny': 3,
+ 'nyn': 3,
+ 'om': 3,
+ 'or': 3,
+ 'pa': 3,
+ 'pap': 3,
+ 'pl': 13,
+ 'ps': 3,
+ 'pt': 3,
+ 'rm': 3,
+ 'ro': 9,
+ 'rof': 3,
+ 'ru': 11,
+ 'rwk': 3,
+ 'sah': 0,
+ 'saq': 3,
+ 'se': 7,
+ 'seh': 3,
+ 'ses': 0,
+ 'sg': 0,
+ 'sh': 11,
+ 'shi': 19,
+ 'sk': 12,
+ 'sl': 14,
+ 'sma': 7,
+ 'smi': 7,
+ 'smj': 7,
+ 'smn': 7,
+ 'sms': 7,
+ 'sn': 3,
+ 'so': 3,
+ 'sq': 3,
+ 'sr': 11,
+ 'ss': 3,
+ 'ssy': 3,
+ 'st': 3,
+ 'sv': 3,
+ 'sw': 3,
+ 'syr': 3,
+ 'ta': 3,
+ 'te': 3,
+ 'teo': 3,
+ 'th': 0,
+ 'ti': 4,
+ 'tig': 3,
+ 'tk': 3,
+ 'tl': 4,
+ 'tn': 3,
+ 'to': 0,
+ 'tr': 0,
+ 'ts': 3,
+ 'tzm': 22,
+ 'uk': 11,
+ 'ur': 3,
+ 've': 3,
+ 'vi': 0,
+ 'vun': 3,
+ 'wa': 4,
+ 'wae': 3,
+ 'wo': 0,
+ 'xh': 3,
+ 'xog': 3,
+ 'yo': 0,
+ 'zh': 0,
+ 'zu': 3
+ };
+
+ // utility functions for plural rules methods
+ function isIn(n, list) {
+ return list.indexOf(n) !== -1;
+ }
+ function isBetween(n, start, end) {
+ return start <= n && n <= end;
+ }
+
+ // list of all plural rules methods:
+ // map an integer to the plural form name to use
+ var pluralRules = {
+ '0': function(n) {
+ return 'other';
+ },
+ '1': function(n) {
+ if ((isBetween((n % 100), 3, 10)))
+ return 'few';
+ if (n === 0)
+ return 'zero';
+ if ((isBetween((n % 100), 11, 99)))
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '2': function(n) {
+ if (n !== 0 && (n % 10) === 0)
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '3': function(n) {
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '4': function(n) {
+ if ((isBetween(n, 0, 1)))
+ return 'one';
+ return 'other';
+ },
+ '5': function(n) {
+ if ((isBetween(n, 0, 2)) && n != 2)
+ return 'one';
+ return 'other';
+ },
+ '6': function(n) {
+ if (n === 0)
+ return 'zero';
+ if ((n % 10) == 1 && (n % 100) != 11)
+ return 'one';
+ return 'other';
+ },
+ '7': function(n) {
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '8': function(n) {
+ if ((isBetween(n, 3, 6)))
+ return 'few';
+ if ((isBetween(n, 7, 10)))
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '9': function(n) {
+ if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
+ return 'few';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '10': function(n) {
+ if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
+ return 'few';
+ if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
+ return 'one';
+ return 'other';
+ },
+ '11': function(n) {
+ if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
+ return 'few';
+ if ((n % 10) === 0 ||
+ (isBetween((n % 10), 5, 9)) ||
+ (isBetween((n % 100), 11, 14)))
+ return 'many';
+ if ((n % 10) == 1 && (n % 100) != 11)
+ return 'one';
+ return 'other';
+ },
+ '12': function(n) {
+ if ((isBetween(n, 2, 4)))
+ return 'few';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '13': function(n) {
+ if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
+ return 'few';
+ if (n != 1 && (isBetween((n % 10), 0, 1)) ||
+ (isBetween((n % 10), 5, 9)) ||
+ (isBetween((n % 100), 12, 14)))
+ return 'many';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '14': function(n) {
+ if ((isBetween((n % 100), 3, 4)))
+ return 'few';
+ if ((n % 100) == 2)
+ return 'two';
+ if ((n % 100) == 1)
+ return 'one';
+ return 'other';
+ },
+ '15': function(n) {
+ if (n === 0 || (isBetween((n % 100), 2, 10)))
+ return 'few';
+ if ((isBetween((n % 100), 11, 19)))
+ return 'many';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '16': function(n) {
+ if ((n % 10) == 1 && n != 11)
+ return 'one';
+ return 'other';
+ },
+ '17': function(n) {
+ if (n == 3)
+ return 'few';
+ if (n === 0)
+ return 'zero';
+ if (n == 6)
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '18': function(n) {
+ if (n === 0)
+ return 'zero';
+ if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
+ return 'one';
+ return 'other';
+ },
+ '19': function(n) {
+ if ((isBetween(n, 2, 10)))
+ return 'few';
+ if ((isBetween(n, 0, 1)))
+ return 'one';
+ return 'other';
+ },
+ '20': function(n) {
+ if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
+ isBetween((n % 100), 10, 19) ||
+ isBetween((n % 100), 70, 79) ||
+ isBetween((n % 100), 90, 99)
+ ))
+ return 'few';
+ if ((n % 1000000) === 0 && n !== 0)
+ return 'many';
+ if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
+ return 'two';
+ if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
+ return 'one';
+ return 'other';
+ },
+ '21': function(n) {
+ if (n === 0)
+ return 'zero';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '22': function(n) {
+ if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
+ return 'one';
+ return 'other';
+ },
+ '23': function(n) {
+ if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
+ return 'one';
+ return 'other';
+ },
+ '24': function(n) {
+ if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
+ return 'few';
+ if (isIn(n, [2, 12]))
+ return 'two';
+ if (isIn(n, [1, 11]))
+ return 'one';
+ return 'other';
+ }
+ };
+
+ // return a function that gives the plural form name for a given integer
+ var index = locales2rules[lang.replace(/-.*$/, '')];
+ if (!(index in pluralRules)) {
+ consoleWarn('plural form unknown for [' + lang + ']');
+ return function() { return 'other'; };
+ }
+ return pluralRules[index];
+ }
+
+ // pre-defined 'plural' macro
+ gMacros.plural = function(str, param, key, prop) {
+ var n = parseFloat(param);
+ if (isNaN(n))
+ return str;
+
+ // TODO: support other properties (l20n still doesn't...)
+ if (prop != gTextProp)
+ return str;
+
+ // initialize _pluralRules
+ if (!gMacros._pluralRules)
+ gMacros._pluralRules = getPluralRules(gLanguage);
+ var index = '[' + gMacros._pluralRules(n) + ']';
+
+ // try to find a [zero|one|two] key if it's defined
+ if (n === 0 && (key + '[zero]') in gL10nData) {
+ str = gL10nData[key + '[zero]'][prop];
+ } else if (n == 1 && (key + '[one]') in gL10nData) {
+ str = gL10nData[key + '[one]'][prop];
+ } else if (n == 2 && (key + '[two]') in gL10nData) {
+ str = gL10nData[key + '[two]'][prop];
+ } else if ((key + index) in gL10nData) {
+ str = gL10nData[key + index][prop];
+ }
+
+ return str;
+ };
+
+
+ /**
+ * l10n dictionary functions
+ */
+
+ // fetch an l10n object, warn if not found, apply `args' if possible
+ function getL10nData(key, args) {
+ var data = gL10nData[key];
+ if (!data) {
+ consoleWarn('#' + key + ' missing for [' + gLanguage + ']');
+ }
+
+ /** This is where l10n expressions should be processed.
+ * The plan is to support C-style expressions from the l20n project;
+ * until then, only two kinds of simple expressions are supported:
+ * {[ index ]} and {{ arguments }}.
+ */
+ var rv = {};
+ for (var prop in data) {
+ var str = data[prop];
+ str = substIndexes(str, args, key, prop);
+ str = substArguments(str, args);
+ rv[prop] = str;
+ }
+ return rv;
+ }
+
+ // replace {[macros]} with their values
+ function substIndexes(str, args, key, prop) {
+ var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
+ var reMatch = reIndex.exec(str);
+ if (!reMatch || !reMatch.length)
+ return str;
+
+ // an index/macro has been found
+ // Note: at the moment, only one parameter is supported
+ var macroName = reMatch[1];
+ var paramName = reMatch[2];
+ var param;
+ if (args && paramName in args) {
+ param = args[paramName];
+ } else if (paramName in gL10nData) {
+ param = gL10nData[paramName];
+ }
+
+ // there's no macro parser yet: it has to be defined in gMacros
+ if (macroName in gMacros) {
+ var macro = gMacros[macroName];
+ str = macro(str, param, key, prop);
+ }
+ return str;
+ }
+
+ // 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][gTextProp];
+ } else {
+ consoleWarn('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 an HTML element
+ function translateElement(element) {
+ var l10n = getL10nAttributes(element);
+ if (!l10n.id)
+ return;
+
+ // get the related l10n object
+ var data = getL10nData(l10n.id, l10n.args);
+ if (!data) {
+ consoleWarn('#' + l10n.id + ' missing for [' + gLanguage + ']');
+ return;
+ }
+
+ // translate element (TODO: security checks?)
+ // for the node content, replace the content of the first child textNode
+ // and clear other child textNodes
+ if (data[gTextProp]) { // XXX
+ if (element.children.length === 0) {
+ element[gTextProp] = data[gTextProp];
+ } else {
+ var children = element.childNodes,
+ found = false;
+ for (var i = 0, l = children.length; i < l; i++) {
+ if (children[i].nodeType === 3 &&
+ /\S/.test(children[i].textContent)) { // XXX
+ // using nodeValue seems cross-browser
+ if (found) {
+ children[i].nodeValue = '';
+ } else {
+ children[i].nodeValue = data[gTextProp];
+ found = true;
+ }
+ }
+ }
+ if (!found) {
+ consoleWarn('unexpected error, could not translate element content');
+ }
+ }
+ delete data[gTextProp];
+ }
+
+ for (var k in data) {
+ element[k] = data[k];
+ }
+ }
+
+ // translate an HTML subtree
+ function translateFragment(element) {
+ element = element || document.documentElement;
+
+ // check all translatable children (= w/ a `data-l10n-id' attribute)
+ var children = getTranslatableChildren(element);
+ var elementCount = children.length;
+ for (var i = 0; i < elementCount; i++) {
+ translateElement(children[i]);
+ }
+
+ // translate element itself if necessary
+ translateElement(element);
+ }
+
+
+ /**
+ * Startup & Public API
+ *
+ * Warning: this part of the code contains browser-specific chunks --
+ * that's where obsolete browsers, namely IE8 and earlier, are handled.
+ *
+ * Unlike the rest of the lib, this section is not shared with FirefoxOS/Gaia.
+ */
+
+ // browser-specific startup
+ if (document.addEventListener) { // modern browsers and IE9+
+ document.addEventListener('DOMContentLoaded', function() {
+ var lang = document.documentElement.lang || navigator.language;
+ loadLocale(lang, translateFragment);
+ }, false);
+ } else if (window.attachEvent) { // IE8 and before (= oldIE)
+ // TODO: check if jQuery is loaded (CSS selector + JSON + events)
+
+ // dummy `console.log' and `console.warn' functions
+ if (!window.console) {
+ consoleLog = function(message) {}; // just ignore console.log calls
+ consoleWarn = function(message) {
+ if (gDEBUG)
+ alert('[l10n] ' + message); // vintage debugging, baby!
+ };
+ }
+
+ // worst hack ever for IE6 and IE7
+ if (!window.JSON) {
+ consoleWarn('[l10n] no JSON support');
+
+ getL10nAttributes = function(element) {
+ if (!element)
+ return {};
+ var l10nId = element.getAttribute('data-l10n-id'),
+ l10nArgs = element.getAttribute('data-l10n-args'),
+ args = {};
+ if (l10nArgs) try {
+ args = eval(l10nArgs); // XXX yeah, I know...
+ } catch (e) {
+ consoleWarn('[l10n] could not parse arguments for #' + l10nId);
+ }
+ return { id: l10nId, args: args };
+ };
+ }
+
+ // override `getTranslatableChildren' and `getL10nResourceLinks'
+ if (!document.querySelectorAll) {
+ consoleWarn('[l10n] no "querySelectorAll" support');
+
+ getTranslatableChildren = function(element) {
+ if (!element)
+ return [];
+ var nodes = element.getElementsByTagName('*'),
+ l10nElements = [],
+ n = nodes.length;
+ for (var i = 0; i < n; i++) {
+ if (nodes[i].getAttribute('data-l10n-id'))
+ l10nElements.push(nodes[i]);
+ }
+ return l10nElements;
+ };
+
+ getL10nResourceLinks = function() {
+ var links = document.getElementsByTagName('link'),
+ l10nLinks = [],
+ n = links.length;
+ for (var i = 0; i < n; i++) {
+ if (links[i].type == 'application/l10n')
+ l10nLinks.push(links[i]);
+ }
+ return l10nLinks;
+ };
+ }
+
+ // fire non-standard `localized' DOM events
+ if (document.createEventObject && !document.createEvent) {
+ fireL10nReadyEvent = function(lang) {
+ // hack to simulate a custom event in IE:
+ // to catch this event, add an event handler to `onpropertychange'
+ document.documentElement.localized = 1;
+ };
+ }
+
+ // startup for IE<9
+ window.attachEvent('onload', function() {
+ gTextProp = document.body.textContent ? 'textContent' : 'innerText';
+ var lang = document.documentElement.lang || window.navigator.userLanguage;
+ loadLocale(lang, translateFragment);
+ });
+ }
+
+ // cross-browser API (sorry, oldIE doesn't support getters & setters)
+ return {
+ // get a localized string
+ get: function(key, args, fallback) {
+ var data = getL10nData(key, args) || fallback;
+ if (data) { // XXX double-check this
+ return 'textContent' in data ? data.textContent : '';
+ }
+ return '{{' + key + '}}';
+ },
+
+ // debug
+ getData: function() { return gL10nData; },
+ getText: function() { return gTextData; },
+
+ // get|set the document language
+ getLanguage: function() { return gLanguage; },
+ setLanguage: function(lang) { loadLocale(lang, translateFragment); },
+
+ // get the direction (ltr|rtl) of the current language
+ getDirection: function() {
+ // 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';
+ },
+
+ // translate an element or document fragment
+ translate: translateFragment,
+
+ // this can be used to prevent race conditions
+ getReadyState: function() { return gReadyState; }
+ };
+
+}) (window, document);
+
+// gettext-like shortcut for navigator.webL10n.get
+if (window._ === undefined)
+ var _ = document.webL10n.get;
+
+// CommonJS
+try {
+ exports = document.webL10n;
+}catch(e){}
\ No newline at end of file
diff --git a/src/static/js/pad.js b/src/static/js/pad.js
index 52d61648..b665c2fb 100644
--- a/src/static/js/pad.js
+++ b/src/static/js/pad.js
@@ -111,6 +111,7 @@ function getParams()
var IsnoColors = params["noColors"];
var rtl = params["rtl"];
var alwaysShowChat = params["alwaysShowChat"];
+ var lang = params["lang"];
if(IsnoColors)
{
@@ -173,6 +174,13 @@ function getParams()
chat.stickToScreen();
}
}
+ if(lang)
+ {
+ if(lang !== "")
+ {
+ document.webL10n.setLanguage(lang);
+ }
+ }
}
function getUrlVars()
@@ -451,6 +459,7 @@ var pad = {
{
pad.collabClient.sendClientMessage(msg);
},
+ createCookie: createCookie,
init: function()
{
diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js
index 5a9e7b9b..690dde37 100644
--- a/src/static/js/pad_editor.js
+++ b/src/static/js/pad_editor.js
@@ -75,6 +75,11 @@ var padeditor = (function()
{
pad.changeViewOption('useMonospaceFont', $("#viewfontmenu").val() == 'monospace');
});
+ $("#languagemenu").val(document.webL10n.getLanguage());
+ $("#languagemenu").change(function() {
+ pad.createCookie("language",$("#languagemenu").val(),null,'/');
+ document.webL10n.setLanguage($("#languagemenu").val());
+ });
},
setViewOptions: function(newOptions)
{
diff --git a/src/templates/index.html b/src/templates/index.html
index 23c3c775..ee3009dd 100644
--- a/src/templates/index.html
+++ b/src/templates/index.html
@@ -31,7 +31,8 @@
-
+
+