move all served file into a dedicated directory
authorLaurent Mazet <mazet@softndesign.org>
Wed, 29 Apr 2020 21:19:17 +0000 (23:19 +0200)
committerLaurent Mazet <mazet@softndesign.org>
Wed, 29 Apr 2020 21:19:17 +0000 (23:19 +0200)
52 files changed:
.gitignore
alert.css [deleted file]
alert.js [deleted file]
bag.svg [deleted file]
baton.svg [deleted file]
config.js.sample [deleted file]
index.html [deleted file]
l10n.js [deleted file]
l10n/Makefile [deleted file]
l10n/js/br.js [deleted file]
l10n/js/en.js [deleted file]
l10n/js/fr.js [deleted file]
l10n/makejs.js [deleted file]
l10n/po/br/alert.po [deleted file]
l10n/po/br/trivabble.po [deleted file]
l10n/po/fr/alert.po [deleted file]
l10n/po/fr/trivabble.po [deleted file]
l10n/pot/alert.pot [deleted file]
l10n/pot/trivabble.pot [deleted file]
notification.mp3 [deleted file]
notification.ogg [deleted file]
receive.mp3 [deleted file]
receive.ogg [deleted file]
server-data/alert.css [new file with mode: 0644]
server-data/alert.js [new file with mode: 0644]
server-data/bag.svg [new file with mode: 0644]
server-data/baton.svg [new file with mode: 0644]
server-data/config.js.sample [new file with mode: 0644]
server-data/index.html [new file with mode: 0644]
server-data/l10n.js [new file with mode: 0644]
server-data/l10n/Makefile [new file with mode: 0644]
server-data/l10n/js/br.js [new file with mode: 0644]
server-data/l10n/js/en.js [new file with mode: 0644]
server-data/l10n/js/fr.js [new file with mode: 0644]
server-data/l10n/makejs.js [new file with mode: 0755]
server-data/l10n/po/br/alert.po [new file with mode: 0644]
server-data/l10n/po/br/trivabble.po [new file with mode: 0644]
server-data/l10n/po/fr/alert.po [new file with mode: 0644]
server-data/l10n/po/fr/trivabble.po [new file with mode: 0644]
server-data/l10n/pot/alert.pot [new file with mode: 0644]
server-data/l10n/pot/trivabble.pot [new file with mode: 0644]
server-data/notification.mp3 [new file with mode: 0644]
server-data/notification.ogg [new file with mode: 0644]
server-data/receive.mp3 [new file with mode: 0644]
server-data/receive.ogg [new file with mode: 0644]
server-data/touch2click.js [new file with mode: 0644]
server-data/trivabble.css [new file with mode: 0644]
server-data/trivabble.js [new file with mode: 0644]
touch2click.js [deleted file]
trivabble-server.js
trivabble.css [deleted file]
trivabble.js [deleted file]

index 44e8497a75e4ee0bebd52195d3dfe98ceeb6693c..4cb193f1071c18c4e6b9e014ac4e718cb81efc1d 100644 (file)
@@ -1,2 +1,2 @@
 /games.backup.json
-/config.js
+/server-data/config.js
diff --git a/alert.css b/alert.css
deleted file mode 100644 (file)
index 2b4c470..0000000
--- a/alert.css
+++ /dev/null
@@ -1,74 +0,0 @@
-.alert {
-    position:fixed;
-    top:0;
-    left:0;
-    right:0;
-    bottom:0;
-    background:rgba(0,0,0,0.2);
-    display:-webkit-flex;
-    display:flex;
-    -webkit-flex-direction:column;
-    flex-direction:column;
-    -webkit-justify-content:center;
-    justify-content:center;
-    line-height:1.5em;
-    text-align:center
-}
-
-.alert-outer {
-    background:white;
-    color:black;
-    border-radius:4px;
-    display:-webkit-flex;
-    display:flex;
-    -webkit-flex-direction:column;
-    flex-direction:column;
-    -webkit-align-self:center;
-    align-self:center;
-    text-align:center;
-    box-shadow:0 0 8px black;
-    max-width:75%;
-    padding:4px;
-}
-
-.alert-content {
-    padding:1ex
-}
-
-.alert input[type=text], .alert input[type=password] {
-   border-radius:none;
-   box-sizing:border-box;
-   min-height:1.5rem;
-   outline:none;
-   width:95%;
-   min-width:10ex;
-   margin:1ex;
-   border:1px solid silver;
-}
-
-.alert input[type=text]:focus, .alert  input[type=password]:focus {
-   border:1px solid #5CF;
-   box-shadow:0 0 1px #5CF
-}
-
-.alert input[type=button], .alert button {
-   margin:1px;
-   border-radius:0;
-   min-height:1.5rem;
-   border:1px solid silver;
-   padding:1ex 2ex;
-   background:white
-}
-
-.alert input[type=button]:active, .alert button:active {
-   background:#D5D5D5
-}
-
-.alert button {
-    margin:0.5ex;
-    border:1px solid gray;
-    background:#F1F1F1;
-    color:black;
-    border-radius:3px
-}
-
diff --git a/alert.js b/alert.js
deleted file mode 100644 (file)
index d31cdc2..0000000
--- a/alert.js
+++ /dev/null
@@ -1,161 +0,0 @@
-/*global libD*/
-
-(function (global) {
-    "use strict";
-
-    let divAlert;
-    let divAlertInput;
-    let divAlertConfirm;
-    let divAlertButton;
-    let alertButtonOK;
-    let divAlertCallback;
-    let divAlertCallbackYes;
-    let divAlertCallbackNo;
-    let alertInput;
-    let divAlertContent;
-
-    const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {return s;};
-
-    function promptOK() {
-        divAlert.style.display = "none";
-        if (divAlertCallback) {
-            divAlertCallback(alertInput.value);
-        }
-    }
-
-    function promptCancel() {
-        divAlert.style.display = "none";
-        if (divAlertCallback) {
-            divAlertCallback(null);
-        }
-    }
-
-    function confirmYes() {
-        divAlert.style.display = "none";
-        if (divAlertCallbackYes) {
-            divAlertCallbackYes();
-        }
-    }
-    function confirmNo() {
-        divAlert.style.display = "none";
-        if (divAlertCallbackNo) {
-            divAlertCallbackNo();
-        }
-    }
-
-    function alertOK() {
-        divAlert.style.display = "none";
-        if (divAlertCallback) {
-            divAlertCallback();
-        }
-    }
-
-    function prepare() {
-        divAlert = document.createElement("div");
-        divAlert.className = "alert";
-        divAlert.style.display = "none";
-
-        divAlertContent = document.createElement("div");
-        divAlertContent.className = "alert-content";
-        divAlertContent.style.whiteSpace = "pre-wrap";
-
-        divAlertInput = document.createElement("div");
-        divAlertInput.className = "alert-prompt";
-
-        alertInput = document.createElement("input");
-        alertInput.type = "text";
-        alertInput.onkeydown = function (e) {
-            if (e.keyCode === 13) {
-                e.preventDefault();
-                promptOK();
-            }
-        };
-
-        divAlertInput.appendChild(alertInput);
-
-        divAlertInput.appendChild(document.createElement("div"));
-        divAlertInput.lastChild.className = "alert-prompt-buttons";
-        divAlertInput.lastChild.appendChild(document.createElement("button"));
-        divAlertInput.lastChild.lastChild.textContent = _("OK");
-        divAlertInput.lastChild.lastChild.onclick = promptOK;
-        divAlertInput.lastChild.appendChild(document.createElement("button"));
-        divAlertInput.lastChild.lastChild.textContent = _("Annuler");
-        divAlertInput.lastChild.lastChild.onclick = promptCancel;
-
-        divAlertConfirm = document.createElement("div");
-        divAlertConfirm.className = _("alert-confirm");
-        divAlertConfirm.appendChild(document.createElement("button"));
-        divAlertConfirm.lastChild.textContent = _("Oui");
-        divAlertConfirm.lastChild.onclick = confirmYes;
-        divAlertConfirm.appendChild(document.createElement("button"));
-        divAlertConfirm.lastChild.textContent = _("Non");
-        divAlertConfirm.lastChild.onclick = confirmNo;
-
-        divAlertButton = document.createElement("div");
-        alertButtonOK = document.createElement("button");
-        divAlertButton.appendChild(alertButtonOK);
-        alertButtonOK.textContent = _("OK");
-        alertButtonOK.onclick = alertOK;
-
-        const divAlertOuter = document.createElement("div");
-        divAlertOuter.className = "alert-outer";
-        divAlertOuter.appendChild(divAlertContent);
-        divAlertOuter.appendChild(divAlertInput);
-        divAlertOuter.appendChild(divAlertConfirm);
-        divAlertOuter.appendChild(divAlertButton);
-        divAlert.appendChild(divAlertOuter);
-        document.body.appendChild(divAlert);
-    }
-
-    global.myAlert = function (msg, callback) {
-        if (!divAlert) {
-            prepare();
-        }
-
-        divAlertContent.textContent = msg;
-        divAlertInput.style.display = "none";
-        divAlertConfirm.style.display = "none";
-        divAlertButton.style.display = "";
-        divAlertCallback = callback;
-        divAlert.style.display = "";
-        divAlertButton.getElementsByTagName("button")[0].focus();
-    };
-
-    global.myAlert.l10n = _;
-
-    global.myPrompt = function (msg, callback, defaultText) {
-        if (!divAlert) {
-            prepare();
-        }
-
-        divAlertContent.textContent = msg;
-        divAlertInput.style.display = "";
-        divAlertInput.firstChild.value = defaultText || "";
-        divAlertConfirm.style.display = "none";
-        divAlertButton.style.display = "none";
-        divAlertCallback = callback;
-        divAlert.style.display = "";
-
-        // eslint-disable-next-line require-unicode-regexp
-        if (!(/iPad|iPhone|iPod/g).test(navigator.userAgent)) {
-            alertInput.focus();
-            alertInput.setSelectionRange(0, alertInput.value.length);
-            alertInput.select();
-        }
-    };
-
-    global.myConfirm = function (msg, callbackYes, callbackNo) {
-        if (!divAlert) {
-            prepare();
-        }
-
-        divAlertContent.textContent = msg;
-        divAlertInput.style.display = "none";
-        divAlertConfirm.style.display = "";
-        divAlertButton.style.display = "none";
-        divAlertCallbackYes = callbackYes;
-        divAlertCallbackNo = callbackNo;
-        divAlert.style.display = "";
-        divAlertConfirm.getElementsByTagName("button")[0].focus();
-    };
-}(this));
diff --git a/bag.svg b/bag.svg
deleted file mode 100644 (file)
index 92bc6f0..0000000
--- a/bag.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" height="319mm" width="331mm" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1172.8346 1130.315" xmlns:dc="http://purl.org/dc/elements/1.1/">
- <defs id="defs4">
-  <linearGradient id="linearGradient5620" y2="482.82" gradientUnits="userSpaceOnUse" x2="1120.7" gradientTransform="matrix(1.21 0 0 1.21 -122.4 -73.147)" y1="482.82" x1="56.525">
-   <stop id="stop5616" offset="0"/>
-   <stop id="stop5618" stop-color="#33333f" stop-opacity=".94118" offset="1"/>
-  </linearGradient>
- </defs>
- <metadata id="metadata7">
-  <rdf:RDF>
-   <cc:Work rdf:about="">
-    <dc:format>image/svg+xml</dc:format>
-    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
-    <dc:title/>
-   </cc:Work>
-  </rdf:RDF>
- </metadata>
- <g id="layer1" transform="translate(0 77.953)">
-  <path id="path3348" d="m510.26 107.75-273.12 245.46-127.91 449.43 490.91 207.46 456.36-183.26-159.04-487.46-221.26-224.71 190.18-148.75c-88.433-9.4796-177.32-14.756-266.25-15.804-96.89-1.1424-193.84 2.7337-290.33 11.607l200.46 146.03z" fill-rule="evenodd" stroke="#000" stroke-width="1.21px" fill="url(#linearGradient5620)"/>
-  <path id="path4164" stroke-linejoin="round" d="m684.88 431.89c-6.34-58.38-23.22-115.59-49.58-168.06-29.85-59.42-71.88-112.69-122.72-155.55 22.722 2.9086 45.556 4.9435 68.435 6.0986 34.007 1.717 68.112 1.489 102.09-0.68242" stroke="#b47f58" stroke-linecap="round" stroke-width="12.1" fill="none"/>
- </g>
-</svg>
diff --git a/baton.svg b/baton.svg
deleted file mode 100644 (file)
index 2a15a9c..0000000
--- a/baton.svg
+++ /dev/null
@@ -1,93 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:xlink="http://www.w3.org/1999/xlink"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="143.40671mm"
-   height="67.173225mm"
-   viewBox="0 0 143.40671 67.173226"
-   version="1.1"
-   id="svg8"
-   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
-   sodipodi:docname="baton.svg">
-  <defs
-     id="defs2">
-    <linearGradient
-       inkscape:collect="always"
-       id="linearGradient837">
-      <stop
-         style="stop-color:#225500;stop-opacity:1;"
-         offset="0"
-         id="stop833" />
-      <stop
-         style="stop-color:#cdde87;stop-opacity:1"
-         offset="1"
-         id="stop835" />
-    </linearGradient>
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient837"
-       id="linearGradient839"
-       x1="46.49107"
-       y1="217.55084"
-       x2="106.49107"
-       y2="217.55084"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="translate(144.34952,-283.94879)" />
-  </defs>
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.98994949"
-     inkscape:cx="358.38013"
-     inkscape:cy="245.7383"
-     inkscape:document-units="mm"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     fit-margin-top="0"
-     fit-margin-left="0"
-     fit-margin-right="0"
-     fit-margin-bottom="0"
-     inkscape:window-width="2560"
-     inkscape:window-height="1355"
-     inkscape:window-x="0"
-     inkscape:window-y="0"
-     inkscape:window-maximized="1" />
-  <metadata
-     id="metadata5">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Calque 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(-4.7877181,-183.96423)">
-    <rect
-       style="opacity:1;fill:url(#linearGradient839);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.79374999;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
-       id="rect815"
-       width="60"
-       height="140"
-       x="190.84059"
-       y="-136.39795"
-       transform="rotate(87.362214)" />
-  </g>
-</svg>
diff --git a/config.js.sample b/config.js.sample
deleted file mode 100644 (file)
index beba4ee..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// This is a sample config file. Copy to config.js if you need to fiddle with the
-// configuration of the client.
-
-window.TrivabbleConf = {
-    // The amount of time to wait after a connection failure
-    POLLING_DELAY:        2000,
-
-    // Whether WebSockets should be used, if possible
-    ENABLE_WEBSOCKETS:    true,
-
-    // Whether Server Sent Events should be used, if available
-    ENABLE_EVENT_SOURCE:  true,
-
-    // Max consecutive tries before blacklisting WebSockets for the current session
-    MAX_WEBSOCKET_ERRORS: 1,
-
-    // to tweak only if your webserver is shared with other conflicting resources at / ( ex: yunohost integration )
-    APP_PATH: "",
-
-    // I don't like trailing commas, here is a nice message for you reading this file :-)
-    HAVE_FUN: true
-};
diff --git a/index.html b/index.html
deleted file mode 100644 (file)
index b71f1b0..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-    <head>
-        <meta charset="utf-8" />
-        <title> Trivabble </title>
-        <link rel="stylesheet" href="trivabble.css" />
-        <link rel="stylesheet" href="alert.css" />
-        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
-        <meta name="apple-mobile-web-app-capable" content="yes" />
-        <meta name="format-detection" content="telephone=no" />
-        <script src="config.js"></script>
-        <script src="l10n.js"></script>
-        <script src="alert.js"></script>
-        <script src="touch2click.js"></script>
-        <script src="trivabble.js"></script>
-    </head>
-    <body>
-        <div id="menu">
-            <button data-l10n="text-content" id="join-game">Join adversaries</button>
-            <p id="p-name"> <span data-l10n="text-content">You are:</span> <span id="name" data-l10n="text-content">(name to give)</span><br /><button class="minibutton" id="change-name" data-l10n="text-content">Change</button></p>
-            <button id="clear-game" data-l10n="text-content">Put back all the tiles&#10;in the bag</button>
-            <p id="p-name"><span data-l10n="text-content">Number of your game:</span><br /><span id="number" data-l10n="text-content">(pending)</span></p>
-            <div id="chat-messages"></div>
-            <textarea id="chat-ta" placeholder="Write a message to your adversaries here" data-l10n="placeholder"></textarea>
-            <button id="chat-btn" class="minibutton" data-l10n="text-content">Send message</button>
-            <div id="prefs-sound">
-                <p><label><input type="checkbox" id="tiles-sound" checked="checked" /><span data-l10n="text-content">Sound of the tiles</span></label></p>
-                <p><label><input type="checkbox" id="msg-sound" checked="checked" /><span data-l10n="text-content">Sound of messages</span></label></p>
-            </div>
-
-            <div id="lang-selection">
-                <label>
-                    <span data-l10n="text-content">Interface language: </span>
-                    <select id="select-lang">
-                        <option value="en">English</option>
-                        <option value="fr">Français</option>
-                        <option value="br">Brezhoneg</option>
-                    </select>
-                </label>
-            </div>
-        </div>
-
-        <div id="content">
-            <table id="board">
-                <tr><td class="corner"></td></tr>
-            </table>
-            <div id="rack-outer">
-                <div id="rack"></div>
-            </div>
-        </div>
-
-        <div id="panel">
-            <div id="bag"></div>
-            <p><span data-l10n="text-content">Number of tiles in the bag:</span><br /><span id="remaining-letters">102</span><br /><span id="help-bag" data-l10n="text-content">Click on it to take one.</span><a href="#" id="help-clear" style="display:none"></a></p>
-            <table id="participants">
-                <tr><th data-l10n="text-content">Participant</th><th data-l10n="text-content">Rack</th><th data-l10n="text-content">Score</th><th data-l10n="text-content">Turn</th></tr>
-                <tr id="participants-placeholder"><td colspan="4" data-l10n="text-content">Waiting for other participants…</td></tr>
-            </table>
-            <p id="help-scores" data-l10n="text-content">Click on someone's score&#10;to change it.</p>
-            <p id="help-turn" data-l10n="text-content">Who's turn? Click on the Turn button!</p>
-            <button id="clear-rack" class="minibutton" data-l10n="text-content">Put back all the tiles of&#10;your rack in the bag</button>
-            <button id="show-rack" class="minibutton" data-l10n="text-content">Show my rack to other players</button>
-        </div>
-    </body>
-</html>
diff --git a/l10n.js b/l10n.js
deleted file mode 100644 (file)
index 1200f08..0000000
--- a/l10n.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/*global libD*/
-
-window.libD = {
-    l10n: function () {
-        "use strict";
-
-        const t = [];
-
-        function f (lang, orig, translated) {
-            if (!orig) {
-                return (
-                    (libD.lang && t[libD.lang] && t[libD.lang][lang])
-                        ? t[libD.lang][lang]
-                        : lang // lang is the default string
-                );
-            }
-
-            if (!t[lang]) {
-                t[lang] = [];
-            }
-
-            t[lang][orig] = translated;
-        };
-
-        return f;
-    },
-    lang: (
-        window.navigator && (
-            (navigator.language || navigator.userLanguage)
-                ? (navigator.language || navigator.userLanguage).split("-")[0].toLowerCase()
-                : "en"
-        )
-    ) || "en"
-};
diff --git a/l10n/Makefile b/l10n/Makefile
deleted file mode 100644 (file)
index fbab671..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-all:
-       node makejs.js
-
diff --git a/l10n/js/br.js b/l10n/js/br.js
deleted file mode 100644 (file)
index e42e101..0000000
+++ /dev/null
@@ -1 +0,0 @@
-(function(){var _=myAlert.l10n;_("br","OK","Mat eo");_("br","Yes","Ya");_("br","No","Ket");_("br","Cancel","Nullañ");_=trivabble.l10n;_("br","","Project-Id-Version: \nPOT-Creation-Date: \nPO-Revision-Date: \nLanguage-Team: \nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nX-Generator: Poedit 1.8.7\nLast-Translator: \nPlural-Forms: nplurals=2; plural=(n > 1);\nLanguage: br\n");_("br","Your game is not over. Are you sure you want to leave now?","N'eo ket echu ho c'hoariadenn. Sur oc'h e fell deoc'h kuitaat bremañ?");_("br","You are about to leave the current game. To recover it, please note its number: {0}","Emaoc'h o vont da guitaat ar c'hoariadenn. Evit hec'h adkavout e c'hallit notenniñ an niverenn da heul: {0}");_("br","Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page.","Digarezit, ur gudenn a zo degouezhet. Ret eo adkargañ ar bajenn. Ma n'eo ket re bouezus ar gudenn e o tu deoc'h kenderc'hel da c'hoari. E mod all, kit e darempred gant drouizig@drouizig.org evit kempenn ar gudenn. Klikit war \"Ya\" evit adkargañ ar bajenn.");_("br","To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.","Evit kevreañ d'ur c'hoariadenn, roit an niverenn diskouezet war skramm ho enebour(ien).\nMa n'anavezit ket anezhañ, goulennit ganto.\n\nDiwallit: ne rank ket hoc'h enebour kemer ho niverenn, ret eo dezhañ pe dezhi derc'hel (h)e hini. Ma fell deoc'h adkavout ho c'hoari bremanel, notennit an niverenn da heul: {0}.");_("br","It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again.","War a seblant n'ho peus ket roet un niverenn dalvoudek, p ekliket ho peus war \"Nullañ\". Kendalc'het e vo gant ar c'hoari bremanel. Evit kevreañ d'ur c'hoari, klikit war \"Kevreañ d'ur c'hoari\" adarre.");_("br","To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name.","Evit kemmañ hoc'h anv, enankit unan nevez. Gallout a rit derc'hel hoc'h hini bremanel en ur glikañ war \"Nullañ\". Diwallit: ma kemmit hoc'h anv en ur c'hoari, ne vo ket tu deoc'h kenderc'hel da c'hoari estreget mar adlakit hoc'h anv bremanel en-dro.");_("br","You cannot take another tile: your rack is full.","N'hallit ket kement ur pezh all. Leun eo ho touger-pezhioù.");_("br","Are you sure you want to put all the tiles back in the bag (in order to play another game)?","Sur oc'h e fell deoc'h adlakaat an holl bezhioù er sac'h (evit c'hoari en-dro)?");_("br","Are you sure you want to put all your tiles back in the bag?","Sur oc'h e fell deoc'h adlakaat holl ho pezhio er sac'h?");_("br","It seems your did not give your name. You need to do it for the game to run properly.","War a-seblant n'ho peus ket roet hoc'h anv. et eo deoc'h en ober evit gellout c'hoari.");_("br","Hello! To begin, enter your name. Your adversaries will see this name when you play with them.","Demat! Evit kregiñ da c'hoari, enankit hoc'h anv. Hoc'h enebourien a welo an anv pa viot o c'hoari gante.");_("br","Double\nLetter","Lizherenn\nZoubl");_("br","Double\nWord","Ger\nDoubl");_("br","Triple\nLetter","Lizherenn\nDripl");_("br","Triple\nWord","Ger\nTripl");_("br","Join adversaries","Kevreañ gant enebourien");_("br","You are:","Hoc'h anv:");_("br","Change","Kemmañ");_("br","(name to give)","(anv da reiñ)");_("br","Put back all the tiles\nin the bag","Adlakaat an holl pezhio er sac'h");_("br","Number of your game:","Niverenn ho c'hoari:");_("br","(pending)","(o c'hortoz)");_("br","Write a message to your adversaries here","Skrivit kemennadenno d'hoc'h enebourien aze");_("br","Send message","Kas ar gemennadenn");_("br","Sound of the tiles","Son ar pezhioù");_("br","Sound of messages","Son ar c'hemennadenoù");_("br","Number of tiles in the bag:","Niver a bezhioù er sac'h:");_("br","Click on it to take one.","Klikit warnañ evit kemer unan");_("br","Put back all the tiles in the bag","Adlakaat an holl pezhioù er sac'h");_("br","Participant","C'hoarier");_("br","Rack","Douger-pezhioù");_("br","Score","Poentoù");_("br","Waiting for other participants…","O c'hortoz c'hoarien all...");_("br","Click on someone's score\nto change it.","Klikit war poentoù unan bennak\nevit kemmañ.");_("br","Put back all the tiles of\nyour rack in the bag","Adlakaat holl pezhioù ho\ntouger-pezhoù er sac'h");_("br","Interface language:","Yezh ar c'hetal:");if(trivabble.applyL10n){trivabble.applyL10n();}})();
\ No newline at end of file
diff --git a/l10n/js/en.js b/l10n/js/en.js
deleted file mode 100644 (file)
index 7822114..0000000
+++ /dev/null
@@ -1 +0,0 @@
-trivabble.applyL10n();
diff --git a/l10n/js/fr.js b/l10n/js/fr.js
deleted file mode 100644 (file)
index 533c13c..0000000
+++ /dev/null
@@ -1 +0,0 @@
-(function(){var _=myAlert.l10n;_("fr","OK","OK");_("fr","Yes","Oui");_("fr","No","Non");_("fr","Cancel","Annuler");_=trivabble.l10n;_("fr","Your game is not over. Are you sure you want to leave now?","Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?");_("fr","You are about to leave the current game. To recover it, please note its number: {0}","Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}");_("fr","Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page.","Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page.");_("fr","To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.","Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}.");_("fr","It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again.","Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie ».");_("fr","To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name.","Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom.");_("fr","You cannot take another tile: your rack is full.","Vous ne pouvez pas tirer de pièce : votre chevalet est complet.");_("fr","Are you sure you want to put all the tiles back in the bag (in order to play another game)?","Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?");_("fr","Are you sure you want to put all your tiles back in the bag?","Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?");_("fr","It seems your did not give your name. You need to do it for the game to run properly.","Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu.");_("fr","Hello! To begin, enter your name. Your adversaries will see this name when you play with them.","Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux.");_("fr","Double\nLetter","Lettre\nDouble");_("fr","Double\nWord","Mot\nDouble");_("fr","Triple\nLetter","Lettre\nTriple");_("fr","Triple\nWord","Mot\nTriple");_("fr","Join adversaries","Rejoindre des adversaires");_("fr","You are:","Vous êtes :");_("fr","Change","Changer");_("fr","(name to give)","(nom à renseigner)");_("fr","Put back all the tiles\nin the bag","Remettre toutes les pièces\ndans le sac");_("fr","Number of your game:","Numéro de votre partie :");_("fr","(pending)","(en attente)");_("fr","Write a message to your adversaries here","Écrivez un message à vos adversaires ici");_("fr","Send message","Envoyer le message");_("fr","Sound of the tiles","Bruit des pièces");_("fr","Sound of messages","Bruit des messages");_("fr","Number of tiles in the bag:","Nombre de pièces dans le sac :");_("fr","Click on it to take one.","Cliquez dessus pour en prendre une.");_("fr","Put back all the tiles in the bag","Ranger toutes les pièces dans le sac");_("fr","Participant","Participant·e");_("fr","Rack","Chevalet");_("fr","Score","Score");_("fr","Waiting for other participants…","En attente des autres participants…");_("fr","Click on someone's score\nto change it.","Cliquez sur le score de quelqu'un\npour le changer.");_("fr","Put back all the tiles of\nyour rack in the bag","Remettre toutes les pièces du\nchevalet dans le sac");_("fr","Interface language:","Langue de l'interface :");_("fr","Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?","Désolé pour le dérangement, mais nous avons besoin de recharger le jeu pour que vous poussiez continuer à jouer. Vous ne perdrez rien sauf les messages dans le chat. Êtes-vous prêt·e ?");_("fr","To continue playing, click here","Pour continuer à jouer, cliquez ici");_("fr","{0}: ","{0} : ");_("fr","Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem.","Oups, il y a un problème. Nous essayons de corriger cela le plus vite possible. Merci de bien vouloir patienter quelques secondes. Si cela persiste, n’hésitez pas à contacter la personne qui peut s’occuper de ce problème");_("fr","The problem is solved, sorry for the inconvenience!","Le problème est résolu. Désolé pour la gêne occasionnée !");_("fr","It seems your browser went offline. Please reconnect to continue playing.","On dirait que votre navigateur est hors ligne. Merci de bien vouloir vous reconnecter pour continuer à jouer.");_("fr","Turn","Tour");_("fr","Who's turn? Click on the Turn button!","À qui le tour ? Cliquez sur le bouton Tour !");_("fr","Show my rack to other players","Montrer mon jeu aux autres");_("fr","Are you sure you want to show your rack to everybody?","Êtes-vous sûr·e de vouloir montrer votre jeu à tout le monde ?");_("fr","jocker","jocker");_("fr","Here is my rack:","Voici mon jeu :");_("fr","{0} shows their rack:","{0} montre son jeu :");_("fr","You cannot take another tile: the bag is empty.","Vous ne pouvez pas tirer de pièce : le sac est vide.");if(trivabble.applyL10n){trivabble.applyL10n();}})();
\ No newline at end of file
diff --git a/l10n/makejs.js b/l10n/makejs.js
deleted file mode 100755 (executable)
index 5dd1877..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env node
-
-/* Builds translation files.*/
-
-var fs = require('fs');
-
-var langs = fs.readdirSync("po");
-var po, i, len;
-
-function skipLine() {
-   while (i < len && po[i] !== '\n') {
-      ++i;
-   }
-   ++i;
-}
-
-function skipSpaces() {
-   while (i < len && !po[i].trim()) {
-      ++i;
-   }
-}
-
-function parseString() {
-   skipSpaces();
-   if (po[i] !== '"') {
-      return "";
-   }
-   ++i;
-   var deb = i, end;
-   while (i < len) {
-      if (po[i] === "\\") {
-         ++i;
-      } else if (po[i] === '"') {
-         var str1 = po.substring(deb, i++);
-         var end = i;
-         skipSpaces();
-         var ndeb = i;
-         var str2 = parseString();
-         if (i === ndeb) { // we did not parse anything
-            i = end;
-            return str1;
-         }
-
-         return str1 + str2;
-      }
-      ++i;
-   }
-   throw new Error("not ended string at character " + deb);
-}
-
-for (var l in langs) {
-   var lang = langs[l];
-   var jsFile = fs.openSync("js/" + lang + ".js", "w");
-   fs.writeSync(jsFile, "(function(){var ");
-   var poFiles = fs.readdirSync("po/" + lang);
-   for (var p in poFiles) {
-      var poFile = poFiles[p];
-
-      var translationFunction = fs.readFileSync("pot/" + poFile + 't', {encoding:'utf-8'})
-                               .match(/\#TranslationFunction[\s]+([\S]+)/)[1];
-
-      fs.writeSync(jsFile, "_=" + translationFunction + ".l10n;");
-
-      po = fs.readFileSync("po/" + lang + '/' + poFile, {encoding:'utf-8'});
-
-      i = 0; len = po.length;
-      while (i < len) {
-         skipSpaces();
-
-         if (po[i] === '#') {
-            skipLine();
-            continue;
-         }
-
-         if (po.substr(i, 5) === "msgid") {
-            if (po[i+5].trim() && po[i+5] !== '"') {
-               skipLine(); // don't understand this line
-               continue;
-            }
-            i+=5;
-            skipSpaces();
-            msgid = parseString();
-         } else if (po.substr(i, 6) === "msgstr") {
-            if (po[i+6].trim() && po[i+6] !== '"') {
-               skipLine(); // don't understand this line
-               continue;
-            }
-            i+=6;
-            msgstr = parseString();
-            fs.writeSync(jsFile, '_("' + lang + '","' + msgid.replace(/\n/g,"") + '","' + msgstr.replace(/\n/g,"") + '");');
-         }
-         skipLine();
-      }
-   }
-
-   fs.writeSync(jsFile, "if(" + translationFunction + ".applyL10n){" + translationFunction + ".applyL10n();}})();");
-   fs.close(jsFile, function (e) {
-       if (e) {
-        console.error(e);
-       }
-   });
-}
diff --git a/l10n/po/br/alert.po b/l10n/po/br/alert.po
deleted file mode 100644 (file)
index 6aec7b2..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-# TranslationFunction myAlert
-msgid "OK"
-msgstr "Mat eo"
-
-msgid "Yes"
-msgstr "Ya"
-
-msgid "No"
-msgstr "Ket"
-
-msgid "Cancel"
-msgstr "Nullañ"
diff --git a/l10n/po/br/trivabble.po b/l10n/po/br/trivabble.po
deleted file mode 100644 (file)
index 815ea08..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"POT-Creation-Date: \n"
-"PO-Revision-Date: \n"
-"Language-Team: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 1.8.7\n"
-"Last-Translator: \n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-"Language: br\n"
-
-# TranslationFunction trivabble
-msgid "Your game is not over. Are you sure you want to leave now?"
-msgstr "N'eo ket echu ho c'hoariadenn. Sur oc'h e fell deoc'h kuitaat bremañ?"
-
-msgid ""
-"You are about to leave the current game. To recover it, please note its "
-"number: {0}"
-msgstr ""
-"Emaoc'h o vont da guitaat ar c'hoariadenn. Evit hec'h adkavout e c'hallit "
-"notenniñ an niverenn da heul: {0}"
-
-msgid ""
-"Sorry, a problem just happened. The page must be reloaded. If the problem is "
-"not too serious, you should be able to keep playing normally. Otherwise, "
-"contact the person who is able to fix the problem. Click on “Yes” to reload "
-"the page."
-msgstr ""
-"Digarezit, ur gudenn a zo degouezhet. Ret eo adkargañ ar bajenn. Ma n'eo ket "
-"re bouezus ar gudenn e o tu deoc'h kenderc'hel da c'hoari. E mod all, kit e "
-"darempred gant drouizig@drouizig.org evit kempenn ar gudenn. Klikit war \"Ya"
-"\" evit adkargañ ar bajenn."
-
-msgid ""
-"To join a game, please give the number which is displayed on your "
-"adversary(ies)' screen.\n"
-"If you do not know it, ask them.\n"
-"\n"
-"Warning: your adversary must not take your number, (s)he must keep his/her "
-"own. If you whish to recover your current game, please not the following "
-"number: {0}."
-msgstr ""
-"Evit kevreañ d'ur c'hoariadenn, roit an niverenn diskouezet war skramm ho "
-"enebour(ien).\n"
-"Ma n'anavezit ket anezhañ, goulennit ganto.\n"
-"\n"
-"Diwallit: ne rank ket hoc'h enebour kemer ho niverenn, ret eo dezhañ pe "
-"dezhi derc'hel (h)e hini. Ma fell deoc'h adkavout ho c'hoari bremanel, "
-"notennit an niverenn da heul: {0}."
-
-msgid ""
-"It seems your did not give a correct number, or you clicked on “Cancel”. As "
-"a result, the current game continues, if any. To join a game, click on “Join "
-"a game” again."
-msgstr ""
-"War a seblant n'ho peus ket roet un niverenn dalvoudek, p ekliket ho peus "
-"war \"Nullañ\". Kendalc'het e vo gant ar c'hoari bremanel. Evit kevreañ d'ur "
-"c'hoari, klikit war \"Kevreañ d'ur c'hoari\" adarre."
-
-msgid ""
-"To change your name, enter a new one. You can keep using your current name "
-"by cancelling. Please note that if you change your name and you have games "
-"in progress, you will not be able to keep playing them anymore unless you "
-"get back to your current name."
-msgstr ""
-"Evit kemmañ hoc'h anv, enankit unan nevez. Gallout a rit derc'hel hoc'h hini "
-"bremanel en ur glikañ war \"Nullañ\". Diwallit: ma kemmit hoc'h anv en ur "
-"c'hoari, ne vo ket tu deoc'h kenderc'hel da c'hoari estreget mar adlakit "
-"hoc'h anv bremanel en-dro."
-
-msgid "You cannot take another tile: your rack is full."
-msgstr "N'hallit ket kement ur pezh all. Leun eo ho touger-pezhioù."
-
-msgid ""
-"Are you sure you want to put all the tiles back in the bag (in order to play "
-"another game)?"
-msgstr ""
-"Sur oc'h e fell deoc'h adlakaat an holl bezhioù er sac'h (evit c'hoari en-"
-"dro)?"
-
-msgid "Are you sure you want to put all your tiles back in the bag?"
-msgstr "Sur oc'h e fell deoc'h adlakaat holl ho pezhio er sac'h?"
-
-msgid ""
-"It seems your did not give your name. You need to do it for the game to run "
-"properly."
-msgstr ""
-"War a-seblant n'ho peus ket roet hoc'h anv. et eo deoc'h en ober evit "
-"gellout c'hoari."
-
-msgid ""
-"Hello! To begin, enter your name. Your adversaries will see this name when "
-"you play with them."
-msgstr ""
-"Demat! Evit kregiñ da c'hoari, enankit hoc'h anv. Hoc'h enebourien a welo an "
-"anv pa viot o c'hoari gante."
-
-msgid ""
-"Double\n"
-"Letter"
-msgstr ""
-"Lizherenn\n"
-"Zoubl"
-
-msgid ""
-"Double\n"
-"Word"
-msgstr ""
-"Ger\n"
-"Doubl"
-
-msgid ""
-"Triple\n"
-"Letter"
-msgstr ""
-"Lizherenn\n"
-"Dripl"
-
-msgid ""
-"Triple\n"
-"Word"
-msgstr ""
-"Ger\n"
-"Tripl"
-
-msgid "Join adversaries"
-msgstr "Kevreañ gant enebourien"
-
-msgid "You are:"
-msgstr "Hoc'h anv:"
-
-msgid "Change"
-msgstr "Kemmañ"
-
-msgid "(name to give)"
-msgstr "(anv da reiñ)"
-
-msgid ""
-"Put back all the tiles\n"
-"in the bag"
-msgstr "Adlakaat an holl pezhio er sac'h"
-
-msgid "Number of your game:"
-msgstr "Niverenn ho c'hoari:"
-
-msgid "(pending)"
-msgstr "(o c'hortoz)"
-
-msgid "Write a message to your adversaries here"
-msgstr "Skrivit kemennadenno d'hoc'h enebourien aze"
-
-msgid "Send message"
-msgstr "Kas ar gemennadenn"
-
-msgid "Sound of the tiles"
-msgstr "Son ar pezhioù"
-
-msgid "Sound of messages"
-msgstr "Son ar c'hemennadenoù"
-
-msgid "Number of tiles in the bag:"
-msgstr "Niver a bezhioù er sac'h:"
-
-msgid "Click on it to take one."
-msgstr "Klikit warnañ evit kemer unan"
-
-msgid "Put back all the tiles in the bag"
-msgstr "Adlakaat an holl pezhioù er sac'h"
-
-msgid "Participant"
-msgstr "C'hoarier"
-
-msgid "Rack"
-msgstr "Douger-pezhioù"
-
-msgid "Score"
-msgstr "Poentoù"
-
-msgid "Waiting for other participants…"
-msgstr "O c'hortoz c'hoarien all..."
-
-msgid ""
-"Click on someone's score\n"
-"to change it."
-msgstr ""
-"Klikit war poentoù unan bennak\n"
-"evit kemmañ."
-
-#. The part of the sentence before and after line break should not
-#. be longer than the original message.
-#. It is okay to put line breaks anywhere to split the sentence so it is not
-#. wider than the original sentence.
-msgid ""
-"Put back all the tiles of\n"
-"your rack in the bag"
-msgstr ""
-"Adlakaat holl pezhioù ho\n"
-"touger-pezhoù er sac'h"
-
-msgid  "Interface language:"
-msgstr "Yezh ar c'hetal:"
diff --git a/l10n/po/fr/alert.po b/l10n/po/fr/alert.po
deleted file mode 100644 (file)
index 312989d..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-msgid  "OK"\r
-msgstr "OK"\r
-\r
-msgid  "Yes"\r
-msgstr "Oui"\r
-\r
-msgid  "No"\r
-msgstr "Non"\r
-\r
-msgid  "Cancel"\r
-msgstr "Annuler"\r
diff --git a/l10n/po/fr/trivabble.po b/l10n/po/fr/trivabble.po
deleted file mode 100644 (file)
index 3132af1..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-msgid  "Your game is not over. Are you sure you want to leave now?"
-msgstr "Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?"
-
-msgid  "You are about to leave the current game. To recover it, please note its number: {0}"
-msgstr "Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}"
-
-msgid  "Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."
-msgstr "Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page."
-
-msgid  "Enter the new score of {0}:"
-"Saississez le nouveau score de {0} :"
-
-msgid  "To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}."
-msgstr "Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}."
-
-msgid  "It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."
-msgstr "Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie »."
-
-msgid  "To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."
-msgstr "Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom."
-
-msgid  "You cannot take another tile: your rack is full."
-msgstr "Vous ne pouvez pas tirer de pièce : votre chevalet est complet."
-
-msgid  "Are you sure you want to put all the tiles back in the bag (in order to play another game)?"
-msgstr "Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?"
-
-msgid  "Are you sure you want to put all your tiles back in the bag?"
-msgstr "Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?"
-
-msgid  "It seems your did not give your name. You need to do it for the game to run properly."
-msgstr "Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu."
-
-msgid  "Hello! To begin, enter your name. Your adversaries will see this name when you play with them."
-msgstr "Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux."
-
-msgid  "Double\nLetter"
-msgstr "Lettre\nDouble"
-
-msgid  "Double\nWord"
-msgstr "Mot\nDouble"
-
-msgid  "Triple\nLetter"
-msgstr "Lettre\nTriple"
-
-msgid  "Triple\nWord"
-msgstr "Mot\nTriple"
-
-msgid "Join adversaries"
-msgstr "Rejoindre des adversaires"
-
-msgid  "You are:"
-msgstr "Vous êtes :"
-
-msgid  "Change"
-msgstr "Changer"
-
-msgid  "(name to give)"
-msgstr "(nom à renseigner)"
-
-msgid  "Put back all the tiles\nin the bag"
-msgstr "Remettre toutes les pièces\ndans le sac"
-
-msgid  "Number of your game:"
-msgstr "Numéro de votre partie :"
-
-msgid  "(pending)"
-msgstr "(en attente)"
-
-msgid  "Write a message to your adversaries here"
-msgstr "Écrivez un message à vos adversaires ici"
-
-msgid  "Send message"
-msgstr  "Envoyer le message"
-
-msgid  "Sound of the tiles"
-msgstr "Bruit des pièces"
-
-msgid  "Sound of messages"
-msgstr "Bruit des messages"
-
-msgid  "Number of tiles in the bag:"
-msgstr "Nombre de pièces dans le sac :"
-
-msgid  "Click on it to take one."
-msgstr "Cliquez dessus pour en prendre une."
-
-msgid  "Put back all the tiles in the bag"
-msgstr "Ranger toutes les pièces dans le sac"
-
-msgid  "Participant"
-msgstr "Participant·e"
-
-msgid  "Rack"
-msgstr "Chevalet"
-
-msgid  "Score"
-msgstr "Score"
-
-msgid  "Waiting for other participants…"
-msgstr "En attente des autres participants…"
-
-msgid  "Click on someone's score\nto change it."
-msgstr "Cliquez sur le score de quelqu'un\npour le changer."
-
-msgid  "Put back all the tiles of\nyour rack in the bag"
-msgstr "Remettre toutes les pièces du\nchevalet dans le sac"
-
-msgid  "Interface language:"
-msgstr "Langue de l'interface :"
-
-msgid "Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"
-msgstr "Désolé pour le dérangement, mais nous avons besoin de recharger le jeu pour que vous poussiez continuer à jouer. Vous ne perdrez rien sauf les messages dans le chat. Êtes-vous prêt·e ?"
-
-msgid "To continue playing, click here"
-msgstr "Pour continuer à jouer, cliquez ici"
-
-msgid "{0}: "
-msgstr "{0} : "
-
-msgid "Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."
-msgstr "Oups, il y a un problème. Nous essayons de corriger cela le plus vite possible. Merci de bien vouloir patienter quelques secondes. Si cela persiste, n’hésitez pas à contacter la personne qui peut s’occuper de ce problème"
-
-msgid "The problem is solved, sorry for the inconvenience!"
-msgstr "Le problème est résolu. Désolé pour la gêne occasionnée !"
-
-msgid "It seems your browser went offline. Please reconnect to continue playing."
-msgstr "On dirait que votre navigateur est hors ligne. Merci de bien vouloir vous reconnecter pour continuer à jouer."
-
-msgid  "Turn"
-msgstr "Tour"
-
-msgid  "Who's turn? Click on the Turn button!"
-msgstr "À qui le tour ? Cliquez sur le bouton Tour !"
-
-msgid  "Show my rack to other players"
-msgstr "Montrer mon jeu aux autres"
-
-msgid  "Are you sure you want to show your rack to everybody?"
-msgstr "Êtes-vous sûr·e de vouloir montrer votre jeu à tout le monde ?"
-
-msgid  "jocker"
-msgstr "jocker"
-
-msgid  "Here is my rack:"
-msgstr "Voici mon jeu :"
-
-msgid  "{0} shows their rack:"
-msgstr "{0} montre son jeu :"
-
-msgid  "You cannot take another tile: the bag is empty."
-msgstr  "Vous ne pouvez pas tirer de pièce : le sac est vide."
diff --git a/l10n/pot/alert.pot b/l10n/pot/alert.pot
deleted file mode 100644 (file)
index 793ed59..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-#TranslationFunction myAlert
-
-msgid  "OK"
-msgstr ""
-
-msgid  "Yes"
-msgstr ""
-
-msgid  "No"
-msgstr ""
-
-msgid  "Cancel"
-msgstr ""
diff --git a/l10n/pot/trivabble.pot b/l10n/pot/trivabble.pot
deleted file mode 100644 (file)
index db631f4..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-#TranslationFunction trivabble
-
-msgid  "Your game is not over. Are you sure you want to leave now?"
-msgstr ""
-
-msgid  "You are about to leave the current game. To recover it, please note its number: {0}"
-msgstr ""
-
-msgid  "Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."
-msgstr ""
-
-msgid  "Enter the new score of {0}:"
-"Saississez le nouveau score de {0} :"
-
-msgid  "To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}."
-msgstr ""
-
-msgid  "It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."
-msgstr ""
-
-msgid  "To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."
-msgstr ""
-
-msgid  "You cannot take another tile: your rack is full."
-msgstr ""
-
-msgid  "Are you sure you want to put all the tiles back in the bag (in order to play another game)?"
-msgstr ""
-
-msgid  "Are you sure you want to put all your tiles back in the bag?"
-msgstr ""
-
-msgid  "It seems your did not give your name. You need to do it for the game to run properly."
-msgstr ""
-
-msgid  "Hello! To begin, enter your name. Your adversaries will see this name when you play with them."
-msgstr ""
-
-msgid  "Double\nLetter"
-msgstr ""
-
-msgid  "Double\nWord"
-msgstr ""
-
-msgid  "Triple\nLetter"
-msgstr ""
-
-msgid  "Triple\nWord"
-msgstr ""
-
-msgid "Join adversaries"
-msgstr ""
-
-msgid  "You are:"
-msgstr ""
-
-msgid  "Change"
-msgstr ""
-
-msgid  "(name to give)"
-msgstr ""
-
-msgid  "Put back all the tiles\nin the bag"
-msgstr ""
-
-msgid  "Number of your game:"
-msgstr ""
-
-msgid  "(pending)"
-msgstr ""
-
-msgid  "Write a message to your adversaries here"
-msgstr ""
-
-msgid  "Send message"
-msgstr ""
-
-msgid  "Sound of the tiles"
-msgstr ""
-
-msgid  "Sound of messages"
-msgstr ""
-
-msgid  "Number of tiles in the bag:"
-msgstr ""
-
-msgid  "Click on it to take one."
-msgstr ""
-
-msgid  "Put back all the tiles in the bag"
-msgstr ""
-
-msgid  "Participant"
-msgstr ""
-
-msgid  "Rack"
-msgstr ""
-
-msgid  "Score"
-msgstr ""
-
-msgid  "Waiting for other participants…"
-msgstr ""
-
-msgid  "Click on someone's score\nto change it."
-msgstr ""
-
-#. The part of the sentence before and after line break should not
-#. be longer than the original message.
-#. It is okay to put line breaks anywhere to split the sentence so it is not
-#. wider than the original sentence.
-msgid  "Put back all the tiles of\nyour rack in the bag"
-msgstr ""
-
-msgid  "Interface language:"
-msgstr ""
-
-msgid "Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"
-msgstr ""
-
-msgid "To continue playing, click here"
-msgstr ""
-
-msgid "{0}: "
-msgstr ""
-
-msgid "Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."
-msgstr ""
-
-msgid "The problem is solved, sorry for the inconvenience!"
-msgstr ""
-
-msgid  "It seems your browser went offline. Please reconnect to continue playing."
-msgstr ""
-
-msgid  "Turn"
-msgstr ""
-
-msgid  "Show my rack to other players"
-msgstr ""
-
-msgid  "Are you sure you want to show your rack to everybody?"
-msgstr ""
-
-msgid  "jocker"
-msgstr ""
-
-msgid  "Here is my rack:"
-msgstr ""
-
-msgid  "{0} shows their rack:"
-msgstr ""
-
-msgid  "You cannot take another tile: the bag is empty."
-msgstr ""
diff --git a/notification.mp3 b/notification.mp3
deleted file mode 100644 (file)
index eeab9fb..0000000
Binary files a/notification.mp3 and /dev/null differ
diff --git a/notification.ogg b/notification.ogg
deleted file mode 100644 (file)
index fae9e26..0000000
Binary files a/notification.ogg and /dev/null differ
diff --git a/receive.mp3 b/receive.mp3
deleted file mode 100644 (file)
index 6f6ff2b..0000000
Binary files a/receive.mp3 and /dev/null differ
diff --git a/receive.ogg b/receive.ogg
deleted file mode 100644 (file)
index 17f3f68..0000000
Binary files a/receive.ogg and /dev/null differ
diff --git a/server-data/alert.css b/server-data/alert.css
new file mode 100644 (file)
index 0000000..2b4c470
--- /dev/null
@@ -0,0 +1,74 @@
+.alert {
+    position:fixed;
+    top:0;
+    left:0;
+    right:0;
+    bottom:0;
+    background:rgba(0,0,0,0.2);
+    display:-webkit-flex;
+    display:flex;
+    -webkit-flex-direction:column;
+    flex-direction:column;
+    -webkit-justify-content:center;
+    justify-content:center;
+    line-height:1.5em;
+    text-align:center
+}
+
+.alert-outer {
+    background:white;
+    color:black;
+    border-radius:4px;
+    display:-webkit-flex;
+    display:flex;
+    -webkit-flex-direction:column;
+    flex-direction:column;
+    -webkit-align-self:center;
+    align-self:center;
+    text-align:center;
+    box-shadow:0 0 8px black;
+    max-width:75%;
+    padding:4px;
+}
+
+.alert-content {
+    padding:1ex
+}
+
+.alert input[type=text], .alert input[type=password] {
+   border-radius:none;
+   box-sizing:border-box;
+   min-height:1.5rem;
+   outline:none;
+   width:95%;
+   min-width:10ex;
+   margin:1ex;
+   border:1px solid silver;
+}
+
+.alert input[type=text]:focus, .alert  input[type=password]:focus {
+   border:1px solid #5CF;
+   box-shadow:0 0 1px #5CF
+}
+
+.alert input[type=button], .alert button {
+   margin:1px;
+   border-radius:0;
+   min-height:1.5rem;
+   border:1px solid silver;
+   padding:1ex 2ex;
+   background:white
+}
+
+.alert input[type=button]:active, .alert button:active {
+   background:#D5D5D5
+}
+
+.alert button {
+    margin:0.5ex;
+    border:1px solid gray;
+    background:#F1F1F1;
+    color:black;
+    border-radius:3px
+}
+
diff --git a/server-data/alert.js b/server-data/alert.js
new file mode 100644 (file)
index 0000000..d31cdc2
--- /dev/null
@@ -0,0 +1,161 @@
+/*global libD*/
+
+(function (global) {
+    "use strict";
+
+    let divAlert;
+    let divAlertInput;
+    let divAlertConfirm;
+    let divAlertButton;
+    let alertButtonOK;
+    let divAlertCallback;
+    let divAlertCallbackYes;
+    let divAlertCallbackNo;
+    let alertInput;
+    let divAlertContent;
+
+    const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {return s;};
+
+    function promptOK() {
+        divAlert.style.display = "none";
+        if (divAlertCallback) {
+            divAlertCallback(alertInput.value);
+        }
+    }
+
+    function promptCancel() {
+        divAlert.style.display = "none";
+        if (divAlertCallback) {
+            divAlertCallback(null);
+        }
+    }
+
+    function confirmYes() {
+        divAlert.style.display = "none";
+        if (divAlertCallbackYes) {
+            divAlertCallbackYes();
+        }
+    }
+    function confirmNo() {
+        divAlert.style.display = "none";
+        if (divAlertCallbackNo) {
+            divAlertCallbackNo();
+        }
+    }
+
+    function alertOK() {
+        divAlert.style.display = "none";
+        if (divAlertCallback) {
+            divAlertCallback();
+        }
+    }
+
+    function prepare() {
+        divAlert = document.createElement("div");
+        divAlert.className = "alert";
+        divAlert.style.display = "none";
+
+        divAlertContent = document.createElement("div");
+        divAlertContent.className = "alert-content";
+        divAlertContent.style.whiteSpace = "pre-wrap";
+
+        divAlertInput = document.createElement("div");
+        divAlertInput.className = "alert-prompt";
+
+        alertInput = document.createElement("input");
+        alertInput.type = "text";
+        alertInput.onkeydown = function (e) {
+            if (e.keyCode === 13) {
+                e.preventDefault();
+                promptOK();
+            }
+        };
+
+        divAlertInput.appendChild(alertInput);
+
+        divAlertInput.appendChild(document.createElement("div"));
+        divAlertInput.lastChild.className = "alert-prompt-buttons";
+        divAlertInput.lastChild.appendChild(document.createElement("button"));
+        divAlertInput.lastChild.lastChild.textContent = _("OK");
+        divAlertInput.lastChild.lastChild.onclick = promptOK;
+        divAlertInput.lastChild.appendChild(document.createElement("button"));
+        divAlertInput.lastChild.lastChild.textContent = _("Annuler");
+        divAlertInput.lastChild.lastChild.onclick = promptCancel;
+
+        divAlertConfirm = document.createElement("div");
+        divAlertConfirm.className = _("alert-confirm");
+        divAlertConfirm.appendChild(document.createElement("button"));
+        divAlertConfirm.lastChild.textContent = _("Oui");
+        divAlertConfirm.lastChild.onclick = confirmYes;
+        divAlertConfirm.appendChild(document.createElement("button"));
+        divAlertConfirm.lastChild.textContent = _("Non");
+        divAlertConfirm.lastChild.onclick = confirmNo;
+
+        divAlertButton = document.createElement("div");
+        alertButtonOK = document.createElement("button");
+        divAlertButton.appendChild(alertButtonOK);
+        alertButtonOK.textContent = _("OK");
+        alertButtonOK.onclick = alertOK;
+
+        const divAlertOuter = document.createElement("div");
+        divAlertOuter.className = "alert-outer";
+        divAlertOuter.appendChild(divAlertContent);
+        divAlertOuter.appendChild(divAlertInput);
+        divAlertOuter.appendChild(divAlertConfirm);
+        divAlertOuter.appendChild(divAlertButton);
+        divAlert.appendChild(divAlertOuter);
+        document.body.appendChild(divAlert);
+    }
+
+    global.myAlert = function (msg, callback) {
+        if (!divAlert) {
+            prepare();
+        }
+
+        divAlertContent.textContent = msg;
+        divAlertInput.style.display = "none";
+        divAlertConfirm.style.display = "none";
+        divAlertButton.style.display = "";
+        divAlertCallback = callback;
+        divAlert.style.display = "";
+        divAlertButton.getElementsByTagName("button")[0].focus();
+    };
+
+    global.myAlert.l10n = _;
+
+    global.myPrompt = function (msg, callback, defaultText) {
+        if (!divAlert) {
+            prepare();
+        }
+
+        divAlertContent.textContent = msg;
+        divAlertInput.style.display = "";
+        divAlertInput.firstChild.value = defaultText || "";
+        divAlertConfirm.style.display = "none";
+        divAlertButton.style.display = "none";
+        divAlertCallback = callback;
+        divAlert.style.display = "";
+
+        // eslint-disable-next-line require-unicode-regexp
+        if (!(/iPad|iPhone|iPod/g).test(navigator.userAgent)) {
+            alertInput.focus();
+            alertInput.setSelectionRange(0, alertInput.value.length);
+            alertInput.select();
+        }
+    };
+
+    global.myConfirm = function (msg, callbackYes, callbackNo) {
+        if (!divAlert) {
+            prepare();
+        }
+
+        divAlertContent.textContent = msg;
+        divAlertInput.style.display = "none";
+        divAlertConfirm.style.display = "";
+        divAlertButton.style.display = "none";
+        divAlertCallbackYes = callbackYes;
+        divAlertCallbackNo = callbackNo;
+        divAlert.style.display = "";
+        divAlertConfirm.getElementsByTagName("button")[0].focus();
+    };
+}(this));
diff --git a/server-data/bag.svg b/server-data/bag.svg
new file mode 100644 (file)
index 0000000..92bc6f0
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" height="319mm" width="331mm" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1172.8346 1130.315" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs id="defs4">
+  <linearGradient id="linearGradient5620" y2="482.82" gradientUnits="userSpaceOnUse" x2="1120.7" gradientTransform="matrix(1.21 0 0 1.21 -122.4 -73.147)" y1="482.82" x1="56.525">
+   <stop id="stop5616" offset="0"/>
+   <stop id="stop5618" stop-color="#33333f" stop-opacity=".94118" offset="1"/>
+  </linearGradient>
+ </defs>
+ <metadata id="metadata7">
+  <rdf:RDF>
+   <cc:Work rdf:about="">
+    <dc:format>image/svg+xml</dc:format>
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+    <dc:title/>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <g id="layer1" transform="translate(0 77.953)">
+  <path id="path3348" d="m510.26 107.75-273.12 245.46-127.91 449.43 490.91 207.46 456.36-183.26-159.04-487.46-221.26-224.71 190.18-148.75c-88.433-9.4796-177.32-14.756-266.25-15.804-96.89-1.1424-193.84 2.7337-290.33 11.607l200.46 146.03z" fill-rule="evenodd" stroke="#000" stroke-width="1.21px" fill="url(#linearGradient5620)"/>
+  <path id="path4164" stroke-linejoin="round" d="m684.88 431.89c-6.34-58.38-23.22-115.59-49.58-168.06-29.85-59.42-71.88-112.69-122.72-155.55 22.722 2.9086 45.556 4.9435 68.435 6.0986 34.007 1.717 68.112 1.489 102.09-0.68242" stroke="#b47f58" stroke-linecap="round" stroke-width="12.1" fill="none"/>
+ </g>
+</svg>
diff --git a/server-data/baton.svg b/server-data/baton.svg
new file mode 100644 (file)
index 0000000..2a15a9c
--- /dev/null
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="143.40671mm"
+   height="67.173225mm"
+   viewBox="0 0 143.40671 67.173226"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
+   sodipodi:docname="baton.svg">
+  <defs
+     id="defs2">
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient837">
+      <stop
+         style="stop-color:#225500;stop-opacity:1;"
+         offset="0"
+         id="stop833" />
+      <stop
+         style="stop-color:#cdde87;stop-opacity:1"
+         offset="1"
+         id="stop835" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient837"
+       id="linearGradient839"
+       x1="46.49107"
+       y1="217.55084"
+       x2="106.49107"
+       y2="217.55084"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(144.34952,-283.94879)" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="358.38013"
+     inkscape:cy="245.7383"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="2560"
+     inkscape:window-height="1355"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Calque 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-4.7877181,-183.96423)">
+    <rect
+       style="opacity:1;fill:url(#linearGradient839);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.79374999;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
+       id="rect815"
+       width="60"
+       height="140"
+       x="190.84059"
+       y="-136.39795"
+       transform="rotate(87.362214)" />
+  </g>
+</svg>
diff --git a/server-data/config.js.sample b/server-data/config.js.sample
new file mode 100644 (file)
index 0000000..beba4ee
--- /dev/null
@@ -0,0 +1,22 @@
+// This is a sample config file. Copy to config.js if you need to fiddle with the
+// configuration of the client.
+
+window.TrivabbleConf = {
+    // The amount of time to wait after a connection failure
+    POLLING_DELAY:        2000,
+
+    // Whether WebSockets should be used, if possible
+    ENABLE_WEBSOCKETS:    true,
+
+    // Whether Server Sent Events should be used, if available
+    ENABLE_EVENT_SOURCE:  true,
+
+    // Max consecutive tries before blacklisting WebSockets for the current session
+    MAX_WEBSOCKET_ERRORS: 1,
+
+    // to tweak only if your webserver is shared with other conflicting resources at / ( ex: yunohost integration )
+    APP_PATH: "",
+
+    // I don't like trailing commas, here is a nice message for you reading this file :-)
+    HAVE_FUN: true
+};
diff --git a/server-data/index.html b/server-data/index.html
new file mode 100644 (file)
index 0000000..b71f1b0
--- /dev/null
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title> Trivabble </title>
+        <link rel="stylesheet" href="trivabble.css" />
+        <link rel="stylesheet" href="alert.css" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
+        <meta name="apple-mobile-web-app-capable" content="yes" />
+        <meta name="format-detection" content="telephone=no" />
+        <script src="config.js"></script>
+        <script src="l10n.js"></script>
+        <script src="alert.js"></script>
+        <script src="touch2click.js"></script>
+        <script src="trivabble.js"></script>
+    </head>
+    <body>
+        <div id="menu">
+            <button data-l10n="text-content" id="join-game">Join adversaries</button>
+            <p id="p-name"> <span data-l10n="text-content">You are:</span> <span id="name" data-l10n="text-content">(name to give)</span><br /><button class="minibutton" id="change-name" data-l10n="text-content">Change</button></p>
+            <button id="clear-game" data-l10n="text-content">Put back all the tiles&#10;in the bag</button>
+            <p id="p-name"><span data-l10n="text-content">Number of your game:</span><br /><span id="number" data-l10n="text-content">(pending)</span></p>
+            <div id="chat-messages"></div>
+            <textarea id="chat-ta" placeholder="Write a message to your adversaries here" data-l10n="placeholder"></textarea>
+            <button id="chat-btn" class="minibutton" data-l10n="text-content">Send message</button>
+            <div id="prefs-sound">
+                <p><label><input type="checkbox" id="tiles-sound" checked="checked" /><span data-l10n="text-content">Sound of the tiles</span></label></p>
+                <p><label><input type="checkbox" id="msg-sound" checked="checked" /><span data-l10n="text-content">Sound of messages</span></label></p>
+            </div>
+
+            <div id="lang-selection">
+                <label>
+                    <span data-l10n="text-content">Interface language: </span>
+                    <select id="select-lang">
+                        <option value="en">English</option>
+                        <option value="fr">Français</option>
+                        <option value="br">Brezhoneg</option>
+                    </select>
+                </label>
+            </div>
+        </div>
+
+        <div id="content">
+            <table id="board">
+                <tr><td class="corner"></td></tr>
+            </table>
+            <div id="rack-outer">
+                <div id="rack"></div>
+            </div>
+        </div>
+
+        <div id="panel">
+            <div id="bag"></div>
+            <p><span data-l10n="text-content">Number of tiles in the bag:</span><br /><span id="remaining-letters">102</span><br /><span id="help-bag" data-l10n="text-content">Click on it to take one.</span><a href="#" id="help-clear" style="display:none"></a></p>
+            <table id="participants">
+                <tr><th data-l10n="text-content">Participant</th><th data-l10n="text-content">Rack</th><th data-l10n="text-content">Score</th><th data-l10n="text-content">Turn</th></tr>
+                <tr id="participants-placeholder"><td colspan="4" data-l10n="text-content">Waiting for other participants…</td></tr>
+            </table>
+            <p id="help-scores" data-l10n="text-content">Click on someone's score&#10;to change it.</p>
+            <p id="help-turn" data-l10n="text-content">Who's turn? Click on the Turn button!</p>
+            <button id="clear-rack" class="minibutton" data-l10n="text-content">Put back all the tiles of&#10;your rack in the bag</button>
+            <button id="show-rack" class="minibutton" data-l10n="text-content">Show my rack to other players</button>
+        </div>
+    </body>
+</html>
diff --git a/server-data/l10n.js b/server-data/l10n.js
new file mode 100644 (file)
index 0000000..1200f08
--- /dev/null
@@ -0,0 +1,34 @@
+/*global libD*/
+
+window.libD = {
+    l10n: function () {
+        "use strict";
+
+        const t = [];
+
+        function f (lang, orig, translated) {
+            if (!orig) {
+                return (
+                    (libD.lang && t[libD.lang] && t[libD.lang][lang])
+                        ? t[libD.lang][lang]
+                        : lang // lang is the default string
+                );
+            }
+
+            if (!t[lang]) {
+                t[lang] = [];
+            }
+
+            t[lang][orig] = translated;
+        };
+
+        return f;
+    },
+    lang: (
+        window.navigator && (
+            (navigator.language || navigator.userLanguage)
+                ? (navigator.language || navigator.userLanguage).split("-")[0].toLowerCase()
+                : "en"
+        )
+    ) || "en"
+};
diff --git a/server-data/l10n/Makefile b/server-data/l10n/Makefile
new file mode 100644 (file)
index 0000000..fbab671
--- /dev/null
@@ -0,0 +1,3 @@
+all:
+       node makejs.js
+
diff --git a/server-data/l10n/js/br.js b/server-data/l10n/js/br.js
new file mode 100644 (file)
index 0000000..e42e101
--- /dev/null
@@ -0,0 +1 @@
+(function(){var _=myAlert.l10n;_("br","OK","Mat eo");_("br","Yes","Ya");_("br","No","Ket");_("br","Cancel","Nullañ");_=trivabble.l10n;_("br","","Project-Id-Version: \nPOT-Creation-Date: \nPO-Revision-Date: \nLanguage-Team: \nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nX-Generator: Poedit 1.8.7\nLast-Translator: \nPlural-Forms: nplurals=2; plural=(n > 1);\nLanguage: br\n");_("br","Your game is not over. Are you sure you want to leave now?","N'eo ket echu ho c'hoariadenn. Sur oc'h e fell deoc'h kuitaat bremañ?");_("br","You are about to leave the current game. To recover it, please note its number: {0}","Emaoc'h o vont da guitaat ar c'hoariadenn. Evit hec'h adkavout e c'hallit notenniñ an niverenn da heul: {0}");_("br","Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page.","Digarezit, ur gudenn a zo degouezhet. Ret eo adkargañ ar bajenn. Ma n'eo ket re bouezus ar gudenn e o tu deoc'h kenderc'hel da c'hoari. E mod all, kit e darempred gant drouizig@drouizig.org evit kempenn ar gudenn. Klikit war \"Ya\" evit adkargañ ar bajenn.");_("br","To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.","Evit kevreañ d'ur c'hoariadenn, roit an niverenn diskouezet war skramm ho enebour(ien).\nMa n'anavezit ket anezhañ, goulennit ganto.\n\nDiwallit: ne rank ket hoc'h enebour kemer ho niverenn, ret eo dezhañ pe dezhi derc'hel (h)e hini. Ma fell deoc'h adkavout ho c'hoari bremanel, notennit an niverenn da heul: {0}.");_("br","It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again.","War a seblant n'ho peus ket roet un niverenn dalvoudek, p ekliket ho peus war \"Nullañ\". Kendalc'het e vo gant ar c'hoari bremanel. Evit kevreañ d'ur c'hoari, klikit war \"Kevreañ d'ur c'hoari\" adarre.");_("br","To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name.","Evit kemmañ hoc'h anv, enankit unan nevez. Gallout a rit derc'hel hoc'h hini bremanel en ur glikañ war \"Nullañ\". Diwallit: ma kemmit hoc'h anv en ur c'hoari, ne vo ket tu deoc'h kenderc'hel da c'hoari estreget mar adlakit hoc'h anv bremanel en-dro.");_("br","You cannot take another tile: your rack is full.","N'hallit ket kement ur pezh all. Leun eo ho touger-pezhioù.");_("br","Are you sure you want to put all the tiles back in the bag (in order to play another game)?","Sur oc'h e fell deoc'h adlakaat an holl bezhioù er sac'h (evit c'hoari en-dro)?");_("br","Are you sure you want to put all your tiles back in the bag?","Sur oc'h e fell deoc'h adlakaat holl ho pezhio er sac'h?");_("br","It seems your did not give your name. You need to do it for the game to run properly.","War a-seblant n'ho peus ket roet hoc'h anv. et eo deoc'h en ober evit gellout c'hoari.");_("br","Hello! To begin, enter your name. Your adversaries will see this name when you play with them.","Demat! Evit kregiñ da c'hoari, enankit hoc'h anv. Hoc'h enebourien a welo an anv pa viot o c'hoari gante.");_("br","Double\nLetter","Lizherenn\nZoubl");_("br","Double\nWord","Ger\nDoubl");_("br","Triple\nLetter","Lizherenn\nDripl");_("br","Triple\nWord","Ger\nTripl");_("br","Join adversaries","Kevreañ gant enebourien");_("br","You are:","Hoc'h anv:");_("br","Change","Kemmañ");_("br","(name to give)","(anv da reiñ)");_("br","Put back all the tiles\nin the bag","Adlakaat an holl pezhio er sac'h");_("br","Number of your game:","Niverenn ho c'hoari:");_("br","(pending)","(o c'hortoz)");_("br","Write a message to your adversaries here","Skrivit kemennadenno d'hoc'h enebourien aze");_("br","Send message","Kas ar gemennadenn");_("br","Sound of the tiles","Son ar pezhioù");_("br","Sound of messages","Son ar c'hemennadenoù");_("br","Number of tiles in the bag:","Niver a bezhioù er sac'h:");_("br","Click on it to take one.","Klikit warnañ evit kemer unan");_("br","Put back all the tiles in the bag","Adlakaat an holl pezhioù er sac'h");_("br","Participant","C'hoarier");_("br","Rack","Douger-pezhioù");_("br","Score","Poentoù");_("br","Waiting for other participants…","O c'hortoz c'hoarien all...");_("br","Click on someone's score\nto change it.","Klikit war poentoù unan bennak\nevit kemmañ.");_("br","Put back all the tiles of\nyour rack in the bag","Adlakaat holl pezhioù ho\ntouger-pezhoù er sac'h");_("br","Interface language:","Yezh ar c'hetal:");if(trivabble.applyL10n){trivabble.applyL10n();}})();
\ No newline at end of file
diff --git a/server-data/l10n/js/en.js b/server-data/l10n/js/en.js
new file mode 100644 (file)
index 0000000..7822114
--- /dev/null
@@ -0,0 +1 @@
+trivabble.applyL10n();
diff --git a/server-data/l10n/js/fr.js b/server-data/l10n/js/fr.js
new file mode 100644 (file)
index 0000000..533c13c
--- /dev/null
@@ -0,0 +1 @@
+(function(){var _=myAlert.l10n;_("fr","OK","OK");_("fr","Yes","Oui");_("fr","No","Non");_("fr","Cancel","Annuler");_=trivabble.l10n;_("fr","Your game is not over. Are you sure you want to leave now?","Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?");_("fr","You are about to leave the current game. To recover it, please note its number: {0}","Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}");_("fr","Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page.","Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page.");_("fr","To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.","Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}.");_("fr","It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again.","Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie ».");_("fr","To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name.","Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom.");_("fr","You cannot take another tile: your rack is full.","Vous ne pouvez pas tirer de pièce : votre chevalet est complet.");_("fr","Are you sure you want to put all the tiles back in the bag (in order to play another game)?","Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?");_("fr","Are you sure you want to put all your tiles back in the bag?","Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?");_("fr","It seems your did not give your name. You need to do it for the game to run properly.","Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu.");_("fr","Hello! To begin, enter your name. Your adversaries will see this name when you play with them.","Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux.");_("fr","Double\nLetter","Lettre\nDouble");_("fr","Double\nWord","Mot\nDouble");_("fr","Triple\nLetter","Lettre\nTriple");_("fr","Triple\nWord","Mot\nTriple");_("fr","Join adversaries","Rejoindre des adversaires");_("fr","You are:","Vous êtes :");_("fr","Change","Changer");_("fr","(name to give)","(nom à renseigner)");_("fr","Put back all the tiles\nin the bag","Remettre toutes les pièces\ndans le sac");_("fr","Number of your game:","Numéro de votre partie :");_("fr","(pending)","(en attente)");_("fr","Write a message to your adversaries here","Écrivez un message à vos adversaires ici");_("fr","Send message","Envoyer le message");_("fr","Sound of the tiles","Bruit des pièces");_("fr","Sound of messages","Bruit des messages");_("fr","Number of tiles in the bag:","Nombre de pièces dans le sac :");_("fr","Click on it to take one.","Cliquez dessus pour en prendre une.");_("fr","Put back all the tiles in the bag","Ranger toutes les pièces dans le sac");_("fr","Participant","Participant·e");_("fr","Rack","Chevalet");_("fr","Score","Score");_("fr","Waiting for other participants…","En attente des autres participants…");_("fr","Click on someone's score\nto change it.","Cliquez sur le score de quelqu'un\npour le changer.");_("fr","Put back all the tiles of\nyour rack in the bag","Remettre toutes les pièces du\nchevalet dans le sac");_("fr","Interface language:","Langue de l'interface :");_("fr","Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?","Désolé pour le dérangement, mais nous avons besoin de recharger le jeu pour que vous poussiez continuer à jouer. Vous ne perdrez rien sauf les messages dans le chat. Êtes-vous prêt·e ?");_("fr","To continue playing, click here","Pour continuer à jouer, cliquez ici");_("fr","{0}: ","{0} : ");_("fr","Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem.","Oups, il y a un problème. Nous essayons de corriger cela le plus vite possible. Merci de bien vouloir patienter quelques secondes. Si cela persiste, n’hésitez pas à contacter la personne qui peut s’occuper de ce problème");_("fr","The problem is solved, sorry for the inconvenience!","Le problème est résolu. Désolé pour la gêne occasionnée !");_("fr","It seems your browser went offline. Please reconnect to continue playing.","On dirait que votre navigateur est hors ligne. Merci de bien vouloir vous reconnecter pour continuer à jouer.");_("fr","Turn","Tour");_("fr","Who's turn? Click on the Turn button!","À qui le tour ? Cliquez sur le bouton Tour !");_("fr","Show my rack to other players","Montrer mon jeu aux autres");_("fr","Are you sure you want to show your rack to everybody?","Êtes-vous sûr·e de vouloir montrer votre jeu à tout le monde ?");_("fr","jocker","jocker");_("fr","Here is my rack:","Voici mon jeu :");_("fr","{0} shows their rack:","{0} montre son jeu :");_("fr","You cannot take another tile: the bag is empty.","Vous ne pouvez pas tirer de pièce : le sac est vide.");if(trivabble.applyL10n){trivabble.applyL10n();}})();
\ No newline at end of file
diff --git a/server-data/l10n/makejs.js b/server-data/l10n/makejs.js
new file mode 100755 (executable)
index 0000000..5dd1877
--- /dev/null
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+
+/* Builds translation files.*/
+
+var fs = require('fs');
+
+var langs = fs.readdirSync("po");
+var po, i, len;
+
+function skipLine() {
+   while (i < len && po[i] !== '\n') {
+      ++i;
+   }
+   ++i;
+}
+
+function skipSpaces() {
+   while (i < len && !po[i].trim()) {
+      ++i;
+   }
+}
+
+function parseString() {
+   skipSpaces();
+   if (po[i] !== '"') {
+      return "";
+   }
+   ++i;
+   var deb = i, end;
+   while (i < len) {
+      if (po[i] === "\\") {
+         ++i;
+      } else if (po[i] === '"') {
+         var str1 = po.substring(deb, i++);
+         var end = i;
+         skipSpaces();
+         var ndeb = i;
+         var str2 = parseString();
+         if (i === ndeb) { // we did not parse anything
+            i = end;
+            return str1;
+         }
+
+         return str1 + str2;
+      }
+      ++i;
+   }
+   throw new Error("not ended string at character " + deb);
+}
+
+for (var l in langs) {
+   var lang = langs[l];
+   var jsFile = fs.openSync("js/" + lang + ".js", "w");
+   fs.writeSync(jsFile, "(function(){var ");
+   var poFiles = fs.readdirSync("po/" + lang);
+   for (var p in poFiles) {
+      var poFile = poFiles[p];
+
+      var translationFunction = fs.readFileSync("pot/" + poFile + 't', {encoding:'utf-8'})
+                               .match(/\#TranslationFunction[\s]+([\S]+)/)[1];
+
+      fs.writeSync(jsFile, "_=" + translationFunction + ".l10n;");
+
+      po = fs.readFileSync("po/" + lang + '/' + poFile, {encoding:'utf-8'});
+
+      i = 0; len = po.length;
+      while (i < len) {
+         skipSpaces();
+
+         if (po[i] === '#') {
+            skipLine();
+            continue;
+         }
+
+         if (po.substr(i, 5) === "msgid") {
+            if (po[i+5].trim() && po[i+5] !== '"') {
+               skipLine(); // don't understand this line
+               continue;
+            }
+            i+=5;
+            skipSpaces();
+            msgid = parseString();
+         } else if (po.substr(i, 6) === "msgstr") {
+            if (po[i+6].trim() && po[i+6] !== '"') {
+               skipLine(); // don't understand this line
+               continue;
+            }
+            i+=6;
+            msgstr = parseString();
+            fs.writeSync(jsFile, '_("' + lang + '","' + msgid.replace(/\n/g,"") + '","' + msgstr.replace(/\n/g,"") + '");');
+         }
+         skipLine();
+      }
+   }
+
+   fs.writeSync(jsFile, "if(" + translationFunction + ".applyL10n){" + translationFunction + ".applyL10n();}})();");
+   fs.close(jsFile, function (e) {
+       if (e) {
+        console.error(e);
+       }
+   });
+}
diff --git a/server-data/l10n/po/br/alert.po b/server-data/l10n/po/br/alert.po
new file mode 100644 (file)
index 0000000..6aec7b2
--- /dev/null
@@ -0,0 +1,12 @@
+# TranslationFunction myAlert
+msgid "OK"
+msgstr "Mat eo"
+
+msgid "Yes"
+msgstr "Ya"
+
+msgid "No"
+msgstr "Ket"
+
+msgid "Cancel"
+msgstr "Nullañ"
diff --git a/server-data/l10n/po/br/trivabble.po b/server-data/l10n/po/br/trivabble.po
new file mode 100644 (file)
index 0000000..815ea08
--- /dev/null
@@ -0,0 +1,204 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.8.7\n"
+"Last-Translator: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"Language: br\n"
+
+# TranslationFunction trivabble
+msgid "Your game is not over. Are you sure you want to leave now?"
+msgstr "N'eo ket echu ho c'hoariadenn. Sur oc'h e fell deoc'h kuitaat bremañ?"
+
+msgid ""
+"You are about to leave the current game. To recover it, please note its "
+"number: {0}"
+msgstr ""
+"Emaoc'h o vont da guitaat ar c'hoariadenn. Evit hec'h adkavout e c'hallit "
+"notenniñ an niverenn da heul: {0}"
+
+msgid ""
+"Sorry, a problem just happened. The page must be reloaded. If the problem is "
+"not too serious, you should be able to keep playing normally. Otherwise, "
+"contact the person who is able to fix the problem. Click on “Yes” to reload "
+"the page."
+msgstr ""
+"Digarezit, ur gudenn a zo degouezhet. Ret eo adkargañ ar bajenn. Ma n'eo ket "
+"re bouezus ar gudenn e o tu deoc'h kenderc'hel da c'hoari. E mod all, kit e "
+"darempred gant drouizig@drouizig.org evit kempenn ar gudenn. Klikit war \"Ya"
+"\" evit adkargañ ar bajenn."
+
+msgid ""
+"To join a game, please give the number which is displayed on your "
+"adversary(ies)' screen.\n"
+"If you do not know it, ask them.\n"
+"\n"
+"Warning: your adversary must not take your number, (s)he must keep his/her "
+"own. If you whish to recover your current game, please not the following "
+"number: {0}."
+msgstr ""
+"Evit kevreañ d'ur c'hoariadenn, roit an niverenn diskouezet war skramm ho "
+"enebour(ien).\n"
+"Ma n'anavezit ket anezhañ, goulennit ganto.\n"
+"\n"
+"Diwallit: ne rank ket hoc'h enebour kemer ho niverenn, ret eo dezhañ pe "
+"dezhi derc'hel (h)e hini. Ma fell deoc'h adkavout ho c'hoari bremanel, "
+"notennit an niverenn da heul: {0}."
+
+msgid ""
+"It seems your did not give a correct number, or you clicked on “Cancel”. As "
+"a result, the current game continues, if any. To join a game, click on “Join "
+"a game” again."
+msgstr ""
+"War a seblant n'ho peus ket roet un niverenn dalvoudek, p ekliket ho peus "
+"war \"Nullañ\". Kendalc'het e vo gant ar c'hoari bremanel. Evit kevreañ d'ur "
+"c'hoari, klikit war \"Kevreañ d'ur c'hoari\" adarre."
+
+msgid ""
+"To change your name, enter a new one. You can keep using your current name "
+"by cancelling. Please note that if you change your name and you have games "
+"in progress, you will not be able to keep playing them anymore unless you "
+"get back to your current name."
+msgstr ""
+"Evit kemmañ hoc'h anv, enankit unan nevez. Gallout a rit derc'hel hoc'h hini "
+"bremanel en ur glikañ war \"Nullañ\". Diwallit: ma kemmit hoc'h anv en ur "
+"c'hoari, ne vo ket tu deoc'h kenderc'hel da c'hoari estreget mar adlakit "
+"hoc'h anv bremanel en-dro."
+
+msgid "You cannot take another tile: your rack is full."
+msgstr "N'hallit ket kement ur pezh all. Leun eo ho touger-pezhioù."
+
+msgid ""
+"Are you sure you want to put all the tiles back in the bag (in order to play "
+"another game)?"
+msgstr ""
+"Sur oc'h e fell deoc'h adlakaat an holl bezhioù er sac'h (evit c'hoari en-"
+"dro)?"
+
+msgid "Are you sure you want to put all your tiles back in the bag?"
+msgstr "Sur oc'h e fell deoc'h adlakaat holl ho pezhio er sac'h?"
+
+msgid ""
+"It seems your did not give your name. You need to do it for the game to run "
+"properly."
+msgstr ""
+"War a-seblant n'ho peus ket roet hoc'h anv. et eo deoc'h en ober evit "
+"gellout c'hoari."
+
+msgid ""
+"Hello! To begin, enter your name. Your adversaries will see this name when "
+"you play with them."
+msgstr ""
+"Demat! Evit kregiñ da c'hoari, enankit hoc'h anv. Hoc'h enebourien a welo an "
+"anv pa viot o c'hoari gante."
+
+msgid ""
+"Double\n"
+"Letter"
+msgstr ""
+"Lizherenn\n"
+"Zoubl"
+
+msgid ""
+"Double\n"
+"Word"
+msgstr ""
+"Ger\n"
+"Doubl"
+
+msgid ""
+"Triple\n"
+"Letter"
+msgstr ""
+"Lizherenn\n"
+"Dripl"
+
+msgid ""
+"Triple\n"
+"Word"
+msgstr ""
+"Ger\n"
+"Tripl"
+
+msgid "Join adversaries"
+msgstr "Kevreañ gant enebourien"
+
+msgid "You are:"
+msgstr "Hoc'h anv:"
+
+msgid "Change"
+msgstr "Kemmañ"
+
+msgid "(name to give)"
+msgstr "(anv da reiñ)"
+
+msgid ""
+"Put back all the tiles\n"
+"in the bag"
+msgstr "Adlakaat an holl pezhio er sac'h"
+
+msgid "Number of your game:"
+msgstr "Niverenn ho c'hoari:"
+
+msgid "(pending)"
+msgstr "(o c'hortoz)"
+
+msgid "Write a message to your adversaries here"
+msgstr "Skrivit kemennadenno d'hoc'h enebourien aze"
+
+msgid "Send message"
+msgstr "Kas ar gemennadenn"
+
+msgid "Sound of the tiles"
+msgstr "Son ar pezhioù"
+
+msgid "Sound of messages"
+msgstr "Son ar c'hemennadenoù"
+
+msgid "Number of tiles in the bag:"
+msgstr "Niver a bezhioù er sac'h:"
+
+msgid "Click on it to take one."
+msgstr "Klikit warnañ evit kemer unan"
+
+msgid "Put back all the tiles in the bag"
+msgstr "Adlakaat an holl pezhioù er sac'h"
+
+msgid "Participant"
+msgstr "C'hoarier"
+
+msgid "Rack"
+msgstr "Douger-pezhioù"
+
+msgid "Score"
+msgstr "Poentoù"
+
+msgid "Waiting for other participants…"
+msgstr "O c'hortoz c'hoarien all..."
+
+msgid ""
+"Click on someone's score\n"
+"to change it."
+msgstr ""
+"Klikit war poentoù unan bennak\n"
+"evit kemmañ."
+
+#. The part of the sentence before and after line break should not
+#. be longer than the original message.
+#. It is okay to put line breaks anywhere to split the sentence so it is not
+#. wider than the original sentence.
+msgid ""
+"Put back all the tiles of\n"
+"your rack in the bag"
+msgstr ""
+"Adlakaat holl pezhioù ho\n"
+"touger-pezhoù er sac'h"
+
+msgid  "Interface language:"
+msgstr "Yezh ar c'hetal:"
diff --git a/server-data/l10n/po/fr/alert.po b/server-data/l10n/po/fr/alert.po
new file mode 100644 (file)
index 0000000..312989d
--- /dev/null
@@ -0,0 +1,11 @@
+msgid  "OK"\r
+msgstr "OK"\r
+\r
+msgid  "Yes"\r
+msgstr "Oui"\r
+\r
+msgid  "No"\r
+msgstr "Non"\r
+\r
+msgid  "Cancel"\r
+msgstr "Annuler"\r
diff --git a/server-data/l10n/po/fr/trivabble.po b/server-data/l10n/po/fr/trivabble.po
new file mode 100644 (file)
index 0000000..3132af1
--- /dev/null
@@ -0,0 +1,152 @@
+msgid  "Your game is not over. Are you sure you want to leave now?"
+msgstr "Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?"
+
+msgid  "You are about to leave the current game. To recover it, please note its number: {0}"
+msgstr "Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}"
+
+msgid  "Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."
+msgstr "Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page."
+
+msgid  "Enter the new score of {0}:"
+"Saississez le nouveau score de {0} :"
+
+msgid  "To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}."
+msgstr "Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}."
+
+msgid  "It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."
+msgstr "Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie »."
+
+msgid  "To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."
+msgstr "Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom."
+
+msgid  "You cannot take another tile: your rack is full."
+msgstr "Vous ne pouvez pas tirer de pièce : votre chevalet est complet."
+
+msgid  "Are you sure you want to put all the tiles back in the bag (in order to play another game)?"
+msgstr "Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?"
+
+msgid  "Are you sure you want to put all your tiles back in the bag?"
+msgstr "Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?"
+
+msgid  "It seems your did not give your name. You need to do it for the game to run properly."
+msgstr "Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu."
+
+msgid  "Hello! To begin, enter your name. Your adversaries will see this name when you play with them."
+msgstr "Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux."
+
+msgid  "Double\nLetter"
+msgstr "Lettre\nDouble"
+
+msgid  "Double\nWord"
+msgstr "Mot\nDouble"
+
+msgid  "Triple\nLetter"
+msgstr "Lettre\nTriple"
+
+msgid  "Triple\nWord"
+msgstr "Mot\nTriple"
+
+msgid "Join adversaries"
+msgstr "Rejoindre des adversaires"
+
+msgid  "You are:"
+msgstr "Vous êtes :"
+
+msgid  "Change"
+msgstr "Changer"
+
+msgid  "(name to give)"
+msgstr "(nom à renseigner)"
+
+msgid  "Put back all the tiles\nin the bag"
+msgstr "Remettre toutes les pièces\ndans le sac"
+
+msgid  "Number of your game:"
+msgstr "Numéro de votre partie :"
+
+msgid  "(pending)"
+msgstr "(en attente)"
+
+msgid  "Write a message to your adversaries here"
+msgstr "Écrivez un message à vos adversaires ici"
+
+msgid  "Send message"
+msgstr  "Envoyer le message"
+
+msgid  "Sound of the tiles"
+msgstr "Bruit des pièces"
+
+msgid  "Sound of messages"
+msgstr "Bruit des messages"
+
+msgid  "Number of tiles in the bag:"
+msgstr "Nombre de pièces dans le sac :"
+
+msgid  "Click on it to take one."
+msgstr "Cliquez dessus pour en prendre une."
+
+msgid  "Put back all the tiles in the bag"
+msgstr "Ranger toutes les pièces dans le sac"
+
+msgid  "Participant"
+msgstr "Participant·e"
+
+msgid  "Rack"
+msgstr "Chevalet"
+
+msgid  "Score"
+msgstr "Score"
+
+msgid  "Waiting for other participants…"
+msgstr "En attente des autres participants…"
+
+msgid  "Click on someone's score\nto change it."
+msgstr "Cliquez sur le score de quelqu'un\npour le changer."
+
+msgid  "Put back all the tiles of\nyour rack in the bag"
+msgstr "Remettre toutes les pièces du\nchevalet dans le sac"
+
+msgid  "Interface language:"
+msgstr "Langue de l'interface :"
+
+msgid "Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"
+msgstr "Désolé pour le dérangement, mais nous avons besoin de recharger le jeu pour que vous poussiez continuer à jouer. Vous ne perdrez rien sauf les messages dans le chat. Êtes-vous prêt·e ?"
+
+msgid "To continue playing, click here"
+msgstr "Pour continuer à jouer, cliquez ici"
+
+msgid "{0}: "
+msgstr "{0} : "
+
+msgid "Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."
+msgstr "Oups, il y a un problème. Nous essayons de corriger cela le plus vite possible. Merci de bien vouloir patienter quelques secondes. Si cela persiste, n’hésitez pas à contacter la personne qui peut s’occuper de ce problème"
+
+msgid "The problem is solved, sorry for the inconvenience!"
+msgstr "Le problème est résolu. Désolé pour la gêne occasionnée !"
+
+msgid "It seems your browser went offline. Please reconnect to continue playing."
+msgstr "On dirait que votre navigateur est hors ligne. Merci de bien vouloir vous reconnecter pour continuer à jouer."
+
+msgid  "Turn"
+msgstr "Tour"
+
+msgid  "Who's turn? Click on the Turn button!"
+msgstr "À qui le tour ? Cliquez sur le bouton Tour !"
+
+msgid  "Show my rack to other players"
+msgstr "Montrer mon jeu aux autres"
+
+msgid  "Are you sure you want to show your rack to everybody?"
+msgstr "Êtes-vous sûr·e de vouloir montrer votre jeu à tout le monde ?"
+
+msgid  "jocker"
+msgstr "jocker"
+
+msgid  "Here is my rack:"
+msgstr "Voici mon jeu :"
+
+msgid  "{0} shows their rack:"
+msgstr "{0} montre son jeu :"
+
+msgid  "You cannot take another tile: the bag is empty."
+msgstr  "Vous ne pouvez pas tirer de pièce : le sac est vide."
diff --git a/server-data/l10n/pot/alert.pot b/server-data/l10n/pot/alert.pot
new file mode 100644 (file)
index 0000000..793ed59
--- /dev/null
@@ -0,0 +1,13 @@
+#TranslationFunction myAlert
+
+msgid  "OK"
+msgstr ""
+
+msgid  "Yes"
+msgstr ""
+
+msgid  "No"
+msgstr ""
+
+msgid  "Cancel"
+msgstr ""
diff --git a/server-data/l10n/pot/trivabble.pot b/server-data/l10n/pot/trivabble.pot
new file mode 100644 (file)
index 0000000..db631f4
--- /dev/null
@@ -0,0 +1,155 @@
+#TranslationFunction trivabble
+
+msgid  "Your game is not over. Are you sure you want to leave now?"
+msgstr ""
+
+msgid  "You are about to leave the current game. To recover it, please note its number: {0}"
+msgstr ""
+
+msgid  "Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."
+msgstr ""
+
+msgid  "Enter the new score of {0}:"
+"Saississez le nouveau score de {0} :"
+
+msgid  "To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}."
+msgstr ""
+
+msgid  "It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."
+msgstr ""
+
+msgid  "To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."
+msgstr ""
+
+msgid  "You cannot take another tile: your rack is full."
+msgstr ""
+
+msgid  "Are you sure you want to put all the tiles back in the bag (in order to play another game)?"
+msgstr ""
+
+msgid  "Are you sure you want to put all your tiles back in the bag?"
+msgstr ""
+
+msgid  "It seems your did not give your name. You need to do it for the game to run properly."
+msgstr ""
+
+msgid  "Hello! To begin, enter your name. Your adversaries will see this name when you play with them."
+msgstr ""
+
+msgid  "Double\nLetter"
+msgstr ""
+
+msgid  "Double\nWord"
+msgstr ""
+
+msgid  "Triple\nLetter"
+msgstr ""
+
+msgid  "Triple\nWord"
+msgstr ""
+
+msgid "Join adversaries"
+msgstr ""
+
+msgid  "You are:"
+msgstr ""
+
+msgid  "Change"
+msgstr ""
+
+msgid  "(name to give)"
+msgstr ""
+
+msgid  "Put back all the tiles\nin the bag"
+msgstr ""
+
+msgid  "Number of your game:"
+msgstr ""
+
+msgid  "(pending)"
+msgstr ""
+
+msgid  "Write a message to your adversaries here"
+msgstr ""
+
+msgid  "Send message"
+msgstr ""
+
+msgid  "Sound of the tiles"
+msgstr ""
+
+msgid  "Sound of messages"
+msgstr ""
+
+msgid  "Number of tiles in the bag:"
+msgstr ""
+
+msgid  "Click on it to take one."
+msgstr ""
+
+msgid  "Put back all the tiles in the bag"
+msgstr ""
+
+msgid  "Participant"
+msgstr ""
+
+msgid  "Rack"
+msgstr ""
+
+msgid  "Score"
+msgstr ""
+
+msgid  "Waiting for other participants…"
+msgstr ""
+
+msgid  "Click on someone's score\nto change it."
+msgstr ""
+
+#. The part of the sentence before and after line break should not
+#. be longer than the original message.
+#. It is okay to put line breaks anywhere to split the sentence so it is not
+#. wider than the original sentence.
+msgid  "Put back all the tiles of\nyour rack in the bag"
+msgstr ""
+
+msgid  "Interface language:"
+msgstr ""
+
+msgid "Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"
+msgstr ""
+
+msgid "To continue playing, click here"
+msgstr ""
+
+msgid "{0}: "
+msgstr ""
+
+msgid "Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."
+msgstr ""
+
+msgid "The problem is solved, sorry for the inconvenience!"
+msgstr ""
+
+msgid  "It seems your browser went offline. Please reconnect to continue playing."
+msgstr ""
+
+msgid  "Turn"
+msgstr ""
+
+msgid  "Show my rack to other players"
+msgstr ""
+
+msgid  "Are you sure you want to show your rack to everybody?"
+msgstr ""
+
+msgid  "jocker"
+msgstr ""
+
+msgid  "Here is my rack:"
+msgstr ""
+
+msgid  "{0} shows their rack:"
+msgstr ""
+
+msgid  "You cannot take another tile: the bag is empty."
+msgstr ""
diff --git a/server-data/notification.mp3 b/server-data/notification.mp3
new file mode 100644 (file)
index 0000000..eeab9fb
Binary files /dev/null and b/server-data/notification.mp3 differ
diff --git a/server-data/notification.ogg b/server-data/notification.ogg
new file mode 100644 (file)
index 0000000..fae9e26
Binary files /dev/null and b/server-data/notification.ogg differ
diff --git a/server-data/receive.mp3 b/server-data/receive.mp3
new file mode 100644 (file)
index 0000000..6f6ff2b
Binary files /dev/null and b/server-data/receive.mp3 differ
diff --git a/server-data/receive.ogg b/server-data/receive.ogg
new file mode 100644 (file)
index 0000000..17f3f68
Binary files /dev/null and b/server-data/receive.ogg differ
diff --git a/server-data/touch2click.js b/server-data/touch2click.js
new file mode 100644 (file)
index 0000000..56e05aa
--- /dev/null
@@ -0,0 +1,210 @@
+//thx http://stackoverflow.com/questions/1517924/javascript-mapping-touch-events-to-mouse-events#1781750
+
+function touchHandler(event) {
+    var nn = event.target.nodeName.toLowerCase();
+
+    if (nn === "input" || nn === "select" || nn === "button" || nn === "textarea") {
+        return;
+    }
+
+    var touches = event.changedTouches,
+        first   = touches ? touches[0] : event,
+        type    = "";
+
+    switch (event.type) {
+    case "touchstart": type = "mousedown"; break;
+    case "touchmove":  type = "mousemove"; break;
+    case "touchend":   type = "mouseup";   break;
+    case "tap":        type = "click";     break;
+    case "dbltap":     type = "dblclick";  break;
+    default: return;
+    }
+
+//initMouseEvent(type, canBubble, cancelable, view, clickCount,
+//              screenX, screenY, clientX, clientY, ctrlKey,
+//              altKey, shiftKey, metaKey, button, relatedTarget);
+
+     var simulatedEvent = document.createEvent("MouseEvent");
+     simulatedEvent.initMouseEvent(type, true, true, window, 1,
+            first.screenX, first.screenY,
+            first.clientX, first.clientY, false,
+            false, false, false, 0/*left*/, null);
+     first.target.dispatchEvent(simulatedEvent);
+     event.preventDefault();
+}
+
+document.addEventListener('touchend', (function(speed, distance) {
+/*
+ * Copyright (c)2012 Stephen M. McKamey.
+ * Licensed under The MIT License.
+ * src: https://raw.github.com/mckamey/doubleTap.js/master/doubleTap.js
+ */
+
+    "use strict";
+
+    // default dblclick speed to half sec (default for Windows & Mac OS X)
+    speed = Math.abs(+speed) || 500;//ms
+    // default dblclick distance to within 40x40 pixel area
+    distance = Math.abs(+distance) || 40;//px
+
+    // Date.now() polyfill
+    var now = Date.now || function() {
+        return +new Date();
+    };
+
+    var cancelEvent = function(e) {
+        e = (e || window.event);
+
+        if (e) {
+            if (e.preventDefault) {
+                e.stopPropagation();
+                e.preventDefault();
+            } else {
+                try {
+                    e.cancelBubble = true;
+                    e.returnValue = false;
+                } catch (ex) {
+                    // IE6
+                }
+            }
+        }
+        return false;
+    };
+
+    var taps = 0,
+        last = 0,
+        // NaN will always test false
+        x = NaN,
+        y = NaN;
+
+    return function (e) {
+        e = (e || window.event);
+
+        var time   = now(),
+            touch  = e.changedTouches ? e.changedTouches[0] : e,
+            nextX  = +touch.clientX,
+            nextY  = +touch.clientY,
+            target = e.target || e.srcElement,
+            e2,
+            parent;
+
+        if ((last + speed) > time &&
+            Math.abs(nextX - x) < distance &&
+            Math.abs(nextY - y) < distance) {
+            // continue series
+            taps++;
+
+        } else {
+            // reset series if too slow or moved
+            taps = 1;
+        }
+
+        // update starting stats
+        last = time;
+        x = nextX;
+        y = nextY;
+
+        // fire tap event
+        if (document.createEvent) {
+            e2 = document.createEvent('MouseEvents');
+            e2.initMouseEvent(
+                'tap',
+                true,             // click bubbles
+                true,             // click cancelable
+                e.view,              // copy view
+                taps,             // click count
+                touch.screenX,     // copy coordinates
+                touch.screenY,
+                touch.clientX,
+                touch.clientY,
+                e.ctrlKey,          // copy key modifiers
+                e.altKey,
+                e.shiftKey,
+                e.metaKey,
+                e.button,            // copy button 0: left, 1: middle, 2: right
+                e.relatedTarget); // copy relatedTarget
+
+            if (!target.dispatchEvent(e2)) {
+                // pass on cancel
+                cancelEvent(e);
+            }
+
+        } else {
+            e.detail = taps;
+
+            // manually bubble up
+            parent = target;
+            while (parent && !parent.tap && !parent.ontap) {
+                parent = parent.parentNode || parent.parent;
+            }
+            if (parent && parent.tap) {
+                // DOM Level 0
+                parent.tap(e);
+
+            } else if (parent && parent.ontap) {
+                // DOM Level 0, IE
+                parent.ontap(e);
+
+            } else if (typeof jQuery !== 'undefined') {
+                // cop out and patch IE6-8 with jQuery
+                jQuery(this).trigger('tap', e);
+            }
+        }
+
+        if (taps === 2) {
+            // fire dbltap event only for 2nd click
+            if (document.createEvent) {
+                e2 = document.createEvent('MouseEvents');
+                e2.initMouseEvent(
+                    'dbltap',
+                    true,             // dblclick bubbles
+                    true,             // dblclick cancelable
+                    e.view,              // copy view
+                    taps,             // click count
+                    touch.screenX,     // copy coordinates
+                    touch.screenY,
+                    touch.clientX,
+                    touch.clientY,
+                    e.ctrlKey,          // copy key modifiers
+                    e.altKey,
+                    e.shiftKey,
+                    e.metaKey,
+                    e.button,            // copy button 0: left, 1: middle, 2: right
+                    e.relatedTarget); // copy relatedTarget
+
+                if (!target.dispatchEvent(e2)) {
+                    // pass on cancel
+                    cancelEvent(e);
+                }
+
+            } else {
+                e.detail = taps;
+
+                // manually bubble up
+                parent = target;
+                while (parent && !parent.dbltap && !parent.ondbltap) {
+                    parent = parent.parentNode || parent.parent;
+                }
+                if (parent && parent.dbltap) {
+                    // DOM Level 0
+                    parent.dbltap(e);
+
+                } else if (parent && parent.ondbltap) {
+                    // DOM Level 0, IE
+                    parent.ondbltap(e);
+
+                } else if (typeof jQuery !== 'undefined') {
+                    // cop out and patch IE6-8 with jQuery
+                    jQuery(this).trigger('dbltap', e);
+                }
+            }
+        }
+    };
+}()), false);
+
+document.addEventListener("tap", touchHandler, true);
+// document.addEventListener("dbltap", touchHandler, true);
+document.addEventListener("touchstart", touchHandler, true);
+document.addEventListener("touchmove", touchHandler, true);
+document.addEventListener("touchend", touchHandler, true);
+document.addEventListener("touchcancel", touchHandler, true);
diff --git a/server-data/trivabble.css b/server-data/trivabble.css
new file mode 100644 (file)
index 0000000..80cf566
--- /dev/null
@@ -0,0 +1,447 @@
+#board {
+    border-collapse:collapse;
+}
+
+a {
+    text-decoration:none;
+    color:blue;
+}
+
+html {
+    font-family: "Droid Sans", sans-serif;
+}
+
+html, #board, [draggable], .tile {
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+
+  -moz-user-select: none;
+  -khtml-user-select: none;
+  -webkit-user-select: none;
+  user-select: none;
+}
+
+#chat-messages, #number, #name {
+    -webkit-touch-callout: default;
+    -webkit-user-select: element;
+    -khtml-user-select: element;
+    -moz-user-select: element;
+    -ms-user-select: element;
+    user-select: element;
+
+  -moz-user-select: element;
+  -khtml-user-select: element;
+  -webkit-user-select: element;
+  user-select: element;
+}
+
+#chat-messages .error {
+    color: #F66;
+}
+
+#chat-messages .ok {
+    color: green;
+}
+
+#clear-game, #help-scores, #clear-rack, #help-turn {
+    white-space:pre-wrap;
+}
+
+[draggable] {
+  /* Required to make elements draggable in old WebKit */
+  -khtml-user-drag: element;
+  -webkit-user-drag: element;
+}
+
+#help-clear, #help-bag, #help-scores, #help-turn {
+    font-size:small
+}
+
+#board td {
+    text-transform:uppercase;
+    border:1px solid #220;
+    background:#FFC;
+    vertical-align:middle;
+    text-align:center;
+}
+
+#board td .tile-placeholder {
+    position: relative;
+    height: 100%;
+    width: 100%;
+    top: 0px;
+    left: 0px;
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+}
+
+#board th {
+    font-size:small;
+    color:gray;
+    font-weight:normal
+}
+
+.corner {
+    visibility:hidden;
+    border:none !important
+}
+
+.special-cell-tripleWord {
+    background:#F77 !important;
+    box-shadow: 0 0 1px #F77;
+}
+
+.special-cell-tripleLetter {
+    background:#77F !important;
+    box-shadow: 0 0 1px #77F;
+}
+
+.special-cell-doubleLetter {
+    background:#7CF !important;
+    box-shadow: 0 0 1px #7CF;
+}
+
+.special-cell-doubleWord {
+    background:#FBB !important;
+    box-shadow: 0 0 1px #FBB;
+}
+
+.special-cell-label {
+    display:inline-block;
+    white-space:pre-wrap;
+    font-size:7px;
+}
+
+#center-cell .special-cell-label {
+    font-weight:bold;
+    color:black;
+    font-size:large
+}
+
+body {
+    display:-webkit-flex;
+    display:flex;
+    -webkit-flex-direction:row;
+    flex-direction:row;
+    justify-content:safe center;
+}
+
+@media (max-width: 940px) {
+    body {
+        justify-content:start;
+    }
+}
+
+#menu, #panel {
+    display:-webkit-flex;
+    display:flex;
+    -webkit-flex-direction:column;
+    flex-direction:column
+}
+
+button {
+    margin: 0.25em 0;
+    min-height:3em;
+    background:#AFA;
+    border:1px solid gray;
+}
+
+button:hover {
+    background:#FD7;
+    cursor:pointer
+}
+
+#rack {
+    background:#EEC;
+    font-size:0;
+    border-radius:4px;
+    display:inline-block;
+    padding:2px 5px;
+    white-space:pre;
+    box-shadow:0 0 2px gray
+}
+
+#board td {
+    height:32px;
+    width: 32px;
+}
+
+#rack .tile-placeholder {
+    height:32px;
+    width: 32px;
+}
+
+.tile {
+    background-color: #EE8;
+    transition:background-color 1s, border 1s;
+    display: inline-block;
+    width: 95%;
+    height: 95%;
+    left: 2.5%;
+    top: 2.5%;
+    position: absolute;
+    border: 1px solid #AA5;
+    box-sizing: border-box;
+    text-align: center;
+    font-size:1rem;
+    cursor:pointer
+}
+
+.tile.tile-highlight {
+    background-color: #FFF;
+    font-weight: bold;
+    animation: highlight 4s ease-in;
+    border: 1px solid black;
+}
+
+@keyframes highlight {
+    0% {
+        background:red;
+        color:white;
+    }
+
+    100% {
+        background:white
+    }
+}
+
+.tile-score {
+    font-size: 0.5em;
+    position: absolute;
+    bottom: 1px;
+    right: 2px;
+}
+
+#chat-messages .tile {
+    position:relative;
+    cursor:auto;
+    top:0;
+    left:0;
+    width: 1.5em;
+    height: 1.5em;
+    margin:2px;
+    vertical-align:bottom;
+}
+
+#chat-messages .tile-letter {
+    font-size:0.9em;
+}
+
+#chat-messages .tile-score {
+    font-size: 0.4em;
+}
+
+#chat-messages.blink {
+    animation:chat-blink 1s
+}
+
+#chat-messages .info {
+    color:gray;
+    font-style:italic;
+}
+
+td.blink {
+    animation:td-blink 1s
+}
+
+.current-player {
+    font-weight:bold;
+}
+
+.current-player td:first-child:before {
+    position:absolute;
+    left:-1em;
+    color:red;
+    content: "→";
+}
+
+@keyframes chat-blink {
+    0% {
+        background:rgba(255, 255, 150, 0);
+    }
+
+    50% {
+        background:rgba(255, 255, 150, 1);
+    }
+
+    100% {
+        background:rgba(255, 255, 150, 0);
+    }
+}
+
+
+@keyframes td-blink {
+    0% {
+        background:rgba(100, 200, 255, 0);
+    }
+
+    50% {
+        background:rgba(100, 200, 255, 1);
+    }
+
+    100% {
+        background:rgba(100, 200, 255, 0);
+    }
+}
+
+
+
+.tile-target {
+    background:rgba(255,255,255, 0.9);
+/* } */
+
+/* #bag.tile-target, td .tile-target { */
+    outline: 1px solid #F33
+}
+
+#rack .tile-placeholder {
+    position:relative;
+    margin-left:1px;
+    margin-right:1px;
+    display:inline-block;
+    vertical-align:middle;
+    background:#CCA;
+}
+
+#panel {
+    -webkit-justify-content:center;
+    justify-content:center;
+    text-align:center
+}
+
+#bag, #rack-outer {
+    -webkit-align-self:center;
+    align-self:center;
+    text-align:center;
+}
+
+#p-name {
+    text-align:center;
+    font-size:small
+}
+
+.minibutton {
+    padding:0;
+    min-height:0;
+    font-size:small
+}
+
+.msg-sender {
+    color:blue;
+}
+
+#chat-messages {
+    overflow:auto
+}
+
+#bag {
+    width:165px;
+    height:160px;
+    background:url(bag.svg) no-repeat;
+    background-size:100% 100%
+}
+
+#bag:hover {
+    cursor:pointer;
+    opacity:0.8
+}
+
+#chat-messages {
+    border:1px solid silver;
+    box-sizing:border-box;
+    width: 100%;
+    margin-bottom:1px;
+    height:15em;
+}
+
+#chat-ta {
+    width:100%;
+    box-sizing:border-box;
+    height:3em
+}
+
+#participants {
+    font-size:small;
+    border-collapse:collapse;
+    margin:0 auto 1ex auto;
+}
+
+#participants th, #participants td {
+    border:1px solid gray;
+    padding:0.33ex 1ex
+}
+
+#participants .score-cell {
+    min-width:4em;
+}
+
+#participants .score-cell:hover, .turn-cell button:hover {
+    cursor:pointer;
+    background:#FFA
+}
+
+#participants th {
+    font-weight:normal;
+    background:#EEE;
+    font-size:smaller
+}
+
+#participants-placeholder {
+    font-style:italic;
+}
+
+.turn-cell button {
+    padding:0 2px;
+    background:white;
+    min-height:1em;
+    height:1em;
+    margin:0;
+    display:flex;
+}
+
+.turn-cell img {
+    width:1em;
+    vertical-align: middle;
+}
+
+@media screen and (max-height:650px) {
+    #board td , #rack .tile-placeholder {
+        height:28px;
+        width: 28px;
+    }
+}
+@media screen and (max-height:600px) {
+    #board td , #rack .tile-placeholder {
+        height:24px;
+        width: 24px;
+    }
+}
+
+@media screen and (max-height:550px) {
+    #chat-messages {
+        height:7em;
+    }
+}
+
+#prefs-sound {
+    margin:1ex 0 0 0;
+}
+
+#prefs-sound p {
+    margin:0;
+    padding:0;
+    font-size:small
+}
+
+#lang-selection {
+    font-size:small;
+}
+
+#prefs-sound input, #prefs-sound label {
+    vertical-align:middle
+}
diff --git a/server-data/trivabble.js b/server-data/trivabble.js
new file mode 100644 (file)
index 0000000..3e5268b
--- /dev/null
@@ -0,0 +1,1611 @@
+/**
+ * Copyright (C) 2016-2020 Raphaël Jakse <raphael.trivabble@jakse.fr>
+ *
+ * @licstart
+ * This file is part of Trivabble.
+ *
+ * Trivabble is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License (GNU AGPL)
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * Trivabble is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Trivabble.  If not, see <http://www.gnu.org/licenses/>.
+ * @licend
+ *
+ * @source: https://trivabble.1s.fr/
+ * @source: https://gitlab.com/raphj/trivabble/
+ */
+
+/*global libD, myConfirm, myAlert, myPrompt*/
+
+(function () {
+    "use strict";
+
+    const VERSION = 202004262313;
+
+    const Conf = window.TrivabbleConf || {};
+
+    function setConf(parameterName, defaultValue) {
+        if (typeof Conf[parameterName] === "undefined") {
+            Conf[parameterName] = defaultValue;
+        }
+    }
+
+    setConf("POLLING_DELAY",        2000);
+    setConf("ENABLE_WEBSOCKETS",    true);
+    setConf("ENABLE_EVENT_SOURCE",  true);
+    setConf("MAX_WEBSOCKET_ERRORS", 1);
+    setConf("APP_PATH",             "");
+
+    const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {
+        return s;
+    };
+
+    const trivabble = window.trivabble = {l10n: _};
+
+    function format(s, v) {
+        return s.replace("{0}", v);
+    }
+
+    let board;
+    let rack;
+    const boardCells = [];
+    let scoreOf;
+    let bag;
+
+    const playerLetters = [];
+    let currentPlayer = "";
+
+    let audioNotification;
+    let audioChat;
+    let chatMessages;
+    let chatTextarea;
+    let helpBag;
+    let helpClear;
+
+    let tablePlayers = {};
+    let participantPlaceholder;
+    let participants;
+    let name;
+
+    let blockMove = 0;
+    let remainingLetters = 0;
+    let needsRestart = false;
+    const gameInProgress = false;
+
+    let eventSource = null;
+    let webSocket = null;
+
+    let connectionLostMessage = null;
+    let boundEventShowConnectionLost = null;
+    let retryPollingTimeout = 0;
+    let pollingServer = false;
+    let connectionStopped = false;
+
+    let currentMessageId = 1;
+    const waitingMsgs = [];
+
+    let serverVersion = 0;
+
+    // HTTP path url prefix for any socket (xhr,ess or ws) from browser to server access
+    function getApiEntryPoint() {
+        return Conf.APP_PATH + "/:trivabble";
+    }
+
+    function mouseDown(ele, fun, stop) {
+        const meth = stop ? "removeEventListener" : "addEventListener";
+        ele[meth]("mousedown", fun, false);
+    }
+
+    function mouseUp(ele, fun, stop) {
+        const meth = stop ? "removeEventListener" : "addEventListener";
+        ele[meth]("mouseup", fun, false);
+    }
+
+    function mouseMove(ele, fun, stop) {
+        const meth = stop ? "removeEventListener" : "addEventListener";
+        ele[meth]("mousemove", fun, false);
+    }
+
+    function setRack(rack) {
+        for (let i = 0; i < 7; i++) {
+            setTileParent(playerLetters[i], rack[i] || "");
+        }
+    }
+
+    function getFreeRackSpaceIndex() {
+        for (let i = 0; i < 7; i++) {
+            if (!playerLetters[i].getElementsByClassName("tile")[0]) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    function showConnectionLost(msg) {
+        unbindEventsShowConnectionLost();
+
+        if (!connectionLostMessage) {
+            connectionLostMessage = chatMessage("", msg || _("Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."));
+            connectionLostMessage.classList.add("error");
+        }
+    }
+
+    function unbindEventsShowConnectionLost() {
+        if (boundEventShowConnectionLost) {
+            document.body.removeEventListener("mousemove", boundEventShowConnectionLost);
+            document.body.removeEventListener("mousedown", boundEventShowConnectionLost);
+            document.body.removeEventListener("touchstart", boundEventShowConnectionLost);
+            boundEventShowConnectionLost = null;
+        }
+    }
+
+    function removeElem(elem) {
+        elem.parentNode.removeChild(elem);
+    }
+
+    function connectionReady() {
+        pollingReady = true;
+        retriedImmediately = false;
+
+        unbindEventsShowConnectionLost();
+
+        if (connectionLostMessage) {
+            connectionLostMessage.querySelector(".msg-content").textContent = _("The problem is solved, sorry for the inconvenience!");
+            connectionLostMessage.classList.remove("error");
+            connectionLostMessage.classList.add("ok");
+
+            const removeMessage = removeElem.bind(null, connectionLostMessage);
+            connectionLostMessage.addEventListener("click", removeMessage);
+            connectionLostMessage.addEventListener("touch", removeMessage);
+
+            connectionLostMessage = null;
+        }
+    }
+
+    let tileInitCoords      = null;
+    let tileInitMouseCoords = null;
+    let tileDest            = null;
+    let tileInitDest        = null;
+    let movingTile          = null;
+    let rackBCR             = null;
+    let boardBCR            = null;
+    let bagBCR              = null;
+    let moveCMD             = null;
+
+    function getLetter(l) {
+        const tile = l.getElementsByClassName("tile")[0];
+        if (!tile) {
+            return "";
+        }
+
+        return tile.getElementsByClassName("tile-letter")[0].textContent;
+    }
+
+    function isParentOf(p, elem) {
+        while (elem) {
+            if (elem === p) {
+                return true;
+            }
+            elem = elem.parentNode;
+        }
+
+        return false;
+    }
+
+    function dragTileEnd() {
+        movingTile.style.left   = "";
+        movingTile.style.top    = "";
+        movingTile.style.width  = "";
+        movingTile.style.height = "";
+
+        mouseUp(document,   dragTileEnd,  true);
+        mouseMove(document, dragTileMove, true);
+
+        if (tileDest === bag) {
+            moveCMD.to = "bag";
+            moveCMD.indexTo = -1;
+            movingTile.parentNode.removeChild(movingTile);
+        } else if (tileDest) {
+            tileDest.appendChild(movingTile);
+
+            if (isParentOf(board, tileDest)) {
+                for (const tile of [].slice.call(board.getElementsByClassName("tile-highlight"))) {
+                    tile.classList.remove("tile-highlight");
+                }
+            }
+        } else if (tileInitDest.getElementsByClassName("tile")[0]) {
+            for (let i = 0; i < 7; i++) {
+                if (!playerLetters[i].getElementsByClassName("tile")[0]) {
+                    playerLetters[i].appendChild(movingTile);
+                    break;
+                }
+            }
+        } else {
+            tileInitDest.appendChild(movingTile);
+        }
+
+        if (tileDest) {
+            tileDest.classList.remove("tile-target");
+
+            const moveRack = {
+                cmd: "setRack",
+                rack: playerLetters.map(getLetter)
+            };
+
+            if (moveCMD.to === moveCMD.from) {
+                if (moveCMD.indexTo !== moveCMD.indexFrom) {
+                    sendCmds([
+                        (moveCMD.from === "rack")
+                            ? moveRack
+                            : moveCMD
+                    ]);
+                }
+            } else if (moveCMD.to === "rack") {
+                moveRack.rack[moveCMD.indexTo] = "";
+                sendCmds([moveRack, moveCMD]);
+            } else {
+                sendCmds([moveCMD]);
+            }
+        }
+
+        tileInitMouseCoords = null;
+        tileInitCoords      = null;
+        tileInitDest        = null;
+        tileDest            = null;
+        movingTile          = null;
+        boardBCR            = null;
+        bagBCR              = null;
+        rackBCR             = null;
+        moveCMD             = null;
+    }
+
+    function dragTileMove(e) {
+        let newLeft = (tileInitCoords.left + (e.clientX - tileInitMouseCoords.clientX));
+        let newTop  = (tileInitCoords.top  + (e.clientY - tileInitMouseCoords.clientY));
+
+        movingTile.style.left = (newLeft + window.scrollX) + "px";
+        movingTile.style.top  = (newTop  + window.scrollY) + "px";
+
+        let newDest = null;
+
+        newTop  += tileInitCoords.height / 2;
+        newLeft += tileInitCoords.width / 2;
+
+        if (
+            (newTop  > boardBCR.top  && newTop  < (boardBCR.top + boardBCR.height)) &&
+            (newLeft > boardBCR.left && newLeft < (boardBCR.left + boardBCR.width))
+        ) {
+            const rowIndex = Math.floor(
+                (
+                    (newTop  - boardBCR.top)  / boardBCR.height
+                ) * board.rows.length
+            );
+
+            if (rowIndex > 0 && rowIndex < board.rows.length - 1) {
+                const row = board.rows[rowIndex];
+
+                const colIndex = Math.floor(
+                    (
+                        (newLeft - boardBCR.left) / boardBCR.width
+                    ) * row.cells.length
+                );
+
+                if (colIndex > 0 && colIndex < row.cells.length - 1) {
+                    newDest = row.cells[colIndex].firstChild;
+                }
+            }
+
+            if (newDest && newDest.getElementsByClassName("tile")[0]) {
+                newDest = null;
+            }
+
+            if (newDest) {
+                moveCMD.to = "board";
+                moveCMD.indexTo = boardCells.indexOf(newDest.parentNode);
+            }
+        } else if (
+            (newTop  > rackBCR.top  && newTop  < (rackBCR.top + rackBCR.height)) &&
+            (newLeft > rackBCR.left && newLeft < (rackBCR.left + rackBCR.width))
+        ) {
+            const index = Math.floor(
+                (
+                    (newLeft - rackBCR.left) / rackBCR.width
+                ) * playerLetters.length
+            );
+
+            newDest = playerLetters[index];
+
+            if (newDest && newDest.getElementsByClassName("tile")[0]) {
+                let i = index + 1;
+                let tile;
+
+                while (playerLetters[i]) {
+                    tile = playerLetters[i].getElementsByClassName("tile")[0];
+                    if (!tile) {
+                        let j = i;
+                        while (j > index) {
+                            playerLetters[j].appendChild(
+                                playerLetters[j - 1].getElementsByClassName("tile")[0]
+                            );
+                            j--;
+                        }
+                        break;
+                    }
+                    i++;
+                }
+
+                if (newDest.getElementsByClassName("tile")[0]) {
+                    i = index - 1;
+
+                    while (playerLetters[i]) {
+                        tile = playerLetters[i].getElementsByClassName("tile")[0];
+                        if (!tile) {
+                            let j = i;
+                            while (j < index) {
+                                playerLetters[j].appendChild(
+                                    playerLetters[j + 1].getElementsByClassName("tile")[0]
+                                );
+                                j++;
+                            }
+                            break;
+                        }
+                        i--;
+                    }
+                }
+            }
+
+            if (newDest.getElementsByClassName("tile")[0]) {
+                newDest = null;
+            } else {
+                moveCMD.to = "rack";
+                moveCMD.indexTo = index;
+            }
+        } else if (
+            (newTop  > bagBCR.top  && newTop  < (bagBCR.top + bagBCR.height)) &&
+            (newLeft > bagBCR.left && newLeft < (bagBCR.left + bagBCR.width))
+        ) {
+            newDest = bag;
+        }
+
+        if (newDest !== tileDest) {
+            if (tileDest) {
+                tileDest.classList.remove("tile-target");
+            }
+
+            if (newDest) {
+                newDest.classList.add("tile-target");
+            }
+        }
+
+        tileDest = newDest;
+    }
+
+    function dragTileBegin(e) {
+        preventDefault(e);
+
+        if (blockMove || waitingMsgs.length) {
+            return;
+        }
+
+        if (!pollingReady) {
+            showConnectionLost();
+            return;
+        }
+
+        loadAudio();
+
+        movingTile = e.currentTarget;
+
+        tileInitMouseCoords = e;
+        tileInitCoords      = movingTile.getBoundingClientRect();
+        tileInitDest        = movingTile.parentNode;
+
+        boardBCR = board.getBoundingClientRect();
+        rackBCR  = rack.getBoundingClientRect();
+        bagBCR   = bag.getBoundingClientRect();
+
+        let from;
+        let index;
+
+        let p = movingTile.parentNode;
+        let oldP = movingTile;
+        let oldOldP = null;
+
+        while (p) {
+            if (p === board) {
+                from  = "board";
+                index = boardCells.indexOf(oldOldP);
+
+                break;
+            }
+
+            if (p === rack) {
+                from  = "rack";
+                index = playerLetters.indexOf(oldP);
+
+                break;
+            }
+
+            oldOldP = oldP;
+            oldP = p;
+            p = p.parentNode;
+        }
+
+        moveCMD = {
+            cmd: "moveLetter",
+            from: from,
+            indexFrom: index
+        };
+
+        mouseMove(document, dragTileMove);
+        mouseUp(document,   dragTileEnd);
+
+        movingTile.style.left   = tileInitCoords.left + window.scrollX + "px";
+        movingTile.style.top    = tileInitCoords.top  + window.scrollY + "px";
+        movingTile.style.width  = tileInitCoords.width  + "px";
+        movingTile.style.height = tileInitCoords.height + "px";
+
+        document.body.appendChild(movingTile);
+    }
+
+    function setLetter(tile, letter, highlight) {
+        tile.firstChild.textContent = letter;
+        tile.lastChild.textContent = scoreOf[letter] || "";
+        if (highlight) {
+            tile.classList.add("tile-highlight");
+            if (tilesSound.checked) {
+                audioNotification.play();
+            }
+        } else {
+            tile.classList.remove("tile-highlight");
+        }
+    }
+
+    function preventDefault(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        return false;
+    }
+
+    function makeLetter(letter, highlight, noEvents) {
+        const tile = document.createElement("span");
+        tile.translate = false;
+        tile.className = "tile";
+        tile.appendChild(document.createElement("span"));
+        tile.lastChild.className = "tile-letter";
+        tile.appendChild(document.createElement("span"));
+        tile.lastChild.className = "tile-score";
+
+        if (!noEvents) {
+            mouseDown(tile, dragTileBegin);
+
+            tile.addEventListener("contextmenu", preventDefault);
+            tile.addEventListener("touchstart", preventDefault);
+            tile.addEventListener("touchmove", preventDefault);
+        }
+
+        setLetter(tile, letter, highlight);
+        return tile;
+    }
+
+    function setTileParent(p, letter, highlight) {
+        let tile = p.getElementsByClassName("tile")[0];
+        if (tile) {
+            if (letter) {
+                setLetter(tile, letter, highlight);
+            } else {
+                p.removeChild(tile);
+            }
+        } else if (letter) {
+            tile = makeLetter(letter, highlight);
+            p.appendChild(tile);
+        }
+    }
+
+    function setCell(index, letter, highlight) {
+        setTileParent(boardCells[index].getElementsByClassName("tile-placeholder")[0], letter, highlight);
+    }
+
+    function setBoard(board) {
+        for (let i = 0; i < 15 * 15; i++) {
+            setCell(i, board[i]);
+        }
+    }
+
+    function set(key, value) {
+        switch (key) {
+            case "playerName":
+                name.textContent = localStorage.trivabblePlayerName = value;
+                break;
+            case "gameNumber":
+                document.getElementById("number").textContent = localStorage.trivabbleGameNumber = value;
+                break;
+        }
+    }
+
+    function checkGameInProgress(f) {
+        if (!gameInProgress) {
+            return f();
+        }
+
+        myConfirm(
+            _("Your game is not over. Are you sure you want to leave now?"),
+            function () {
+                myAlert(
+                    format(
+                        _("You are about to leave the current game. To recover it, please note its number: {0}"),
+                        localStorage.trivabbleGameNumber
+                    ),
+                    f
+                );
+            }
+        );
+    }
+
+    let pollingReady = false;
+    let retriedImmediately = false;
+
+    function forceReload(msg) {
+        needsRestart = true;
+        myConfirm(
+            msg,
+            function () {
+                location.reload();
+            }, function () {
+                blockMove = 1000;
+                document.getElementById("panel").appendChild(document.createElement("a"));
+                document.getElementById("panel").lastChild.href = "#";
+                document.getElementById("panel").lastChild.onclick = location.reload.bind(location);
+                document.getElementById("panel").lastChild.textContent = _("To continue playing, click here");
+            }
+        );
+    }
+
+    function fatalError(e) {
+        if (!needsRestart) {
+            // No need to show the user that a problem just happened if a message asking to restart has already been shown.
+            forceReload(_("Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."));
+        }
+
+        throw (e || new Error("interrupting the game because of a fatal error"));
+    }
+
+    function jsonError(json, e) {
+        console.error("An error ocurred while parsing this JSON message:", json);
+        fatalError(e);
+    }
+
+    function setRackCell(index, letter) {
+        setTileParent(playerLetters[index], letter);
+    }
+
+    function blinkTransitionEnd(element) {
+        element.removeEventListener("animationend", element._te, false);
+        element.classList.remove("blink");
+    }
+
+    function blink(element) {
+        element._te = blinkTransitionEnd.bind(null, element);
+        element.addEventListener("animationend", element._te, false);
+        setTimeout(element._te, 5000);
+        element.classList.add("blink");
+    }
+
+    function chatMessage(sender, content) {
+        const msgDom = document.createElement("div");
+
+        msgDom.className = "msg";
+
+        if (sender) {
+            msgDom.appendChild(document.createElement("span"));
+            msgDom.lastChild.className = "msg-sender";
+            msgDom.lastChild.textContent = _("{0}: ").replace("{0}", sender);
+        }
+
+        if (content instanceof Element) {
+            msgDom.appendChild(content);
+        } else {
+            msgDom.appendChild(document.createElement("span"));
+            msgDom.lastChild.className = "msg-content";
+            msgDom.lastChild.textContent = content;
+        }
+
+        chatMessages.appendChild(msgDom);
+
+        chatMessages.scrollTop = chatMessages.scrollHeight;
+
+        if (sender && sender !== localStorage.trivabblePlayerName) {
+            if (msgSound.checked) {
+                audioChat.play();
+            }
+
+            blink(chatMessages);
+        }
+
+        return msgDom;
+    }
+
+    function handleChatMessage(msg) {
+        if (msg.specialMsg) {
+            if (msg.specialMsg.type === "rack") {
+                const content = document.createElement("div");
+                content.appendChild(document.createElement("span"));
+                content.lastChild.className = "info";
+                content.lastChild.textContent = format(_("{0} shows their rack:"), msg.sender);
+
+                const letters = document.createElement("div");
+                letters.className = "tile-list";
+
+                for (let i = 0; i < msg.specialMsg.rack.length; i++) {
+                    letters.appendChild(makeLetter(msg.specialMsg.rack[i], false, true));
+                }
+
+                content.appendChild(letters);
+                chatMessage("", content);
+                return;
+            }
+        }
+
+        chatMessage(msg.sender, msg.content);
+    }
+
+
+    function refreshCurrentPlayer() {
+        const row = tablePlayers[currentPlayer];
+        if (currentPlayer && row && !row.classList.contains("current-player")) {
+            row.classList.add("current-player");
+            blink(row.firstChild);
+        }
+    }
+
+    function setCurrentPlayer(player) {
+        if (currentPlayer && tablePlayers[currentPlayer]) {
+            tablePlayers[currentPlayer].classList.remove("current-player");
+        }
+
+        currentPlayer = player;
+        refreshCurrentPlayer();
+    }
+
+    function handleReceivedData(data) {
+        if (Array.isArray(data)) {
+            data.forEach(handleReceivedData);
+            return;
+        }
+
+        if (data.id) {
+            const pos = waitingMsgs.indexOf(data.id);
+            if (pos !== -1) {
+                waitingMsgs.splice(pos, 1);
+            }
+        }
+
+        if (data.version) {
+            serverVersion = data.version;
+        }
+
+        if (data.pleaseRestart) {
+            forceReload(_("Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"));
+        }
+
+        if (data.error) {
+            fatalError(new Error("Error from the server: " + data.error + " - " + data.reason));
+        }
+
+        if (data.stopping) {
+            stopConnection();
+
+            retryPolling(
+                (typeof data.stopping === "number" && data.stopping > 0)
+                    ? data.stopping
+                    : 0
+            );
+        }
+
+        if (data.msg) {
+            handleChatMessage(data.msg);
+        }
+
+        if (typeof data.currentPlayer === "string") {
+            setCurrentPlayer(data.currentPlayer);
+        }
+
+        if (data.players) {
+            if (participantPlaceholder) {
+                participantPlaceholder.parentNode.removeChild(participantPlaceholder);
+                participantPlaceholder = null;
+            }
+
+            for (let i = 0; i < data.players.length; i++) {
+                const player = data.players[i];
+                const playerName = data.players[i].player;
+
+                if (!tablePlayers[playerName]) {
+                    let before = null;
+
+                    for (let j = 1;  j < participants.rows.length; j++) {
+                        if (playerName < participants.rows[j].cells[0].textContent) {
+                            before = participants.rows[j];
+                            break;
+                        }
+                    }
+
+                    const row = document.createElement("tr");
+                    participants.insertBefore(row, before);
+                    row.appendChild(document.createElement("td"));
+                    row.lastChild.textContent = playerName;
+                    row.appendChild(document.createElement("td"));
+                    row.appendChild(document.createElement("td"));
+
+                    row.lastChild.className = "score-cell";
+                    row.lastChild.onclick = (
+                        function (row) {
+                            return function () {
+                                if (!pollingReady) {
+                                    showConnectionLost();
+                                    return;
+                                }
+
+                                myPrompt(
+                                    format(
+                                        _("Enter the new score of {0}:"),
+                                        row.firstChild.textContent
+                                    ),
+                                    function (res) {
+                                        res = parseInt(res);
+                                        if (!isNaN(res)) {
+                                            sendCmds([{
+                                                cmd: "score",
+                                                player: row.firstChild.textContent,
+                                                score: res
+                                            }]);
+                                        }
+                                    },
+                                    row.lastChild.textContent
+                                );
+                            };
+                        }
+                    )(row);
+
+                    row.appendChild(document.createElement("td"));
+                    row.lastChild.className = "turn-cell";
+                    row.lastChild.appendChild(document.createElement("button"));
+                    const img = new Image();
+                    img.src = "baton.svg";
+                    row.lastChild.lastChild.appendChild(img);
+                    row.lastChild.lastChild.onclick = (function (playerName) {
+                        sendCmds([{
+                            cmd: "currentPlayer",
+                            player: playerName === currentPlayer ? "" : playerName
+                        }]);
+                    }).bind(null, playerName);
+
+                    tablePlayers[playerName] = row;
+                }
+
+                if (Object.prototype.hasOwnProperty.call(player, "score")) {
+                    const scoreCell = tablePlayers[playerName].childNodes[2];
+                    scoreCell.textContent = player.score;
+                    blink(scoreCell);
+                }
+
+                if (Object.prototype.hasOwnProperty.call(player, "rackCount")) {
+                    const countCell = tablePlayers[playerName].childNodes[1];
+                    countCell.textContent = player.rackCount;
+                    blink(countCell);
+                }
+            }
+
+            refreshCurrentPlayer();
+        }
+
+        if (data.playerName) {
+            set("playerName", data.playerName);
+        }
+
+        if (data.gameNumber) {
+            set("gameNumber", data.gameNumber);
+        }
+
+
+        if (data.letterValues) {
+            scoreOf = data.letterValues;
+        }
+
+        if (data.board) {
+            setBoard(data.board);
+        }
+
+        if (typeof data.remainingLetters === "number") {
+            remainingLetters = data.remainingLetters;
+            document.getElementById("remaining-letters").textContent = data.remainingLetters;
+            if (data.remainingLetters === 0) {
+                helpBag.style.display = "none";
+                helpClear.style.display = "";
+            } else {
+                helpBag.style.display = "";
+                helpClear.style.display = "none";
+            }
+        }
+
+        if (data.rack) {
+            setRack(data.rack);
+        }
+
+        if (!data.action) {
+            return;
+        }
+
+        switch (data.action) {
+            case "pushBag": //TODO
+                break;
+
+            case "popBag": //TODO
+                break;
+
+            case "reset": //TODO
+                tablePlayers = {};
+                while (participants.rows[1]) {
+                    participants.removeChild(participants.rows[1]);
+                }
+                sendCmds([{cmd: "hello"}]);
+                break;
+
+            case "moveLetter":
+                if (data.from === "board") {
+                    setCell(data.indexFrom, "");
+                } else if (data.from === "rack") {
+                    setRackCell(data.indexFrom, "");
+                }
+
+                if (data.to === "board") {
+                    setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
+                } else if (data.to === "rack") {
+                    setRackCell(data.indexTo, data.letter);
+                }
+                break;
+            case "setCell":
+                setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
+                break;
+            case "setRackCell":
+                setRackCell(data.indexTo, data.letter);
+        }
+    }
+
+    function retryPolling(delay) {
+        if (needsRestart) {
+            return;
+        }
+
+        pollingReady = false;
+
+        if (!boundEventShowConnectionLost) {
+            boundEventShowConnectionLost = showConnectionLost.bind(null, null);
+
+            document.body.addEventListener("mousemove", boundEventShowConnectionLost);
+            document.body.addEventListener("mousedown", boundEventShowConnectionLost);
+            document.body.addEventListener("touchstart", boundEventShowConnectionLost);
+        }
+
+        if (retryPollingTimeout) {
+            return;
+        }
+
+        if (delay || retriedImmediately) {
+            retryPollingTimeout = setTimeout(
+                function () {
+                    connectionStopped = false;
+                    pollingServer = false;
+                    startConnection();
+                    retryPollingTimeout = 0;
+                },
+                delay || Conf.POLLING_DELAY
+            );
+        } else {
+            retriedImmediately = true;
+            connectionStopped = false;
+            pollingServer = false;
+            startConnection();
+        }
+    }
+
+    function parseAndHandleReceivedData(r) {
+        let m;
+
+        try {
+            m = JSON.parse(r);
+        } catch (e) {
+            jsonError(r, e);
+        }
+
+        try {
+            handleReceivedData(m);
+        } catch (e) {
+            fatalError(e);
+        }
+    }
+
+    function closeConnections() {
+        if (eventSource) {
+            eventSource.onerror = null;
+            eventSource.close();
+            eventSource = null;
+        }
+
+        if (webSocket) {
+            webSocket.onclose = null;
+            webSocket.onerror = null;
+            webSocket.close();
+            webSocket = null;
+        }
+    }
+
+    function stopConnection() {
+        closeConnections();
+
+        if (retryPollingTimeout) {
+            clearTimeout(retryPollingTimeout);
+            retryPollingTimeout = 0;
+        }
+
+        connectionStopped = true;
+        pollingServer = false;
+        pollingReady = false;
+    }
+
+    let blacklistWebsockets = false;
+    let webSocketErrors = 0;
+
+    function bindConnectionEvents(connection) {
+        connection.onopen = connectionReady;
+
+        connection.onmessage = function (e) {
+            if (connection === webSocket) {
+                webSocketErrors = 0;
+            }
+
+            connectionReady();
+            parseAndHandleReceivedData(e.data);
+        };
+
+        connection.onerror = function (e) {
+            console.error("Connection error:", e);
+
+            if (connection === eventSource) {
+                retryPolling();
+            } else {
+                webSocketErrors++;
+
+                if (webSocketErrors > Conf.MAX_WEBSOCKET_ERRORS) {
+                    blacklistWebsockets = true;
+                }
+            }
+        };
+
+        if (connection === webSocket) {
+            connection.onclose = function (e) {
+                webSocket = null;
+                if (e.code === 1002 || e.code === 1003) {
+                    // CLOSE_PROTOCOL_ERROR or CLOSE_UNSUPPORTED
+                    blacklistWebsockets = true;
+                    pollServerWithEventSource();
+                    return;
+                }
+                retryPolling();
+            };
+        }
+    }
+
+    function canConnect() {
+        return !pollingServer && !needsRestart && !connectionStopped && navigator.onLine !== false;
+        // DO NOT change navigator.onLine !== false by navigator.onLine === true
+        // This would block connection on a browser not supporting this property
+    }
+
+    function pollServerWithEventSource() {
+        if (canConnect() && Conf.ENABLE_EVENT_SOURCE && window.EventSource) {
+            closeConnections();
+            pollingServer = true;
+
+            eventSource = new EventSource(getApiEntryPoint() + "/sse/" + JSON.stringify(cmdsWithContext()));
+            bindConnectionEvents(eventSource);
+            return;
+        }
+
+        pollServerWithXHR();
+    }
+
+    function pollServerWithWebSocket() {
+        if (canConnect() && Conf.ENABLE_WEBSOCKETS && !blacklistWebsockets && window.WebSocket) {
+            closeConnections();
+            pollingServer = true;
+
+            webSocket = new WebSocket(
+                (window.location.protocol === "http:" ? "ws://" : "wss://") +
+                window.location.host +
+                getApiEntryPoint() + "/ws/" +
+                JSON.stringify(cmdsWithContext())
+            );
+
+            bindConnectionEvents(webSocket);
+            return;
+        }
+
+        pollServerWithEventSource();
+    }
+
+    function xhrRequest(data, onreadystatechange) {
+        const xhr = new XMLHttpRequest();
+
+        xhr.open("POST", getApiEntryPoint(), true);
+        xhr.setRequestHeader("Content-Type", "text/plain");
+        xhr.send(JSON.stringify(data));
+
+        xhr.onreadystatechange = onreadystatechange.bind(null, xhr);
+    }
+
+    function pollServerWithXHR() {
+        if (!canConnect()) {
+            return;
+        }
+
+        pollingServer = true;
+
+        let currentIndex = 0;
+        let expectedLength = 0;
+
+        xhrRequest(cmdsWithContext(), function (xhr) {
+            if (xhr.readyState === 2) {
+                connectionReady();
+            } else if (xhr.readyState === 3) {
+                connectionReady();
+
+                while (true) {
+                    if (!expectedLength) {
+                        let i = currentIndex;
+                        while (i < xhr.responseText.length) {
+                            if ("0123456789".indexOf(xhr.responseText.charAt(i)) === -1) {
+                                expectedLength = parseInt(xhr.responseText.substring(currentIndex, i));
+                                currentIndex = i;
+                                break;
+                            }
+                            ++i;
+                        }
+                    }
+
+                    if (expectedLength && (xhr.responseText.length >= currentIndex + expectedLength)) {
+                        const end = currentIndex + expectedLength;
+                        let msgs;
+
+                        try {
+                            msgs = JSON.parse(
+                                xhr.responseText.substring(
+                                    currentIndex,
+                                    end
+                                )
+                            );
+                            currentIndex = end;
+                            expectedLength = 0;
+                        } catch (e) {
+                            jsonError(xhr.responseText.substring(
+                                currentIndex,
+                                end
+                            ), e);
+                        }
+
+                        handleReceivedData(msgs);
+                    } else {
+                        break;
+                    }
+                }
+            } else if (xhr.readyState === 4) {
+                startConnection();
+            }
+        });
+    }
+
+    function startConnection() {
+        pollServerWithWebSocket();
+    }
+
+    function sendXHR(cmds) {
+        if (needsRestart) {
+            return;
+        }
+
+        blockMove++;
+
+        xhrRequest(cmdsWithContext(cmds), function (xhr) {
+            if (xhr.readyState === 4) {
+                if (xhr.status === 0 || xhr.status >= 300) {
+                    setTimeout(sendCmds.bind(null, cmds), Conf.POLLING_DELAY);
+                    return;
+                }
+
+                parseAndHandleReceivedData(xhr.responseText);
+
+                blockMove--;
+
+                if (!pollingReady) {
+                    startConnection();
+                }
+            }
+        });
+    }
+
+    function cmdsWithContext(cmds) {
+        return {
+            gameNumber: localStorage.trivabbleGameNumber || "",
+            playerName: localStorage.trivabblePlayerName,
+            version: VERSION,
+            cmds: cmds
+        };
+    }
+
+    function sendCmds(cmds) {
+        if (webSocket && webSocket.readyState === 1) {
+            if (serverVersion >= 202004281800) {
+                for (const cmd of cmds) {
+                    waitingMsgs.push(currentMessageId);
+                    cmd.id = currentMessageId++;
+                }
+            }
+
+            webSocket.send(JSON.stringify(cmds));
+            return;
+        }
+
+        if (!pollingReady) {
+            startConnection();
+            setTimeout(sendCmds, Conf.POLLING_DELAY, cmds);
+            return;
+        }
+
+        sendXHR(cmds);
+    }
+
+    function joinGame() {
+        checkGameInProgress(
+            function () {
+                myPrompt(
+                    _("To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.").replace("{0}", localStorage.trivabbleGameNumber),
+                    function (n) {
+                        n = parseInt(n);
+                        if (isNaN(n)) {
+                            myAlert(_("It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."));
+                        } else {
+                            localStorage.trivabbleGameNumber = n;
+                            location.reload();
+                        }
+                    }
+                );
+            }
+        );
+    }
+
+    const specialTypesText = {
+        doubleLetter: _("Double\nLetter"),
+        doubleWord:   _("Double\nWord"),
+        tripleLetter: _("Triple\nLetter"),
+        tripleWord:   _("Triple\nWord")
+    };
+
+    function specialCell(type, cell) {
+        cell.firstChild.appendChild(document.createElement("span"));
+        cell.classList.add("special-cell");
+        cell.classList.add("special-cell-" + type);
+        cell.lastChild.lastChild.textContent = _(specialTypesText[type]);
+        cell.lastChild.lastChild.className = "special-cell-label";
+    }
+
+    function changeName() {
+        myPrompt(
+            _("To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."),
+            function (newName) {
+                if (newName && newName.trim()) {
+                    localStorage.trivabblePlayerName = newName.trim();
+                    name.textContent = localStorage.trivabblePlayerName;
+                }
+            },
+            localStorage.trivabblePlayerName
+        );
+    }
+
+    function startGame(number) {
+        if (number) {
+            localStorage.trivabbleGameNumber = number;
+        }
+        startConnection();
+    }
+
+    let audioTileLoaded = false;
+    let audioMsgLoaded = false;
+    let tilesSound;
+    let msgSound;
+
+    function loadAudio() {
+        if (!audioTileLoaded && tilesSound.checked) {
+            audioTileLoaded = true;
+            audioNotification.load();
+        }
+
+        if (!audioMsgLoaded  && msgSound.checked) {
+            audioMsgLoaded = true;
+            audioChat.load();
+        }
+    }
+
+    function bagClicked(e) {
+        preventDefault(e);
+
+        if (blockMove || waitingMsgs.length) {
+            return;
+        }
+
+        if (!remainingLetters) {
+            myAlert(_("You cannot take another tile: the bag is empty."));
+            return;
+        }
+
+        loadAudio();
+
+        if (!pollingReady) {
+            showConnectionLost();
+            return;
+        }
+
+        const index = getFreeRackSpaceIndex();
+        if (index === -1) {
+            myAlert(_("You cannot take another tile: your rack is full."));
+        } else {
+            sendCmds(
+                [{cmd: "moveLetter", from: "bag", to: "rack", indexTo: index}]
+            );
+        }
+    }
+
+    function clearGame() {
+        myConfirm(
+            _("Are you sure you want to put all the tiles back in the bag (in order to play another game)?"),
+            function () {
+                sendCmds([{cmd: "resetGame"}]);
+            }
+        );
+    }
+
+    function clearRack() {
+        myConfirm(
+            _("Are you sure you want to put all your tiles back in the bag?"),
+            function () {
+                sendCmds([
+                    {cmd: "moveLetter", from: "rack", indexFrom: 0, to: "bag", indexTo: -1},
+                    {cmd: "moveLetter", from: "rack", indexFrom: 1, to: "bag", indexTo: -1},
+                    {cmd: "moveLetter", from: "rack", indexFrom: 2, to: "bag", indexTo: -1},
+                    {cmd: "moveLetter", from: "rack", indexFrom: 3, to: "bag", indexTo: -1},
+                    {cmd: "moveLetter", from: "rack", indexFrom: 4, to: "bag", indexTo: -1},
+                    {cmd: "moveLetter", from: "rack", indexFrom: 5, to: "bag", indexTo: -1},
+                    {cmd: "moveLetter", from: "rack", indexFrom: 6, to: "bag", indexTo: -1}
+                ]);
+                setRack("");
+            }
+        );
+    }
+
+    function showRack() {
+        myConfirm(
+            _("Are you sure you want to show your rack to everybody?"),
+            function () {
+                const letters = playerLetters.map(
+                    function (tilePlaceholer) {
+                        return tilePlaceholer.querySelector(".tile");
+                    }
+                ).filter(function (tile) {
+                    return tile !== null;
+                });
+
+                sendCmds([{
+                    cmd: "msg",
+                    msg: _("Here is my rack:") + " " + letters.map(
+                        function (tile) {
+                            return (
+                                tile.textContent === ""
+                                    ? _("joker")
+                                    : tile.querySelector(".tile-letter").textContent
+                            );
+                        }
+                    ).join(", "),
+                    specialMsg: {
+                        type: "rack",
+                        rack: letters.map(
+                            function (tile) {
+                                return tile.querySelector(".tile-letter").textContent;
+                            }
+                        )
+                    }
+                }]);
+            }
+        );
+
+    }
+
+    function initChat() {
+        chatMessages.style.width = chatMessages.offsetWidth + "px";
+
+        const btn = document.getElementById("chat-btn");
+
+        chatTextarea.onmouseup = function () {
+            chatMessages.style.width = chatTextarea.offsetWidth + "px";
+        };
+
+        btn.onclick = function () {
+            if (!pollingReady) {
+                return;
+            }
+
+            loadAudio();
+            sendCmds([{
+                cmd: "msg",
+                msg: chatTextarea.value
+            }]);
+
+            chatTextarea.value = "";
+        };
+
+        chatTextarea.onkeydown = function (e) {
+            if (e.keyCode === 13) {
+                preventDefault(e);
+                chatTextarea.focus();
+                btn.onclick();
+            }
+        };
+    }
+
+    function initSound() {
+        audioNotification = new Audio();
+        audioNotification.preload = "auto";
+        audioNotification.volume  = 1;
+
+        let audioSourceOGG = document.createElement("source");
+        audioSourceOGG.src = "notification.ogg";
+
+        let audioSourceMP3 = document.createElement("source");
+        audioSourceMP3.src = "notification.mp3";
+        audioNotification.appendChild(audioSourceOGG);
+        audioNotification.appendChild(audioSourceMP3);
+
+        audioChat = new Audio();
+        audioChat.preload = "auto";
+        audioChat.volume  = 1;
+
+        audioSourceOGG = document.createElement("source");
+        audioSourceOGG.src = "receive.ogg";
+
+        audioSourceMP3 = document.createElement("source");
+        audioSourceMP3.src = "receive.mp3";
+        audioChat.appendChild(audioSourceOGG);
+        audioChat.appendChild(audioSourceMP3);
+
+
+        tilesSound = document.getElementById("tiles-sound");
+        msgSound = document.getElementById("msg-sound");
+
+        tilesSound.onclick = function () {
+            localStorage.trivabbleTileSound = tilesSound.checked;
+        };
+
+        msgSound.onclick = function () {
+            localStorage.trivabbleMsgSound = msgSound.checked;
+        };
+
+        if (Object.prototype.hasOwnProperty.call(localStorage, "trivabbleMsgSound")) {
+            msgSound.checked = localStorage.trivabbleMsgSound === "true";
+        } else {
+            localStorage.trivabbleMsgSound = msgSound.checked;
+        }
+
+        if (Object.prototype.hasOwnProperty.call(localStorage, "trivabbleTileSound")) {
+            tilesSound.checked = localStorage.trivabbleTileSound === "true";
+        } else {
+            localStorage.trivabbleTilesSound = tilesSound.checked;
+        }
+    }
+
+    function repromptName(f) {
+        if (localStorage.trivabblePlayerName && localStorage.trivabblePlayerName.trim()) {
+            f();
+        } else {
+            myPrompt(
+                _("It seems your did not give your name. You need to do it for the game to run properly."),
+                function (name) {
+                    if (name && name.trim()) {
+                        localStorage.trivabblePlayerName = name.trim();
+                    }
+
+                    repromptName(f);
+                }
+            );
+        }
+    }
+
+    trivabble.applyL10n = function () {
+        document.documentElement.lang = libD.lang;
+        document.documentElement.setAttribute("xml:lang", libD.lang);
+
+        for (const node of [].slice.call(document.querySelectorAll("[data-l10n]"))) {
+            if (node.dataset.l10n === "text-content") {
+                node.textContent = _(node.textContent.trim());
+            } else {
+                node.setAttribute(node.dataset.l10n, _(node.getAttribute(node.dataset.l10n)));
+            }
+        }
+
+        trivabble.run();
+    };
+
+    function langSelectionChange(e) {
+        localStorage.trivabbleLang = e.target.value;
+        location.reload();
+    }
+
+    function initGlobals() {
+        board         = document.getElementById("board");
+        rack          = document.getElementById("rack");
+        participants  = document.getElementById("participants");
+        name          = document.getElementById("name");
+        bag           = document.getElementById("bag");
+        chatMessages  = document.getElementById("chat-messages");
+        chatTextarea  = document.getElementById("chat-ta");
+        helpBag       = document.getElementById("help-bag");
+        helpClear     = document.getElementById("help-clear");
+        participantPlaceholder = document.getElementById("participants-placeholder");
+    }
+
+    function initEvents() {
+        mouseDown(bag, bagClicked);
+
+        document.getElementById("clear-game").onclick  = clearGame;
+        document.getElementById("change-name").onclick = changeName;
+        document.getElementById("join-game").onclick   = joinGame;
+        document.getElementById("clear-rack").onclick  = clearRack;
+        document.getElementById("show-rack").onclick   = showRack;
+        helpClear.onclick  = clearGame;
+    }
+
+    function initGame() {
+        if (!localStorage.trivabblePlayerName) {
+            myPrompt(
+                _("Hello! To begin, enter your name. Your adversaries will see this name when you play with them."),
+                function (name) {
+                    if (name && name.trim()) {
+                        localStorage.trivabblePlayerName = name;
+                    }
+                    repromptName(initGame);
+                }
+            );
+
+            return;
+        }
+
+        name.textContent = localStorage.trivabblePlayerName;
+
+        const letters = "ABCDEFGHIJKLMNO";
+
+        const doubleLetter = {
+            "0,3":  true,
+            "0,11": true,
+            "2,6":  true,
+            "2,8":  true,
+            "3,0":  true,
+            "3,7":  true,
+            "3,14": true,
+            "6,2":  true,
+            "6,6":  true,
+            "6,8":  true,
+            "6,12": true
+        };
+
+        let cell;
+
+        for (let i = 0; i < 7; i++) {
+            const span = document.createElement("span");
+            span.className = "tile-placeholder";
+            rack.appendChild(span);
+            playerLetters.push(span);
+        }
+
+        for (let i = 0; i < 15; i++) {
+            board.rows[0].appendChild(document.createElement("th"));
+            board.rows[0].lastChild.textContent = i + 1;
+
+            board.appendChild(document.createElement("tr"));
+            board.lastChild.appendChild(document.createElement("th"));
+            board.lastChild.lastChild.textContent = letters[i];
+            for (let j = 0; j < 15; j++) {
+                cell = document.createElement("td");
+                boardCells.push(cell);
+                board.lastChild.appendChild(cell);
+                cell.appendChild(document.createElement("div"));
+                cell.lastChild.className = "tile-placeholder";
+
+                if (i === j && i === 7) {
+                    specialCell("doubleWord", board.lastChild.lastChild);
+                    cell = board.lastChild.lastChild.getElementsByClassName("special-cell-label")[0];
+                    cell.textContent = "★";
+                    board.lastChild.lastChild.id = "center-cell";
+                } else if (i % 7 === 0 && j % 7 === 0) {
+                    specialCell("tripleWord", board.lastChild.lastChild);
+                } else if ((i === j || i + j === 14) && (i < 5 || i > 9)) {
+                    specialCell("doubleWord", board.lastChild.lastChild);
+                } else if ((i % 4 === 1) && (j % 4 === 1)) {
+                    specialCell("tripleLetter", board.lastChild.lastChild);
+                } else if ((i < 8 && doubleLetter[i + "," + j]) || (i > 7 && doubleLetter[(14-i) + "," + j]) || (i === 7 && (j === 3 || j === 11))) {
+                    specialCell("doubleLetter", board.lastChild.lastChild);
+                }
+            }
+
+            board.lastChild.appendChild(document.createElement("th"));
+            board.lastChild.lastChild.textContent = letters[i];
+        }
+
+        board.rows[0].appendChild(board.rows[0].cells[0].cloneNode(false));
+
+        board.appendChild(document.createElement("tr"));
+        board.lastChild.appendChild(board.rows[0].cells[0].cloneNode(false));
+
+        for (let i = 0; i < 15; i++) {
+            board.lastChild.appendChild(document.createElement("th"));
+            board.lastChild.lastChild.textContent = i + 1;
+        }
+
+        board.lastChild.appendChild(board.rows[0].cells[0].cloneNode(false));
+
+        if (localStorage.trivabbleGameNumber) {
+            document.getElementById("number").textContent = localStorage.trivabbleGameNumber;
+        }
+
+        startGame(localStorage.trivabbleGameNumber);
+    }
+
+    function initLang() {
+        const lang = libD.lang = localStorage.trivabbleLang || libD.lang;
+
+        const langSel = document.getElementById("select-lang");
+        langSel.value = lang;
+        langSel.onchange = langSelectionChange;
+
+        const script = document.createElement("script");
+        script.src = "l10n/js/" + lang + ".js";
+        script.onerror = trivabble.l10nError;
+        document.getElementsByTagName("head")[0].appendChild(script);
+    }
+
+    trivabble.run = function () {
+        initGlobals();
+        initEvents();
+        initChat();
+        initGame();
+        initSound();
+    };
+
+    trivabble.l10nError = trivabble.run;
+
+    window.addEventListener("DOMContentLoaded", initLang);
+
+    window.addEventListener("beforeunload", stopConnection);
+
+    window.addEventListener("offline", function () {
+        stopConnection();
+        showConnectionLost(_("It seems your browser went offline. Please reconnect to continue playing."));
+    });
+
+    window.addEventListener("online", startConnection);
+
+}());
diff --git a/touch2click.js b/touch2click.js
deleted file mode 100644 (file)
index 56e05aa..0000000
+++ /dev/null
@@ -1,210 +0,0 @@
-//thx http://stackoverflow.com/questions/1517924/javascript-mapping-touch-events-to-mouse-events#1781750
-
-function touchHandler(event) {
-    var nn = event.target.nodeName.toLowerCase();
-
-    if (nn === "input" || nn === "select" || nn === "button" || nn === "textarea") {
-        return;
-    }
-
-    var touches = event.changedTouches,
-        first   = touches ? touches[0] : event,
-        type    = "";
-
-    switch (event.type) {
-    case "touchstart": type = "mousedown"; break;
-    case "touchmove":  type = "mousemove"; break;
-    case "touchend":   type = "mouseup";   break;
-    case "tap":        type = "click";     break;
-    case "dbltap":     type = "dblclick";  break;
-    default: return;
-    }
-
-//initMouseEvent(type, canBubble, cancelable, view, clickCount,
-//              screenX, screenY, clientX, clientY, ctrlKey,
-//              altKey, shiftKey, metaKey, button, relatedTarget);
-
-     var simulatedEvent = document.createEvent("MouseEvent");
-     simulatedEvent.initMouseEvent(type, true, true, window, 1,
-            first.screenX, first.screenY,
-            first.clientX, first.clientY, false,
-            false, false, false, 0/*left*/, null);
-     first.target.dispatchEvent(simulatedEvent);
-     event.preventDefault();
-}
-
-document.addEventListener('touchend', (function(speed, distance) {
-/*
- * Copyright (c)2012 Stephen M. McKamey.
- * Licensed under The MIT License.
- * src: https://raw.github.com/mckamey/doubleTap.js/master/doubleTap.js
- */
-
-    "use strict";
-
-    // default dblclick speed to half sec (default for Windows & Mac OS X)
-    speed = Math.abs(+speed) || 500;//ms
-    // default dblclick distance to within 40x40 pixel area
-    distance = Math.abs(+distance) || 40;//px
-
-    // Date.now() polyfill
-    var now = Date.now || function() {
-        return +new Date();
-    };
-
-    var cancelEvent = function(e) {
-        e = (e || window.event);
-
-        if (e) {
-            if (e.preventDefault) {
-                e.stopPropagation();
-                e.preventDefault();
-            } else {
-                try {
-                    e.cancelBubble = true;
-                    e.returnValue = false;
-                } catch (ex) {
-                    // IE6
-                }
-            }
-        }
-        return false;
-    };
-
-    var taps = 0,
-        last = 0,
-        // NaN will always test false
-        x = NaN,
-        y = NaN;
-
-    return function (e) {
-        e = (e || window.event);
-
-        var time   = now(),
-            touch  = e.changedTouches ? e.changedTouches[0] : e,
-            nextX  = +touch.clientX,
-            nextY  = +touch.clientY,
-            target = e.target || e.srcElement,
-            e2,
-            parent;
-
-        if ((last + speed) > time &&
-            Math.abs(nextX - x) < distance &&
-            Math.abs(nextY - y) < distance) {
-            // continue series
-            taps++;
-
-        } else {
-            // reset series if too slow or moved
-            taps = 1;
-        }
-
-        // update starting stats
-        last = time;
-        x = nextX;
-        y = nextY;
-
-        // fire tap event
-        if (document.createEvent) {
-            e2 = document.createEvent('MouseEvents');
-            e2.initMouseEvent(
-                'tap',
-                true,             // click bubbles
-                true,             // click cancelable
-                e.view,              // copy view
-                taps,             // click count
-                touch.screenX,     // copy coordinates
-                touch.screenY,
-                touch.clientX,
-                touch.clientY,
-                e.ctrlKey,          // copy key modifiers
-                e.altKey,
-                e.shiftKey,
-                e.metaKey,
-                e.button,            // copy button 0: left, 1: middle, 2: right
-                e.relatedTarget); // copy relatedTarget
-
-            if (!target.dispatchEvent(e2)) {
-                // pass on cancel
-                cancelEvent(e);
-            }
-
-        } else {
-            e.detail = taps;
-
-            // manually bubble up
-            parent = target;
-            while (parent && !parent.tap && !parent.ontap) {
-                parent = parent.parentNode || parent.parent;
-            }
-            if (parent && parent.tap) {
-                // DOM Level 0
-                parent.tap(e);
-
-            } else if (parent && parent.ontap) {
-                // DOM Level 0, IE
-                parent.ontap(e);
-
-            } else if (typeof jQuery !== 'undefined') {
-                // cop out and patch IE6-8 with jQuery
-                jQuery(this).trigger('tap', e);
-            }
-        }
-
-        if (taps === 2) {
-            // fire dbltap event only for 2nd click
-            if (document.createEvent) {
-                e2 = document.createEvent('MouseEvents');
-                e2.initMouseEvent(
-                    'dbltap',
-                    true,             // dblclick bubbles
-                    true,             // dblclick cancelable
-                    e.view,              // copy view
-                    taps,             // click count
-                    touch.screenX,     // copy coordinates
-                    touch.screenY,
-                    touch.clientX,
-                    touch.clientY,
-                    e.ctrlKey,          // copy key modifiers
-                    e.altKey,
-                    e.shiftKey,
-                    e.metaKey,
-                    e.button,            // copy button 0: left, 1: middle, 2: right
-                    e.relatedTarget); // copy relatedTarget
-
-                if (!target.dispatchEvent(e2)) {
-                    // pass on cancel
-                    cancelEvent(e);
-                }
-
-            } else {
-                e.detail = taps;
-
-                // manually bubble up
-                parent = target;
-                while (parent && !parent.dbltap && !parent.ondbltap) {
-                    parent = parent.parentNode || parent.parent;
-                }
-                if (parent && parent.dbltap) {
-                    // DOM Level 0
-                    parent.dbltap(e);
-
-                } else if (parent && parent.ondbltap) {
-                    // DOM Level 0, IE
-                    parent.ondbltap(e);
-
-                } else if (typeof jQuery !== 'undefined') {
-                    // cop out and patch IE6-8 with jQuery
-                    jQuery(this).trigger('dbltap', e);
-                }
-            }
-        }
-    };
-}()), false);
-
-document.addEventListener("tap", touchHandler, true);
-// document.addEventListener("dbltap", touchHandler, true);
-document.addEventListener("touchstart", touchHandler, true);
-document.addEventListener("touchmove", touchHandler, true);
-document.addEventListener("touchend", touchHandler, true);
-document.addEventListener("touchcancel", touchHandler, true);
index 6f511e3bc4cbd38e512c535ba8c749cca44656d5..ae14ff0896a1913927dbb565ebc27c019f55a940 100644 (file)
@@ -972,9 +972,9 @@ function handleRequest(request, response) {
 
             debuglog("Serving " + request.url);
 
-            fs.exists("." + request.url, function (exists) {
+            fs.exists("server-data/" + request.url, function (exists) {
                 if (exists) {
-                    fs.readFile("." + request.url, function(err, contents) {
+                    fs.readFile("server-data/" + request.url, function(err, contents) {
                         if (err) {
                             response.statusCode = 500;
                             response.setHeader("Content-Type", "text/plain; charset=utf-8");
diff --git a/trivabble.css b/trivabble.css
deleted file mode 100644 (file)
index 80cf566..0000000
+++ /dev/null
@@ -1,447 +0,0 @@
-#board {
-    border-collapse:collapse;
-}
-
-a {
-    text-decoration:none;
-    color:blue;
-}
-
-html {
-    font-family: "Droid Sans", sans-serif;
-}
-
-html, #board, [draggable], .tile {
-    -webkit-touch-callout: none;
-    -webkit-user-select: none;
-    -khtml-user-select: none;
-    -moz-user-select: none;
-    -ms-user-select: none;
-    user-select: none;
-
-  -moz-user-select: none;
-  -khtml-user-select: none;
-  -webkit-user-select: none;
-  user-select: none;
-}
-
-#chat-messages, #number, #name {
-    -webkit-touch-callout: default;
-    -webkit-user-select: element;
-    -khtml-user-select: element;
-    -moz-user-select: element;
-    -ms-user-select: element;
-    user-select: element;
-
-  -moz-user-select: element;
-  -khtml-user-select: element;
-  -webkit-user-select: element;
-  user-select: element;
-}
-
-#chat-messages .error {
-    color: #F66;
-}
-
-#chat-messages .ok {
-    color: green;
-}
-
-#clear-game, #help-scores, #clear-rack, #help-turn {
-    white-space:pre-wrap;
-}
-
-[draggable] {
-  /* Required to make elements draggable in old WebKit */
-  -khtml-user-drag: element;
-  -webkit-user-drag: element;
-}
-
-#help-clear, #help-bag, #help-scores, #help-turn {
-    font-size:small
-}
-
-#board td {
-    text-transform:uppercase;
-    border:1px solid #220;
-    background:#FFC;
-    vertical-align:middle;
-    text-align:center;
-}
-
-#board td .tile-placeholder {
-    position: relative;
-    height: 100%;
-    width: 100%;
-    top: 0px;
-    left: 0px;
-    display: flex;
-    justify-content: center;
-    flex-direction: column;
-}
-
-#board th {
-    font-size:small;
-    color:gray;
-    font-weight:normal
-}
-
-.corner {
-    visibility:hidden;
-    border:none !important
-}
-
-.special-cell-tripleWord {
-    background:#F77 !important;
-    box-shadow: 0 0 1px #F77;
-}
-
-.special-cell-tripleLetter {
-    background:#77F !important;
-    box-shadow: 0 0 1px #77F;
-}
-
-.special-cell-doubleLetter {
-    background:#7CF !important;
-    box-shadow: 0 0 1px #7CF;
-}
-
-.special-cell-doubleWord {
-    background:#FBB !important;
-    box-shadow: 0 0 1px #FBB;
-}
-
-.special-cell-label {
-    display:inline-block;
-    white-space:pre-wrap;
-    font-size:7px;
-}
-
-#center-cell .special-cell-label {
-    font-weight:bold;
-    color:black;
-    font-size:large
-}
-
-body {
-    display:-webkit-flex;
-    display:flex;
-    -webkit-flex-direction:row;
-    flex-direction:row;
-    justify-content:safe center;
-}
-
-@media (max-width: 940px) {
-    body {
-        justify-content:start;
-    }
-}
-
-#menu, #panel {
-    display:-webkit-flex;
-    display:flex;
-    -webkit-flex-direction:column;
-    flex-direction:column
-}
-
-button {
-    margin: 0.25em 0;
-    min-height:3em;
-    background:#AFA;
-    border:1px solid gray;
-}
-
-button:hover {
-    background:#FD7;
-    cursor:pointer
-}
-
-#rack {
-    background:#EEC;
-    font-size:0;
-    border-radius:4px;
-    display:inline-block;
-    padding:2px 5px;
-    white-space:pre;
-    box-shadow:0 0 2px gray
-}
-
-#board td {
-    height:32px;
-    width: 32px;
-}
-
-#rack .tile-placeholder {
-    height:32px;
-    width: 32px;
-}
-
-.tile {
-    background-color: #EE8;
-    transition:background-color 1s, border 1s;
-    display: inline-block;
-    width: 95%;
-    height: 95%;
-    left: 2.5%;
-    top: 2.5%;
-    position: absolute;
-    border: 1px solid #AA5;
-    box-sizing: border-box;
-    text-align: center;
-    font-size:1rem;
-    cursor:pointer
-}
-
-.tile.tile-highlight {
-    background-color: #FFF;
-    font-weight: bold;
-    animation: highlight 4s ease-in;
-    border: 1px solid black;
-}
-
-@keyframes highlight {
-    0% {
-        background:red;
-        color:white;
-    }
-
-    100% {
-        background:white
-    }
-}
-
-.tile-score {
-    font-size: 0.5em;
-    position: absolute;
-    bottom: 1px;
-    right: 2px;
-}
-
-#chat-messages .tile {
-    position:relative;
-    cursor:auto;
-    top:0;
-    left:0;
-    width: 1.5em;
-    height: 1.5em;
-    margin:2px;
-    vertical-align:bottom;
-}
-
-#chat-messages .tile-letter {
-    font-size:0.9em;
-}
-
-#chat-messages .tile-score {
-    font-size: 0.4em;
-}
-
-#chat-messages.blink {
-    animation:chat-blink 1s
-}
-
-#chat-messages .info {
-    color:gray;
-    font-style:italic;
-}
-
-td.blink {
-    animation:td-blink 1s
-}
-
-.current-player {
-    font-weight:bold;
-}
-
-.current-player td:first-child:before {
-    position:absolute;
-    left:-1em;
-    color:red;
-    content: "→";
-}
-
-@keyframes chat-blink {
-    0% {
-        background:rgba(255, 255, 150, 0);
-    }
-
-    50% {
-        background:rgba(255, 255, 150, 1);
-    }
-
-    100% {
-        background:rgba(255, 255, 150, 0);
-    }
-}
-
-
-@keyframes td-blink {
-    0% {
-        background:rgba(100, 200, 255, 0);
-    }
-
-    50% {
-        background:rgba(100, 200, 255, 1);
-    }
-
-    100% {
-        background:rgba(100, 200, 255, 0);
-    }
-}
-
-
-
-.tile-target {
-    background:rgba(255,255,255, 0.9);
-/* } */
-
-/* #bag.tile-target, td .tile-target { */
-    outline: 1px solid #F33
-}
-
-#rack .tile-placeholder {
-    position:relative;
-    margin-left:1px;
-    margin-right:1px;
-    display:inline-block;
-    vertical-align:middle;
-    background:#CCA;
-}
-
-#panel {
-    -webkit-justify-content:center;
-    justify-content:center;
-    text-align:center
-}
-
-#bag, #rack-outer {
-    -webkit-align-self:center;
-    align-self:center;
-    text-align:center;
-}
-
-#p-name {
-    text-align:center;
-    font-size:small
-}
-
-.minibutton {
-    padding:0;
-    min-height:0;
-    font-size:small
-}
-
-.msg-sender {
-    color:blue;
-}
-
-#chat-messages {
-    overflow:auto
-}
-
-#bag {
-    width:165px;
-    height:160px;
-    background:url(bag.svg) no-repeat;
-    background-size:100% 100%
-}
-
-#bag:hover {
-    cursor:pointer;
-    opacity:0.8
-}
-
-#chat-messages {
-    border:1px solid silver;
-    box-sizing:border-box;
-    width: 100%;
-    margin-bottom:1px;
-    height:15em;
-}
-
-#chat-ta {
-    width:100%;
-    box-sizing:border-box;
-    height:3em
-}
-
-#participants {
-    font-size:small;
-    border-collapse:collapse;
-    margin:0 auto 1ex auto;
-}
-
-#participants th, #participants td {
-    border:1px solid gray;
-    padding:0.33ex 1ex
-}
-
-#participants .score-cell {
-    min-width:4em;
-}
-
-#participants .score-cell:hover, .turn-cell button:hover {
-    cursor:pointer;
-    background:#FFA
-}
-
-#participants th {
-    font-weight:normal;
-    background:#EEE;
-    font-size:smaller
-}
-
-#participants-placeholder {
-    font-style:italic;
-}
-
-.turn-cell button {
-    padding:0 2px;
-    background:white;
-    min-height:1em;
-    height:1em;
-    margin:0;
-    display:flex;
-}
-
-.turn-cell img {
-    width:1em;
-    vertical-align: middle;
-}
-
-@media screen and (max-height:650px) {
-    #board td , #rack .tile-placeholder {
-        height:28px;
-        width: 28px;
-    }
-}
-@media screen and (max-height:600px) {
-    #board td , #rack .tile-placeholder {
-        height:24px;
-        width: 24px;
-    }
-}
-
-@media screen and (max-height:550px) {
-    #chat-messages {
-        height:7em;
-    }
-}
-
-#prefs-sound {
-    margin:1ex 0 0 0;
-}
-
-#prefs-sound p {
-    margin:0;
-    padding:0;
-    font-size:small
-}
-
-#lang-selection {
-    font-size:small;
-}
-
-#prefs-sound input, #prefs-sound label {
-    vertical-align:middle
-}
diff --git a/trivabble.js b/trivabble.js
deleted file mode 100644 (file)
index 3e5268b..0000000
+++ /dev/null
@@ -1,1611 +0,0 @@
-/**
- * Copyright (C) 2016-2020 Raphaël Jakse <raphael.trivabble@jakse.fr>
- *
- * @licstart
- * This file is part of Trivabble.
- *
- * Trivabble is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License (GNU AGPL)
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Trivabble is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with Trivabble.  If not, see <http://www.gnu.org/licenses/>.
- * @licend
- *
- * @source: https://trivabble.1s.fr/
- * @source: https://gitlab.com/raphj/trivabble/
- */
-
-/*global libD, myConfirm, myAlert, myPrompt*/
-
-(function () {
-    "use strict";
-
-    const VERSION = 202004262313;
-
-    const Conf = window.TrivabbleConf || {};
-
-    function setConf(parameterName, defaultValue) {
-        if (typeof Conf[parameterName] === "undefined") {
-            Conf[parameterName] = defaultValue;
-        }
-    }
-
-    setConf("POLLING_DELAY",        2000);
-    setConf("ENABLE_WEBSOCKETS",    true);
-    setConf("ENABLE_EVENT_SOURCE",  true);
-    setConf("MAX_WEBSOCKET_ERRORS", 1);
-    setConf("APP_PATH",             "");
-
-    const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {
-        return s;
-    };
-
-    const trivabble = window.trivabble = {l10n: _};
-
-    function format(s, v) {
-        return s.replace("{0}", v);
-    }
-
-    let board;
-    let rack;
-    const boardCells = [];
-    let scoreOf;
-    let bag;
-
-    const playerLetters = [];
-    let currentPlayer = "";
-
-    let audioNotification;
-    let audioChat;
-    let chatMessages;
-    let chatTextarea;
-    let helpBag;
-    let helpClear;
-
-    let tablePlayers = {};
-    let participantPlaceholder;
-    let participants;
-    let name;
-
-    let blockMove = 0;
-    let remainingLetters = 0;
-    let needsRestart = false;
-    const gameInProgress = false;
-
-    let eventSource = null;
-    let webSocket = null;
-
-    let connectionLostMessage = null;
-    let boundEventShowConnectionLost = null;
-    let retryPollingTimeout = 0;
-    let pollingServer = false;
-    let connectionStopped = false;
-
-    let currentMessageId = 1;
-    const waitingMsgs = [];
-
-    let serverVersion = 0;
-
-    // HTTP path url prefix for any socket (xhr,ess or ws) from browser to server access
-    function getApiEntryPoint() {
-        return Conf.APP_PATH + "/:trivabble";
-    }
-
-    function mouseDown(ele, fun, stop) {
-        const meth = stop ? "removeEventListener" : "addEventListener";
-        ele[meth]("mousedown", fun, false);
-    }
-
-    function mouseUp(ele, fun, stop) {
-        const meth = stop ? "removeEventListener" : "addEventListener";
-        ele[meth]("mouseup", fun, false);
-    }
-
-    function mouseMove(ele, fun, stop) {
-        const meth = stop ? "removeEventListener" : "addEventListener";
-        ele[meth]("mousemove", fun, false);
-    }
-
-    function setRack(rack) {
-        for (let i = 0; i < 7; i++) {
-            setTileParent(playerLetters[i], rack[i] || "");
-        }
-    }
-
-    function getFreeRackSpaceIndex() {
-        for (let i = 0; i < 7; i++) {
-            if (!playerLetters[i].getElementsByClassName("tile")[0]) {
-                return i;
-            }
-        }
-
-        return -1;
-    }
-
-    function showConnectionLost(msg) {
-        unbindEventsShowConnectionLost();
-
-        if (!connectionLostMessage) {
-            connectionLostMessage = chatMessage("", msg || _("Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."));
-            connectionLostMessage.classList.add("error");
-        }
-    }
-
-    function unbindEventsShowConnectionLost() {
-        if (boundEventShowConnectionLost) {
-            document.body.removeEventListener("mousemove", boundEventShowConnectionLost);
-            document.body.removeEventListener("mousedown", boundEventShowConnectionLost);
-            document.body.removeEventListener("touchstart", boundEventShowConnectionLost);
-            boundEventShowConnectionLost = null;
-        }
-    }
-
-    function removeElem(elem) {
-        elem.parentNode.removeChild(elem);
-    }
-
-    function connectionReady() {
-        pollingReady = true;
-        retriedImmediately = false;
-
-        unbindEventsShowConnectionLost();
-
-        if (connectionLostMessage) {
-            connectionLostMessage.querySelector(".msg-content").textContent = _("The problem is solved, sorry for the inconvenience!");
-            connectionLostMessage.classList.remove("error");
-            connectionLostMessage.classList.add("ok");
-
-            const removeMessage = removeElem.bind(null, connectionLostMessage);
-            connectionLostMessage.addEventListener("click", removeMessage);
-            connectionLostMessage.addEventListener("touch", removeMessage);
-
-            connectionLostMessage = null;
-        }
-    }
-
-    let tileInitCoords      = null;
-    let tileInitMouseCoords = null;
-    let tileDest            = null;
-    let tileInitDest        = null;
-    let movingTile          = null;
-    let rackBCR             = null;
-    let boardBCR            = null;
-    let bagBCR              = null;
-    let moveCMD             = null;
-
-    function getLetter(l) {
-        const tile = l.getElementsByClassName("tile")[0];
-        if (!tile) {
-            return "";
-        }
-
-        return tile.getElementsByClassName("tile-letter")[0].textContent;
-    }
-
-    function isParentOf(p, elem) {
-        while (elem) {
-            if (elem === p) {
-                return true;
-            }
-            elem = elem.parentNode;
-        }
-
-        return false;
-    }
-
-    function dragTileEnd() {
-        movingTile.style.left   = "";
-        movingTile.style.top    = "";
-        movingTile.style.width  = "";
-        movingTile.style.height = "";
-
-        mouseUp(document,   dragTileEnd,  true);
-        mouseMove(document, dragTileMove, true);
-
-        if (tileDest === bag) {
-            moveCMD.to = "bag";
-            moveCMD.indexTo = -1;
-            movingTile.parentNode.removeChild(movingTile);
-        } else if (tileDest) {
-            tileDest.appendChild(movingTile);
-
-            if (isParentOf(board, tileDest)) {
-                for (const tile of [].slice.call(board.getElementsByClassName("tile-highlight"))) {
-                    tile.classList.remove("tile-highlight");
-                }
-            }
-        } else if (tileInitDest.getElementsByClassName("tile")[0]) {
-            for (let i = 0; i < 7; i++) {
-                if (!playerLetters[i].getElementsByClassName("tile")[0]) {
-                    playerLetters[i].appendChild(movingTile);
-                    break;
-                }
-            }
-        } else {
-            tileInitDest.appendChild(movingTile);
-        }
-
-        if (tileDest) {
-            tileDest.classList.remove("tile-target");
-
-            const moveRack = {
-                cmd: "setRack",
-                rack: playerLetters.map(getLetter)
-            };
-
-            if (moveCMD.to === moveCMD.from) {
-                if (moveCMD.indexTo !== moveCMD.indexFrom) {
-                    sendCmds([
-                        (moveCMD.from === "rack")
-                            ? moveRack
-                            : moveCMD
-                    ]);
-                }
-            } else if (moveCMD.to === "rack") {
-                moveRack.rack[moveCMD.indexTo] = "";
-                sendCmds([moveRack, moveCMD]);
-            } else {
-                sendCmds([moveCMD]);
-            }
-        }
-
-        tileInitMouseCoords = null;
-        tileInitCoords      = null;
-        tileInitDest        = null;
-        tileDest            = null;
-        movingTile          = null;
-        boardBCR            = null;
-        bagBCR              = null;
-        rackBCR             = null;
-        moveCMD             = null;
-    }
-
-    function dragTileMove(e) {
-        let newLeft = (tileInitCoords.left + (e.clientX - tileInitMouseCoords.clientX));
-        let newTop  = (tileInitCoords.top  + (e.clientY - tileInitMouseCoords.clientY));
-
-        movingTile.style.left = (newLeft + window.scrollX) + "px";
-        movingTile.style.top  = (newTop  + window.scrollY) + "px";
-
-        let newDest = null;
-
-        newTop  += tileInitCoords.height / 2;
-        newLeft += tileInitCoords.width / 2;
-
-        if (
-            (newTop  > boardBCR.top  && newTop  < (boardBCR.top + boardBCR.height)) &&
-            (newLeft > boardBCR.left && newLeft < (boardBCR.left + boardBCR.width))
-        ) {
-            const rowIndex = Math.floor(
-                (
-                    (newTop  - boardBCR.top)  / boardBCR.height
-                ) * board.rows.length
-            );
-
-            if (rowIndex > 0 && rowIndex < board.rows.length - 1) {
-                const row = board.rows[rowIndex];
-
-                const colIndex = Math.floor(
-                    (
-                        (newLeft - boardBCR.left) / boardBCR.width
-                    ) * row.cells.length
-                );
-
-                if (colIndex > 0 && colIndex < row.cells.length - 1) {
-                    newDest = row.cells[colIndex].firstChild;
-                }
-            }
-
-            if (newDest && newDest.getElementsByClassName("tile")[0]) {
-                newDest = null;
-            }
-
-            if (newDest) {
-                moveCMD.to = "board";
-                moveCMD.indexTo = boardCells.indexOf(newDest.parentNode);
-            }
-        } else if (
-            (newTop  > rackBCR.top  && newTop  < (rackBCR.top + rackBCR.height)) &&
-            (newLeft > rackBCR.left && newLeft < (rackBCR.left + rackBCR.width))
-        ) {
-            const index = Math.floor(
-                (
-                    (newLeft - rackBCR.left) / rackBCR.width
-                ) * playerLetters.length
-            );
-
-            newDest = playerLetters[index];
-
-            if (newDest && newDest.getElementsByClassName("tile")[0]) {
-                let i = index + 1;
-                let tile;
-
-                while (playerLetters[i]) {
-                    tile = playerLetters[i].getElementsByClassName("tile")[0];
-                    if (!tile) {
-                        let j = i;
-                        while (j > index) {
-                            playerLetters[j].appendChild(
-                                playerLetters[j - 1].getElementsByClassName("tile")[0]
-                            );
-                            j--;
-                        }
-                        break;
-                    }
-                    i++;
-                }
-
-                if (newDest.getElementsByClassName("tile")[0]) {
-                    i = index - 1;
-
-                    while (playerLetters[i]) {
-                        tile = playerLetters[i].getElementsByClassName("tile")[0];
-                        if (!tile) {
-                            let j = i;
-                            while (j < index) {
-                                playerLetters[j].appendChild(
-                                    playerLetters[j + 1].getElementsByClassName("tile")[0]
-                                );
-                                j++;
-                            }
-                            break;
-                        }
-                        i--;
-                    }
-                }
-            }
-
-            if (newDest.getElementsByClassName("tile")[0]) {
-                newDest = null;
-            } else {
-                moveCMD.to = "rack";
-                moveCMD.indexTo = index;
-            }
-        } else if (
-            (newTop  > bagBCR.top  && newTop  < (bagBCR.top + bagBCR.height)) &&
-            (newLeft > bagBCR.left && newLeft < (bagBCR.left + bagBCR.width))
-        ) {
-            newDest = bag;
-        }
-
-        if (newDest !== tileDest) {
-            if (tileDest) {
-                tileDest.classList.remove("tile-target");
-            }
-
-            if (newDest) {
-                newDest.classList.add("tile-target");
-            }
-        }
-
-        tileDest = newDest;
-    }
-
-    function dragTileBegin(e) {
-        preventDefault(e);
-
-        if (blockMove || waitingMsgs.length) {
-            return;
-        }
-
-        if (!pollingReady) {
-            showConnectionLost();
-            return;
-        }
-
-        loadAudio();
-
-        movingTile = e.currentTarget;
-
-        tileInitMouseCoords = e;
-        tileInitCoords      = movingTile.getBoundingClientRect();
-        tileInitDest        = movingTile.parentNode;
-
-        boardBCR = board.getBoundingClientRect();
-        rackBCR  = rack.getBoundingClientRect();
-        bagBCR   = bag.getBoundingClientRect();
-
-        let from;
-        let index;
-
-        let p = movingTile.parentNode;
-        let oldP = movingTile;
-        let oldOldP = null;
-
-        while (p) {
-            if (p === board) {
-                from  = "board";
-                index = boardCells.indexOf(oldOldP);
-
-                break;
-            }
-
-            if (p === rack) {
-                from  = "rack";
-                index = playerLetters.indexOf(oldP);
-
-                break;
-            }
-
-            oldOldP = oldP;
-            oldP = p;
-            p = p.parentNode;
-        }
-
-        moveCMD = {
-            cmd: "moveLetter",
-            from: from,
-            indexFrom: index
-        };
-
-        mouseMove(document, dragTileMove);
-        mouseUp(document,   dragTileEnd);
-
-        movingTile.style.left   = tileInitCoords.left + window.scrollX + "px";
-        movingTile.style.top    = tileInitCoords.top  + window.scrollY + "px";
-        movingTile.style.width  = tileInitCoords.width  + "px";
-        movingTile.style.height = tileInitCoords.height + "px";
-
-        document.body.appendChild(movingTile);
-    }
-
-    function setLetter(tile, letter, highlight) {
-        tile.firstChild.textContent = letter;
-        tile.lastChild.textContent = scoreOf[letter] || "";
-        if (highlight) {
-            tile.classList.add("tile-highlight");
-            if (tilesSound.checked) {
-                audioNotification.play();
-            }
-        } else {
-            tile.classList.remove("tile-highlight");
-        }
-    }
-
-    function preventDefault(e) {
-        e.preventDefault();
-        e.stopPropagation();
-        return false;
-    }
-
-    function makeLetter(letter, highlight, noEvents) {
-        const tile = document.createElement("span");
-        tile.translate = false;
-        tile.className = "tile";
-        tile.appendChild(document.createElement("span"));
-        tile.lastChild.className = "tile-letter";
-        tile.appendChild(document.createElement("span"));
-        tile.lastChild.className = "tile-score";
-
-        if (!noEvents) {
-            mouseDown(tile, dragTileBegin);
-
-            tile.addEventListener("contextmenu", preventDefault);
-            tile.addEventListener("touchstart", preventDefault);
-            tile.addEventListener("touchmove", preventDefault);
-        }
-
-        setLetter(tile, letter, highlight);
-        return tile;
-    }
-
-    function setTileParent(p, letter, highlight) {
-        let tile = p.getElementsByClassName("tile")[0];
-        if (tile) {
-            if (letter) {
-                setLetter(tile, letter, highlight);
-            } else {
-                p.removeChild(tile);
-            }
-        } else if (letter) {
-            tile = makeLetter(letter, highlight);
-            p.appendChild(tile);
-        }
-    }
-
-    function setCell(index, letter, highlight) {
-        setTileParent(boardCells[index].getElementsByClassName("tile-placeholder")[0], letter, highlight);
-    }
-
-    function setBoard(board) {
-        for (let i = 0; i < 15 * 15; i++) {
-            setCell(i, board[i]);
-        }
-    }
-
-    function set(key, value) {
-        switch (key) {
-            case "playerName":
-                name.textContent = localStorage.trivabblePlayerName = value;
-                break;
-            case "gameNumber":
-                document.getElementById("number").textContent = localStorage.trivabbleGameNumber = value;
-                break;
-        }
-    }
-
-    function checkGameInProgress(f) {
-        if (!gameInProgress) {
-            return f();
-        }
-
-        myConfirm(
-            _("Your game is not over. Are you sure you want to leave now?"),
-            function () {
-                myAlert(
-                    format(
-                        _("You are about to leave the current game. To recover it, please note its number: {0}"),
-                        localStorage.trivabbleGameNumber
-                    ),
-                    f
-                );
-            }
-        );
-    }
-
-    let pollingReady = false;
-    let retriedImmediately = false;
-
-    function forceReload(msg) {
-        needsRestart = true;
-        myConfirm(
-            msg,
-            function () {
-                location.reload();
-            }, function () {
-                blockMove = 1000;
-                document.getElementById("panel").appendChild(document.createElement("a"));
-                document.getElementById("panel").lastChild.href = "#";
-                document.getElementById("panel").lastChild.onclick = location.reload.bind(location);
-                document.getElementById("panel").lastChild.textContent = _("To continue playing, click here");
-            }
-        );
-    }
-
-    function fatalError(e) {
-        if (!needsRestart) {
-            // No need to show the user that a problem just happened if a message asking to restart has already been shown.
-            forceReload(_("Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."));
-        }
-
-        throw (e || new Error("interrupting the game because of a fatal error"));
-    }
-
-    function jsonError(json, e) {
-        console.error("An error ocurred while parsing this JSON message:", json);
-        fatalError(e);
-    }
-
-    function setRackCell(index, letter) {
-        setTileParent(playerLetters[index], letter);
-    }
-
-    function blinkTransitionEnd(element) {
-        element.removeEventListener("animationend", element._te, false);
-        element.classList.remove("blink");
-    }
-
-    function blink(element) {
-        element._te = blinkTransitionEnd.bind(null, element);
-        element.addEventListener("animationend", element._te, false);
-        setTimeout(element._te, 5000);
-        element.classList.add("blink");
-    }
-
-    function chatMessage(sender, content) {
-        const msgDom = document.createElement("div");
-
-        msgDom.className = "msg";
-
-        if (sender) {
-            msgDom.appendChild(document.createElement("span"));
-            msgDom.lastChild.className = "msg-sender";
-            msgDom.lastChild.textContent = _("{0}: ").replace("{0}", sender);
-        }
-
-        if (content instanceof Element) {
-            msgDom.appendChild(content);
-        } else {
-            msgDom.appendChild(document.createElement("span"));
-            msgDom.lastChild.className = "msg-content";
-            msgDom.lastChild.textContent = content;
-        }
-
-        chatMessages.appendChild(msgDom);
-
-        chatMessages.scrollTop = chatMessages.scrollHeight;
-
-        if (sender && sender !== localStorage.trivabblePlayerName) {
-            if (msgSound.checked) {
-                audioChat.play();
-            }
-
-            blink(chatMessages);
-        }
-
-        return msgDom;
-    }
-
-    function handleChatMessage(msg) {
-        if (msg.specialMsg) {
-            if (msg.specialMsg.type === "rack") {
-                const content = document.createElement("div");
-                content.appendChild(document.createElement("span"));
-                content.lastChild.className = "info";
-                content.lastChild.textContent = format(_("{0} shows their rack:"), msg.sender);
-
-                const letters = document.createElement("div");
-                letters.className = "tile-list";
-
-                for (let i = 0; i < msg.specialMsg.rack.length; i++) {
-                    letters.appendChild(makeLetter(msg.specialMsg.rack[i], false, true));
-                }
-
-                content.appendChild(letters);
-                chatMessage("", content);
-                return;
-            }
-        }
-
-        chatMessage(msg.sender, msg.content);
-    }
-
-
-    function refreshCurrentPlayer() {
-        const row = tablePlayers[currentPlayer];
-        if (currentPlayer && row && !row.classList.contains("current-player")) {
-            row.classList.add("current-player");
-            blink(row.firstChild);
-        }
-    }
-
-    function setCurrentPlayer(player) {
-        if (currentPlayer && tablePlayers[currentPlayer]) {
-            tablePlayers[currentPlayer].classList.remove("current-player");
-        }
-
-        currentPlayer = player;
-        refreshCurrentPlayer();
-    }
-
-    function handleReceivedData(data) {
-        if (Array.isArray(data)) {
-            data.forEach(handleReceivedData);
-            return;
-        }
-
-        if (data.id) {
-            const pos = waitingMsgs.indexOf(data.id);
-            if (pos !== -1) {
-                waitingMsgs.splice(pos, 1);
-            }
-        }
-
-        if (data.version) {
-            serverVersion = data.version;
-        }
-
-        if (data.pleaseRestart) {
-            forceReload(_("Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"));
-        }
-
-        if (data.error) {
-            fatalError(new Error("Error from the server: " + data.error + " - " + data.reason));
-        }
-
-        if (data.stopping) {
-            stopConnection();
-
-            retryPolling(
-                (typeof data.stopping === "number" && data.stopping > 0)
-                    ? data.stopping
-                    : 0
-            );
-        }
-
-        if (data.msg) {
-            handleChatMessage(data.msg);
-        }
-
-        if (typeof data.currentPlayer === "string") {
-            setCurrentPlayer(data.currentPlayer);
-        }
-
-        if (data.players) {
-            if (participantPlaceholder) {
-                participantPlaceholder.parentNode.removeChild(participantPlaceholder);
-                participantPlaceholder = null;
-            }
-
-            for (let i = 0; i < data.players.length; i++) {
-                const player = data.players[i];
-                const playerName = data.players[i].player;
-
-                if (!tablePlayers[playerName]) {
-                    let before = null;
-
-                    for (let j = 1;  j < participants.rows.length; j++) {
-                        if (playerName < participants.rows[j].cells[0].textContent) {
-                            before = participants.rows[j];
-                            break;
-                        }
-                    }
-
-                    const row = document.createElement("tr");
-                    participants.insertBefore(row, before);
-                    row.appendChild(document.createElement("td"));
-                    row.lastChild.textContent = playerName;
-                    row.appendChild(document.createElement("td"));
-                    row.appendChild(document.createElement("td"));
-
-                    row.lastChild.className = "score-cell";
-                    row.lastChild.onclick = (
-                        function (row) {
-                            return function () {
-                                if (!pollingReady) {
-                                    showConnectionLost();
-                                    return;
-                                }
-
-                                myPrompt(
-                                    format(
-                                        _("Enter the new score of {0}:"),
-                                        row.firstChild.textContent
-                                    ),
-                                    function (res) {
-                                        res = parseInt(res);
-                                        if (!isNaN(res)) {
-                                            sendCmds([{
-                                                cmd: "score",
-                                                player: row.firstChild.textContent,
-                                                score: res
-                                            }]);
-                                        }
-                                    },
-                                    row.lastChild.textContent
-                                );
-                            };
-                        }
-                    )(row);
-
-                    row.appendChild(document.createElement("td"));
-                    row.lastChild.className = "turn-cell";
-                    row.lastChild.appendChild(document.createElement("button"));
-                    const img = new Image();
-                    img.src = "baton.svg";
-                    row.lastChild.lastChild.appendChild(img);
-                    row.lastChild.lastChild.onclick = (function (playerName) {
-                        sendCmds([{
-                            cmd: "currentPlayer",
-                            player: playerName === currentPlayer ? "" : playerName
-                        }]);
-                    }).bind(null, playerName);
-
-                    tablePlayers[playerName] = row;
-                }
-
-                if (Object.prototype.hasOwnProperty.call(player, "score")) {
-                    const scoreCell = tablePlayers[playerName].childNodes[2];
-                    scoreCell.textContent = player.score;
-                    blink(scoreCell);
-                }
-
-                if (Object.prototype.hasOwnProperty.call(player, "rackCount")) {
-                    const countCell = tablePlayers[playerName].childNodes[1];
-                    countCell.textContent = player.rackCount;
-                    blink(countCell);
-                }
-            }
-
-            refreshCurrentPlayer();
-        }
-
-        if (data.playerName) {
-            set("playerName", data.playerName);
-        }
-
-        if (data.gameNumber) {
-            set("gameNumber", data.gameNumber);
-        }
-
-
-        if (data.letterValues) {
-            scoreOf = data.letterValues;
-        }
-
-        if (data.board) {
-            setBoard(data.board);
-        }
-
-        if (typeof data.remainingLetters === "number") {
-            remainingLetters = data.remainingLetters;
-            document.getElementById("remaining-letters").textContent = data.remainingLetters;
-            if (data.remainingLetters === 0) {
-                helpBag.style.display = "none";
-                helpClear.style.display = "";
-            } else {
-                helpBag.style.display = "";
-                helpClear.style.display = "none";
-            }
-        }
-
-        if (data.rack) {
-            setRack(data.rack);
-        }
-
-        if (!data.action) {
-            return;
-        }
-
-        switch (data.action) {
-            case "pushBag": //TODO
-                break;
-
-            case "popBag": //TODO
-                break;
-
-            case "reset": //TODO
-                tablePlayers = {};
-                while (participants.rows[1]) {
-                    participants.removeChild(participants.rows[1]);
-                }
-                sendCmds([{cmd: "hello"}]);
-                break;
-
-            case "moveLetter":
-                if (data.from === "board") {
-                    setCell(data.indexFrom, "");
-                } else if (data.from === "rack") {
-                    setRackCell(data.indexFrom, "");
-                }
-
-                if (data.to === "board") {
-                    setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
-                } else if (data.to === "rack") {
-                    setRackCell(data.indexTo, data.letter);
-                }
-                break;
-            case "setCell":
-                setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
-                break;
-            case "setRackCell":
-                setRackCell(data.indexTo, data.letter);
-        }
-    }
-
-    function retryPolling(delay) {
-        if (needsRestart) {
-            return;
-        }
-
-        pollingReady = false;
-
-        if (!boundEventShowConnectionLost) {
-            boundEventShowConnectionLost = showConnectionLost.bind(null, null);
-
-            document.body.addEventListener("mousemove", boundEventShowConnectionLost);
-            document.body.addEventListener("mousedown", boundEventShowConnectionLost);
-            document.body.addEventListener("touchstart", boundEventShowConnectionLost);
-        }
-
-        if (retryPollingTimeout) {
-            return;
-        }
-
-        if (delay || retriedImmediately) {
-            retryPollingTimeout = setTimeout(
-                function () {
-                    connectionStopped = false;
-                    pollingServer = false;
-                    startConnection();
-                    retryPollingTimeout = 0;
-                },
-                delay || Conf.POLLING_DELAY
-            );
-        } else {
-            retriedImmediately = true;
-            connectionStopped = false;
-            pollingServer = false;
-            startConnection();
-        }
-    }
-
-    function parseAndHandleReceivedData(r) {
-        let m;
-
-        try {
-            m = JSON.parse(r);
-        } catch (e) {
-            jsonError(r, e);
-        }
-
-        try {
-            handleReceivedData(m);
-        } catch (e) {
-            fatalError(e);
-        }
-    }
-
-    function closeConnections() {
-        if (eventSource) {
-            eventSource.onerror = null;
-            eventSource.close();
-            eventSource = null;
-        }
-
-        if (webSocket) {
-            webSocket.onclose = null;
-            webSocket.onerror = null;
-            webSocket.close();
-            webSocket = null;
-        }
-    }
-
-    function stopConnection() {
-        closeConnections();
-
-        if (retryPollingTimeout) {
-            clearTimeout(retryPollingTimeout);
-            retryPollingTimeout = 0;
-        }
-
-        connectionStopped = true;
-        pollingServer = false;
-        pollingReady = false;
-    }
-
-    let blacklistWebsockets = false;
-    let webSocketErrors = 0;
-
-    function bindConnectionEvents(connection) {
-        connection.onopen = connectionReady;
-
-        connection.onmessage = function (e) {
-            if (connection === webSocket) {
-                webSocketErrors = 0;
-            }
-
-            connectionReady();
-            parseAndHandleReceivedData(e.data);
-        };
-
-        connection.onerror = function (e) {
-            console.error("Connection error:", e);
-
-            if (connection === eventSource) {
-                retryPolling();
-            } else {
-                webSocketErrors++;
-
-                if (webSocketErrors > Conf.MAX_WEBSOCKET_ERRORS) {
-                    blacklistWebsockets = true;
-                }
-            }
-        };
-
-        if (connection === webSocket) {
-            connection.onclose = function (e) {
-                webSocket = null;
-                if (e.code === 1002 || e.code === 1003) {
-                    // CLOSE_PROTOCOL_ERROR or CLOSE_UNSUPPORTED
-                    blacklistWebsockets = true;
-                    pollServerWithEventSource();
-                    return;
-                }
-                retryPolling();
-            };
-        }
-    }
-
-    function canConnect() {
-        return !pollingServer && !needsRestart && !connectionStopped && navigator.onLine !== false;
-        // DO NOT change navigator.onLine !== false by navigator.onLine === true
-        // This would block connection on a browser not supporting this property
-    }
-
-    function pollServerWithEventSource() {
-        if (canConnect() && Conf.ENABLE_EVENT_SOURCE && window.EventSource) {
-            closeConnections();
-            pollingServer = true;
-
-            eventSource = new EventSource(getApiEntryPoint() + "/sse/" + JSON.stringify(cmdsWithContext()));
-            bindConnectionEvents(eventSource);
-            return;
-        }
-
-        pollServerWithXHR();
-    }
-
-    function pollServerWithWebSocket() {
-        if (canConnect() && Conf.ENABLE_WEBSOCKETS && !blacklistWebsockets && window.WebSocket) {
-            closeConnections();
-            pollingServer = true;
-
-            webSocket = new WebSocket(
-                (window.location.protocol === "http:" ? "ws://" : "wss://") +
-                window.location.host +
-                getApiEntryPoint() + "/ws/" +
-                JSON.stringify(cmdsWithContext())
-            );
-
-            bindConnectionEvents(webSocket);
-            return;
-        }
-
-        pollServerWithEventSource();
-    }
-
-    function xhrRequest(data, onreadystatechange) {
-        const xhr = new XMLHttpRequest();
-
-        xhr.open("POST", getApiEntryPoint(), true);
-        xhr.setRequestHeader("Content-Type", "text/plain");
-        xhr.send(JSON.stringify(data));
-
-        xhr.onreadystatechange = onreadystatechange.bind(null, xhr);
-    }
-
-    function pollServerWithXHR() {
-        if (!canConnect()) {
-            return;
-        }
-
-        pollingServer = true;
-
-        let currentIndex = 0;
-        let expectedLength = 0;
-
-        xhrRequest(cmdsWithContext(), function (xhr) {
-            if (xhr.readyState === 2) {
-                connectionReady();
-            } else if (xhr.readyState === 3) {
-                connectionReady();
-
-                while (true) {
-                    if (!expectedLength) {
-                        let i = currentIndex;
-                        while (i < xhr.responseText.length) {
-                            if ("0123456789".indexOf(xhr.responseText.charAt(i)) === -1) {
-                                expectedLength = parseInt(xhr.responseText.substring(currentIndex, i));
-                                currentIndex = i;
-                                break;
-                            }
-                            ++i;
-                        }
-                    }
-
-                    if (expectedLength && (xhr.responseText.length >= currentIndex + expectedLength)) {
-                        const end = currentIndex + expectedLength;
-                        let msgs;
-
-                        try {
-                            msgs = JSON.parse(
-                                xhr.responseText.substring(
-                                    currentIndex,
-                                    end
-                                )
-                            );
-                            currentIndex = end;
-                            expectedLength = 0;
-                        } catch (e) {
-                            jsonError(xhr.responseText.substring(
-                                currentIndex,
-                                end
-                            ), e);
-                        }
-
-                        handleReceivedData(msgs);
-                    } else {
-                        break;
-                    }
-                }
-            } else if (xhr.readyState === 4) {
-                startConnection();
-            }
-        });
-    }
-
-    function startConnection() {
-        pollServerWithWebSocket();
-    }
-
-    function sendXHR(cmds) {
-        if (needsRestart) {
-            return;
-        }
-
-        blockMove++;
-
-        xhrRequest(cmdsWithContext(cmds), function (xhr) {
-            if (xhr.readyState === 4) {
-                if (xhr.status === 0 || xhr.status >= 300) {
-                    setTimeout(sendCmds.bind(null, cmds), Conf.POLLING_DELAY);
-                    return;
-                }
-
-                parseAndHandleReceivedData(xhr.responseText);
-
-                blockMove--;
-
-                if (!pollingReady) {
-                    startConnection();
-                }
-            }
-        });
-    }
-
-    function cmdsWithContext(cmds) {
-        return {
-            gameNumber: localStorage.trivabbleGameNumber || "",
-            playerName: localStorage.trivabblePlayerName,
-            version: VERSION,
-            cmds: cmds
-        };
-    }
-
-    function sendCmds(cmds) {
-        if (webSocket && webSocket.readyState === 1) {
-            if (serverVersion >= 202004281800) {
-                for (const cmd of cmds) {
-                    waitingMsgs.push(currentMessageId);
-                    cmd.id = currentMessageId++;
-                }
-            }
-
-            webSocket.send(JSON.stringify(cmds));
-            return;
-        }
-
-        if (!pollingReady) {
-            startConnection();
-            setTimeout(sendCmds, Conf.POLLING_DELAY, cmds);
-            return;
-        }
-
-        sendXHR(cmds);
-    }
-
-    function joinGame() {
-        checkGameInProgress(
-            function () {
-                myPrompt(
-                    _("To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.").replace("{0}", localStorage.trivabbleGameNumber),
-                    function (n) {
-                        n = parseInt(n);
-                        if (isNaN(n)) {
-                            myAlert(_("It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."));
-                        } else {
-                            localStorage.trivabbleGameNumber = n;
-                            location.reload();
-                        }
-                    }
-                );
-            }
-        );
-    }
-
-    const specialTypesText = {
-        doubleLetter: _("Double\nLetter"),
-        doubleWord:   _("Double\nWord"),
-        tripleLetter: _("Triple\nLetter"),
-        tripleWord:   _("Triple\nWord")
-    };
-
-    function specialCell(type, cell) {
-        cell.firstChild.appendChild(document.createElement("span"));
-        cell.classList.add("special-cell");
-        cell.classList.add("special-cell-" + type);
-        cell.lastChild.lastChild.textContent = _(specialTypesText[type]);
-        cell.lastChild.lastChild.className = "special-cell-label";
-    }
-
-    function changeName() {
-        myPrompt(
-            _("To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."),
-            function (newName) {
-                if (newName && newName.trim()) {
-                    localStorage.trivabblePlayerName = newName.trim();
-                    name.textContent = localStorage.trivabblePlayerName;
-                }
-            },
-            localStorage.trivabblePlayerName
-        );
-    }
-
-    function startGame(number) {
-        if (number) {
-            localStorage.trivabbleGameNumber = number;
-        }
-        startConnection();
-    }
-
-    let audioTileLoaded = false;
-    let audioMsgLoaded = false;
-    let tilesSound;
-    let msgSound;
-
-    function loadAudio() {
-        if (!audioTileLoaded && tilesSound.checked) {
-            audioTileLoaded = true;
-            audioNotification.load();
-        }
-
-        if (!audioMsgLoaded  && msgSound.checked) {
-            audioMsgLoaded = true;
-            audioChat.load();
-        }
-    }
-
-    function bagClicked(e) {
-        preventDefault(e);
-
-        if (blockMove || waitingMsgs.length) {
-            return;
-        }
-
-        if (!remainingLetters) {
-            myAlert(_("You cannot take another tile: the bag is empty."));
-            return;
-        }
-
-        loadAudio();
-
-        if (!pollingReady) {
-            showConnectionLost();
-            return;
-        }
-
-        const index = getFreeRackSpaceIndex();
-        if (index === -1) {
-            myAlert(_("You cannot take another tile: your rack is full."));
-        } else {
-            sendCmds(
-                [{cmd: "moveLetter", from: "bag", to: "rack", indexTo: index}]
-            );
-        }
-    }
-
-    function clearGame() {
-        myConfirm(
-            _("Are you sure you want to put all the tiles back in the bag (in order to play another game)?"),
-            function () {
-                sendCmds([{cmd: "resetGame"}]);
-            }
-        );
-    }
-
-    function clearRack() {
-        myConfirm(
-            _("Are you sure you want to put all your tiles back in the bag?"),
-            function () {
-                sendCmds([
-                    {cmd: "moveLetter", from: "rack", indexFrom: 0, to: "bag", indexTo: -1},
-                    {cmd: "moveLetter", from: "rack", indexFrom: 1, to: "bag", indexTo: -1},
-                    {cmd: "moveLetter", from: "rack", indexFrom: 2, to: "bag", indexTo: -1},
-                    {cmd: "moveLetter", from: "rack", indexFrom: 3, to: "bag", indexTo: -1},
-                    {cmd: "moveLetter", from: "rack", indexFrom: 4, to: "bag", indexTo: -1},
-                    {cmd: "moveLetter", from: "rack", indexFrom: 5, to: "bag", indexTo: -1},
-                    {cmd: "moveLetter", from: "rack", indexFrom: 6, to: "bag", indexTo: -1}
-                ]);
-                setRack("");
-            }
-        );
-    }
-
-    function showRack() {
-        myConfirm(
-            _("Are you sure you want to show your rack to everybody?"),
-            function () {
-                const letters = playerLetters.map(
-                    function (tilePlaceholer) {
-                        return tilePlaceholer.querySelector(".tile");
-                    }
-                ).filter(function (tile) {
-                    return tile !== null;
-                });
-
-                sendCmds([{
-                    cmd: "msg",
-                    msg: _("Here is my rack:") + " " + letters.map(
-                        function (tile) {
-                            return (
-                                tile.textContent === ""
-                                    ? _("joker")
-                                    : tile.querySelector(".tile-letter").textContent
-                            );
-                        }
-                    ).join(", "),
-                    specialMsg: {
-                        type: "rack",
-                        rack: letters.map(
-                            function (tile) {
-                                return tile.querySelector(".tile-letter").textContent;
-                            }
-                        )
-                    }
-                }]);
-            }
-        );
-
-    }
-
-    function initChat() {
-        chatMessages.style.width = chatMessages.offsetWidth + "px";
-
-        const btn = document.getElementById("chat-btn");
-
-        chatTextarea.onmouseup = function () {
-            chatMessages.style.width = chatTextarea.offsetWidth + "px";
-        };
-
-        btn.onclick = function () {
-            if (!pollingReady) {
-                return;
-            }
-
-            loadAudio();
-            sendCmds([{
-                cmd: "msg",
-                msg: chatTextarea.value
-            }]);
-
-            chatTextarea.value = "";
-        };
-
-        chatTextarea.onkeydown = function (e) {
-            if (e.keyCode === 13) {
-                preventDefault(e);
-                chatTextarea.focus();
-                btn.onclick();
-            }
-        };
-    }
-
-    function initSound() {
-        audioNotification = new Audio();
-        audioNotification.preload = "auto";
-        audioNotification.volume  = 1;
-
-        let audioSourceOGG = document.createElement("source");
-        audioSourceOGG.src = "notification.ogg";
-
-        let audioSourceMP3 = document.createElement("source");
-        audioSourceMP3.src = "notification.mp3";
-        audioNotification.appendChild(audioSourceOGG);
-        audioNotification.appendChild(audioSourceMP3);
-
-        audioChat = new Audio();
-        audioChat.preload = "auto";
-        audioChat.volume  = 1;
-
-        audioSourceOGG = document.createElement("source");
-        audioSourceOGG.src = "receive.ogg";
-
-        audioSourceMP3 = document.createElement("source");
-        audioSourceMP3.src = "receive.mp3";
-        audioChat.appendChild(audioSourceOGG);
-        audioChat.appendChild(audioSourceMP3);
-
-
-        tilesSound = document.getElementById("tiles-sound");
-        msgSound = document.getElementById("msg-sound");
-
-        tilesSound.onclick = function () {
-            localStorage.trivabbleTileSound = tilesSound.checked;
-        };
-
-        msgSound.onclick = function () {
-            localStorage.trivabbleMsgSound = msgSound.checked;
-        };
-
-        if (Object.prototype.hasOwnProperty.call(localStorage, "trivabbleMsgSound")) {
-            msgSound.checked = localStorage.trivabbleMsgSound === "true";
-        } else {
-            localStorage.trivabbleMsgSound = msgSound.checked;
-        }
-
-        if (Object.prototype.hasOwnProperty.call(localStorage, "trivabbleTileSound")) {
-            tilesSound.checked = localStorage.trivabbleTileSound === "true";
-        } else {
-            localStorage.trivabbleTilesSound = tilesSound.checked;
-        }
-    }
-
-    function repromptName(f) {
-        if (localStorage.trivabblePlayerName && localStorage.trivabblePlayerName.trim()) {
-            f();
-        } else {
-            myPrompt(
-                _("It seems your did not give your name. You need to do it for the game to run properly."),
-                function (name) {
-                    if (name && name.trim()) {
-                        localStorage.trivabblePlayerName = name.trim();
-                    }
-
-                    repromptName(f);
-                }
-            );
-        }
-    }
-
-    trivabble.applyL10n = function () {
-        document.documentElement.lang = libD.lang;
-        document.documentElement.setAttribute("xml:lang", libD.lang);
-
-        for (const node of [].slice.call(document.querySelectorAll("[data-l10n]"))) {
-            if (node.dataset.l10n === "text-content") {
-                node.textContent = _(node.textContent.trim());
-            } else {
-                node.setAttribute(node.dataset.l10n, _(node.getAttribute(node.dataset.l10n)));
-            }
-        }
-
-        trivabble.run();
-    };
-
-    function langSelectionChange(e) {
-        localStorage.trivabbleLang = e.target.value;
-        location.reload();
-    }
-
-    function initGlobals() {
-        board         = document.getElementById("board");
-        rack          = document.getElementById("rack");
-        participants  = document.getElementById("participants");
-        name          = document.getElementById("name");
-        bag           = document.getElementById("bag");
-        chatMessages  = document.getElementById("chat-messages");
-        chatTextarea  = document.getElementById("chat-ta");
-        helpBag       = document.getElementById("help-bag");
-        helpClear     = document.getElementById("help-clear");
-        participantPlaceholder = document.getElementById("participants-placeholder");
-    }
-
-    function initEvents() {
-        mouseDown(bag, bagClicked);
-
-        document.getElementById("clear-game").onclick  = clearGame;
-        document.getElementById("change-name").onclick = changeName;
-        document.getElementById("join-game").onclick   = joinGame;
-        document.getElementById("clear-rack").onclick  = clearRack;
-        document.getElementById("show-rack").onclick   = showRack;
-        helpClear.onclick  = clearGame;
-    }
-
-    function initGame() {
-        if (!localStorage.trivabblePlayerName) {
-            myPrompt(
-                _("Hello! To begin, enter your name. Your adversaries will see this name when you play with them."),
-                function (name) {
-                    if (name && name.trim()) {
-                        localStorage.trivabblePlayerName = name;
-                    }
-                    repromptName(initGame);
-                }
-            );
-
-            return;
-        }
-
-        name.textContent = localStorage.trivabblePlayerName;
-
-        const letters = "ABCDEFGHIJKLMNO";
-
-        const doubleLetter = {
-            "0,3":  true,
-            "0,11": true,
-            "2,6":  true,
-            "2,8":  true,
-            "3,0":  true,
-            "3,7":  true,
-            "3,14": true,
-            "6,2":  true,
-            "6,6":  true,
-            "6,8":  true,
-            "6,12": true
-        };
-
-        let cell;
-
-        for (let i = 0; i < 7; i++) {
-            const span = document.createElement("span");
-            span.className = "tile-placeholder";
-            rack.appendChild(span);
-            playerLetters.push(span);
-        }
-
-        for (let i = 0; i < 15; i++) {
-            board.rows[0].appendChild(document.createElement("th"));
-            board.rows[0].lastChild.textContent = i + 1;
-
-            board.appendChild(document.createElement("tr"));
-            board.lastChild.appendChild(document.createElement("th"));
-            board.lastChild.lastChild.textContent = letters[i];
-            for (let j = 0; j < 15; j++) {
-                cell = document.createElement("td");
-                boardCells.push(cell);
-                board.lastChild.appendChild(cell);
-                cell.appendChild(document.createElement("div"));
-                cell.lastChild.className = "tile-placeholder";
-
-                if (i === j && i === 7) {
-                    specialCell("doubleWord", board.lastChild.lastChild);
-                    cell = board.lastChild.lastChild.getElementsByClassName("special-cell-label")[0];
-                    cell.textContent = "★";
-                    board.lastChild.lastChild.id = "center-cell";
-                } else if (i % 7 === 0 && j % 7 === 0) {
-                    specialCell("tripleWord", board.lastChild.lastChild);
-                } else if ((i === j || i + j === 14) && (i < 5 || i > 9)) {
-                    specialCell("doubleWord", board.lastChild.lastChild);
-                } else if ((i % 4 === 1) && (j % 4 === 1)) {
-                    specialCell("tripleLetter", board.lastChild.lastChild);
-                } else if ((i < 8 && doubleLetter[i + "," + j]) || (i > 7 && doubleLetter[(14-i) + "," + j]) || (i === 7 && (j === 3 || j === 11))) {
-                    specialCell("doubleLetter", board.lastChild.lastChild);
-                }
-            }
-
-            board.lastChild.appendChild(document.createElement("th"));
-            board.lastChild.lastChild.textContent = letters[i];
-        }
-
-        board.rows[0].appendChild(board.rows[0].cells[0].cloneNode(false));
-
-        board.appendChild(document.createElement("tr"));
-        board.lastChild.appendChild(board.rows[0].cells[0].cloneNode(false));
-
-        for (let i = 0; i < 15; i++) {
-            board.lastChild.appendChild(document.createElement("th"));
-            board.lastChild.lastChild.textContent = i + 1;
-        }
-
-        board.lastChild.appendChild(board.rows[0].cells[0].cloneNode(false));
-
-        if (localStorage.trivabbleGameNumber) {
-            document.getElementById("number").textContent = localStorage.trivabbleGameNumber;
-        }
-
-        startGame(localStorage.trivabbleGameNumber);
-    }
-
-    function initLang() {
-        const lang = libD.lang = localStorage.trivabbleLang || libD.lang;
-
-        const langSel = document.getElementById("select-lang");
-        langSel.value = lang;
-        langSel.onchange = langSelectionChange;
-
-        const script = document.createElement("script");
-        script.src = "l10n/js/" + lang + ".js";
-        script.onerror = trivabble.l10nError;
-        document.getElementsByTagName("head")[0].appendChild(script);
-    }
-
-    trivabble.run = function () {
-        initGlobals();
-        initEvents();
-        initChat();
-        initGame();
-        initSound();
-    };
-
-    trivabble.l10nError = trivabble.run;
-
-    window.addEventListener("DOMContentLoaded", initLang);
-
-    window.addEventListener("beforeunload", stopConnection);
-
-    window.addEventListener("offline", function () {
-        stopConnection();
-        showConnectionLost(_("It seems your browser went offline. Please reconnect to continue playing."));
-    });
-
-    window.addEventListener("online", startConnection);
-
-}());