From 98db2082fd31e7a7bc0348e31ce119f39dbc31b3 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 23 Jan 2019 16:33:21 +0100
Subject: [PATCH] Advance on client side

---
 client/TODO                                   | 10 --
 client/client_OLD/javascripts/contactForm.js  | 32 ------
 .../{shared => data}/challengeCheck.js        |  0
 .../javascripts/{shared => data}/nbPlayers.js |  0
 .../javascripts/{shared => data}/userCheck.js |  0
 client/client_OLD/javascripts/utils/misc.js   | 47 ---------
 client/client_OLD/javascripts/variant.js      |  7 --
 client/client_OLD/views/app.pug               | 51 ----------
 client/client_OLD/views/modals.pug            | 66 -------------
 client/client_OLD/views/translations/en.pug   | 97 -------------------
 client/client_OLD/views/translations/es.pug   | 90 -----------------
 client/client_OLD/views/translations/fr.pug   | 90 -----------------
 client/client_OLD/views/welcome/en.pug        | 49 ----------
 client/client_OLD/views/welcome/es.pug        | 52 ----------
 client/client_OLD/views/welcome/fr.pug        | 49 ----------
 client/package-lock.json                      | 29 ++++++
 client/package.json                           |  1 +
 {server/favicon => client/public}/SOURCE      |  3 +-
 client/public/index.html                      |  6 +-
 client/src/App.vue                            | 52 ++++++++--
 client/src/components/ContactForm.vue         | 61 ++++++++++++
 client/src/components/Language.vue            | 33 +++++++
 client/src/components/Settings.vue            | 51 ++++++++++
 client/src/main.js                            | 22 ++++-
 client/src/modals/welcome/en.pug              | 49 ++++++++++
 client/src/modals/welcome/es.pug              | 35 +++++++
 client/src/modals/welcome/fr.pug              | 40 ++++++++
 .../views => src}/rules/Alice/en.pug          |  0
 .../views => src}/rules/Alice/es.pug          |  0
 .../views => src}/rules/Alice/fr.pug          |  0
 .../views => src}/rules/Antiking/en.pug       |  0
 .../views => src}/rules/Antiking/es.pug       |  0
 .../views => src}/rules/Antiking/fr.pug       |  0
 .../views => src}/rules/Atomic/en.pug         |  0
 .../views => src}/rules/Atomic/es.pug         |  0
 .../views => src}/rules/Atomic/fr.pug         |  0
 .../views => src}/rules/Baroque/en.pug        |  0
 .../views => src}/rules/Baroque/fr.pug        |  0
 .../views => src}/rules/Berolina/en.pug       |  0
 .../views => src}/rules/Berolina/fr.pug       |  0
 .../views => src}/rules/Checkered/en.pug      |  0
 .../views => src}/rules/Checkered/fr.pug      |  0
 .../views => src}/rules/Chess960/en.pug       |  0
 .../views => src}/rules/Chess960/fr.pug       |  0
 .../views => src}/rules/Crazyhouse/en.pug     |  0
 .../views => src}/rules/Crazyhouse/fr.pug     |  0
 .../views => src}/rules/Dark/en.pug           |  0
 .../views => src}/rules/Dark/fr.pug           |  0
 .../views => src}/rules/Extinction/en.pug     |  0
 .../views => src}/rules/Extinction/fr.pug     |  0
 .../views => src}/rules/Grand/en.pug          |  0
 .../views => src}/rules/Grand/fr.pug          |  0
 .../views => src}/rules/Losers/en.pug         |  0
 .../views => src}/rules/Losers/fr.pug         |  0
 .../views => src}/rules/Magnetic/en.pug       |  0
 .../views => src}/rules/Magnetic/fr.pug       |  0
 .../views => src}/rules/Marseille/en.pug      |  0
 .../views => src}/rules/Marseille/fr.pug      |  0
 .../views => src}/rules/Switching/en.pug      |  0
 .../views => src}/rules/Switching/fr.pug      |  0
 .../views => src}/rules/Upsidedown/en.pug     |  0
 .../views => src}/rules/Upsidedown/fr.pug     |  0
 .../views => src}/rules/Wildebeest/en.pug     |  0
 .../views => src}/rules/Wildebeest/fr.pug     |  0
 .../views => src}/rules/Zen/en.pug            |  0
 .../views => src}/rules/Zen/fr.pug            |  0
 client/src/translations/en.js                 | 96 ++++++++++++++++++
 client/src/translations/es.js                 | 89 +++++++++++++++++
 client/src/translations/fr.js                 | 89 +++++++++++++++++
 client/src/utils/misc.js                      | 43 ++++++++
 client/src/views/Home.vue                     |  2 +-
 server/.gitignore                             |  2 +-
 server/TODO                                   |  9 ++
 server/app.js                                 |  8 +-
 server/config/parameters.js.dist              |  6 ++
 server/data/challengeCheck.js                 | 34 -------
 server/data/nbPlayers.js                      | 23 -----
 server/data/userCheck.js                      | 19 ----
 server/favicon/android-chrome-192x192.png     |  1 -
 server/favicon/android-chrome-512x512.png     |  1 -
 server/favicon/apple-touch-icon.png           |  1 -
 server/favicon/browserconfig.xml              |  9 --
 server/favicon/favicon-16x16.png              |  1 -
 server/favicon/favicon-32x32.png              |  1 -
 server/favicon/favicon.ico                    |  1 -
 server/favicon/manifest.json                  | 18 ----
 server/favicon/mstile-150x150.png             |  1 -
 server/favicon/safari-pinned-tab.svg          | 85 ----------------
 server/models/Challenge.js                    | 33 +++++++
 server/models/User.js                         | 18 ++++
 server/models/Variant.js                      | 23 +++++
 server/routes/challenges.js                   |  3 +-
 server/routes/users.js                        |  7 +-
 server/sockets.js                             | 11 ++-
 94 files changed, 791 insertions(+), 865 deletions(-)
 delete mode 100644 client/TODO
 delete mode 100644 client/client_OLD/javascripts/contactForm.js
 rename client/client_OLD/javascripts/{shared => data}/challengeCheck.js (100%)
 rename client/client_OLD/javascripts/{shared => data}/nbPlayers.js (100%)
 rename client/client_OLD/javascripts/{shared => data}/userCheck.js (100%)
 delete mode 100644 client/client_OLD/javascripts/utils/misc.js
 delete mode 100644 client/client_OLD/views/modals.pug
 delete mode 100644 client/client_OLD/views/translations/en.pug
 delete mode 100644 client/client_OLD/views/translations/es.pug
 delete mode 100644 client/client_OLD/views/translations/fr.pug
 delete mode 100644 client/client_OLD/views/welcome/en.pug
 delete mode 100644 client/client_OLD/views/welcome/es.pug
 delete mode 100644 client/client_OLD/views/welcome/fr.pug
 rename {server/favicon => client/public}/SOURCE (60%)
 create mode 100644 client/src/components/ContactForm.vue
 create mode 100644 client/src/components/Language.vue
 create mode 100644 client/src/components/Settings.vue
 create mode 100644 client/src/modals/welcome/en.pug
 create mode 100644 client/src/modals/welcome/es.pug
 create mode 100644 client/src/modals/welcome/fr.pug
 rename client/{client_OLD/views => src}/rules/Alice/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Alice/es.pug (100%)
 rename client/{client_OLD/views => src}/rules/Alice/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Antiking/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Antiking/es.pug (100%)
 rename client/{client_OLD/views => src}/rules/Antiking/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Atomic/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Atomic/es.pug (100%)
 rename client/{client_OLD/views => src}/rules/Atomic/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Baroque/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Baroque/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Berolina/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Berolina/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Checkered/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Checkered/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Chess960/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Chess960/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Crazyhouse/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Crazyhouse/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Dark/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Dark/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Extinction/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Extinction/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Grand/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Grand/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Losers/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Losers/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Magnetic/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Magnetic/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Marseille/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Marseille/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Switching/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Switching/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Upsidedown/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Upsidedown/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Wildebeest/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Wildebeest/fr.pug (100%)
 rename client/{client_OLD/views => src}/rules/Zen/en.pug (100%)
 rename client/{client_OLD/views => src}/rules/Zen/fr.pug (100%)
 create mode 100644 client/src/translations/en.js
 create mode 100644 client/src/translations/es.js
 create mode 100644 client/src/translations/fr.js
 create mode 100644 client/src/utils/misc.js
 create mode 100644 server/TODO
 delete mode 100644 server/data/challengeCheck.js
 delete mode 100644 server/data/nbPlayers.js
 delete mode 100644 server/data/userCheck.js
 delete mode 100644 server/favicon/android-chrome-192x192.png
 delete mode 100644 server/favicon/android-chrome-512x512.png
 delete mode 100644 server/favicon/apple-touch-icon.png
 delete mode 100644 server/favicon/browserconfig.xml
 delete mode 100644 server/favicon/favicon-16x16.png
 delete mode 100644 server/favicon/favicon-32x32.png
 delete mode 100644 server/favicon/favicon.ico
 delete mode 100644 server/favicon/manifest.json
 delete mode 100644 server/favicon/mstile-150x150.png
 delete mode 100644 server/favicon/safari-pinned-tab.svg

diff --git a/client/TODO b/client/TODO
deleted file mode 100644
index 58732706..00000000
--- a/client/TODO
+++ /dev/null
@@ -1,10 +0,0 @@
-En dev, faire tourner les deux serveurs 3000 et 8080, server + client
-En prod, client est statique ==> juste serveur, comme d'hab
-
-index.js ne va rien chercher sur serveur, pas même les trucs indispensables comme variant ou variantArray
-==> ils sont demandés au serveur en arrivant sur la page (avec éventuellement "Variant does not exist")
-
-https://alligator.io/vuejs/lazy-loading-vue-cli-3-webpack/
-https://webpack.js.org/guides/code-splitting/#dynamic-imports
-https://vue-loader.vuejs.org/guide/pre-processors.html#pug
-https://cli.vuejs.org/guide/webpack.html#simple-configuration
diff --git a/client/client_OLD/javascripts/contactForm.js b/client/client_OLD/javascripts/contactForm.js
deleted file mode 100644
index 8b1a079f..00000000
--- a/client/client_OLD/javascripts/contactForm.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// Note: not using Vue, but would be possible
-function trySendMessage()
-{
-	let email = document.getElementById("userEmail");
-	let subject = document.getElementById("mailSubject");
-	let content = document.getElementById("mailContent");
-	const error = checkNameEmail({email: email});
-	if (!!error)
-		return alert(error);
-	if (content.value.trim().length == 0)
-		return alert("Empty message");
-	if (subject.value.trim().length == 0 && !confirm("No subject. Send anyway?"))
-		return;
-
-	// Message sending:
-	ajax(
-		"/messages",
-		"POST",
-		{
-			email: email.value,
-			subject: subject.value,
-			content: content.value,
-		},
-		() => {
-			subject.value = "";
-			content.value = "";
-			let emailSent = document.getElementById("emailSent");
-			emailSent.style.display = "inline-block";
-			setTimeout(() => { emailSent.style.display = "none"; }, 2000);
-		}
-	);
-}
diff --git a/client/client_OLD/javascripts/shared/challengeCheck.js b/client/client_OLD/javascripts/data/challengeCheck.js
similarity index 100%
rename from client/client_OLD/javascripts/shared/challengeCheck.js
rename to client/client_OLD/javascripts/data/challengeCheck.js
diff --git a/client/client_OLD/javascripts/shared/nbPlayers.js b/client/client_OLD/javascripts/data/nbPlayers.js
similarity index 100%
rename from client/client_OLD/javascripts/shared/nbPlayers.js
rename to client/client_OLD/javascripts/data/nbPlayers.js
diff --git a/client/client_OLD/javascripts/shared/userCheck.js b/client/client_OLD/javascripts/data/userCheck.js
similarity index 100%
rename from client/client_OLD/javascripts/shared/userCheck.js
rename to client/client_OLD/javascripts/data/userCheck.js
diff --git a/client/client_OLD/javascripts/utils/misc.js b/client/client_OLD/javascripts/utils/misc.js
deleted file mode 100644
index 287c0ea6..00000000
--- a/client/client_OLD/javascripts/utils/misc.js
+++ /dev/null
@@ -1,47 +0,0 @@
-// Source: https://www.quirksmode.org/js/cookies.html
-function setCookie(name, value)
-{
-	var date = new Date();
-	date.setTime(date.getTime()+(183*24*60*60*1000)); //6 months
-	var expires = "; expires="+date.toGMTString();
-	document.cookie = name+"="+value+expires+"; path=/";
-}
-
-function getCookie(name, defaut) {
-	var nameEQ = name + "=";
-	var ca = document.cookie.split(';');
-	for (var i=0;i < ca.length;i++)
-	{
-		var c = ca[i];
-		while (c.charAt(0)==' ')
-			c = c.substring(1,c.length);
-		if (c.indexOf(nameEQ) == 0)
-			return c.substring(nameEQ.length,c.length);
-	}
-	return defaut; //cookie not found
-}
-
-// Random (enough) string for socket and game IDs
-function getRandString()
-{
-	return (Date.now().toString(36) + Math.random().toString(36).substr(2, 7))
-		.toUpperCase();
-}
-
-// Used both on index and variant page, to switch language
-function setLanguage(e)
-{
-	setCookie("lang", e.target.value);
-	location.reload(); //to include the right .pug file
-}
-
-// Shortcut for an often used click (on a modal)
-function doClick(elemId)
-{
-	document.getElementById(elemId).click(); //or ".checked = true"
-}
-
-function translate(msg)
-{
-	return translations[msg];
-}
diff --git a/client/client_OLD/javascripts/variant.js b/client/client_OLD/javascripts/variant.js
index 5920c0c7..c45de0ac 100644
--- a/client/client_OLD/javascripts/variant.js
+++ b/client/client_OLD/javascripts/variant.js
@@ -33,13 +33,6 @@ new Vue({
 		this.conn.onclose = socketCloseListener;
 	},
 	methods: {
-		updateSettings: function(event) {
-			const propName =
-				event.target.id.substr(3).replace(/^\w/, c => c.toLowerCase())
-			localStorage[propName] = ["highlight","coords"].includes(propName)
-				? event.target.checked
-				: event.target.value;
-		},
 		// Game is over, clear storage and put it in indexedDB
 		archiveGame: function() {
 			// TODO: ...
diff --git a/client/client_OLD/views/app.pug b/client/client_OLD/views/app.pug
index fb9853a3..c2469602 100644
--- a/client/client_OLD/views/app.pug
+++ b/client/client_OLD/views/app.pug
@@ -1,60 +1,9 @@
 doctype html
 html
-
-	head
-		meta(charset="UTF-8")
-		title vchess - club
-		meta(name="viewport" content="width=device-width, initial-scale=1")
-		meta(name="msapplication-config"
-			content="/images/favicon/browserconfig.xml")
-		meta(name="theme-color" content="#ffffff")
-		link(rel="stylesheet"
-			href="//cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.0/mini-default.min.css")
-		link(rel="stylesheet"
-			href="//fonts.googleapis.com/css?family=Open+Sans:400,700")
-		link(rel="apple-touch-icon" sizes="180x180"
-			href="/images/favicon/apple-touch-icon.png")
-		link(rel="icon" type="image/png" sizes="32x32"
-			href="/images/favicon/favicon-32x32.png")
-		link(rel="icon" type="image/png" sizes="16x16"
-			href="/images/favicon/favicon-16x16.png")
-		link(rel="manifest" href="/images/favicon/manifest.json")
-		link(rel="mask-icon" href="/images/favicon/safari-pinned-tab.svg"
-			color="#5bbad5")
-		link(rel="shortcut icon" href="/images/favicon/favicon.ico")
-		link(rel="stylesheet" href="/stylesheets/app.css")
-
-	// TODO: on-demand components, do not load all at startup
-	body
-		-
-			var langName = {
-				"en": "English",
-				"es": "Español",
-				"fr": "Français",
-			};
-		case lang
-			when "en"
-				include translations/en
-				include welcome/en
-			when "es"
-				include translations/es
-				include welcome/es
-			when "fr"
-				include translations/fr
-				include welcome/fr
-		include modals
 		main#VueElement
 			my-upsert-user
 			.container
-				// Header (on index only)
 				.row(v-show="display=='index'")
-					.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
-						header
-							img(src="/images/index/unicorn.svg")
-							.info-container
-								p vchess.club
-							img(src="/images/index/wildebeest.svg")
-				// Menu (top of page)
 				.row
 					.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
 						label.drawer-toggle(for="drawerControl")
diff --git a/client/client_OLD/views/modals.pug b/client/client_OLD/views/modals.pug
deleted file mode 100644
index e24a7389..00000000
--- a/client/client_OLD/views/modals.pug
+++ /dev/null
@@ -1,66 +0,0 @@
-input#modalLang.modal(type="checkbox")
-div(role="dialog")
-	#language.card
-		label.modal-close(for="modalLang")
-		form
-			fieldset
-				label(for="langSelect")= translations["Language"]
-				select#langSelect
-					each language,langCode in langName
-						option(value=langCode selected=(lang==langCode))
-							=language
-
-input#modalSettings.modal(type="checkbox")
-div(role="dialog" aria-labelledby="settingsTitle")
-	.card.smallpad(@change="updateSettings")
-		label.modal-close(for="modalSettings")
-		h3#settingsTitle.section= translations["Preferences"]
-		fieldset
-			label(for="setSqSize")= translations["Square size (in pixels). 0 for 'adaptative'"]
-			input#setSqSize(type="number" v-model="settings.sqSize")
-		fieldset
-			label(for="selectHints")= translations["Show move hints?"]
-			select#setHints(v-model="settings.hints")
-				option(value="0")= translations["None"]
-				option(value="1")= translations["Moves from a square"]
-				option(value="2")= translations["Pieces which can move"]
-		fieldset
-			label(for="setHighlight")= translations["Highlight squares? (Last move & checks)"]
-			input#setHighlight(type="checkbox" v-model="settings.highlight")
-		fieldset
-			label(for="setCoords")= translations["Show board coordinates?"]
-			input#setCoords(type="checkbox" v-model="settings.coords")
-		fieldset
-			label(for="selectColor")= translations["Board colors"]
-			select#setBcolor(v-model="settings.bcolor")
-				option(value="lichess")
-					= translations["brown"]
-				option(value="chesscom")
-					= translations["green"]
-				option(value="chesstempo")
-					= translations["blue"]
-		fieldset
-			label(for="selectSound")= translations["Play sounds?"]
-			select#setSound(v-model="settings.sound")
-				option(value="0")= translations["None"]
-				option(value="1")= translations["New game"]
-				option(value="2")= translations["All"]
-
-input#modalContact.modal(type="checkbox")
-div(role="dialog" aria-labelledby="contactTitle")
-	form.card.smallpad
-		label.modal-close(for="modalContact")
-		h3#contactTitle.section= translations["Contact form"]
-		fieldset
-			label(for="userEmail")= translations["Email"]
-			input#userEmail(type="email")
-		fieldset
-			label(for="mailSubject")= translations["Subject"]
-			input#mailSubject(type="text")
-		fieldset
-			label(for="mailContent")= translations["Content"]
-			br
-			textarea#mailContent
-		fieldset
-			button(type="button" onClick="trySendMessage()") Send
-			p#emailSent= translations["Email sent!"]
diff --git a/client/client_OLD/views/translations/en.pug b/client/client_OLD/views/translations/en.pug
deleted file mode 100644
index 611236c7..00000000
--- a/client/client_OLD/views/translations/en.pug
+++ /dev/null
@@ -1,97 +0,0 @@
--
-	var translations =
-	{
-		"Language": "Language",
-		"Contact form": "Contact form",
-		"Email": "Email",
-		"Subject": "Subject",
-		"Content": "Content",
-		"Email sent!": "Email sent!",
-		"Hall": "Hall",
-		"My games": "My games",
-
-		// Index page:
-		"Help": "Help",
-		"First visit?": "First visit?",
-		">>> Please read this <<<": ">>> Please read this <<<",
-		// Variants boxes:
-		"Both sides of the mirror": "Both sides of the mirror",
-		"Keep antiking in check": "Keep antiking in check",
-		"Explosive captures": "Explosive captures",
-		"Shared pieces": "Shared pieces",
-		"Standard rules": "Standard rules",
-		"Captures reborn": "Captures reborn",
-		"Capture all of a kind": "Capture all of a kind",
-		"Big board": "Big board",
-		"Lose all pieces": "Lose all pieces",
-		"Laws of attraction": "Laws of attraction",
-		"Exchange pieces positions": "Exchange pieces positions",
-		"Exotic captures": "Exotic captures",
-		"Balanced sliders & leapers": "Balanced sliders & leapers",
-		"Reverse captures": "Reverse captures",
-		"Pawns move diagonally": "Pawns move diagonally",
-		"In the shadow": "In the shadow",
-		"Move twice": "Move twice",
-		"Board upside down": "Board upside down",
-
-		// Variant page:
-		"New game": "New game",
-		"Waiting for opponent...": "Waiting for opponent...",
-		"Rules": "Rules",
-		"Play": "Play",
-		"Problems": "Problems",
-		"White win": "White win",
-		"Black win": "Black win",
-		"Draw": "Draw",
-		"New live game": "New live game",
-		"New game versus computer": "New game versus computer",
-		"Analysis mode": "Analysis mode",
-		"Start chat": "Start chat",
-		"Clear current game": "Clear current game",
-		"Settings": "Settings",
-		"Resign": "Resign",
-		"Undo": "Undo",
-		"Flip board": "Flip board",
-		"Game state (FEN):": "Game state (FEN):",
-		"Ok": "Ok",
-		"Random": "Random",
-		"Preferences": "Preferences",
-		"My name is...": "My name is...",
-		"Show hints?": "Show hints?",
-		"Board colors": "Board colors",
-		"brown": "brown",
-		"green": "green",
-		"blue": "blue",
-		"Play sounds?": "Play sounds?",
-		"None": "None",
-		"All": "All",
-		"Chat with ": "Chat with ",
-		"Type here": "Type here",
-		"Send": "Send",
-		"Download PGN": "Download PGN",
-		"Show solution": "Show solution",
-		"Load previous problems": "Load previous problems",
-		"Load next problems": "Load next problems",
-		"New": "New",
-		"Add a problem": "Add a problem",
-		"Full FEN description": "Full FEN description",
-		"Safe HTML tags allowed": "Safe HTML tags allowed",
-		"Instructions": "Instructions",
-		"Describe the problem goal": "Describe the problem goal",
-		"Solution": "Solution",
-		"How to solve the problem?": "How to solve the problem?",
-		"Preview": "Preview",
-		"Cancel": "Cancel",
-		"Solve": "Solve",
-		"Bad FEN description": "Bad FEN description",
-		"Empty instructions": "Empty instructions",
-		"Empty solution": "Empty solution",
-		"Already playing a game in this variant on another tab!":
-			"Already playing a game in this variant on another tab!",
-		"Finish your ": "Finish your ",
-		" game first!": " game first!",
-		": unfinished computer game will be erased":
-			": unfinished computer game will be erased",
-		": current analysis will be erased":
-			": current analysis will be erased",
-	};
diff --git a/client/client_OLD/views/translations/es.pug b/client/client_OLD/views/translations/es.pug
deleted file mode 100644
index ce219666..00000000
--- a/client/client_OLD/views/translations/es.pug
+++ /dev/null
@@ -1,90 +0,0 @@
--
-	var translations =
-	{
-		"Language": "Idioma",
-
-		// Index page:
-		"Help": "Ayuda",
-		"First visit?": "¿ Primera visita ?",
-		">>> Please read this <<<": ">>> Por favor lee esto <<<",
-		// Variants boxes:
-		"Both sides of the mirror": "Ambos lados del espejo",
-		"Keep antiking in check": "Mantener el antirey en jaque",
-		"Explosive captures": "Capturas explosivas",
-		"Shared pieces": "Piezas compartidas",
-		"Standard rules": "Reglas estandar",
-		"Captures reborn": "Las capturas renacen",
-		"Capture all of a kind": "Capturar todo del mismo tipo",
-		"Big board": "Gran tablero",
-		"Lose all pieces": "Perder todas las piezas",
-		"Laws of attraction": "Las leyes de las atracciones",
-		"Exchange pieces positions": "Intercambiar las posiciones de las piezas",
-		"Exotic captures": "Capturas exóticas",
-		"Balanced sliders & leapers": "Modos de desplazamiento equilibrados",
-		"Reverse captures": "Capturas invertidas",
-		"Pawns move diagonally": "Peones se mueven en diagonal",
-		"In the shadow": "En la sombra",
-		"Move twice": "Mover dos veces",
-		"Board upside down": "Tablero al revés",
-
-		// Variant page:
-		"New game": "Nueva partida",
-		"Waiting for opponent...": "Esperando a un oponente...",
-		"Rules": "Reglas",
-		"Play": "Jugar",
-		"Problems": "Problemas",
-		"White win": "Las blancas ganan",
-		"Black win": "Las negras ganan",
-		"Draw": "Empate",
-		"New live game": "Nueva partida en vivo",
-		"New game versus computer": "Nueva partida contra la computadora",
-		"Analysis mode": "Modo de análisis",
-		"Start chat": "Iniciar chat",
-		"Clear current game": "Borrar la partida actual",
-		"Settings": "Ajustes",
-		"Resign": "Abandonar",
-		"Undo": "Deshacer",
-		"Flip board": "Girar el tablero",
-		"Game state (FEN):": "Estado del juego (FEN) :",
-		"Ok": "Ok",
-		"Random": "Aleatorio",
-		"Preferences": "Preferencias",
-		"My name is...": "Mi nombre es...",
-		"Show hints?": "Ayudas visuales ?",
-		"Board colors": "Colores del tablero",
-		"brown": "marrón",
-		"green": "verde",
-		"blue": "azul",
-		"Play sounds?": "¿ Tocar los sonidos ?",
-		"None": "No",
-		"All": "Todos",
-		"Chat with ": "Hablar con ",
-		"Type here": "Escribe aqui",
-		"Send": "Enviar",
-		"Download PGN": "Descargar el PGN",
-		"Show solution": "Mostrar la solucion",
-		"Load previous problems": "Cargar los problemas anteriores",
-		"Load next problems": "Cargar los siguientes problemas",
-		"New": "Nuevo",
-		"Add a problem": "Añadir un problema",
-		"Full FEN description": "Descripción FEN completa",
-		"Safe HTML tags allowed": "HTML 'seguro' autorizado",
-		"Instructions": "Instrucciones",
-		"Describe the problem goal": "Describe el objetivo del problema",
-		"Solution": "Solución",
-		"How to solve the problem?": "¿ Como resolver el problema ?",
-		"Preview": "Previsualizar",
-		"Cancel": "Anular",
-		"Solve": "Resolver",
-		"Bad FEN string": "Mala descripción FEN",
-		"Empty instructions": "Instrucciones vacias",
-		"Empty solution": "Solución vacía",
-		"Already playing a game in this variant on another tab!":
-			"¡ Una partida está en progreso en esta variante en otra pestaña !",
-		"Finish your ": "¡ Termina tu ",
-		" game first!": " partida primero !",
-		": unfinished computer game will be erased":
-			" : una partida inconclusa contra la computadora será borrado",
-		": current analysis will be erased":
-			" : el análisis actual será borrado",
-	};
diff --git a/client/client_OLD/views/translations/fr.pug b/client/client_OLD/views/translations/fr.pug
deleted file mode 100644
index 3f3b1fe0..00000000
--- a/client/client_OLD/views/translations/fr.pug
+++ /dev/null
@@ -1,90 +0,0 @@
--
-	var translations =
-	{
-		"Language": "Langue",
-
-		// Index page:
-		"Help": "Aide",
-		"First visit?": "Première visite ?",
-		">>> Please read this <<<": ">>> SVP lisez ceci <<<",
-		// Variants boxes:
-		"Both sides of the mirror": "Les deux côté du miroir",
-		"Keep antiking in check": "Gardez l'antiroi en échec",
-		"Explosive captures": "Captures explosives",
-		"Shared pieces": "Pièces partagées",
-		"Standard rules": "Règles usuelles",
-		"Captures reborn": "Les captures renaissent",
-		"Capture all of a kind": "Capturez tout d'un même type",
-		"Big board": "Grand échiquier",
-		"Lose all pieces": "Perdez toutes les pièces",
-		"Laws of attraction": "Les lois de l'attraction",
-		"Exchange pieces positions": "Échangez les positions des pièces",
-		"Exotic captures": "Captures exotiques",
-		"Balanced sliders & leapers": "Modes de déplacement équilibrés",
-		"Reverse captures": "Captures inversées",
-		"Pawns move diagonally": "Les pions vont en diagonale",
-		"In the shadow": "Dans l'ombre",
-		"Move twice": "Jouer deux coups",
-		"Board upside down": "Échiquier à l'envers",
-
-		// Variant page:
-		"New game": "Nouvelle partie",
-		"Waiting for opponent...": "En attente d'un adversaire...",
-		"Rules": "Règles",
-		"Play": "Jouer",
-		"Problems": "Problèmes",
-		"White win": "Les blancs gagnent",
-		"Black win": "Les noirs gagnent",
-		"Draw": "Match nul",
-		"New live game": "Nouvelle partie en direct",
-		"New game versus computer": "Nouvelle partie contre l'ordinateur",
-		"Analysis mode": "Mode analyse",
-		"Start chat": "Démarrer le chat",
-		"Clear current game": "Effacer la partie courante",
-		"Settings": "Réglages",
-		"Resign": "Abandonner",
-		"Undo": "Annuler",
-		"Flip board": "Tourner l'échiquier",
-		"Game state (FEN):": "État de la partie (FEN) :",
-		"Ok": "Ok",
-		"Random": "Aléatoire",
-		"Preferences": "Préférences",
-		"My name is...": "Je m'appelle...",
-		"Show hints?": "Aides visuelles ?",
-		"Board colors": "Couleurs de l'échiquier",
-		"brown": "marron",
-		"green": "vert",
-		"blue": "bleu",
-		"Play sounds?": "Jouer les sons ?",
-		"None": "Aucun",
-		"All": "Tous",
-		"Chat with ": "Discuter avec ",
-		"Type here": "Écrivez ici",
-		"Send": "Envoyer",
-		"Download PGN": "Télécharger le PGN",
-		"Show solution": "Montrer la solution",
-		"Load previous problems": "Charger les problèmes précédents",
-		"Load next problems": "Charger les problèmes suivants",
-		"New": "Nouveau",
-		"Add a problem": "Ajouter un problème",
-		"Full FEN description": "Description FEN complète",
-		"Safe HTML tags allowed": "HTML 'sûr' autorisé",
-		"Instructions": "Instructions",
-		"Describe the problem goal": "Décrire le but du problème",
-		"Solution": "Solution",
-		"How to solve the problem?": "Comment résoudre le problème ?",
-		"Preview": "Prévisualiser",
-		"Cancel": "Annuler",
-		"Solve": "Résoudre",
-		"Bad FEN string": "Mauvaise description FEN",
-		"Empty instructions": "Instructions vides",
-		"Empty solution": "Solution vide",
-		"Already playing a game in this variant on another tab!":
-			"Une partie est en cours sur cette variante dans un autre onglet !",
-		"Finish your ": "Terminez votre ",
-		" game first!": " partie d'abord !",
-		": unfinished computer game will be erased":
-			" : une partie inachevée contre l'ordinateur sera effacée",
-		": current analysis will be erased":
-			" : l'analyse en cours sera effacée",
-	};
diff --git a/client/client_OLD/views/welcome/en.pug b/client/client_OLD/views/welcome/en.pug
deleted file mode 100644
index 06a9ca84..00000000
--- a/client/client_OLD/views/welcome/en.pug
+++ /dev/null
@@ -1,49 +0,0 @@
-input#modalWelcome.modal(type="checkbox")
-div(role="dialog")
-	#welcome.card.text-center
-		label.modal-close(for="modalWelcome")
-		h3.blue.section Welcome to v[ariant]chess.club!
-		.section
-			p A fun place to play chess variants in real time.
-			p But wait... what is a chess variant?
-			img(src="/images/Hexagonal_chess.svg")
-			p.
-				As suggested by the picture, a variant setup generally looks
-				more or less like a chessboard with regular pieces
-				(otherwise it's no longer a variant but a whole new game).
-			p.emphasis.purple However...
-			p Each variant has its own new rules, which can involve
-			table.list-table
-				tbody
-					tr
-						td * different pieces movements
-					tr
-						td * different chessboard(s)
-					tr
-						td * new pieces
-					tr
-						td * moves side effects
-					tr
-						td ...and so on
-		.section
-			p.
-				Example: imagine that a capture is an atomic explosion, wiping all
-				adjacent squares &ndash; except the pawns, which as cockroaches can
-				resist this kind of event.
-			p Also state a goal: make the opponent's king explode.
-			p &rarr; Congrats, you defined Atomic chess! (Playable here)
-		.section
-			p.emphasis.purple
-				| OK, this all sounds interesting, but why would that be fun?
-			p.
-				Because all games here start with a random setup: no more boring
-				openings memorization, you have to rely on your chess skills only.
-				No game is like another one.
-			-
-				var wikipediaUrl = "https://en.wikipedia.org/wiki/" +
-					"List_of_chess_variants#/media/File:Hexagonal_chess.svg";
-			p.
-				For informations about hundreds (if not thousands) of variants, you
-				can visit the excellent
-				#[a(href="https://www.chessvariants.com/") chessvariants] website.
-		p.smallfont Image credit: #[a(href=wikipediaUrl) Wikipedia]
diff --git a/client/client_OLD/views/welcome/es.pug b/client/client_OLD/views/welcome/es.pug
deleted file mode 100644
index 0c561bc2..00000000
--- a/client/client_OLD/views/welcome/es.pug
+++ /dev/null
@@ -1,52 +0,0 @@
-input#modalWelcome.modal(type="checkbox")
-div(role="dialog")
-	#welcome.card.text-center
-		label.modal-close(for="modalWelcome")
-		h3.blue.section ¡ Bienvenido a v[ariant]chess.club !
-		.section
-			p Un sitio donde jugar variantes del juego de ajedrez en vivo.
-			p Pero espera... ¿ qué es una "variante" ?
-			img(src="/images/Hexagonal_chess.svg")
-			p.
-				Como lo sugiere la imagen, el punto de inicio de una variante generalmente
-				se ve como un tablero de ajedrez con las piezas habituales.
-				(sino ya no es una variante, pero un nuevo juego).
-			p.emphasis.purple Sin embargo...
-			p Cada variante tiene sus propias reglas, que pueden definir
-			table.list-table
-				tbody
-					tr
-						td * diferentes desplazamientos de piezas
-					tr
-						td * differentes(s) tablero(s)
-					tr
-						td * nuevas piezas
-					tr
-						td * efectos de borde en los movimientos
-					tr
-						td ...etc
-		.section
-			p.
-				Ejemplo : Imagina que una captura es una explosión atómica que destruye
-				todo en los 8 hexes cercanos, excepto los peones, que como las cucarachas
-				se resisten a este tipo de cosas.
-			p Define también un objetivo : hacer explotar al rey del adversario.
-			p.
-				&rarr; ¡ Bien hecho, acabas de describir el ajedrez atómico !
-				(Se puede jugar aquí)
-		.section
-			p.emphasis.purple
-				| OK, parece interesante, pero ¿ por qué sería divertido ?!
-			p.
-				Como todas las partidas comienzan con una posición aleatoria :
-				¡ terminadas las laboriosas memorizaciones de aperturas, puedes expresar
-				tus habilidades de ajedrez desde el primer movimiento ! Ninguna partida
-				es como otra.
-			-
-				var wikipediaUrl = "https://en.wikipedia.org/wiki/" +
-					"List_of_chess_variants#/media/File:Hexagonal_chess.svg";
-			p.
-				Pour s'informer sur des centaines de variantes (au moins), je vous invite à
-				visiter l'excellent site
-				#[a(href="https://www.chessvariants.com/") chessvariants].
-		p.smallfont Credito de imagen : #[a(href=wikipediaUrl) Wikipedia]
diff --git a/client/client_OLD/views/welcome/fr.pug b/client/client_OLD/views/welcome/fr.pug
deleted file mode 100644
index d650a739..00000000
--- a/client/client_OLD/views/welcome/fr.pug
+++ /dev/null
@@ -1,49 +0,0 @@
-input#modalWelcome.modal(type="checkbox")
-div(role="dialog")
-	#welcome.card.text-center
-		label.modal-close(for="modalWelcome")
-		h3.blue.section Bienvenue sur v[ariant]chess.club!
-		.section
-			p Un site où jouer à des variantes du jeu d'échecs en direct.
-			p Mais attendez... c'est quoi une "variante" ?
-			img(src="/images/Hexagonal_chess.svg")
-			p.
-				Comme suggéré par l'image, le point de départ d'une variante
-				ressemble en général à un échiquier avec les pièces habituelles
-				(sinon ce n'est plus une variante, mais un nouveau jeu).
-			p.emphasis.purple Cependant...
-			p Chaque variante a ses propres règles, qui peuvent définir
-			table.list-table
-				tbody
-					tr
-						td * différents déplacements de pièces
-					tr
-						td * différent(s) échiquier(s)
-					tr
-						td * de nouvelles pièces
-					tr
-						td * des effets de bord sur les coups
-					tr
-						td ...etc
-		.section
-			p.
-				Exemple: imaginez qu'une capture soit une explosion atomique,
-				détruisant tout ce qui se trouve sur les 8 cases à proximité &ndash;
-				sauf les pions, qui tels les cafards résistent à ce genre de truc.
-			p Définissez également un but : faire exploser le roi adverse.
-			p &rarr; Bravo, vous venez de décrire les échecs atomiques ! (Jouable ici)
-		.section
-			p.emphasis.purple
-				| OK ça a l'air intéressant, mais pourquoi ce serait amusant ?!
-			p.
-				Car toutes les parties démarrent avec une position aléatoire : terminées
-				les laborieuses mémorisations d'ouverture, vous pouvez exprimer vos talents
-				échiquéens dès le premier coup ! Aucune partie ne ressemble à une autre.
-			-
-				var wikipediaUrl = "https://en.wikipedia.org/wiki/" +
-					"List_of_chess_variants#/media/File:Hexagonal_chess.svg";
-			p.
-				Pour s'informer sur des centaines de variantes (au moins), je vous invite à
-				visiter l'excellent site
-				#[a(href="https://www.chessvariants.com/") chessvariants].
-		p.smallfont Crédit image : #[a(href=wikipediaUrl) Wikipedia]
diff --git a/client/package-lock.json b/client/package-lock.json
index 3a6f0fc1..fe7eae3d 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -8994,6 +8994,35 @@
         }
       }
     },
+    "raw-loader": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-1.0.0.tgz",
+      "integrity": "sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^1.0.0"
+      },
+      "dependencies": {
+        "ajv-keywords": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+          "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
+          "dev": true
+        },
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        }
+      }
+    },
     "read-pkg": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",
diff --git a/client/package.json b/client/package.json
index a074534e..7f51be97 100644
--- a/client/package.json
+++ b/client/package.json
@@ -22,6 +22,7 @@
     "node-sass": "^4.9.0",
     "pug": "^2.0.3",
     "pug-plain-loader": "^1.0.0",
+    "raw-loader": "^1.0.0",
     "sass-loader": "^7.0.1",
     "vue-template-compiler": "^2.5.21"
   },
diff --git a/server/favicon/SOURCE b/client/public/SOURCE
similarity index 60%
rename from server/favicon/SOURCE
rename to client/public/SOURCE
index 7d25d193..5565b455 100644
--- a/server/favicon/SOURCE
+++ b/client/public/SOURCE
@@ -1,3 +1,2 @@
-Generated using https://realfavicongenerator.net/ ,
-with an image from here:
+Favicon image from here:
 https://www.freefavicon.com/freefavicons/objects/iconinfo/chess-piece-silhouette---red-king--rey-rojo-152-275290.html
diff --git a/client/public/index.html b/client/public/index.html
index 0115e9f5..6549d7a7 100644
--- a/client/public/index.html
+++ b/client/public/index.html
@@ -1,11 +1,15 @@
 <!DOCTYPE html>
-<html lang="en">
+<html>
   <head>
     <meta charset="utf-8">
 		<title>vchess - club</title>
 		<meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+		<link rel="stylesheet"
+			href="//cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.0/mini-default.min.css">
+		<link rel="stylesheet"
+			href="//fonts.googleapis.com/css?family=Open+Sans:400,700">
   </head>
   <body>
     <div id="app"></div>
diff --git a/client/src/App.vue b/client/src/App.vue
index 0199db21..1ece7921 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -1,14 +1,50 @@
 <template lang="pug">
-  #app
-    #nav
-      router-link(to="/") Home
-      | &nbsp;|&nbsp;
-      router-link(to="/about") About
-      | &nbsp;|&nbsp;
-      router-link(to="/test") Test
-    router-view
+#app
+  // modal "welcome" will be filled in the selected language
+  #modalWelcome
+  Language
+  Settings(:settings="settings")
+  ContactForm
+  .container
+    .row(v-show="$route.path == '/'")
+      // Header (on index only)
+      header
+        .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+          header
+            img(src="./assets/images/index/unicorn.svg")
+            .info-container
+              p vchess.club {{ $lang }}
+            img(src="./assets/images/index/wildebeest.svg")
+    .row
+      // Menu (top of page)
+      nav
+        router-link(to="/") Home
+        | &nbsp;|&nbsp;
+        router-link(to="/about") About
+        | &nbsp;|&nbsp;
+        router-link(to="/test") Test
+      router-view
 </template>
 
+<script>
+// See https://stackoverflow.com/a/35417159
+import ContactForm from "@/components/ContactForm.vue";
+import Language from "@/components/Language.vue";
+import Settings from "@/components/Settings.vue";
+export default {
+  data: function() {
+    return {
+      settings: {}, //TODO
+    };
+  },
+  components: {
+    ContactForm,
+    Language,
+    Settings,
+  },
+};
+</script>
+
 <style lang="sass">
 #app
   font-family: "Avenir", Helvetica, Arial, sans-serif
diff --git a/client/src/components/ContactForm.vue b/client/src/components/ContactForm.vue
new file mode 100644
index 00000000..cbfa622c
--- /dev/null
+++ b/client/src/components/ContactForm.vue
@@ -0,0 +1,61 @@
+<template lang="pug">
+div
+  input#modalContact.modal(type="checkbox")
+  div(role="dialog" aria-labelledby="contactTitle")
+    form.card.smallpad
+      label.modal-close(for="modalContact")
+      h3#contactTitle.section {{ $tr["Contact form"] }}
+      fieldset
+        label(for="userEmail") {{ $tr["Email"] }}
+        input#userEmail(type="email")
+      fieldset
+        label(for="mailSubject") {{ $tr["Subject"] }}
+        input#mailSubject(type="text")
+      fieldset
+        label(for="mailContent") {{ $tr["Content"] }}
+        br
+        textarea#mailContent
+      fieldset
+        button(type="button" onClick="trySendMessage()") Send
+        p#emailSent {{ $tr["Email sent!"] }}
+</template>
+
+<script>
+import { ajax } from "../utils/ajax";
+export default {
+  name: "ContactForm",
+	methods: {
+		// Note: not using Vue here, but would be possible
+    trySendMessage: function() {
+      let email = document.getElementById("userEmail");
+      let subject = document.getElementById("mailSubject");
+      let content = document.getElementById("mailContent");
+      const error = checkNameEmail({email: email});
+      if (!!error)
+        return alert(error);
+      if (content.value.trim().length == 0)
+        return alert("Empty message");
+      if (subject.value.trim().length == 0 && !confirm("No subject. Send anyway?"))
+        return;
+
+      // Message sending:
+      ajax(
+        "/messages",
+        "POST",
+        {
+          email: email.value,
+          subject: subject.value,
+          content: content.value,
+        },
+        () => {
+          subject.value = "";
+          content.value = "";
+          let emailSent = document.getElementById("emailSent");
+          emailSent.style.display = "inline-block";
+          setTimeout(() => { emailSent.style.display = "none"; }, 2000);
+        }
+      );
+    },
+	},
+};
+</script>
diff --git a/client/src/components/Language.vue b/client/src/components/Language.vue
new file mode 100644
index 00000000..35c717ff
--- /dev/null
+++ b/client/src/components/Language.vue
@@ -0,0 +1,33 @@
+<template lang="pug">
+div
+  -
+    var langName = {
+      "en": "English",
+      "es": "Español",
+      "fr": "Français",
+    };
+  input#modalLang.modal(type="checkbox")
+  div(role="dialog")
+    #language.card
+      label.modal-close(for="modalLang")
+      form
+        fieldset
+          label(for="langSelect") {{ $tr["Language"] }}
+          select#langSelect
+            each language,langCode in langName
+              option(value=langCode selected=(lang==langCode))
+                =language
+</template>
+
+<script>
+export default {
+  name: "Language",
+	methods: {
+    // Used both on index and variant page, to switch language
+    setLanguage: function(e) {
+      localStorage["lang"] = e.target.value;
+      this.$lang = e.target.value;
+    },
+	},
+};
+</script>
diff --git a/client/src/components/Settings.vue b/client/src/components/Settings.vue
new file mode 100644
index 00000000..34431a5d
--- /dev/null
+++ b/client/src/components/Settings.vue
@@ -0,0 +1,51 @@
+<template lang="pug">
+div
+  input#modalSettings.modal(type="checkbox")
+  div(role="dialog" aria-labelledby="settingsTitle")
+    .card.smallpad(@change="updateSettings")
+      label.modal-close(for="modalSettings")
+      h3#settingsTitle.section {{ $tr["Preferences"] }}
+      fieldset
+        label(for="setSqSize") {{ $tr["Square size (in pixels). 0 for 'adaptative'"] }}
+        input#setSqSize(type="number" v-model="settings.sqSize")
+      fieldset
+        label(for="selectHints") {{ $tr["Show move hints?"] }}
+        select#setHints(v-model="settings.hints")
+          option(value="0") {{ $tr["None"] }}
+          option(value="1") {{ $tr["Moves from a square"] }}
+          option(value="2") {{ $tr["Pieces which can move"] }}
+      fieldset
+        label(for="setHighlight") {{ $tr["Highlight squares? (Last move & checks)"] }}
+        input#setHighlight(type="checkbox" v-model="settings.highlight")
+      fieldset
+        label(for="setCoords") {{ $tr["Show board coordinates?"] }}
+        input#setCoords(type="checkbox" v-model="settings.coords")
+      fieldset
+        label(for="selectColor") {{ $tr["Board colors"] }}
+        select#setBcolor(v-model="settings.bcolor")
+          option(value="lichess") {{ $tr["brown"] }}
+          option(value="chesscom") {{ $tr["green"] }}
+          option(value="chesstempo") {{ $tr["blue"] }}
+      fieldset
+        label(for="selectSound") {{ $tr["Play sounds?"] }}
+        select#setSound(v-model="settings.sound")
+          option(value="0") {{ $tr["None"] }}
+          option(value="1") {{ $tr["New game"] }}
+          option(value="2") {{ $tr["All"] }}
+</template>
+
+<script>
+export default {
+  name: "Settings",
+  props: ["settings"],
+	methods: {
+    updateSettings: function(event) {
+      const propName =
+        event.target.id.substr(3).replace(/^\w/, c => c.toLowerCase())
+      localStorage[propName] = ["highlight","coords"].includes(propName)
+        ? event.target.checked
+        : event.target.value;
+    },
+	},
+};
+</script>
diff --git a/client/src/main.js b/client/src/main.js
index e68447ee..da3e6022 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -3,6 +3,7 @@ import App from "./App.vue";
 import router from "./router";
 import params from "./parameters"; //for socket connection
 import { ajax } from "./utils/ajax";
+import { util } from "./utils/misc";
 
 Vue.config.productionTip = false;
 
@@ -10,13 +11,30 @@ new Vue({
   router,
   render: function(h) {
     return h(App);
+  },
+//  data: {
+//    lang: "",
+//  },
+  watch: {
+    $lang: async function(newLang) {
+      // Fill modalWelcome, and import translations from "./translations/$lang.js"
+      document.getElementById("modalWelcome").innerHTML =
+        require("raw-loader!pug-plain-loader!./modals/welcome/" + newLang + ".pug");
+      const tModule = await import("./translations/" + newLang + ".js");
+      Vue.prototype.$tr = tModule.translations;
+      //console.log(tModule.translations);
+    },
   },
 	created: function() {
-		//alert("test");
+    const supportedLangs = ["en","es","fr"];
+    Vue.prototype.$lang = localStorage["lang"] ||
+      supportedLangs.includes(navigator.language)
+        ? navigator.language
+        : "en";
 		ajax("/variants", "GET", res => {
 			Vue.prototype.$variants = res.variantArray;
 		});
-		Vue.prototype.$conn = null; //TODO
+    Vue.prototype.$tr = {}; //to avoid a compiler error
 		const myid = localStorage["myid"] || util.getRandString();
 		// NOTE: in this version, we don't say on which page we are, yet
 		// ==> we'll say "enter/leave" page XY (in fact juste "enter", seemingly)
diff --git a/client/src/modals/welcome/en.pug b/client/src/modals/welcome/en.pug
new file mode 100644
index 00000000..c4ceabc4
--- /dev/null
+++ b/client/src/modals/welcome/en.pug
@@ -0,0 +1,49 @@
+input#modalWelcome.modal(type="checkbox")
+div(role="dialog")
+  #welcome.card.text-center
+    label.modal-close(for="modalWelcome")
+    h3.blue.section Welcome to v[ariant]chess.club!
+    .section
+      p A fun place to play chess variants in real time.
+      p But wait... what is a chess variant?
+      img(src="/images/Hexagonal_chess.svg")
+      p.
+        As suggested by the picture, a variant setup generally looks more or less
+        like a chessboard with regular pieces (otherwise it's no longer a variant
+        but a whole new game).
+      p.emphasis.purple However...
+      p Each variant has its own new rules, which can involve
+      table.list-table
+        tbody
+          tr
+            td * different pieces movements
+          tr
+            td * different chessboard(s)
+          tr
+            td * new pieces
+          tr
+            td * moves side effects
+          tr
+            td ...and so on
+    .section
+      p.
+        Example: imagine that a capture is an atomic explosion, wiping all
+        adjacent squares &ndash; except the pawns, which as cockroaches can
+        resist this kind of event.
+      p Also state a goal: make the opponent's king explode.
+      p &rarr; Congrats, you defined Atomic chess! (Playable here)
+    .section
+      p.emphasis.purple
+        | OK, this all sounds interesting, but why would that be fun?
+      p.
+        Because all games here start with a random setup: no more boring
+        openings memorization, you have to rely on your chess skills only.
+        No game is like another one.
+      -
+        var wikipediaUrl = "https://en.wikipedia.org/wiki/" +
+          "List_of_chess_variants#/media/File:Hexagonal_chess.svg";
+      p.
+        For informations about hundreds (if not thousands) of variants, you
+        can visit the excellent
+        #[a(href="https://www.chessvariants.com/") chessvariants] website.
+    p.smallfont Image credit: #[a(href=wikipediaUrl) Wikipedia]
diff --git a/client/src/modals/welcome/es.pug b/client/src/modals/welcome/es.pug
new file mode 100644
index 00000000..1efba989
--- /dev/null
+++ b/client/src/modals/welcome/es.pug
@@ -0,0 +1,35 @@
+input#modalWelcome.modal(type="checkbox")
+div(role="dialog")
+  #welcome.card.text-center
+    label.modal-close(for="modalWelcome")
+    h3.blue.section ¡ Bienvenido a v[ariant]chess.club !
+    .section
+      p Un sitio donde jugar variantes del juego de ajedrez en vivo.
+      p Pero espera... ¿ qué es una "variante" ?
+      img(src="/images/Hexagonal_chess.svg")
+      p Como lo sugiere la imagen, el punto de inicio de una variante generalmente se ve como un tablero de ajedrez con las piezas habituales (sino ya no es una variante, pero un nuevo juego).
+      p.emphasis.purple Sin embargo...
+      p Cada variante tiene sus propias reglas, que pueden definir
+      table.list-table
+        tbody
+          tr
+            td * diferentes desplazamientos de piezas
+          tr
+            td * differentes(s) tablero(s)
+          tr
+            td * nuevas piezas
+          tr
+            td * efectos de borde en los movimientos
+          tr
+            td ...etc
+    .section
+      p Ejemplo : Imagina que una captura es una explosión atómica que destruye todo en los 8 hexes cercanos, excepto los peones, que como las cucarachas se resisten a este tipo de cosas.
+      p Define también un objetivo : hacer explotar al rey del adversario.
+      p &rarr; ¡ Bien hecho, acabas de describir el ajedrez atómico ! (Se puede jugar aquí)
+    .section
+      p.emphasis.purple
+        | OK, parece interesante, pero ¿ por qué sería divertido ?!
+      p Como todas las partidas comienzan con una posición aleatoria : ¡ terminadas las laboriosas memorizaciones de aperturas, puedes expresar tus habilidades de ajedrez desde el primer movimiento ! Ninguna partida es como otra.
+      p Para conocer cientos de variantes (al menos), te invito a visitar el excelente sitio
+      a(href="https://www.chessvariants.com/" target="_blank" rel="noopener") chessvariants.
+    p.smallfont Credito de imagen : #[a(href="https://en.wikipedia.org/wiki/List_of_chess_variants#/media/File:Hexagonal_chess.svg") Wikipedia]
diff --git a/client/src/modals/welcome/fr.pug b/client/src/modals/welcome/fr.pug
new file mode 100644
index 00000000..819dbcc6
--- /dev/null
+++ b/client/src/modals/welcome/fr.pug
@@ -0,0 +1,40 @@
+input#modalWelcome.modal(type="checkbox")
+div(role="dialog")
+  #welcome.card.text-center
+    label.modal-close(for="modalWelcome")
+    h3.blue.section Bienvenue sur v[ariant]chess.club!
+    .section
+      p Un site où jouer à des variantes du jeu d'échecs en direct.
+      p Mais attendez... c'est quoi une "variante" ?
+      img(src="/images/Hexagonal_chess.svg")
+      p Comme suggéré par l'image, le point de départ d'une variante ressemble en général à un échiquier avec les pièces habituelles (sinon ce n'est plus une variante, mais un nouveau jeu).
+      p.emphasis.purple Cependant...
+      p Chaque variante a ses propres règles, qui peuvent définir
+      table.list-table
+        tbody
+          tr
+            td * différents déplacements de pièces
+          tr
+            td * différent(s) échiquier(s)
+          tr
+            td * de nouvelles pièces
+          tr
+            td * des effets de bord sur les coups
+          tr
+            td ...etc
+    .section
+      p Exemple: imaginez qu'une capture soit une explosion atomique, détruisant tout ce qui se trouve sur les 8 cases à proximité &ndash; sauf les pions, qui tels les cafards résistent à ce genre de truc.
+      p Définissez également un but : faire exploser le roi adverse.
+      p &rarr; Bravo, vous venez de décrire les échecs atomiques ! (Jouable ici)
+    .section
+      p.emphasis.purple
+        | OK ça a l'air intéressant, mais pourquoi ce serait amusant ?!
+      p Car toutes les parties démarrent avec une position aléatoire : terminées les laborieuses mémorisations d'ouverture, vous pouvez exprimer vos talents échiquéens dès le premier coup ! Aucune partie ne ressemble à une autre.
+      -
+        var wikipediaUrl = "https://en.wikipedia.org/wiki/" +
+          "List_of_chess_variants#/media/File:Hexagonal_chess.svg";
+      p.
+        Pour s'informer sur des centaines de variantes (au moins), je vous invite à
+        visiter l'excellent site
+        #[a(href="https://www.chessvariants.com/") chessvariants].
+    p.smallfont Crédit image : #[a(href=wikipediaUrl) Wikipedia]
diff --git a/client/client_OLD/views/rules/Alice/en.pug b/client/src/rules/Alice/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Alice/en.pug
rename to client/src/rules/Alice/en.pug
diff --git a/client/client_OLD/views/rules/Alice/es.pug b/client/src/rules/Alice/es.pug
similarity index 100%
rename from client/client_OLD/views/rules/Alice/es.pug
rename to client/src/rules/Alice/es.pug
diff --git a/client/client_OLD/views/rules/Alice/fr.pug b/client/src/rules/Alice/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Alice/fr.pug
rename to client/src/rules/Alice/fr.pug
diff --git a/client/client_OLD/views/rules/Antiking/en.pug b/client/src/rules/Antiking/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Antiking/en.pug
rename to client/src/rules/Antiking/en.pug
diff --git a/client/client_OLD/views/rules/Antiking/es.pug b/client/src/rules/Antiking/es.pug
similarity index 100%
rename from client/client_OLD/views/rules/Antiking/es.pug
rename to client/src/rules/Antiking/es.pug
diff --git a/client/client_OLD/views/rules/Antiking/fr.pug b/client/src/rules/Antiking/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Antiking/fr.pug
rename to client/src/rules/Antiking/fr.pug
diff --git a/client/client_OLD/views/rules/Atomic/en.pug b/client/src/rules/Atomic/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Atomic/en.pug
rename to client/src/rules/Atomic/en.pug
diff --git a/client/client_OLD/views/rules/Atomic/es.pug b/client/src/rules/Atomic/es.pug
similarity index 100%
rename from client/client_OLD/views/rules/Atomic/es.pug
rename to client/src/rules/Atomic/es.pug
diff --git a/client/client_OLD/views/rules/Atomic/fr.pug b/client/src/rules/Atomic/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Atomic/fr.pug
rename to client/src/rules/Atomic/fr.pug
diff --git a/client/client_OLD/views/rules/Baroque/en.pug b/client/src/rules/Baroque/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Baroque/en.pug
rename to client/src/rules/Baroque/en.pug
diff --git a/client/client_OLD/views/rules/Baroque/fr.pug b/client/src/rules/Baroque/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Baroque/fr.pug
rename to client/src/rules/Baroque/fr.pug
diff --git a/client/client_OLD/views/rules/Berolina/en.pug b/client/src/rules/Berolina/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Berolina/en.pug
rename to client/src/rules/Berolina/en.pug
diff --git a/client/client_OLD/views/rules/Berolina/fr.pug b/client/src/rules/Berolina/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Berolina/fr.pug
rename to client/src/rules/Berolina/fr.pug
diff --git a/client/client_OLD/views/rules/Checkered/en.pug b/client/src/rules/Checkered/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Checkered/en.pug
rename to client/src/rules/Checkered/en.pug
diff --git a/client/client_OLD/views/rules/Checkered/fr.pug b/client/src/rules/Checkered/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Checkered/fr.pug
rename to client/src/rules/Checkered/fr.pug
diff --git a/client/client_OLD/views/rules/Chess960/en.pug b/client/src/rules/Chess960/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Chess960/en.pug
rename to client/src/rules/Chess960/en.pug
diff --git a/client/client_OLD/views/rules/Chess960/fr.pug b/client/src/rules/Chess960/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Chess960/fr.pug
rename to client/src/rules/Chess960/fr.pug
diff --git a/client/client_OLD/views/rules/Crazyhouse/en.pug b/client/src/rules/Crazyhouse/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Crazyhouse/en.pug
rename to client/src/rules/Crazyhouse/en.pug
diff --git a/client/client_OLD/views/rules/Crazyhouse/fr.pug b/client/src/rules/Crazyhouse/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Crazyhouse/fr.pug
rename to client/src/rules/Crazyhouse/fr.pug
diff --git a/client/client_OLD/views/rules/Dark/en.pug b/client/src/rules/Dark/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Dark/en.pug
rename to client/src/rules/Dark/en.pug
diff --git a/client/client_OLD/views/rules/Dark/fr.pug b/client/src/rules/Dark/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Dark/fr.pug
rename to client/src/rules/Dark/fr.pug
diff --git a/client/client_OLD/views/rules/Extinction/en.pug b/client/src/rules/Extinction/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Extinction/en.pug
rename to client/src/rules/Extinction/en.pug
diff --git a/client/client_OLD/views/rules/Extinction/fr.pug b/client/src/rules/Extinction/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Extinction/fr.pug
rename to client/src/rules/Extinction/fr.pug
diff --git a/client/client_OLD/views/rules/Grand/en.pug b/client/src/rules/Grand/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Grand/en.pug
rename to client/src/rules/Grand/en.pug
diff --git a/client/client_OLD/views/rules/Grand/fr.pug b/client/src/rules/Grand/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Grand/fr.pug
rename to client/src/rules/Grand/fr.pug
diff --git a/client/client_OLD/views/rules/Losers/en.pug b/client/src/rules/Losers/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Losers/en.pug
rename to client/src/rules/Losers/en.pug
diff --git a/client/client_OLD/views/rules/Losers/fr.pug b/client/src/rules/Losers/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Losers/fr.pug
rename to client/src/rules/Losers/fr.pug
diff --git a/client/client_OLD/views/rules/Magnetic/en.pug b/client/src/rules/Magnetic/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Magnetic/en.pug
rename to client/src/rules/Magnetic/en.pug
diff --git a/client/client_OLD/views/rules/Magnetic/fr.pug b/client/src/rules/Magnetic/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Magnetic/fr.pug
rename to client/src/rules/Magnetic/fr.pug
diff --git a/client/client_OLD/views/rules/Marseille/en.pug b/client/src/rules/Marseille/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Marseille/en.pug
rename to client/src/rules/Marseille/en.pug
diff --git a/client/client_OLD/views/rules/Marseille/fr.pug b/client/src/rules/Marseille/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Marseille/fr.pug
rename to client/src/rules/Marseille/fr.pug
diff --git a/client/client_OLD/views/rules/Switching/en.pug b/client/src/rules/Switching/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Switching/en.pug
rename to client/src/rules/Switching/en.pug
diff --git a/client/client_OLD/views/rules/Switching/fr.pug b/client/src/rules/Switching/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Switching/fr.pug
rename to client/src/rules/Switching/fr.pug
diff --git a/client/client_OLD/views/rules/Upsidedown/en.pug b/client/src/rules/Upsidedown/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Upsidedown/en.pug
rename to client/src/rules/Upsidedown/en.pug
diff --git a/client/client_OLD/views/rules/Upsidedown/fr.pug b/client/src/rules/Upsidedown/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Upsidedown/fr.pug
rename to client/src/rules/Upsidedown/fr.pug
diff --git a/client/client_OLD/views/rules/Wildebeest/en.pug b/client/src/rules/Wildebeest/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Wildebeest/en.pug
rename to client/src/rules/Wildebeest/en.pug
diff --git a/client/client_OLD/views/rules/Wildebeest/fr.pug b/client/src/rules/Wildebeest/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Wildebeest/fr.pug
rename to client/src/rules/Wildebeest/fr.pug
diff --git a/client/client_OLD/views/rules/Zen/en.pug b/client/src/rules/Zen/en.pug
similarity index 100%
rename from client/client_OLD/views/rules/Zen/en.pug
rename to client/src/rules/Zen/en.pug
diff --git a/client/client_OLD/views/rules/Zen/fr.pug b/client/src/rules/Zen/fr.pug
similarity index 100%
rename from client/client_OLD/views/rules/Zen/fr.pug
rename to client/src/rules/Zen/fr.pug
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
new file mode 100644
index 00000000..6f7cab10
--- /dev/null
+++ b/client/src/translations/en.js
@@ -0,0 +1,96 @@
+export const translations =
+{
+  "Language": "Language",
+  "Contact form": "Contact form",
+  "Email": "Email",
+  "Subject": "Subject",
+  "Content": "Content",
+  "Email sent!": "Email sent!",
+  "Hall": "Hall",
+  "My games": "My games",
+
+  // Index page:
+  "Help": "Help",
+  "First visit?": "First visit?",
+  ">>> Please read this <<<": ">>> Please read this <<<",
+  // Variants boxes:
+  "Both sides of the mirror": "Both sides of the mirror",
+  "Keep antiking in check": "Keep antiking in check",
+  "Explosive captures": "Explosive captures",
+  "Shared pieces": "Shared pieces",
+  "Standard rules": "Standard rules",
+  "Captures reborn": "Captures reborn",
+  "Capture all of a kind": "Capture all of a kind",
+  "Big board": "Big board",
+  "Lose all pieces": "Lose all pieces",
+  "Laws of attraction": "Laws of attraction",
+  "Exchange pieces positions": "Exchange pieces positions",
+  "Exotic captures": "Exotic captures",
+  "Balanced sliders & leapers": "Balanced sliders & leapers",
+  "Reverse captures": "Reverse captures",
+  "Pawns move diagonally": "Pawns move diagonally",
+  "In the shadow": "In the shadow",
+  "Move twice": "Move twice",
+  "Board upside down": "Board upside down",
+
+  // Variant page:
+  "New game": "New game",
+  "Waiting for opponent...": "Waiting for opponent...",
+  "Rules": "Rules",
+  "Play": "Play",
+  "Problems": "Problems",
+  "White win": "White win",
+  "Black win": "Black win",
+  "Draw": "Draw",
+  "New live game": "New live game",
+  "New game versus computer": "New game versus computer",
+  "Analysis mode": "Analysis mode",
+  "Start chat": "Start chat",
+  "Clear current game": "Clear current game",
+  "Settings": "Settings",
+  "Resign": "Resign",
+  "Undo": "Undo",
+  "Flip board": "Flip board",
+  "Game state (FEN):": "Game state (FEN):",
+  "Ok": "Ok",
+  "Random": "Random",
+  "Preferences": "Preferences",
+  "My name is...": "My name is...",
+  "Show hints?": "Show hints?",
+  "Board colors": "Board colors",
+  "brown": "brown",
+  "green": "green",
+  "blue": "blue",
+  "Play sounds?": "Play sounds?",
+  "None": "None",
+  "All": "All",
+  "Chat with ": "Chat with ",
+  "Type here": "Type here",
+  "Send": "Send",
+  "Download PGN": "Download PGN",
+  "Show solution": "Show solution",
+  "Load previous problems": "Load previous problems",
+  "Load next problems": "Load next problems",
+  "New": "New",
+  "Add a problem": "Add a problem",
+  "Full FEN description": "Full FEN description",
+  "Safe HTML tags allowed": "Safe HTML tags allowed",
+  "Instructions": "Instructions",
+  "Describe the problem goal": "Describe the problem goal",
+  "Solution": "Solution",
+  "How to solve the problem?": "How to solve the problem?",
+  "Preview": "Preview",
+  "Cancel": "Cancel",
+  "Solve": "Solve",
+  "Bad FEN description": "Bad FEN description",
+  "Empty instructions": "Empty instructions",
+  "Empty solution": "Empty solution",
+  "Already playing a game in this variant on another tab!":
+    "Already playing a game in this variant on another tab!",
+  "Finish your ": "Finish your ",
+  " game first!": " game first!",
+  ": unfinished computer game will be erased":
+    ": unfinished computer game will be erased",
+  ": current analysis will be erased":
+    ": current analysis will be erased",
+};
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
new file mode 100644
index 00000000..e3482187
--- /dev/null
+++ b/client/src/translations/es.js
@@ -0,0 +1,89 @@
+export const translations =
+{
+  "Language": "Idioma",
+
+  // Index page:
+  "Help": "Ayuda",
+  "First visit?": "¿ Primera visita ?",
+  ">>> Please read this <<<": ">>> Por favor lee esto <<<",
+  // Variants boxes:
+  "Both sides of the mirror": "Ambos lados del espejo",
+  "Keep antiking in check": "Mantener el antirey en jaque",
+  "Explosive captures": "Capturas explosivas",
+  "Shared pieces": "Piezas compartidas",
+  "Standard rules": "Reglas estandar",
+  "Captures reborn": "Las capturas renacen",
+  "Capture all of a kind": "Capturar todo del mismo tipo",
+  "Big board": "Gran tablero",
+  "Lose all pieces": "Perder todas las piezas",
+  "Laws of attraction": "Las leyes de las atracciones",
+  "Exchange pieces positions": "Intercambiar las posiciones de las piezas",
+  "Exotic captures": "Capturas exóticas",
+  "Balanced sliders & leapers": "Modos de desplazamiento equilibrados",
+  "Reverse captures": "Capturas invertidas",
+  "Pawns move diagonally": "Peones se mueven en diagonal",
+  "In the shadow": "En la sombra",
+  "Move twice": "Mover dos veces",
+  "Board upside down": "Tablero al revés",
+
+  // Variant page:
+  "New game": "Nueva partida",
+  "Waiting for opponent...": "Esperando a un oponente...",
+  "Rules": "Reglas",
+  "Play": "Jugar",
+  "Problems": "Problemas",
+  "White win": "Las blancas ganan",
+  "Black win": "Las negras ganan",
+  "Draw": "Empate",
+  "New live game": "Nueva partida en vivo",
+  "New game versus computer": "Nueva partida contra la computadora",
+  "Analysis mode": "Modo de análisis",
+  "Start chat": "Iniciar chat",
+  "Clear current game": "Borrar la partida actual",
+  "Settings": "Ajustes",
+  "Resign": "Abandonar",
+  "Undo": "Deshacer",
+  "Flip board": "Girar el tablero",
+  "Game state (FEN):": "Estado del juego (FEN) :",
+  "Ok": "Ok",
+  "Random": "Aleatorio",
+  "Preferences": "Preferencias",
+  "My name is...": "Mi nombre es...",
+  "Show hints?": "Ayudas visuales ?",
+  "Board colors": "Colores del tablero",
+  "brown": "marrón",
+  "green": "verde",
+  "blue": "azul",
+  "Play sounds?": "¿ Tocar los sonidos ?",
+  "None": "No",
+  "All": "Todos",
+  "Chat with ": "Hablar con ",
+  "Type here": "Escribe aqui",
+  "Send": "Enviar",
+  "Download PGN": "Descargar el PGN",
+  "Show solution": "Mostrar la solucion",
+  "Load previous problems": "Cargar los problemas anteriores",
+  "Load next problems": "Cargar los siguientes problemas",
+  "New": "Nuevo",
+  "Add a problem": "Añadir un problema",
+  "Full FEN description": "Descripción FEN completa",
+  "Safe HTML tags allowed": "HTML 'seguro' autorizado",
+  "Instructions": "Instrucciones",
+  "Describe the problem goal": "Describe el objetivo del problema",
+  "Solution": "Solución",
+  "How to solve the problem?": "¿ Como resolver el problema ?",
+  "Preview": "Previsualizar",
+  "Cancel": "Anular",
+  "Solve": "Resolver",
+  "Bad FEN string": "Mala descripción FEN",
+  "Empty instructions": "Instrucciones vacias",
+  "Empty solution": "Solución vacía",
+  "Already playing a game in this variant on another tab!":
+    "¡ Una partida está en progreso en esta variante en otra pestaña !",
+  "Finish your ": "¡ Termina tu ",
+  " game first!": " partida primero !",
+  ": unfinished computer game will be erased":
+    " : una partida inconclusa contra la computadora será borrado",
+  ": current analysis will be erased":
+    " : el análisis actual será borrado",
+};
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
new file mode 100644
index 00000000..ee65449d
--- /dev/null
+++ b/client/src/translations/fr.js
@@ -0,0 +1,89 @@
+export const translations =
+{
+  "Language": "Langue",
+
+  // Index page:
+  "Help": "Aide",
+  "First visit?": "Première visite ?",
+  ">>> Please read this <<<": ">>> SVP lisez ceci <<<",
+  // Variants boxes:
+  "Both sides of the mirror": "Les deux côté du miroir",
+  "Keep antiking in check": "Gardez l'antiroi en échec",
+  "Explosive captures": "Captures explosives",
+  "Shared pieces": "Pièces partagées",
+  "Standard rules": "Règles usuelles",
+  "Captures reborn": "Les captures renaissent",
+  "Capture all of a kind": "Capturez tout d'un même type",
+  "Big board": "Grand échiquier",
+  "Lose all pieces": "Perdez toutes les pièces",
+  "Laws of attraction": "Les lois de l'attraction",
+  "Exchange pieces positions": "Échangez les positions des pièces",
+  "Exotic captures": "Captures exotiques",
+  "Balanced sliders & leapers": "Modes de déplacement équilibrés",
+  "Reverse captures": "Captures inversées",
+  "Pawns move diagonally": "Les pions vont en diagonale",
+  "In the shadow": "Dans l'ombre",
+  "Move twice": "Jouer deux coups",
+  "Board upside down": "Échiquier à l'envers",
+
+  // Variant page:
+  "New game": "Nouvelle partie",
+  "Waiting for opponent...": "En attente d'un adversaire...",
+  "Rules": "Règles",
+  "Play": "Jouer",
+  "Problems": "Problèmes",
+  "White win": "Les blancs gagnent",
+  "Black win": "Les noirs gagnent",
+  "Draw": "Match nul",
+  "New live game": "Nouvelle partie en direct",
+  "New game versus computer": "Nouvelle partie contre l'ordinateur",
+  "Analysis mode": "Mode analyse",
+  "Start chat": "Démarrer le chat",
+  "Clear current game": "Effacer la partie courante",
+  "Settings": "Réglages",
+  "Resign": "Abandonner",
+  "Undo": "Annuler",
+  "Flip board": "Tourner l'échiquier",
+  "Game state (FEN):": "État de la partie (FEN) :",
+  "Ok": "Ok",
+  "Random": "Aléatoire",
+  "Preferences": "Préférences",
+  "My name is...": "Je m'appelle...",
+  "Show hints?": "Aides visuelles ?",
+  "Board colors": "Couleurs de l'échiquier",
+  "brown": "marron",
+  "green": "vert",
+  "blue": "bleu",
+  "Play sounds?": "Jouer les sons ?",
+  "None": "Aucun",
+  "All": "Tous",
+  "Chat with ": "Discuter avec ",
+  "Type here": "Écrivez ici",
+  "Send": "Envoyer",
+  "Download PGN": "Télécharger le PGN",
+  "Show solution": "Montrer la solution",
+  "Load previous problems": "Charger les problèmes précédents",
+  "Load next problems": "Charger les problèmes suivants",
+  "New": "Nouveau",
+  "Add a problem": "Ajouter un problème",
+  "Full FEN description": "Description FEN complète",
+  "Safe HTML tags allowed": "HTML 'sûr' autorisé",
+  "Instructions": "Instructions",
+  "Describe the problem goal": "Décrire le but du problème",
+  "Solution": "Solution",
+  "How to solve the problem?": "Comment résoudre le problème ?",
+  "Preview": "Prévisualiser",
+  "Cancel": "Annuler",
+  "Solve": "Résoudre",
+  "Bad FEN string": "Mauvaise description FEN",
+  "Empty instructions": "Instructions vides",
+  "Empty solution": "Solution vide",
+  "Already playing a game in this variant on another tab!":
+    "Une partie est en cours sur cette variante dans un autre onglet !",
+  "Finish your ": "Terminez votre ",
+  " game first!": " partie d'abord !",
+  ": unfinished computer game will be erased":
+    " : une partie inachevée contre l'ordinateur sera effacée",
+  ": current analysis will be erased":
+    " : l'analyse en cours sera effacée",
+};
diff --git a/client/src/utils/misc.js b/client/src/utils/misc.js
new file mode 100644
index 00000000..178f3cfe
--- /dev/null
+++ b/client/src/utils/misc.js
@@ -0,0 +1,43 @@
+export const util =
+{
+  // Source: https://www.quirksmode.org/js/cookies.html
+  setCookie: function(name, value)
+  {
+    var date = new Date();
+    date.setTime(date.getTime()+(183*24*60*60*1000)); //6 months
+    var expires = "; expires="+date.toGMTString();
+    document.cookie = name+"="+value+expires+"; path=/";
+  },
+
+  getCookie: function(name, defaut) {
+    var nameEQ = name + "=";
+    var ca = document.cookie.split(';');
+    for (var i=0;i < ca.length;i++)
+    {
+      var c = ca[i];
+      while (c.charAt(0)==' ')
+        c = c.substring(1,c.length);
+      if (c.indexOf(nameEQ) == 0)
+        return c.substring(nameEQ.length,c.length);
+    }
+    return defaut; //cookie not found
+  },
+
+  // Random (enough) string for socket and game IDs
+  getRandString: function()
+  {
+    return (Date.now().toString(36) + Math.random().toString(36).substr(2, 7))
+      .toUpperCase();
+  },
+
+  // Shortcut for an often used click (on a modal)
+  doClick: function(elemId)
+  {
+    document.getElementById(elemId).click(); //or ".checked = true"
+  },
+
+  translate: function(msg)
+  {
+    return translations[msg];
+  },
+};
diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue
index 156976c1..de83b095 100644
--- a/client/src/views/Home.vue
+++ b/client/src/views/Home.vue
@@ -12,7 +12,7 @@ import HelloWorld from "@/components/HelloWorld.vue";
 export default {
   name: "home",
   components: {
-    HelloWorld
+    HelloWorld,
   }
 };
 </script>
diff --git a/server/.gitignore b/server/.gitignore
index 20ffe9f0..aeb7c87d 100644
--- a/server/.gitignore
+++ b/server/.gitignore
@@ -18,7 +18,7 @@ pids
 # Various files
 /db/vchess.sqlite
 /config/parameters.js
-/serve/
+/static/
 
 # Compiled binary addons (https://nodejs.org/api/addons.html)
 build/Release
diff --git a/server/TODO b/server/TODO
new file mode 100644
index 00000000..bcffc7bd
--- /dev/null
+++ b/server/TODO
@@ -0,0 +1,9 @@
+Later: use http2
+https://webapplog.com/http2-node/
+https://www.npmjs.com/package/spdy
+Express 5?
+
+Finish :
+ - models
+ - routes
+ - sockets
diff --git a/server/app.js b/server/app.js
index aa8cade0..b1aee52c 100644
--- a/server/app.js
+++ b/server/app.js
@@ -7,7 +7,7 @@ var favicon = require('serve-favicon');
 
 var app = express();
 
-app.use(favicon(path.join(__dirname, "favicon", "favicon.ico")));
+app.use(favicon(path.join(__dirname, "static", "favicon.ico")));
 
 if (app.get('env') === 'development')
 {
@@ -27,13 +27,13 @@ else
 app.use(express.json());
 app.use(express.urlencoded({ extended: false }));
 app.use(cookieParser());
-app.use(express.static(path.join(__dirname, 'serve'))); //client "prod" files
+app.use(express.static(path.join(__dirname, 'static'))); //client "prod" files
 
 // In development stage the client side has its own server
-if (app.get('env') === 'development')
+if (params.cors.enable)
 {
 	app.use(function(req, res, next) {
-		res.header("Access-Control-Allow-Origin", "*");
+		res.header("Access-Control-Allow-Origin", params.cors.allowedOrigin);
 		res.header("Access-Control-Allow-Headers",
 			"Origin, X-Requested-With, Content-Type, Accept");
 		next();
diff --git a/server/config/parameters.js.dist b/server/config/parameters.js.dist
index 6f274dd3..0beabed0 100644
--- a/server/config/parameters.js.dist
+++ b/server/config/parameters.js.dist
@@ -5,6 +5,12 @@ module.exports =
 
 	// To know in which environment the code run
 	env: process.env.NODE_ENV || 'development',
+	
+	// CORS: cross-origin resource sharing options
+	cors: {
+		enable: Parameters.env === "development",
+		allowedOrigin: "*",
+	},
 
 	// Lifespan of a (login) cookie
 	cookieExpire: 183*24*60*60*1000, //6 months in milliseconds
diff --git a/server/data/challengeCheck.js b/server/data/challengeCheck.js
deleted file mode 100644
index 459396af..00000000
--- a/server/data/challengeCheck.js
+++ /dev/null
@@ -1,34 +0,0 @@
-function checkChallenge(c)
-{
-	const vid = parseInt(c.vid);
-	if (isNaN(vid) || vid <= 0)
-		return "Please select a variant";
-
-	const mainTime = parseInt(c.mainTime);
-	const increment = parseInt(c.increment);
-	if (isNaN(mainTime) || mainTime <= 0)
-		return "Main time should be strictly positive";
-	if (isNaN(increment) || increment < 0)
-		return "Increment must be positive";
-
-	// Basic alphanumeric check for players names
-	let playerCount = 0;
-	for (p of c.players)
-	{
-		if (p.name.length > 0)
-		{
-			if (!p.name.match(/^[\w]+$/))
-				return "Wrong characters in players names";
-			playerCount++;
-		}
-	}
-
-	if (playerCount > 0 && playerCount != c.nbPlayers-1)
-		return "None, or all of the opponent names must be filled"
-
-	// Just characters check on server:
-	if (!c.fen.match(/^[a-zA-Z0-9, /-]*$/))
-		return "Bad FEN string";
-}
-
-module.exports = checkChallenge;
diff --git a/server/data/nbPlayers.js b/server/data/nbPlayers.js
deleted file mode 100644
index 3b41a94b..00000000
--- a/server/data/nbPlayers.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const NbPlayers =
-{
-	"Alice": [2,3,4],
-	"Antiking": [2,3,4],
-	"Atomic": [2,3,4],
-	"Baroque": [2,3,4],
-	"Berolina": [2,4],
-	"Checkered": [2,3,4],
-	"Chess960": [2,3,4],
-	"Crazyhouse": [2,3,4],
-	"Dark": [2,3,4],
-	"Extinction": [2,3,4],
-	"Grand": [2],
-	"Losers": [2,3,4],
-	"Magnetic": [2],
-	"Marseille": [2],
-	"Switching": [2,3,4],
-	"Upsidedown": [2],
-	"Wildebeest": [2],
-	"Zen": [2,3,4],
-};
-
-module.exports = NbPlayers;
diff --git a/server/data/userCheck.js b/server/data/userCheck.js
deleted file mode 100644
index 9c46b2ab..00000000
--- a/server/data/userCheck.js
+++ /dev/null
@@ -1,19 +0,0 @@
-function checkNameEmail(o)
-{
-	if (typeof o.name === "string")
-	{
-		if (o.name.length == 0)
-			return "Empty name";
-		if (!o.name.match(/^[\w]+$/))
-			return "Bad characters in name";
-	}
-	if (typeof o.email === "string")
-	{
-		if (o.email.length == 0)
-			return "Empty email";
-		if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/))
-			return "Bad characters in email";
-	}
-}
-
-module.exports = checkNameEmail;
diff --git a/server/favicon/android-chrome-192x192.png b/server/favicon/android-chrome-192x192.png
deleted file mode 100644
index 727630a2..00000000
--- a/server/favicon/android-chrome-192x192.png
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat 9249f7ccb03573152b1210b27c244462ac286aea                12108
diff --git a/server/favicon/android-chrome-512x512.png b/server/favicon/android-chrome-512x512.png
deleted file mode 100644
index bd462bbe..00000000
--- a/server/favicon/android-chrome-512x512.png
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat 510e77fc0cd116eb8ef2e8743718f4c1c22393ec                33049
diff --git a/server/favicon/apple-touch-icon.png b/server/favicon/apple-touch-icon.png
deleted file mode 100644
index 1b453ab6..00000000
--- a/server/favicon/apple-touch-icon.png
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat 60b73e04ab8fc7a125745cbdc8f6f5cb298c105f                12621
diff --git a/server/favicon/browserconfig.xml b/server/favicon/browserconfig.xml
deleted file mode 100644
index bb8e477b..00000000
--- a/server/favicon/browserconfig.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<browserconfig>
-    <msapplication>
-        <tile>
-					<square150x150logo src="/images/favicon/mstile-150x150.png"/>
-            <TileColor>#da532c</TileColor>
-        </tile>
-    </msapplication>
-</browserconfig>
diff --git a/server/favicon/favicon-16x16.png b/server/favicon/favicon-16x16.png
deleted file mode 100644
index b334454c..00000000
--- a/server/favicon/favicon-16x16.png
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat 10c5fd36c2cb305a3f7e6a6f39eccba03a679087                  995
diff --git a/server/favicon/favicon-32x32.png b/server/favicon/favicon-32x32.png
deleted file mode 100644
index 386456d5..00000000
--- a/server/favicon/favicon-32x32.png
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat 53091595a131897b202391f3f0ffaa61f283c56a                 1877
diff --git a/server/favicon/favicon.ico b/server/favicon/favicon.ico
deleted file mode 100644
index 75bd50b7..00000000
--- a/server/favicon/favicon.ico
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat c12f062ae7f59219c96acae47fafca862cb035a2                15086
diff --git a/server/favicon/manifest.json b/server/favicon/manifest.json
deleted file mode 100644
index e967fc59..00000000
--- a/server/favicon/manifest.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "name": "",
-    "icons": [
-        {
-            "src": "/images/favicon/android-chrome-192x192.png",
-            "sizes": "192x192",
-            "type": "image/png"
-        },
-        {
-            "src": "/images/favicon/android-chrome-512x512.png",
-            "sizes": "512x512",
-            "type": "image/png"
-        }
-    ],
-    "theme_color": "#ffffff",
-    "background_color": "#ffffff",
-    "display": "standalone"
-}
diff --git a/server/favicon/mstile-150x150.png b/server/favicon/mstile-150x150.png
deleted file mode 100644
index 928fe055..00000000
--- a/server/favicon/mstile-150x150.png
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat fc323034e5b15647a98909a7cce05bb80eecef93                 5784
diff --git a/server/favicon/safari-pinned-tab.svg b/server/favicon/safari-pinned-tab.svg
deleted file mode 100644
index da1d457a..00000000
--- a/server/favicon/safari-pinned-tab.svg
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
- "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
-<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
- width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
- preserveAspectRatio="xMidYMid meet">
-<metadata>
-Created by potrace 1.11, written by Peter Selinger 2001-2013
-</metadata>
-<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
-fill="#000000" stroke="none">
-<path d="M2379 4963 c-1 -10 -2 -85 -3 -168 l-1 -150 -160 0 -160 0 -3 -168
--2 -167 47 -1 c27 0 100 -1 163 -2 l115 -2 1 -173 0 -173 -35 -33 c-120 -110
--227 -244 -237 -296 0 -3 -6 -25 -13 -49 -23 -82 -40 -186 -42 -269 -2 -93 -8
--132 -19 -132 -4 0 -74 36 -156 80 -81 44 -149 80 -150 80 -1 0 -19 8 -40 17
--22 9 -41 17 -44 18 -3 0 -10 3 -16 7 -15 10 -160 46 -219 54 -72 10 -300 12
--365 2 -153 -23 -378 -121 -489 -213 -52 -43 -136 -133 -176 -190 -180 -252
--206 -530 -74 -805 84 -176 177 -292 453 -565 l213 -210 2 -190 c5 -364 17
--639 32 -711 7 -39 19 -60 47 -86 44 -40 123 -86 207 -121 117 -47 100 -41
-140 -52 17 -4 35 -10 40 -14 6 -3 15 -6 20 -7 6 -1 37 -10 70 -19 76 -21 161
--42 200 -49 17 -2 41 -7 55 -10 58 -13 77 -16 120 -21 25 -3 56 -7 70 -10 69
--14 250 -20 585 -20 390 0 553 8 665 31 14 3 39 7 55 9 17 1 55 8 85 15 30 6
-69 14 85 17 17 2 71 16 120 30 50 14 97 27 105 28 8 2 16 4 18 5 1 2 5 3 10 5
-4 1 16 5 27 9 11 4 52 19 90 33 87 32 202 94 242 130 31 28 55 65 60 93 9 54
-14 122 18 240 2 74 7 225 11 335 3 110 7 230 8 267 l2 67 65 62 c35 33 120
-116 187 182 419 413 559 725 470 1047 -7 25 -13 47 -14 50 0 3 -6 19 -13 35
--131 321 -408 538 -766 602 -22 4 -110 7 -195 7 -169 -1 -273 -17 -395 -62
--64 -24 -205 -93 -231 -113 -26 -21 -175 -91 -181 -85 -4 3 -8 43 -9 89 -4
-209 -38 342 -116 455 -16 23 -73 86 -125 139 l-96 96 1 141 c0 78 1 156 1 173
-l1 33 161 -2 c106 -1 163 2 167 9 5 9 8 298 3 323 -1 3 -75 5 -166 5 l-165 -1
--1 30 c0 17 -1 93 -1 169 l0 137 -179 0 c-163 0 -179 -2 -180 -17z m285 -1203
-c98 -44 194 -185 213 -311 10 -63 9 -189 -1 -271 -11 -87 -57 -214 -151 -415
--24 -50 -46 -101 -50 -112 -3 -12 -10 -21 -15 -21 -4 0 -11 -11 -14 -25 -10
--39 -77 -146 -89 -142 -12 4 -107 174 -101 180 3 2 -2 10 -11 17 -8 7 -15 16
--15 21 0 5 -16 44 -36 86 -91 199 -139 329 -149 405 -11 85 -8 279 5 328 35
-129 126 240 228 276 49 17 128 11 186 -16z m-1245 -490 c100 -19 209 -55 257
--87 16 -10 31 -17 34 -16 17 8 196 -123 288 -211 154 -146 325 -411 387 -596
-36 -110 60 -213 70 -305 3 -27 8 -70 11 -95 11 -97 7 -281 -7 -295 -18 -17
--116 -30 -304 -40 -148 -8 -318 -22 -380 -31 -135 -19 -170 -26 -377 -72 -133
--30 -161 -25 -260 44 -140 99 -238 185 -420 367 -258 260 -330 401 -331 647 0
-132 11 176 71 299 41 83 246 281 290 281 6 0 12 4 14 8 6 16 170 87 228 99 14
-2 43 8 65 12 69 14 263 9 364 -9z m2661 7 c76 -18 119 -31 150 -44 270 -115
-439 -297 490 -527 10 -47 14 -191 6 -248 -16 -113 -88 -259 -181 -368 -39 -46
--321 -328 -360 -360 -17 -14 -55 -45 -85 -70 -30 -25 -57 -47 -60 -50 -3 -3
--39 -27 -80 -55 -72 -48 -78 -50 -142 -49 -37 1 -75 5 -85 9 -20 9 -25 10 -68
-19 -16 3 -50 10 -75 15 -170 38 -291 54 -480 66 -155 9 -321 22 -385 30 -11 1
--28 3 -37 4 -42 3 -47 23 -43 190 2 88 6 167 8 178 3 10 8 41 11 68 34 306
-240 683 501 912 193 170 375 257 586 279 35 3 66 8 68 10 6 6 232 -3 261 -9z
-m-909 -2041 c176 -9 246 -19 383 -53 247 -60 370 -151 296 -218 -26 -23 -75
--21 -230 10 -134 27 -284 54 -340 60 -19 2 -53 6 -75 9 -22 3 -69 8 -105 11
--36 4 -83 8 -105 10 -228 22 -667 22 -880 0 -22 -2 -69 -7 -105 -10 -104 -10
--216 -24 -240 -30 -8 -2 -35 -7 -60 -10 -25 -4 -112 -20 -195 -36 -188 -38
--218 -39 -248 -8 -13 13 -22 30 -21 37 1 6 2 16 3 22 2 20 58 63 118 91 65 31
-245 79 353 94 67 9 136 14 265 21 72 3 131 7 133 8 4 5 960 -3 1053 -8z m-381
--371 c125 -7 306 -22 360 -30 14 -2 48 -6 75 -10 28 -3 66 -9 85 -11 74 -10
-248 -46 315 -66 85 -24 204 -80 228 -106 33 -37 28 -89 -11 -118 -14 -10 -97
-3 -197 30 -5 2 -28 7 -50 11 -49 11 -47 10 -65 17 -8 4 -69 18 -135 32 -107
-22 -155 31 -286 50 -92 14 -269 20 -549 21 -361 0 -509 -9 -700 -44 -30 -6
--66 -12 -80 -15 -14 -2 -81 -18 -150 -36 -69 -17 -134 -33 -145 -35 -11 -3
--59 -13 -107 -24 l-87 -19 -20 22 c-11 11 -23 33 -26 48 -19 75 164 162 450
-214 41 7 161 26 195 30 19 2 50 6 69 9 18 3 68 8 110 10 42 3 92 7 111 10 19
-2 87 7 150 11 63 3 116 7 118 9 4 4 223 -2 342 -10z"/>
-<path d="M2525 3631 c-31 -13 -71 -58 -93 -104 -36 -77 -44 -226 -17 -344 3
--13 7 -33 10 -44 17 -78 85 -227 117 -256 19 -17 20 -17 35 2 32 42 94 183
-114 260 20 79 29 235 17 300 -16 82 -60 155 -110 181 -30 16 -44 16 -73 5z"/>
-<path d="M1197 3094 c-1 -1 -31 -4 -67 -7 -97 -9 -223 -49 -282 -89 -14 -10
--29 -18 -32 -18 -34 0 -206 -193 -206 -231 0 -5 -4 -17 -9 -27 -39 -71 -51
--165 -31 -241 13 -52 81 -192 118 -241 64 -87 236 -266 369 -387 48 -43 101
--91 118 -107 58 -54 74 -58 177 -41 51 8 104 17 118 20 14 3 41 8 60 11 19 3
-80 14 135 24 117 21 143 26 310 51 44 6 91 13 105 14 113 14 139 17 169 19 20
-1 37 10 43 20 22 39 -19 225 -92 416 -96 254 -210 452 -319 552 -166 154 -387
-249 -604 260 -42 3 -78 3 -80 2z"/>
-<path d="M3858 3095 c-2 -1 -34 -5 -73 -8 -68 -6 -144 -21 -175 -34 -8 -3 -20
--7 -25 -8 -23 -5 -121 -49 -135 -60 -8 -7 -25 -16 -37 -19 -13 -4 -23 -12 -23
--17 0 -5 -7 -9 -15 -9 -8 0 -15 -4 -15 -10 0 -5 -5 -10 -11 -10 -6 0 -17 -6
--24 -12 -8 -7 -44 -40 -80 -73 -82 -73 -139 -152 -198 -270 -59 -120 -87 -182
--87 -194 0 -6 -4 -11 -10 -11 -5 0 -10 -9 -10 -19 0 -11 -4 -23 -10 -26 -5 -3
--10 -13 -10 -22 0 -8 -6 -29 -14 -46 -37 -82 -85 -266 -86 -327 -1 -58 3 -70
-19 -71 9 -1 27 -2 41 -4 14 -2 52 -6 85 -10 33 -3 71 -8 85 -10 14 -3 43 -7
-65 -10 64 -10 84 -13 190 -30 55 -9 109 -18 120 -19 21 -3 126 -23 319 -62 66
--13 125 -20 132 -16 53 30 475 446 546 539 123 159 158 294 113 438 -42 135
--126 248 -239 320 -73 47 -215 93 -307 101 -41 3 -86 7 -101 9 -15 2 -28 2
--30 0z"/>
-</g>
-</svg>
diff --git a/server/models/Challenge.js b/server/models/Challenge.js
index 96db0a2b..e1fb448b 100644
--- a/server/models/Challenge.js
+++ b/server/models/Challenge.js
@@ -18,6 +18,39 @@ var db = require("../utils/database");
 
 const ChallengeModel =
 {
+	checkChallenge: function(c)
+	{
+		const vid = parseInt(c.vid);
+		if (isNaN(vid) || vid <= 0)
+			return "Please select a variant";
+
+		const mainTime = parseInt(c.mainTime);
+		const increment = parseInt(c.increment);
+		if (isNaN(mainTime) || mainTime <= 0)
+			return "Main time should be strictly positive";
+		if (isNaN(increment) || increment < 0)
+			return "Increment must be positive";
+
+		// Basic alphanumeric check for players names
+		let playerCount = 0;
+		for (p of c.players)
+		{
+			if (p.name.length > 0)
+			{
+				if (!p.name.match(/^[\w]+$/))
+					return "Wrong characters in players names";
+				playerCount++;
+			}
+		}
+
+		if (playerCount > 0 && playerCount != c.nbPlayers-1)
+			return "None, or all of the opponent names must be filled"
+
+		// Just characters check on server:
+		if (!c.fen.match(/^[a-zA-Z0-9, /-]*$/))
+			return "Bad FEN string";
+	},
+
 	// fen cannot be undefined; TODO: generate fen on server instead
 	create: function(c, cb)
 	{
diff --git a/server/models/User.js b/server/models/User.js
index a36ab683..cf4c5293 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -16,6 +16,24 @@ var params = require("../config/parameters");
 
 const UserModel =
 {
+	checkNameEmail: function(o)
+	{
+		if (typeof o.name === "string")
+		{
+			if (o.name.length == 0)
+				return "Empty name";
+			if (!o.name.match(/^[\w]+$/))
+				return "Bad characters in name";
+		}
+		if (typeof o.email === "string")
+		{
+			if (o.email.length == 0)
+				return "Empty email";
+			if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/))
+				return "Bad characters in email";
+		}
+	},
+
 	// NOTE: parameters are already cleaned (in controller), thus no sanitization here
 	create: function(name, email, notify, callback)
 	{
diff --git a/server/models/Variant.js b/server/models/Variant.js
index 233d938b..ae417932 100644
--- a/server/models/Variant.js
+++ b/server/models/Variant.js
@@ -9,6 +9,29 @@ var db = require("../utils/database");
 
 const VariantModel =
 {
+	// This is duplicated in client. TODO: really required here?
+	NbPlayers:
+	{
+		"Alice": [2,3,4],
+		"Antiking": [2,3,4],
+		"Atomic": [2,3,4],
+		"Baroque": [2,3,4],
+		"Berolina": [2,4],
+		"Checkered": [2,3,4],
+		"Chess960": [2,3,4],
+		"Crazyhouse": [2,3,4],
+		"Dark": [2,3,4],
+		"Extinction": [2,3,4],
+		"Grand": [2],
+		"Losers": [2,3,4],
+		"Magnetic": [2],
+		"Marseille": [2],
+		"Switching": [2,3,4],
+		"Upsidedown": [2],
+		"Wildebeest": [2],
+		"Zen": [2,3,4],
+	},
+
 	getByName: function(name, callback)
 	{
 		db.serialize(function() {
diff --git a/server/routes/challenges.js b/server/routes/challenges.js
index 962b8d42..174cf451 100644
--- a/server/routes/challenges.js
+++ b/server/routes/challenges.js
@@ -3,7 +3,6 @@
 let router = require("express").Router();
 const access = require("../utils/access");
 const ChallengeModel = require("../models/Challenge");
-const checkChallenge = require("../data/challengeCheck.js");
 
 router.post("/challenges/:vid([0-9]+)", access.logged, access.ajax, (req,res) => {
 	const vid = req.params["vid"];
@@ -16,7 +15,7 @@ router.post("/challenges/:vid([0-9]+)", access.logged, access.ajax, (req,res) =>
 		nbPlayers: req.body["nbPlayers"],
 		players: req.body["players"],
 	};
-	const error = checkChallenge(chall);
+	const error = ChallengeModel.checkChallenge(chall);
 	ChallengeModel.create(chall, (err,lastId) => {
 		res.json(err || {cid: lastId["rowid"]});
 	});
diff --git a/server/routes/users.js b/server/routes/users.js
index 2b39cc05..8df0c43e 100644
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -6,7 +6,6 @@ var sendEmail = require('../utils/mailer');
 var genToken = require("../utils/tokenGenerator");
 var access = require("../utils/access");
 var params = require("../config/parameters");
-var checkNameEmail = require("../data/userCheck")
 
 // to: object user (to who we send an email)
 function setAndSendLoginToken(subject, to, res)
@@ -33,7 +32,7 @@ router.post('/register', access.unlogged, access.ajax, (req,res) => {
 	const name = req.body.name;
 	const email = req.body.email;
 	const notify = !!req.body.notify;
-	const error = checkNameEmail({name: name, email: email});
+	const error = UserModel.checkNameEmail({name: name, email: email});
 	if (!!error)
 		return res.json({errmsg: error});
 	UserModel.create(name, email, notify, (err,uid) => {
@@ -51,7 +50,7 @@ router.post('/register', access.unlogged, access.ajax, (req,res) => {
 router.get('/sendtoken', access.unlogged, access.ajax, (req,res) => {
 	const nameOrEmail = decodeURIComponent(req.query.nameOrEmail);
 	const type = (nameOrEmail.indexOf('@') >= 0 ? "email" : "name");
-	const error = checkNameEmail({[type]: nameOrEmail});
+	const error = UserModel.checkNameEmail({[type]: nameOrEmail});
 	if (!!error)
 		return res.json({errmsg: error});
 	UserModel.getOne(type, nameOrEmail, (err,user) => {
@@ -86,7 +85,7 @@ router.get('/authenticate', access.unlogged, (req,res) => {
 router.put('/update', access.logged, access.ajax, (req,res) => {
 	const name = req.body.name;
 	const email = req.body.email;
-	const error = checkNameEmail({name: name, email: email});
+	const error = UserModel.checkNameEmail({name: name, email: email});
 	if (!!error)
 		return res.json({errmsg: error});
 	const user = {
diff --git a/server/sockets.js b/server/sockets.js
index 1ebd5212..8a0614b1 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -2,11 +2,12 @@ const url = require('url');
 const VariantModel = require("./models/Variant");
 
 // Node version in Ubuntu 16.04 does not know about URL class
-function getJsonFromUrl(url) {
-	var query = url.substr(2); //starts with "/?"
-	var result = {};
-	query.split("&").forEach(function(part) {
-		var item = part.split("=");
+function getJsonFromUrl(url)
+{
+	const query = url.substr(2); //starts with "/?"
+	let result = {};
+	query.split("&").forEach((part) => {
+		const item = part.split("=");
 		result[item[0]] = decodeURIComponent(item[1]);
 	});
 	return result;
-- 
2.44.0