diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..77f0c8557
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,22 @@
+language: node_js
+node_js:
+ - "0.8"
+install:
+ - "bin/installDeps.sh"
+ - "export GIT_HASH=$(cat .git/HEAD | head -c 7)"
+before_script:
+ - "tests/frontend/travis/sauce_tunnel.sh"
+script:
+ - "tests/frontend/travis/runner.sh"
+env:
+ global:
+ - secure: "OxZ2s724S96xu02746LUN+4lBckAe1BOICJjfA4jnFPNpiNU6XoMH52f+LgG\nZzAwu6xMTv+NsaLGp6Avm3cx4GZ+jIiHe4NB9XOgYPa0r0TBIi3ueWYPDyVv\nCniS/4qX68DoFNV4lh7zMBXn0IIPxT4Wppm3desBpjWDP/SdoRs="
+ - SAUCE_USER=pita
+jdk:
+ - oraclejdk6
+notifications:
+ email:
+ - petermartischka@googlemail.com
+ irc:
+ channels:
+ - "irc.freenode.org#etherpad-lite-dev"
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5810ed255..abcf0a210 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,34 @@
+# v1.2
+ * Internationalization / Language / Translation support (i18n) with support for German/French
+ * A frontend/client side testing framework and backend build tests
+ * Customizable robots.txt
+ * Customizable app title (finally you can name your epl instance!)
+ * eejs render arguments are now passed on to eejs hooks through the newly introduced `renderContext` argument.
+ * Plugin-specific settings in settings.json (finally allowing for things like a google analytics plugin)
+ * Serve admin dashboard at /admin (still very limited, though)
+ * Modify your settings.json through the newly created UI at /admin/settings
+ * Fix: Import
's as 's and not as 's!
+ * Added solaris compatibility (bin/installDeps.sh was broken on solaris)
+ * Fix a bug with IE9 and Password Protected Pads using HTTPS
+
+# v1.1.5
+ * We updated to express v3 (please [make sure](https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x) your plugin works under express v3)
+ * `userColor` URL parameter which sets the initial author color
+ * Hooks for "padCreate", "padRemove", "padUpdate" and "padLoad" events
+ * Security patches concerning the handling of messages originating from clients
+ * Our database abstraction layer now natively supports couchDB, levelDB, mongoDB, postgres, and redis!
+ * We now provide a script helping you to migrate from dirtyDB to MySQL
+ * Support running Etherpad Lite behind IIS, using [iisnode](https://github.com/tjanczuk/iisnode/wiki)
+ * LibreJS Licensing information in headers of HTML templates
+ * Default port number to PORT env var, if port isn't specified in settings
+ * Fix for `convert.js`
+ * Raise upper char limit in chat to 999 characters
+ * Fixes for mobile layout
+ * Fixes for usage behind reverse proxy
+ * Improved documentation
+ * Fixed some opera style bugs
+ * Update npm and fix some bugs, this introduces
+
# v1.1
* Introduced Plugin framework
* Many bugfixes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7b6cb4d3f..b0fff543e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,5 +1,5 @@
# Developer Guidelines
-(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/Pita/etherpad-lite#get-in-touch))
+(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch))
**Our goal is to iterate in small steps. Release often, release early. Evolution instead of a revolution**
diff --git a/README.md b/README.md
deleted file mode 100644
index 5f19dab94..000000000
--- a/README.md
+++ /dev/null
@@ -1,133 +0,0 @@
-<<<<<<< HEAD
-# Making collaborative editing the standard on the web
-
-# About
-Etherpad lite is a really-real time collaborative editor spawned from the Hell fire of Etherpad.
-We're reusing the well tested Etherpad easysync library to make it really realtime. Etherpad Lite
-is based on node.js ergo is much lighter and more stable than the original Etherpad. Our hope
-is that this will encourage more users to use and install a realtime collaborative editor. A smaller, manageable and well
-documented codebase makes it easier for developers to improve the code and contribute towards the project.
-
-**Etherpad vs Etherpad lite**
-
-
- Etherpad Etherpad Lite
-
-
- Size of the folder (without git history) 30 MB 1.5 MB
-
-
- Languages used server side Javascript (Rhino), Java, Scala Javascript (node.js)
-
-
- Lines of server side Javascript code ~101k ~9k
-
-
- RAM Usage immediately after start 257 MB (grows to ~1GB) 16 MB (grows to ~30MB)
-
-
-
-
-Etherpad Lite is designed to be easily embeddable and provides a [HTTP API](https://github.com/Pita/etherpad-lite/wiki/HTTP-API)
-that allows your web application to manage pads, users and groups. It is recommended to use the client implementations available for this API, listed on [this wiki page](https://github.com/Pita/etherpad-lite/wiki/HTTP-API-client-libraries).
-There is also a [jQuery plugin](https://github.com/johnyma22/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website
-
-**Visit [beta.etherpad.org](http://beta.etherpad.org) to test it live**
-
-Also, check out the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**, really!
-
-# Installation
-
-## Windows
-
-### Prebuilt windows package
-This package works out of the box on any windows machine, but it's not very useful for developing purposes...
-
-1. Download the windows package
-2. Extract the folder
-
-Now, run `start.bat` and open in your browser. You like it? [Next steps](#next-steps).
-
-### Fancy install
-You'll need [node.js](http://nodejs.org) and (optionally, though recommended) git.
-
-1. Grab the source, either
- - download
- - or `git clone https://github.com/Pita/etherpad-lite.git` (for this you need git, obviously)
-2. start `bin\installOnWindows.bat`
-
-Now, run `start.bat` and open in your browser.
-
-Update to the latest version with `git pull origin`, then run `bin\installOnWindows.bat`, again.
-
-[Next steps](#next-steps).
-
-## Linux
-You'll need gzip, git, curl, libssl develop libraries, python and gcc.
-*For Debian/Ubuntu*: `apt-get install gzip git-core curl python libssl-dev pkg-config build-essential`
-*For Fedora/CentOS*: `yum install gzip git-core curl python openssl-devel && yum groupinstall "Development Tools"`
-
-Additionally, you'll need [node.js](http://nodejs.org).
-
-**As any user (we recommend creating a separate user called etherpad-lite):**
-
-1. Move to a folder where you want to install Etherpad Lite. Clone the git repository `git clone git://github.com/Pita/etherpad-lite.git`
-2. Change into the new directory containing the cloned source code `cd etherpad-lite`
-
-Now, run `bin/run.sh` and open in your browser.
-
-Update to the latest version with `git pull origin`. The next start with bin/run.sh will update the dependencies.
-
-You like it? [Next steps](#next-steps).
-
-# Next Steps
-
-## Tweak the settings
-You can modify the settings in `settings.json`. (If you need to handle multiple settings files, you can pass the path to a settings file to `bin/run.sh` using the `-s|--settings` option. This allows you to run multiple Etherpad Lite instances from the same installation.)
-
-You should use a dedicated database such as "mysql", if you are planning on using etherpad-lite in a production environment, since the "dirtyDB" database driver is only for testing and/or development purposes.
-
-## Helpful resources
-The [wiki](https://github.com/Pita/etherpad-lite/wiki) is your one-stop resource for Tutorials and How-to's, really check it out! Also, feel free to improve these wiki pages.
-
-Documentation can be found in `docs/`.
-
-# Development
-
-## Things you should know
-Read this [git guide](http://learn.github.com/p/intro.html) and watch this [video on getting started with Etherpad Lite Development](http://youtu.be/67-Q26YH97E).
-
-If you're new to node.js, start with Ryan Dahl's [Introduction to Node.js](http://youtu.be/jo_B4LTHi3I).
-
-You can debug Etherpad lite using `bin/debugRun.sh`.
-
-If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/Pita/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).
-
-## Getting started
-You know all this and just want to know how you can help?
-
-Look at the [TODO list](https://github.com/Pita/etherpad-lite/wiki/TODO) and our [Issue tracker](https://github.com/Pita/etherpad-lite/issues). (Please consider using [jshint](http://www.jshint.com/about/), if you plan to contribute code.)
-
-Also, and most importantly, read our [**Developer Guidelines**](https://github.com/Pita/etherpad-lite/wiki/Developer-Guidelines), really!
-
-# Get in touch
-Join the [mailinglist](http://groups.google.com/group/etherpad-lite-dev) and make some noise on our freenode irc channel [#etherpad-lite-dev](http://webchat.freenode.net?channels=#etherpad-lite-dev)!
-
-# Modules created for this project
-
-* [ueberDB](https://github.com/Pita/ueberDB) "transforms every database into a object key value store" - manages all database access
-* [channels](https://github.com/Pita/channels) "Event channels in node.js" - ensures that ueberDB operations are atomic and in series for each key
-* [async-stacktrace](https://github.com/Pita/async-stacktrace) "Improves node.js stacktraces and makes it easier to handle errors"
-
-# Donate!
-* [Flattr] (http://flattr.com/thing/71378/Etherpad-Foundation)
-* Paypal - Press the donate button on [etherpad.org](http://etherpad.org)
-
-# License
-[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
-=======
-We moved!
-=============
-
-You can now find Etherpad Lite under [github.com/ether/etherpad-lite](https://github.com/ether/etherpad-lite)
->>>>>>> a5c4fb154f7414cd5dca9e50c3f2ad00e42672f5
diff --git a/bin/buildForWindows.sh b/bin/buildForWindows.sh
index 1d47bff1b..c67a3701c 100755
--- a/bin/buildForWindows.sh
+++ b/bin/buildForWindows.sh
@@ -52,6 +52,13 @@ echo "download windows node..."
cd bin
wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O ../node.exe
+echo "remove git history to reduce folder size"
+rm -rf .git/objects
+
+echo "remove windows jsdom-nocontextify/test folder"
+rm -rf /tmp/etherpad-lite-win/node_modules/ep_etherpad-lite/node_modules/jsdom-nocontextifiy/test/
+rm -rf /tmp/etherpad-lite-win/src/node_modules/jsdom-nocontextifiy/test/
+
echo "create the zip..."
cd /tmp
zip -9 -r etherpad-lite-win.zip etherpad-lite-win
diff --git a/bin/installDeps.sh b/bin/installDeps.sh
index 15731ae91..6f5c732cd 100755
--- a/bin/installDeps.sh
+++ b/bin/installDeps.sh
@@ -69,7 +69,7 @@ echo "Ensure that all dependencies are up to date..."
cd node_modules
[ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite
cd ep_etherpad-lite
- npm install
+ npm install --loglevel warn
) || {
rm -rf node_modules
exit 1
@@ -79,7 +79,7 @@ echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true"
NEEDED_VERSION="1.7.1"
if [ -f "src/static/js/jquery.js" ]; then
- if [ $(uname) = "SunOS"]; then
+ if [ $(uname) = "SunOS" ]; then
VERSION=$(cat src/static/js/jquery.js | head -n 3 | ggrep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
else
VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
diff --git a/bin/installOnWindows.bat b/bin/installOnWindows.bat
index b4e4f5400..32ff847f6 100644
--- a/bin/installOnWindows.bat
+++ b/bin/installOnWindows.bat
@@ -13,7 +13,7 @@ cmd /C node -e %check_version% || exit /B 1
echo _
echo Installing etherpad-lite and dependencies...
-cmd /C npm install src/ || exit /B 1
+cmd /C npm install src/ --loglevel warn || exit /B 1
echo _
echo Copying custom templates...
diff --git a/bin/migrateDirtyDBtoMySQL.js b/bin/migrateDirtyDBtoMySQL.js
index f2bc8efe2..d0273de0d 100644
--- a/bin/migrateDirtyDBtoMySQL.js
+++ b/bin/migrateDirtyDBtoMySQL.js
@@ -1,11 +1,17 @@
-var dirty = require("../src/node_modules/ueberDB/node_modules/dirty")('var/dirty.db');
-var db = require("../src/node/db/DB");
+require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
-db.init(function() {
- db = db.db;
- dirty.on("load", function() {
- dirty.forEach(function(key, value) {
- db.set(key, value);
+ process.chdir(npm.root+'/..')
+
+ var dirty = require("ep_etherpad-lite/node_modules/ueberDB/node_modules/dirty")('var/dirty.db');
+ var db = require("ep_etherpad-lite/node/db/DB");
+
+ db.init(function() {
+ db = db.db;
+ dirty.on("load", function() {
+ dirty.forEach(function(key, value) {
+ db.set(key, value);
+ });
});
});
+
});
diff --git a/doc/api/embed_parameters.md b/doc/api/embed_parameters.md
index 7f84f0640..3100fff9d 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 db7cefaaa..5d3022be5 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 000000000..3f0901cad
--- /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 26e4f603f..89c8964aa 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" } },
@@ -13,6 +14,7 @@
{ "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } },
{ "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } },
{ "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } },
+ { "name": "tests", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests:expressCreateServer" } },
{ "name": "admin", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin:expressCreateServer" } },
{ "name": "adminplugins", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",
diff --git a/src/locales/bn.ini b/src/locales/bn.ini
new file mode 100644
index 000000000..7eca9e383
--- /dev/null
+++ b/src/locales/bn.ini
@@ -0,0 +1,32 @@
+; Exported from translatewiki.net
+; Author: Nasir8891
+[bn]
+index.newPad = নতà§à¦¨ পà§à¦¯à¦¾à¦¡
+index.createOpenPad = অথবা নাম লিখে পà§à¦¯à¦¾à¦¡ খà§à¦²à§à¦¨/তৈরী করà§à¦¨:
+pad.toolbar.bold.title = গাড় করা (Ctrl-B)
+pad.toolbar.italic.title = বাà¦à¦•à¦¾ করা (Ctrl-I)
+pad.toolbar.settings.title = সেটিং
+pad.colorpicker.save = সংরকà§à¦·à¦£
+pad.colorpicker.cancel = বাতিল
+pad.loading = লোডিং...
+pad.settings.language = à¦à¦¾à¦·à¦¾:
+pad.importExport.successful = সফল!
+; Fuzzy
+pad.importExport.export = à¦à¦‡ পà§à¦¯à¦¾à¦¡à¦Ÿà¦¿ à¦à¦•à§à¦¸à¦ªà§‹à¦°à§à¦Ÿ করà§à¦¨
+pad.importExport.exporthtml = à¦à¦‡à¦šà¦Ÿà¦¿à¦à¦®à¦à¦²
+pad.importExport.exportplain = সাধারণ লেখা
+pad.importExport.exportword = মাইকà§à¦°à§‹à¦¸à¦«à¦Ÿ ওয়ারà§à¦¡
+pad.importExport.exportpdf = পিডিà¦à¦«
+pad.importExport.exportopen = ওডিà¦à¦« (ওপেন ডকà§à¦®à§‡à¦¨à§à¦Ÿ ফরমà§à¦¯à¦¾à¦Ÿ)
+pad.modals.deleted = অপসারিত।
+pad.modals.deleted.explanation = à¦à¦‡ পà§à¦¯à¦¾à¦¡à¦Ÿà¦¿ অপসারণ করা হয়েছে।
+pad.modals.disconnected.explanation = সারà§à¦à¦¾à¦°à§‡à¦° সাথে যোগাযোগ করা যাচà§à¦›à§‡ না
+pad.share = শেয়ার করà§à¦¨
+pad.share.link = লিংক
+pad.share.emebdcode = ইউআরà¦à¦² সংযোজন
+pad.chat = চà§à¦¯à¦¾à¦Ÿ
+pad.chat.title = à¦à¦‡ পà§à¦¯à¦¾à¦¡à§‡à¦° জনà§à¦¯ চà§à¦¯à¦¾à¦Ÿ চালৠকরà§à¦¨à¥¤
+timeslider.toolbar.returnbutton = পà§à¦¯à¦¾à¦¡à§‡ ফিরে যাও
+timeslider.toolbar.authors = লেখকগণ:
+timeslider.toolbar.authorsList = কোনো লেখক নেই
+timeslider.exportCurrent = বরà§à¦¤à¦®à¦¾à¦¨ সংসà§à¦•à¦°à¦£à¦Ÿà¦¿ à¦à¦•à§à¦¸à¦ªà§‹à¦°à§à¦Ÿ করà§à¦¨:
diff --git a/src/locales/de.ini b/src/locales/de.ini
new file mode 100644
index 000000000..177746a56
--- /dev/null
+++ b/src/locales/de.ini
@@ -0,0 +1,78 @@
+; Exported from translatewiki.net
+[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.globalView = Gemeinsame Ansicht
+pad.settings.language = Sprache:
+pad.importExport.import_export = Import/Export
+pad.importExport.import = Datei oder Dokument hochladen
+pad.importExport.successful = Erfolgreich!
+; Fuzzy
+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:
diff --git a/src/locales/en.ini b/src/locales/en.ini
new file mode 100644
index 000000000..a110583ea
--- /dev/null
+++ b/src/locales/en.ini
@@ -0,0 +1,77 @@
+[*]
+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 = Opened 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 window instead.
+pad.modals.unauth = Not authorized
+pad.modals.unauth.explanation = Your permissions have changed while viewing this page. Try to reconnect.
+pad.modals.looping = Disconnected.
+pad.modals.looping.explanation = There are communication problems with the synchronization server.
+pad.modals.looping.cause = Perhaps you connected through an incompatible firewall or proxy.
+pad.modals.initsocketfail = Server is unreachable.
+pad.modals.initsocketfail.explanation = Couldn't connect to the synchronization server.
+pad.modals.initsocketfail.cause = This is probably due to a problem with your browser or your internet connection.
+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/es.ini b/src/locales/es.ini
new file mode 100644
index 000000000..acb6a5cf1
--- /dev/null
+++ b/src/locales/es.ini
@@ -0,0 +1,78 @@
+; Exported from translatewiki.net
+[es]
+index.newPad = Nuevo Pad
+index.createOpenPad = o puedes crear/abrir un Pad con el nombre:
+pad.toolbar.bold.title = Negrita (Ctrl-B)
+pad.toolbar.italic.title = Cursiva (Ctrl-I)
+pad.toolbar.underline.title = Subrayado (Ctrl-U)
+pad.toolbar.strikethrough.title = Tachado
+pad.toolbar.ol.title = Lista ordenada
+pad.toolbar.ul.title = Lista desordenada
+pad.toolbar.indent.title = Sangrar
+pad.toolbar.unindent.title = Desangrar
+pad.toolbar.undo.title = Deshacer (Ctrl-Z)
+pad.toolbar.redo.title = Rehacer (Ctrl-Y)
+pad.toolbar.clearAuthorship.title = Eliminar los colores de los autores
+pad.toolbar.import_export.title = Importar/Exportar a diferentes formatos de archivos
+pad.toolbar.timeslider.title = LÃnea de tiempo
+pad.toolbar.savedRevision.title = Revisiones guardadas
+pad.toolbar.settings.title = Configuración
+pad.toolbar.embed.title = Incrustar este pad
+pad.toolbar.showusers.title = Mostrar los usuarios de este pad
+pad.colorpicker.save = Guardar
+pad.colorpicker.cancel = Cancelar
+pad.loading = Cargando...
+pad.settings.padSettings = Configuración del Pad
+pad.settings.myView = Mi vista
+pad.settings.stickychat = Chat siempre encima
+pad.settings.colorcheck = Color de autorÃa
+pad.settings.linenocheck = Números de lÃnea
+pad.settings.fontType = TipografÃa:
+pad.settings.fontType.normal = Normal
+pad.settings.fontType.monospaced = Monoespacio
+pad.settings.globalView = Vista global
+pad.settings.language = Idioma:
+pad.importExport.import_export = Importar/Exportar
+pad.importExport.import = Subir cualquier texto o documento
+pad.importExport.successful = ¡Operación exitosa!
+; Fuzzy
+pad.importExport.export = Exporta el pad actual como
+pad.importExport.exporthtml = HTML
+pad.importExport.exportplain = Texto plano
+pad.importExport.exportword = Microsoft Word
+pad.importExport.exportpdf = PDF
+pad.importExport.exportopen = ODF (Open Document Format)
+pad.importExport.exportdokuwiki = DokuWiki
+pad.modals.connected = Conectado.
+pad.modals.reconnecting = Reconectando a tu pad..
+pad.modals.forcereconnect = Reconexión forzosa
+pad.modals.uderdup = Abrir en otra ventana
+pad.modals.userdup.explanation = Este pad parece estar abierto en más de una ventana de tu navegador.
+pad.modals.userdup.advice = Reconectar para usar esta ventana.
+pad.modals.unauth = No autorizado.
+pad.modals.unauth.explanation = Los permisos han cambiado mientras estabas viendo esta página. Intenta reconectar de nuevo.
+pad.modals.looping = Desconectado.
+pad.modals.looping.explanation = Estamos teniendo problemas con la sincronización en el servidor.
+pad.modals.looping.cause = Quizás su conexión fluya a través de un proxy o un cortafuegos incompatible.
+pad.modals.initsocketfail = Servidor incalcanzable.
+pad.modals.initsocketfail.explanation = No se pudo conectar al sevidor de sincronización.
+pad.modals.initsocketfail.cause = Puede ser a causa de tu navegador o de una caÃda en tu conexión de Internet.
+pad.modals.slowcommit = Desconectado.
+pad.modals.slowcommit.explanation = El servidor no responde.
+pad.modals.slowcommit.cause = Puede deberse a problemas con tu conexión de red.
+pad.modals.deleted = Borrado.
+pad.modals.deleted.explanation = Este pad ha sido borrado.
+pad.modals.disconnected = Has sido desconectado.
+pad.modals.disconnected.explanation = Se perdió la conexión con el servidor
+pad.modals.disconnected.cause = El servidor podrÃa no estar disponible. Contacte con nosotros si esto continúa sucediendo.
+pad.share = Compatir el pad
+pad.share.readonly = Sólo lectura
+pad.share.link = Enlace
+pad.share.emebdcode = Incrustar URL
+pad.chat = Chat
+pad.chat.title = Abrir el chat para este pad.
+timeslider.pageTitle = {{appTitle}} LÃnea de tiempo
+timeslider.toolbar.returnbutton = Volver al pad
+timeslider.toolbar.authors = Autores:
+timeslider.toolbar.authorsList = Sin autores
+timeslider.exportCurrent = Exportar la versión actual como:
diff --git a/src/locales/fi.ini b/src/locales/fi.ini
new file mode 100644
index 000000000..cbaea8856
--- /dev/null
+++ b/src/locales/fi.ini
@@ -0,0 +1,49 @@
+; Exported from translatewiki.net
+; Author: Nike
+[fi]
+index.newPad = Uusi muistio
+index.createOpenPad = tai avaa muistio nimellä:
+pad.toolbar.bold.title = Lihavointi (Ctrl-B)
+pad.toolbar.italic.title = Kursivointi (Ctrl-I)
+pad.toolbar.underline.title = Alleviivaus (Ctrl-U)
+pad.toolbar.strikethrough.title = Yliviivaus
+pad.toolbar.ol.title = Numeroitu lista
+pad.toolbar.ul.title = Numeroimaton lista
+pad.toolbar.indent.title = Sisennä
+pad.toolbar.unindent.title = Ulonna
+pad.toolbar.undo.title = Kumoa (Ctrl-Z)
+pad.toolbar.redo.title = Tee uudelleen (Ctrl-Y)
+pad.toolbar.clearAuthorship.title = Poista kirjoittavärit
+pad.toolbar.import_export.title = Tuo tai vie eri muotoihin
+pad.toolbar.savedRevision.title = Tallennetut versiot
+pad.toolbar.settings.title = Asetukset
+pad.toolbar.embed.title = Upota muistio
+pad.toolbar.showusers.title = Näytä muistion käyttäjät
+pad.colorpicker.save = Tallenna
+pad.colorpicker.cancel = Peru
+pad.loading = Ladataan…
+pad.settings.padSettings = Muistion asetukset
+pad.settings.myView = Oma näkymä
+pad.settings.stickychat = Keskustelu aina näkyvissä
+pad.settings.colorcheck = Kirjoittavärit
+pad.settings.linenocheck = Rivinumerot
+pad.settings.fontType = Kirjasintyyppi:
+pad.settings.fontType.normal = normaali
+pad.settings.fontType.monospaced = tasalevyinen
+pad.settings.language = Kieli:
+pad.importExport.import_export = Tuonti/vienti
+pad.importExport.exporthtml = HTML
+pad.importExport.exportplain = Muotoilematon teksti
+pad.importExport.exportword = Microsoft Word
+pad.importExport.exportpdf = PDF
+pad.importExport.exportopen = ODF (Open Document Format)
+pad.importExport.exportdokuwiki = DokuWiki
+pad.modals.connected = Yhdistetty.
+pad.modals.reconnecting = Herätellään yhteyttä muistioon...
+pad.modals.forcereconnect = Pakota uudelleenyhdistäminen
+pad.share = Jaa muistio
+pad.share.readonly = Vain luku
+pad.share.link = Linkki
+pad.share.emebdcode = Upotusosoite
+pad.chat = Keskustelu
+timeslider.toolbar.returnbutton = Palaa muistioon
diff --git a/src/locales/fr.ini b/src/locales/fr.ini
new file mode 100644
index 000000000..a80ff205b
--- /dev/null
+++ b/src/locales/fr.ini
@@ -0,0 +1,78 @@
+; Exported from translatewiki.net
+[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 identifiant 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é!
+; Fuzzy
+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/locales/nl.ini b/src/locales/nl.ini
new file mode 100644
index 000000000..87eaeb13e
--- /dev/null
+++ b/src/locales/nl.ini
@@ -0,0 +1,79 @@
+; Exported from translatewiki.net
+; Author: Siebrand
+[nl]
+index.newPad = Nieuw pad
+index.createOpenPad = Maak of open pad met de naam:
+pad.toolbar.bold.title = Vet (Ctrl-B)
+pad.toolbar.italic.title = Cursief (Ctrl-I)
+pad.toolbar.underline.title = Onderstrepen (Ctrl-U)
+pad.toolbar.strikethrough.title = Doorhalen
+pad.toolbar.ol.title = Geordende lijst
+pad.toolbar.ul.title = Ongeordende lijst
+pad.toolbar.indent.title = Inspringen
+pad.toolbar.unindent.title = Inspringing verkleinen
+pad.toolbar.undo.title = Ongedaan maken (Ctrl-Z)
+pad.toolbar.redo.title = Opnieuw uitvoeren (Ctrl-Y)
+pad.toolbar.clearAuthorship.title = Kleuren auteurs wissen
+pad.toolbar.import_export.title = Naar/van andere opmaak exporteren/importeren
+pad.toolbar.timeslider.title = Tijdlijn
+pad.toolbar.savedRevision.title = Opgeslagen versies
+pad.toolbar.settings.title = Instellingen
+pad.toolbar.embed.title = Pad insluiten
+pad.toolbar.showusers.title = Gebruikers van dit pad weergeven
+pad.colorpicker.save = Opslaan
+pad.colorpicker.cancel = Annuleren
+pad.loading = Bezig met laden…
+pad.settings.padSettings = Padinstellingen
+pad.settings.myView = Mijn overzicht
+pad.settings.stickychat = Chat altijd zichtbaar
+pad.settings.colorcheck = Kleuren auteurs
+pad.settings.linenocheck = Regelnummers
+pad.settings.fontType = Lettertype:
+pad.settings.fontType.normal = Normaal
+pad.settings.fontType.monospaced = Monospace
+pad.settings.globalView = Globaal overzicht
+pad.settings.language = Taal:
+pad.importExport.import_export = Importeren/exporteren
+pad.importExport.import = Upload een tekstbestand of document
+pad.importExport.successful = Afgerond
+; Fuzzy
+pad.importExport.export = Huidige pad exporteren als
+pad.importExport.exporthtml = HTML
+pad.importExport.exportplain = Tekst zonder opmaak
+pad.importExport.exportword = Microsoft Word
+pad.importExport.exportpdf = PDF
+pad.importExport.exportopen = ODF (Open Document Format)
+pad.importExport.exportdokuwiki = DokuWiki
+pad.modals.connected = Verbonden.
+pad.modals.reconnecting = Opnieuw verbinding maken met uw pad...
+pad.modals.forcereconnect = Opnieuw verbinden
+pad.modals.uderdup = Openen in ander venster
+pad.modals.userdup.explanation = Dit pad is meer dan één keer geopend in een browservenster op deze computer.
+pad.modals.userdup.advice = Opnieuw verbinden en dit venster gebruiken.
+pad.modals.unauth = Niet toegestaan
+pad.modals.unauth.explanation = Uw rechten zijn gewijzigd terwijl u de pagina aan het bekijken was. Probeer opnieuw te verbinden.
+pad.modals.looping = Verbinding verbroken.
+pad.modals.looping.explanation = Er is een probleem opgetreden tijdens de communicatie met de synchronisatieserver.
+pad.modals.looping.cause = Mogelijk gebruikt de server een niet compatibele firewall of proxy server.
+pad.modals.initsocketfail = Server is niet bereikbaar.
+pad.modals.initsocketfail.explanation = Het was niet mogelijk te verbinden met de synchronisatieserver.
+pad.modals.initsocketfail.cause = Mogelijk komt dit door uw browser of internetverbinding.
+pad.modals.slowcommit = Verbinding verbroken.
+pad.modals.slowcommit.explanation = De server reageert niet.
+pad.modals.slowcommit.cause = Dit komt mogelijk door netwerkproblemen.
+pad.modals.deleted = Verwijderd.
+pad.modals.deleted.explanation = Dit pad is verwijderd.
+pad.modals.disconnected = Uw verbinding is verbroken.
+pad.modals.disconnected.explanation = De verbinding met de server is verbroken
+pad.modals.disconnected.cause = De server is mogelijk niet beschikbaar. Stel alstublieft de beheerder op de hoogte.
+pad.share = Pad delen
+pad.share.readonly = Alleen-lezen
+pad.share.link = Verwijzing
+pad.share.emebdcode = URL insluiten
+pad.chat = Chatten
+pad.chat.title = Chat voor dit pad opnenen
+timeslider.pageTitle = Tijdlijn voor {{appTitle}}
+timeslider.toolbar.returnbutton = Terug naar pad
+timeslider.toolbar.authors = Auteurs:
+timeslider.toolbar.authorsList = Geen auteurs
+timeslider.exportCurrent = Huidige versie exporteren als:
diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
index a30e4e818..a0bccfc51 100644
--- a/src/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -619,7 +619,7 @@ exports.updatePadClients = function(pad, callback)
//https://github.com/caolan/async#whilst
//send them all new changesets
async.whilst(
- function (){ return sessioninfos[session].rev < pad.getHeadRevisionNumber()},
+ function (){ return sessioninfos[session] && sessioninfos[session].rev < pad.getHeadRevisionNumber()},
function(callback)
{
var author, revChangeset, currentTime;
diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js
new file mode 100644
index 000000000..94cd5fb62
--- /dev/null
+++ b/src/node/hooks/express/tests.js
@@ -0,0 +1,47 @@
+var path = require("path")
+ , npm = require("npm")
+ , fs = require("fs");
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+ args.app.get('/tests/frontend/specs_list.js', function(req, res){
+ fs.readdir('tests/frontend/specs', function(err, files){
+ if(err){ return res.send(500); }
+
+ res.send("var specs_list = " + JSON.stringify(files.sort()) + ";\n");
+ });
+ });
+
+ var url2FilePath = function(url){
+ var subPath = url.substr("/tests/frontend".length);
+ if (subPath == ""){
+ subPath = "index.html"
+ }
+ subPath = subPath.split("?")[0];
+
+ var filePath = path.normalize(npm.root + "/../tests/frontend/")
+ filePath += subPath.replace("..", "");
+ return filePath;
+ }
+
+ args.app.get('/tests/frontend/specs/*', function (req, res) {
+ var specFilePath = url2FilePath(req.url);
+ var specFileName = path.basename(specFilePath);
+
+ fs.readFile(specFilePath, function(err, content){
+ if(err){ return res.send(500); }
+
+ content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });";
+
+ res.send(content);
+ });
+ });
+
+ args.app.get('/tests/frontend/*', function (req, res) {
+ var filePath = url2FilePath(req.url);
+ res.sendfile(filePath);
+ });
+
+ args.app.get('/tests/frontend', function (req, res) {
+ res.redirect('/tests/frontend/');
+ });
+}
\ 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..4d42de048
--- /dev/null
+++ b/src/node/hooks/i18n.js
@@ -0,0 +1,35 @@
+var languages = require('languages')
+ , fs = require('fs')
+ , path = require('path')
+ , express = require('express')
+
+var localesPath = __dirname+"/../../locales";
+
+// Serve English strings directly with /locales.ini
+var localeIndex = fs.readFileSync(localesPath+'/en.ini')+'\r\n';
+
+// add language base 'en' to availableLangs
+exports.availableLangs = {en: languages.getLanguageInfo('en')}
+
+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'
+
+ // add info language {name, nativeName, direction} to availableLangs
+ exports.availableLangs[locale]=languages.getLanguageInfo(locale);
+ })
+})
+
+exports.expressCreateServer = function(n, args) {
+
+ args.app.use('/locales', express.static(localesPath));
+
+ args.app.get('/locales.ini', function(req, res) {
+ res.send(localeIndex);
+ })
+
+}
diff --git a/src/node/padaccess.js b/src/node/padaccess.js
index 4388ab946..d87809149 100644
--- a/src/node/padaccess.js
+++ b/src/node/padaccess.js
@@ -7,7 +7,7 @@ module.exports = function (req, res, callback) {
// FIXME: Why is this ever undefined??
if (req.cookies === undefined) req.cookies = {};
- securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj) {
+ securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) {
if(ERR(err, callback)) return;
//there is access, continue
diff --git a/src/package.json b/src/package.json
index c3c4968a6..9fd180abd 100644
--- a/src/package.json
+++ b/src/package.json
@@ -17,7 +17,7 @@
"resolve" : "0.2.x",
"socket.io" : "0.9.x",
"ueberDB" : "0.1.8",
- "async" : "0.1.22",
+ "async" : "0.1.x",
"express" : "3.x",
"connect" : "2.4.x",
"clean-css" : "0.3.2",
@@ -35,14 +35,16 @@
"security" : "1.0.0",
"tinycon" : "0.0.1",
"underscore" : "1.3.1",
- "unorm" : "1.0.0"
+ "unorm" : "1.0.0",
+ "languages" : "0.1.1"
},
"bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": {
- "jshint" : "*"
+ "jshint" : "*",
+ "wd" : "0.0.26"
},
"engines" : { "node" : ">=0.6.0",
"npm" : ">=1.0"
},
- "version" : "1.1.4"
+ "version" : "1.2.0"
}
diff --git a/src/static/css/iframe_editor.css b/src/static/css/iframe_editor.css
index bca00ff42..5134fcdb1 100644
--- a/src/static/css/iframe_editor.css
+++ b/src/static/css/iframe_editor.css
@@ -126,7 +126,7 @@ body.doesWrap {
.sidedivdelayed { /* class set after sizes are set */
background-color: #eee;
color: #888 !important;
- border-right: 1px solid #999;
+ border-right: 1px solid #ccc;
}
.sidedivhidden {
display: none;
diff --git a/src/static/css/pad.css b/src/static/css/pad.css
index 5ee6b3c56..64f9f0d44 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/chat.js b/src/static/js/chat.js
index 486c7256d..4493ed154 100644
--- a/src/static/js/chat.js
+++ b/src/static/js/chat.js
@@ -150,7 +150,7 @@ var chat = (function()
$("#chatinput").keypress(function(evt)
{
//if the user typed enter, fire the send
- if(evt.which == 13)
+ if(evt.which == 13 || evt.which == 10)
{
evt.preventDefault();
self.send();
diff --git a/src/static/js/l10n.js b/src/static/js/l10n.js
new file mode 100644
index 000000000..ef8218d3e
--- /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*$/;// Use ; for comments!
+ 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 || navigator.userLanguage || 'en';
+ 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 || navigator.language || navigator.userLanguage || 'en';
+ 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..b665c2fbd 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()
@@ -389,6 +397,10 @@ function handshake()
});
// Bind the colorpicker
var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220});
+ // Bind the read only button
+ $('#readonlyinput').on('click',function(){
+ padeditbar.setEmbedLinks();
+ });
}
var pad = {
@@ -447,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 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/admin/settings.html b/src/templates/admin/settings.html
index 880f0eb17..c4f505788 100644
--- a/src/templates/admin/settings.html
+++ b/src/templates/admin/settings.html
@@ -23,8 +23,8 @@
Etherpad Lite Settings
- Example production settings template
- Example development settings template
+ Example production settings template
+ Example development settings template
diff --git a/src/templates/index.html b/src/templates/index.html
index 23c3c7755..9fd33a26a 100644
--- a/src/templates/index.html
+++ b/src/templates/index.html
@@ -31,8 +31,17 @@
-
+
+
+
+