diff --git a/setup.py b/setup.py
index 369bfc7c91484bc762f3d14bd081d05aaa2a7dca..bcdb8527f50431c155e3c42ce60afbd08d051002 100755
--- a/setup.py
+++ b/setup.py
@@ -84,6 +84,7 @@ setup(name=name,
           'certificate_authority = slapos.recipe.certificate_authority:Recipe',
           'certificate_authority.request = slapos.recipe.certificate_authority:Request',
           'check_page_content = slapos.recipe.check_page_content:Recipe',
+          'check_page_content_phantomjs = slapos.recipe.check_page_content:PhantomJSRecipe',
           'check_port_listening = slapos.recipe.check_port_listening:Recipe',
           'check_url_available = slapos.recipe.check_url_available:Recipe',
           'cloud9 = slapos.recipe.cloud9:Recipe',
diff --git a/slapos/recipe/check_page_content/__init__.py b/slapos/recipe/check_page_content/__init__.py
index 40180cb9156356a892140bfc473ba76d731e3f83..48694669896b4402b1b20ebc5761ca6a4bf8f348 100644
--- a/slapos/recipe/check_page_content/__init__.py
+++ b/slapos/recipe/check_page_content/__init__.py
@@ -49,3 +49,26 @@ class Recipe(GenericBaseRecipe):
     )
 
     return [promise]
+
+class PhantomJSRecipe(GenericBaseRecipe):
+  """
+  Create script for checking page content at url with js script
+  """
+  
+  def install(self):
+    config = {
+      'script-path': self.options['script-path'].strip(),
+      'dash-path': self.options['dash-path'].strip(),
+      'phantomjs-path': self.options['phantomjs-path'].strip(),
+      'phantomjs-options': self.options.get('phantomjs-options','')
+    }
+    
+    promise = self.createExecutable(
+      self.options['path'].strip(),
+      self.substituteTemplate(
+        self.getTemplateFilename('check_page_content_phantomjs.in'),
+        config
+      )
+    )
+
+    return [promise]
\ No newline at end of file
diff --git a/slapos/recipe/check_page_content/template/check_page_content_phantomjs.in b/slapos/recipe/check_page_content/template/check_page_content_phantomjs.in
new file mode 100644
index 0000000000000000000000000000000000000000..c8c720ec5fe5c02988159ca2940c04090b458407
--- /dev/null
+++ b/slapos/recipe/check_page_content/template/check_page_content_phantomjs.in
@@ -0,0 +1,10 @@
+#!%(dash-path)s
+# BEWARE: This file is operated by slapgrid
+# BEWARE: It will be overwritten automatically
+
+%(phantomjs-path)s %(phantomjs-options)s %(script-path)s
+
+if [ $? != 0 ]; then
+  echo "PhantomJS script returned non zero output" >&2
+  exit 1
+fi
\ No newline at end of file
diff --git a/software/etherpad-lite/common.cfg b/software/etherpad-lite/common.cfg
index 7514f8c86e929fa38aa2c39105bdd3c581676db3..4aaa7f562104f57ba9ace203068abf5132c8da00 100644
--- a/software/etherpad-lite/common.cfg
+++ b/software/etherpad-lite/common.cfg
@@ -16,13 +16,18 @@ extends =
   ../../component/git/buildout.cfg
   ../../component/nodejs/buildout.cfg
   ../../component/postgresql/buildout.cfg
+  ../../component/phantomjs/buildout.cfg
 
 parts =
   postgresql
   nodejs
+  phantomjs
   etherpad-lite-repository
   install-deps
   template
+  template-html10n
+  template-index-promise-js
+  template-pad-promise-js
   lxml-python
   eggs
   instance-recipe-egg
@@ -59,16 +64,24 @@ mode = 0644
 [instance-etherpad-lite]
 recipe = slapos.recipe.template
 url = ${:_profile_base_location_}/instance-etherpad-lite.cfg
-md5sum = 64ae9271f20e432ddc0516fe1bb17076
+md5sum = dea0ee49ff3a87bd146e3d6bd6d68167
 output = ${buildout:directory}/template-etherpad-lite.cfg
 mode = 0644
 
+[template-html10n]
+recipe = slapos.recipe.template
+url = ${:_profile_base_location_}/templates/${:filename}
+filename = html10n.js
+mode = 0644
+md5sum = 6f90afdcc50bb5020896c95162d83834
+output = ${etherpad-lite-repository:location}/src/static/js/${:filename}
+
 [template-conf]
 recipe = slapos.recipe.download
 url = ${:_profile_base_location_}/templates/${:filename}
 mode = 0644
 filename = settings.json.in
-md5sum = f9baee09003676fc1141f9bf4481f6a3
+md5sum = d95264e66e2691b094d40a65d88ce681
 location = ${buildout:parts-directory}/${:_buildout_section_name_}
 
 [template-run-script]
@@ -96,5 +109,21 @@ recipe = plone.recipe.command
 command = ${template-deps-script:output}
 update-command = command
 
+[template-index-promise-js]
+recipe = slapos.recipe.download
+url = ${:_profile_base_location_}/templates/${:filename}
+filename = test-index.js.in
+mode = 0644
+md5sum = 7ee12b1c284c2c6260689b21bb35176e
+location = ${buildout:parts-directory}/${:_buildout_section_name_}
+
+[template-pad-promise-js]
+recipe = slapos.recipe.download
+url = ${:_profile_base_location_}/templates/${:filename}
+filename = test-pad.js.in
+mode = 0644
+md5sum = 43dc2ee94e65cc7f5fa4c3d6a868eebe
+location = ${buildout:parts-directory}/${:_buildout_section_name_}
+
 [lxml-python]
 python = python2.7
diff --git a/software/etherpad-lite/instance-etherpad-lite.cfg b/software/etherpad-lite/instance-etherpad-lite.cfg
index e2d5ab8090edbeb2bc4af82e65317e73a5531a5e..2b538a882a6f3d7b46bcb50291e5fe7872162bfe 100644
--- a/software/etherpad-lite/instance-etherpad-lite.cfg
+++ b/software/etherpad-lite/instance-etherpad-lite.cfg
@@ -2,6 +2,9 @@
 parts =
   etherpad-lite
   publish-connection-informations
+  check-index-promise
+# XXX-Vivien: postponed till we can make it fast enough
+#  check-pad-promise 
 
 eggs-directory = ${buildout:eggs-directory}
 develop-eggs-directory = ${buildout:develop-eggs-directory}
@@ -27,10 +30,11 @@ promises = $${rootdirectory:etc}/promise/
 recipe = slapos.cookbook:mkdirectory
 etherpad-conf = $${rootdirectory:etc}/etherpad/
 etherpad-repository-location = $${buildout:directory}/parts/etherpad-lite-repository
+promise-js = $${rootdirectory:etc}/js/
 
 [publish-connection-informations]
 recipe = slapos.cookbook:publish
-url = $${request-frontend:connection-site_url}
+url = $${request-frontend:config-url}
 
 [etherpad-conf-generation]
 recipe = slapos.recipe.template
@@ -38,6 +42,7 @@ url = ${template-conf:location}/${template-conf:filename}
 ip = $${slap-network-information:global-ipv6}
 dirtydb-location = $${rootdirectory:var}/dirty.db
 port = 9001
+welcome-message = Bienvenue sur Etherpad!\n\nLe texte que vous écrivez est synchronisez en ce moment même, pour que tout le monde puisse voir la page avec le même texte. Cela vous permet de collaborer sur des documents sans aucun problème!\n
 mode = 0644
 output = $${directory:etherpad-conf}/settings.json
 
@@ -67,5 +72,34 @@ name = Frontend
 software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg
 slave = true
 config = url
-config-url = http://$${etherpad-conf-generation:ip}:$${etherpad-conf-generation:port}
+config-url = http://[$${etherpad-conf-generation:ip}]:$${etherpad-conf-generation:port}
 return = site_url
+
+[index-promise-js]
+recipe = slapos.recipe.template
+url = ${template-index-promise-js:location}/${template-index-promise-js:filename}
+content-url = $${publish-connection-informations:url}
+mode = 0644
+output = $${directory:promise-js}/test-index.js
+  
+[pad-promise-js]
+recipe = slapos.recipe.template
+url = ${template-pad-promise-js:location}/${template-pad-promise-js:filename}
+content-url = $${publish-connection-informations:url}
+welcome-message = $${etherpad-conf-generation:welcome-message}
+mode = 0644
+output = $${directory:promise-js}/test-pad.js
+
+[check-index-promise]
+recipe = slapos.cookbook:check_page_content_phantomjs
+path = $${basedirectory:promises}/check-index-promise
+dash-path = ${dash:location}/bin/dash
+phantomjs-path = ${phantomjs:location}/phantomjs-slapos
+script-path = $${index-promise-js:output}
+
+[check-pad-promise]
+recipe = slapos.cookbook:check_page_content_phantomjs
+path = $${basedirectory:promises}/check-pad-promise
+dash-path = ${dash:location}/bin/dash
+phantomjs-path = ${phantomjs:location}/phantomjs-slapos
+script-path = $${pad-promise-js:output}
\ No newline at end of file
diff --git a/software/etherpad-lite/software.cfg b/software/etherpad-lite/software.cfg
index 690fa3953b9f2bc80f80ac53c6adf927d8f6b3f3..c87af2ef6226aa3f37a16a023f1be35b201fa2c7 100644
--- a/software/etherpad-lite/software.cfg
+++ b/software/etherpad-lite/software.cfg
@@ -15,7 +15,8 @@ develop =
 recipe = slapos.recipe.build:gitclone
 repository = http://git.erp5.org/repos/slapos.git
 git-executable = ${git:location}/bin/git
-revision = 45243101321daeecaf755b9788b11259dd6bdfff
+#revision = 45243101321daeecaf755b9788b11259dd6bdfff
+branch = etherpad-lite
 
 [slapos.toolbox-repository]
 recipe = slapos.recipe.build:gitclone
diff --git a/software/etherpad-lite/templates/html10n.js b/software/etherpad-lite/templates/html10n.js
new file mode 100644
index 0000000000000000000000000000000000000000..b061d7212b6315a9292786c91a15b0bfa2fb6ac5
--- /dev/null
+++ b/software/etherpad-lite/templates/html10n.js
@@ -0,0 +1,970 @@
+/**
+ * Copyright (c) 2012 Marcel Klehr
+ * 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.
+ */
+ function html10n (window, document, undefined) {
+  
+  // fix console
+  var console = null
+  function interceptConsole(method){
+      if (!console) return function() {}
+
+      var original = console[method]
+
+      // do sneaky stuff
+      if (original.bind){
+        // Do this for normal browsers
+        return original.bind(console)
+      }else{
+        return function() {
+          // Do this for IE
+          var message = Array.prototype.slice.apply(arguments).join(' ')
+          original(message)
+        }
+      }
+  }
+  var consoleLog = interceptConsole('log')
+    , consoleWarn = interceptConsole('warn')
+    , consoleError = interceptConsole('warn')
+
+
+  // fix Array#forEach in IE
+  // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
+  if (!Array.prototype.forEach) {
+    Array.prototype.forEach = function(fn, scope) {
+      for(var i = 0, len = this.length; i < len; ++i) {
+        if (i in this) {
+          fn.call(scope, this[i], i, this);
+        }
+      }
+    };
+  }
+
+  // fix Array#indexOf in, guess what, IE! <3
+  // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
+  if (!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {
+      "use strict";
+      if (this == null) {
+        throw new TypeError();
+      }
+      var t = Object(this);
+      var len = t.length >>> 0;
+      if (len === 0) {
+        return -1;
+      }
+      var n = 0;
+      if (arguments.length > 1) {
+        n = Number(arguments[1]);
+        if (n != n) { // shortcut for verifying if it's NaN
+            n = 0;
+        } else if (n != 0 && n != Infinity && n != -Infinity) {
+            n = (n > 0 || -1) * Math.floor(Math.abs(n));
+        }
+      }
+      if (n >= len) {
+        return -1;
+      }
+      var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
+      for (; k < len; k++) {
+        if (k in t && t[k] === searchElement) {
+            return k;
+        }
+      }
+      return -1;
+    }
+  }
+    
+  /**
+   * MicroEvent - to make any js object an event emitter (server or browser)
+   */
+
+  var MicroEvent    = function(){}
+  MicroEvent.prototype	= {
+    bind	: function(event, fct){
+      this._events = this._events || {};
+      this._events[event] = this._events[event]	|| [];
+      this._events[event].push(fct);
+    },
+    unbind	: function(event, fct){
+      this._events = this._events || {};
+      if( event in this._events === false  )	return;
+      this._events[event].splice(this._events[event].indexOf(fct), 1);
+    },
+    trigger	: function(event /* , args... */){
+      this._events = this._events || {};
+      if( event in this._events === false  )	return;
+      for(var i = 0; i < this._events[event].length; i++){
+        this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1))
+      }
+    }
+  };
+  /**
+   * mixin will delegate all MicroEvent.js function in the destination object
+   * @param {Object} the object which will support MicroEvent
+   */
+  MicroEvent.mixin	= function(destObject){
+    var props	= ['bind', 'unbind', 'trigger'];
+    if(!destObject) return;
+    for(var i = 0; i < props.length; i ++){
+      destObject[props[i]] = MicroEvent.prototype[props[i]];
+    }
+  }
+  
+  /**
+   * Loader
+   * The loader is responsible for loading
+   * and caching all necessary resources
+   */
+  function Loader(resources) {
+    this.resources = resources
+    this.cache = {} // file => contents
+    this.langs = {} // lang => strings
+  }
+  
+  Loader.prototype.load = function(lang, cb) {
+    if(this.langs[lang]) return cb()
+
+    if (this.resources.length > 0) {
+      var reqs = 0;
+      for (var i=0, n=this.resources.length; i < n; i++) {
+        this.fetch(this.resources[i], lang, function(e) {
+          reqs++;
+          if(e) consoleWarn(e)
+          
+          if (reqs < n) return;// Call back once all reqs are completed
+          cb && cb()
+        })
+      }
+    }
+  }
+  
+  Loader.prototype.fetch = function(href, lang, cb) {
+    var that = this
+    
+    if (this.cache[href]) {
+      this.parse(lang, href, this.cache[href], cb)
+      return;
+    }
+    
+    var xhr = new XMLHttpRequest()
+    xhr.open('GET', href, /*async: */true)
+    if (xhr.overrideMimeType) {
+      xhr.overrideMimeType('application/json; charset=utf-8');
+    }
+    xhr.onreadystatechange = function() {
+      if (xhr.readyState == 4) {
+        if (xhr.status == 200 || xhr.status === 0) {
+          var data = JSON.parse(xhr.responseText)
+          that.cache[href] = data
+          // Pass on the contents for parsing
+          that.parse(lang, href, data, cb)
+        } else {
+          cb(new Error('Failed to load '+href))
+        }
+      }
+    };
+    xhr.send(null);
+  }
+  
+  Loader.prototype.parse = function(lang, currHref, data, cb) {
+    if ('object' != typeof data) {
+      cb(new Error('A file couldn\'t be parsed as json.'))
+      return
+    }
+
+    if (!data[lang]) {
+      cb(new Error('Couldn\'t find translations for '+lang))
+      return
+    }
+    
+    if ('string' == typeof data[lang]) {
+      // Import rule
+
+      // absolute path
+      var importUrl = data[lang]
+
+      // relative path
+      if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) {  
+        importUrl = currHref+"/../"+data[lang]
+      }
+
+      this.fetch(importUrl, lang, cb)
+      return
+    }
+
+    if ('object' != typeof data[lang]) {
+      cb(new Error('Translations should be specified as JSON objects!'))
+      return
+    }
+
+    this.langs[lang] = data[lang]
+    // TODO: Also store accompanying langs
+    cb()
+  }
+  
+  
+  /**
+   * The html10n object
+   */
+  var html10n = 
+  { language : null
+  }
+  MicroEvent.mixin(html10n)
+  
+  html10n.macros = {}
+
+  html10n.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"]
+  
+  /**
+   * 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
+   */
+  html10n.macros.plural = function(key, param, opts) {
+    var str
+      , n = parseFloat(param);
+    if (isNaN(n))
+      return;
+
+    // initialize _pluralRules
+    if (!this._pluralRules)
+      this._pluralRules = getPluralRules(html10n.language);
+    var index = this._pluralRules(n);
+
+    // try to find a [zero|one|two] key if it's defined
+    if (n === 0 && ('zero') in opts) {
+      str = opts['zero'];
+    } else if (n == 1 && ('one') in opts) {
+      str = opts['one'];
+    } else if (n == 2 && ('two') in opts) {
+      str = opts['two'];
+    } else if (index in opts) {
+      str = opts[index];
+    }
+
+    return str;
+  };
+  
+  /**
+   * Localize a document
+   * @param langs An array of lang codes defining fallbacks
+   */
+  html10n.localize = function(langs) {
+    var that = this
+    // if only one string => create an array
+    if ('string' == typeof langs) langs = [langs]
+
+    // Expand two-part locale specs
+    var i=0
+    langs.forEach(function(lang) {
+      if(!lang) return
+      langs[i++] = lang
+      if(~lang.indexOf('-')) langs[i++] = lang.substr(0, lang.indexOf('-'))
+    })
+
+    this.build(langs, function(er, translations) {
+      html10n.translations = translations
+      html10n.translateElement(translations)
+      that.trigger('localized')
+    })
+  }
+
+  /**
+   * Triggers the translation process
+   * for an element
+   * @param translations A hash of all translation strings
+   * @param element A DOM element, if omitted, the document element will be used
+   */
+  html10n.translateElement = function(translations, element) {
+    element = element || document.documentElement
+
+    var children = element? getTranslatableChildren(element) : document.childNodes;
+    for (var i=0, n=children.length; i < n; i++) {
+      this.translateNode(translations, children[i])
+    }
+
+    // translate element itself if necessary
+    this.translateNode(translations, element)
+  }
+  
+  function asyncForEach(list, iterator, cb) {
+    var i = 0
+      , n = list.length
+    iterator(list[i], i, function each(err) {
+      if(err) consoleLog(err)
+      i++
+      if (i < n) return iterator(list[i],i, each);
+      cb()
+    })
+  }
+  
+  function getTranslatableChildren(element) {
+    if(!document.querySelectorAll) {
+      if (!element) return []
+      var nodes = element.getElementsByTagName('*')
+        , l10nElements = []
+      for (var i=0, n=nodes.length; i < n; i++) {
+        if (nodes[i].getAttribute('data-l10n-id'))
+          l10nElements.push(nodes[i]);
+      }
+      return l10nElements
+    }
+    return element.querySelectorAll('*[data-l10n-id]')
+  }
+  
+  html10n.get = function(id, args) {
+    var translations = html10n.translations
+    if(!translations) return consoleWarn('No translations available (yet)')
+    if(!translations[id]) return consoleWarn('Could not find string '+id)
+    
+    // apply macros
+    var str = translations[id]
+    
+    str = substMacros(id, str, args)
+    
+    // apply args
+    str = substArguments(str, args)
+    
+    return str
+  }
+  
+  // replace {{arguments}} with their values or the
+  // associated translation string (based on its key)
+  function substArguments(str, args) {
+    var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/
+      , match
+    
+    while (match = reArgs.exec(str)) {
+      if (!match || match.length < 2)
+        return str // argument key not found
+
+      var arg = match[1]
+        , sub = ''
+      if (arg in args) {
+        sub = args[arg]
+      } else if (arg in translations) {
+        sub = translations[arg]
+      } else {
+        consoleWarn('Could not find argument {{' + arg + '}}')
+        return str
+      }
+
+      str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length)
+    }
+    
+    return str
+  }
+  
+  // replace {[macros]} with their values
+  function substMacros(key, str, args) {
+    var regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}')
+      , match
+    
+    while(match = regex.exec(str)) {
+      // a macro has been found
+      // Note: at the moment, only one parameter is supported
+      var macroName = match[1]
+        , paramName = match[2]
+        , optv = match[3]
+        , opts = {}
+      
+      if (!(macroName in html10n.macros)) continue
+      
+      if(optv) {
+        optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g).forEach(function(arg) {
+          var parts = arg.split(':')
+            , name = parts[0]
+            , value = parts[1].trim()
+          opts[name] = value
+        })
+      }
+      
+      var param
+      if (args && paramName in args) {
+        param = args[paramName]
+      } else if (paramName in html10n.translations) {
+        param = translations[paramName]
+      }
+
+      // there's no macro parser: it has to be defined in html10n.macros
+      var macro = html10n.macros[macroName]
+      str = str.substr(0, match.index) + macro(key, param, opts) + str.substr(match.index+match[0].length)
+    }
+    
+    return str
+  }
+  
+  /**
+   * Applies translations to a DOM node (recursive)
+   */
+  html10n.translateNode = function(translations, node) {
+    var str = {}
+
+    // get id
+    str.id = node.getAttribute('data-l10n-id')
+    if (!str.id) return
+
+    if(!translations[str.id]) return consoleWarn('Couldn\'t find translation key '+str.id)
+
+    // get args
+    if(window.JSON) {
+      str.args = JSON.parse(node.getAttribute('data-l10n-args'))
+    }else{
+      try{
+        str.args = eval(node.getAttribute('data-l10n-args'))
+      }catch(e) {
+        consoleWarn('Couldn\'t parse args for '+str.id)
+      }
+    }
+    
+    str.str = html10n.get(str.id, str.args)
+    
+    // get attribute name to apply str to
+    var prop
+      , index = str.id.lastIndexOf('.')
+      , attrList = // allowed attributes
+       { "title": 1
+       , "innerHTML": 1
+       , "alt": 1
+       , "textContent": 1
+       }
+    if (index > 0 && str.id.substr(index + 1) in attrList) { // an attribute has been specified
+      prop = str.id.substr(index + 1)
+    } else { // no attribute: assuming text content by default
+      prop = document.body.textContent ? 'textContent' : 'innerText'
+    }
+
+    // Apply translation
+    if (node.children.length === 0 || prop != 'textContent') {
+      node[prop] = str.str
+    } else {
+      var children = node.childNodes,
+          found = false
+      for (var i=0, n=children.length; i < n; i++) {
+        if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) {
+          if (!found) {
+            children[i].nodeValue = str.str
+            found = true
+          } else {
+            children[i].nodeValue = ''
+          }
+        }
+      }
+      if (!found) {
+        consoleWarn('Unexpected error: could not translate element content for key '+str.id, node)
+      }
+    }
+  }
+  
+  /**
+   * Builds a translation object from a list of langs (loads the necessary translations)
+   * @param langs Array - a list of langs sorted by priority (default langs should go last)
+   */
+  html10n.build = function(langs, cb) {
+    var that = this
+      , build = {}
+
+    asyncForEach(langs, function (lang, i, next) {
+      if(!lang) return next();
+      that.loader.load(lang, next)
+    }, function() {
+      var lang
+      langs.reverse()
+      
+      // loop through priority array...
+      for (var i=0, n=langs.length; i < n; i++) {
+        lang = langs[i]
+        
+        if(!lang || !(lang in that.loader.langs)) continue;
+        
+        // ... and apply all strings of the current lang in the list
+        // to our build object
+        for (var string in that.loader.langs[lang]) {
+          build[string] = that.loader.langs[lang][string]
+        }
+        
+        // the last applied lang will be exposed as the
+        // lang the page was translated to
+        that.language = lang
+      }
+      cb(null, build)
+    })
+  }
+  
+  /**
+   * Returns the language that was last applied to the translations hash
+   * thus overriding most of the formerly applied langs
+   */
+  html10n.getLanguage = function() {
+    return this.language;
+  }
+
+  /**
+   * Returns the direction of the language returned be html10n#getLanguage
+   */
+  html10n.getDirection = function() {
+    if(!this.language) return
+    var langCode = this.language.indexOf('-') == -1? this.language : this.language.substr(0, this.language.indexOf('-'))
+    return html10n.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl'
+  }
+
+  /**
+   * Index all <link>s
+   */
+  html10n.index = function () {
+    // Find all <link>s
+    var links = document.getElementsByTagName('link')
+       , resources = []
+    for (var i=0, n=links.length; i < n; i++) {
+      if (links[i].type != 'application/l10n+json')
+        continue;
+      resources.push(links[i].href)
+    }
+    this.loader = new Loader(resources)
+    this.trigger('indexed')
+  }
+  
+  if (document.addEventListener) // modern browsers and IE9+
+   document.addEventListener('DOMContentLoaded', function() {
+     html10n.index()
+   }, false)
+  else if (window.attachEvent)
+    window.attachEvent('onload', function() {
+     html10n.index()
+   }, false)
+
+  // gettext-like shortcut
+  if (window._ === undefined)
+    window._ = html10n.get;
+
+  return html10n
+}
+
+window.html10n = html10n(window, document)
diff --git a/software/etherpad-lite/templates/settings.json.in b/software/etherpad-lite/templates/settings.json.in
index 79772b5d8471d0c45b1962877866c62eb00d4e26..dec50d735a004d42c4923157b779d5f21fd5502b 100644
--- a/software/etherpad-lite/templates/settings.json.in
+++ b/software/etherpad-lite/templates/settings.json.in
@@ -52,7 +52,7 @@
   */
   
   //the default text of a pad
-  "defaultPadText" : "Bienvenue sur Etherpad!\n\nLe texte que vous écrivez est synchronisez en ce moment même, pour que tout le monde puisse voir la page avec le même texte. Cela vous permet de collaborer sur des documents sans aucun problème!\n",
+  "defaultPadText" : "${:welcome-message}",
   
   /* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
   "requireSession" : false,
diff --git a/software/etherpad-lite/templates/test-index.js.in b/software/etherpad-lite/templates/test-index.js.in
new file mode 100644
index 0000000000000000000000000000000000000000..03ff9572183bba8e125bc02395d09c12138d1f3f
--- /dev/null
+++ b/software/etherpad-lite/templates/test-index.js.in
@@ -0,0 +1,13 @@
+var page = require('webpage').create();
+url = '${:content-url}'
+page.open(url, function (status) {
+  var text = page.evaluate(function(){
+    return document.getElementById('button').textContent
+  });
+
+  if(text !== "" && text !== null) {
+    phantom.exit();
+  } else {
+    phantom.exit(1);
+  }
+});
diff --git a/software/etherpad-lite/templates/test-pad.js.in b/software/etherpad-lite/templates/test-pad.js.in
new file mode 100644
index 0000000000000000000000000000000000000000..cc2ecaee388d28c6fe83558c1ea9ac99544a5920
--- /dev/null
+++ b/software/etherpad-lite/templates/test-pad.js.in
@@ -0,0 +1,20 @@
+var page = require('webpage').create();
+url = '${:content-url}/p/test'
+page.open(url, function (status) {
+setTimeout(function(){
+    var text = page.evaluate(function(){
+        var container = document.getElementById('editorcontainer');
+        var iframe = container.firstChild;
+        container = iframe.contentDocument.getElementById('outerdocbody');
+        iframe = container.firstChild;
+        container = iframe.contentDocument.getElementById('innerdocbody');
+        return container.textContent;
+    });
+
+    if (text === null) {
+        phantom.exit();
+    } else {
+       phantom.exit(1);
+    }
+ },2500);
+});