diff --git a/src/ep.json b/src/ep.json
index 26e4f603f..02ddbc277 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 000000000..0bd2a8fe3
--- /dev/null
+++ b/src/locales/de.ini
@@ -0,0 +1,84 @@
+[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 mit anderen 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.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 000000000..1b262af96
--- /dev/null
+++ b/src/locales/en.ini
@@ -0,0 +1,76 @@
+[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.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/node/hooks/i18n.js b/src/node/hooks/i18n.js
new file mode 100644
index 000000000..67170ab35
--- /dev/null
+++ b/src/node/hooks/i18n.js
@@ -0,0 +1,28 @@
+var Globalize = require('globalize')
+ , fs = require('fs')
+ , path = require('path')
+
+exports.availableLangs = {en: 'English'}
+fs.readdir(__dirname+"/../../locales", function(er, files) {
+ files.forEach(function(locale) {
+ locale = locale.split('.')[0]
+ if(locale.toLowerCase() == 'en') return;
+
+ 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.get('/locale.ini', function(req, res) {
+ // let gloablize find out the preferred locale and default to 'en'
+ Globalize.culture(req.cookies['language'] || req.header('Accept-Language') || 'en');
+ var localePath = path.normalize(__dirname +"/../../locales/"+Globalize.culture().name+".ini");
+ res.sendfile(localePath, function(er) {
+ if(er) console.error(er)
+ });
+ })
+
+}
\ No newline at end of file
diff --git a/src/package.json b/src/package.json
index c3c4968a6..c27688d41 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/js/l10n.js b/src/static/js/l10n.js
new file mode 100644
index 000000000..e2584cc4c
--- /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 c55f8dfeb..a07b29b5e 100644
--- a/src/static/js/pad.js
+++ b/src/static/js/pad.js
@@ -447,6 +447,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 5a9e7b9b6..690dde377 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 23c3c7755..23f24c53a 100644
--- a/src/templates/index.html
+++ b/src/templates/index.html
@@ -31,7 +31,8 @@
-
+
+