From 247356cde3f4e36004942c5f57187d667f7ef27c Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sun, 23 Dec 2018 12:52:27 +0100
Subject: [PATCH] Full translation

---
 public/javascripts/components/game.js         | 73 ++++++++++---------
 .../javascripts/components/problemSummary.js  |  5 +-
 public/javascripts/components/problems.js     | 29 ++++----
 .../javascripts/components/variantSummary.js  |  7 +-
 views/index.pug                               | 31 +++++---
 views/langNames/en.pug                        |  5 ++
 views/langNames/fr.pug                        |  5 ++
 views/{modal-help.pug => modal-help/en.pug}   |  2 +-
 views/modal-help/fr.pug                       | 18 +++++
 views/{modal-lang.pug => modal-lang/en.pug}   | 11 +--
 views/modal-lang/fr.pug                       | 25 +++++++
 views/translations/en.pug                     | 73 +++++++++++++++++++
 views/translations/fr.pug                     | 73 +++++++++++++++++++
 views/variant.pug                             | 36 +++++----
 14 files changed, 312 insertions(+), 81 deletions(-)
 create mode 100644 views/langNames/en.pug
 create mode 100644 views/langNames/fr.pug
 rename views/{modal-help.pug => modal-help/en.pug} (90%)
 create mode 100644 views/modal-help/fr.pug
 rename views/{modal-lang.pug => modal-lang/en.pug} (69%)
 create mode 100644 views/modal-lang/fr.pug
 create mode 100644 views/translations/en.pug
 create mode 100644 views/translations/fr.pug

diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index 6d42f27f..54334e30 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -52,7 +52,7 @@ Vue.component('my-game', {
 			h('button',
 			{
 				on: { click: this.clickGameSeek },
-				attrs: { "aria-label": 'New online game' },
+				attrs: { "aria-label": translations['New online game'] },
 				'class': {
 					"tooltip": true,
 					"play": true,
@@ -69,7 +69,7 @@ Vue.component('my-game', {
 				h('button',
 				{
 					on: { click: this.clickComputerGame },
-					attrs: { "aria-label": 'New game VS computer' },
+					attrs: { "aria-label": translations['New game versus computer'] },
 					'class': {
 						"tooltip":true,
 						"play": true,
@@ -86,7 +86,7 @@ Vue.component('my-game', {
 				h('button',
 				{
 					on: { click: this.clickFriendGame },
-					attrs: { "aria-label": 'New IRL game' },
+					attrs: { "aria-label": translations['Analysis mode'] },
 					'class': {
 						"tooltip":true,
 						"play": true,
@@ -139,7 +139,7 @@ Vue.component('my-game', {
 					{
 						on: { click: this.startChat },
 						attrs: {
-							"aria-label": 'Start chat',
+							"aria-label": translations['Start chat'],
 							"id": "chatBtn",
 						},
 						'class': {
@@ -160,7 +160,7 @@ Vue.component('my-game', {
 					{
 						on: { click: this.clearComputerGame },
 						attrs: {
-							"aria-label": 'Clear computer game',
+							"aria-label": translations['Clear game versus computer'],
 							"id": "clearBtn",
 						},
 						'class': {
@@ -194,7 +194,7 @@ Vue.component('my-game', {
 				{
 					on: { click: this.showSettings },
 					attrs: {
-						"aria-label": 'Settings',
+						"aria-label": translations['Settings'],
 						"id": "settingsBtn",
 					},
 					'class': {
@@ -361,7 +361,7 @@ Vue.component('my-game', {
 					h('button',
 						{
 							on: { click: this.resign },
-							attrs: { "aria-label": 'Resign' },
+							attrs: { "aria-label": translations['Resign'] },
 							'class': {
 								"tooltip":true,
 								"play": true,
@@ -377,7 +377,7 @@ Vue.component('my-game', {
 					h('button',
 						{
 							on: { click: e => this.undo() },
-							attrs: { "aria-label": 'Undo' },
+							attrs: { "aria-label": translations['Undo'] },
 							"class": {
 								"play": true,
 								"spaceleft": true,
@@ -387,7 +387,7 @@ Vue.component('my-game', {
 					h('button',
 						{
 							on: { click: e => this.play() },
-							attrs: { "aria-label": 'Play' },
+							attrs: { "aria-label": translations['Play'] },
 							"class": {
 								"play": true,
 								"spaceleft": true,
@@ -404,7 +404,7 @@ Vue.component('my-game', {
 					h('button',
 						{
 							on: { click: this.undoInGame },
-							attrs: { "aria-label": 'Undo' },
+							attrs: { "aria-label": trnaslations['Undo'] },
 							"class": {
 								"play": true,
 								"spaceleft": true,
@@ -415,7 +415,7 @@ Vue.component('my-game', {
 					h('button',
 						{
 							on: { click: () => { this.mycolor = this.vr.getOppCol(this.mycolor) } },
-							attrs: { "aria-label": 'Flip' },
+							attrs: { "aria-label": translations['Flip'] },
 							"class": {
 								"play": true,
 								"spaceleft": true,
@@ -514,7 +514,12 @@ Vue.component('my-game', {
 					[
 						h('div',
 							{
-								"class": { "card": true, "smallpad": true },
+								"class": {
+									"card": true,
+									"smallpad": true,
+									"small-modal": true,
+									"text-center": true,
+								},
 							},
 							[
 								h('label',
@@ -563,7 +568,7 @@ Vue.component('my-game', {
 								{
 									attrs: { "id": "titleFenedit" },
 									"class": { "section": true },
-									domProps: { innerHTML: "Position + flags (FEN):" },
+									domProps: { innerHTML: translations["Position + flags (FEN):"] },
 								}
 							),
 							h('input',
@@ -584,7 +589,7 @@ Vue.component('my-game', {
 											this.newGame("friend", fen);
 										}
 									},
-									domProps: { innerHTML: "Ok" },
+									domProps: { innerHTML: translations["Ok"] },
 								}
 							),
 							h('button',
@@ -595,7 +600,7 @@ Vue.component('my-game', {
 												VariantRules.GenRandInitFen();
 										}
 									},
-									domProps: { innerHTML: "Random" },
+									domProps: { innerHTML: translations["Random"] },
 								}
 							),
 						]
@@ -630,7 +635,7 @@ Vue.component('my-game', {
 								{
 									attrs: { "id": "settingsTitle" },
 									"class": { "section": true },
-									domProps: { innerHTML: "Preferences" },
+									domProps: { innerHTML: translations["Preferences"] },
 								}
 							),
 							h('fieldset',
@@ -639,7 +644,7 @@ Vue.component('my-game', {
 									h('label',
 										{
 											attrs: { for: "nameSetter" },
-											domProps: { innerHTML: "My name is..." },
+											domProps: { innerHTML: translations["My name is..."] },
 										},
 									),
 									h('input',
@@ -660,7 +665,7 @@ Vue.component('my-game', {
 									h('label',
 										{
 											attrs: { for: "setHints" },
-											domProps: { innerHTML: "Show hints?" },
+											domProps: { innerHTML: translations["Show hints?"] },
 										},
 									),
 									h('input',
@@ -681,7 +686,7 @@ Vue.component('my-game', {
 									h('label',
 										{
 											attrs: { for: "selectColor" },
-											domProps: { innerHTML: "Board colors" },
+											domProps: { innerHTML: translations["Board colors"] },
 										},
 									),
 									h("select",
@@ -694,7 +699,7 @@ Vue.component('my-game', {
 												{
 													domProps: {
 														"value": "lichess",
-														innerHTML: "brown"
+														innerHTML: translations["brown"]
 													},
 													attrs: { "selected": this.color=="lichess" },
 												}
@@ -703,7 +708,7 @@ Vue.component('my-game', {
 												{
 													domProps: {
 														"value": "chesscom",
-														innerHTML: "green"
+														innerHTML: translations["green"]
 													},
 													attrs: { "selected": this.color=="chesscom" },
 												}
@@ -712,7 +717,7 @@ Vue.component('my-game', {
 												{
 													domProps: {
 														"value": "chesstempo",
-														innerHTML: "blue"
+														innerHTML: translations["blue"]
 													},
 													attrs: { "selected": this.color=="chesstempo" },
 												}
@@ -727,7 +732,7 @@ Vue.component('my-game', {
 									h('label',
 										{
 											attrs: { for: "selectSound" },
-											domProps: { innerHTML: "Play sounds?" },
+											domProps: { innerHTML: translations["Play sounds?"] },
 										},
 									),
 									h("select",
@@ -740,7 +745,7 @@ Vue.component('my-game', {
 												{
 													domProps: {
 														"value": "0",
-														innerHTML: "None"
+														innerHTML: translations["None"]
 													},
 													attrs: { "selected": this.sound==0 },
 												}
@@ -749,7 +754,7 @@ Vue.component('my-game', {
 												{
 													domProps: {
 														"value": "1",
-														innerHTML: "Newgame"
+														innerHTML: translations["New game"]
 													},
 													attrs: { "selected": this.sound==1 },
 												}
@@ -758,7 +763,7 @@ Vue.component('my-game', {
 												{
 													domProps: {
 														"value": "2",
-														innerHTML: "All"
+														innerHTML: translations["All"]
 													},
 													attrs: { "selected": this.sound==2 },
 												}
@@ -785,7 +790,7 @@ Vue.component('my-game', {
 				{
 					attrs: { "id": "titleChat" },
 					"class": { "section": true },
-					domProps: { innerHTML: "Chat with " + this.oppName },
+					domProps: { innerHTML: translations["Chat with "] + this.oppName },
 				}
 			)
 		];
@@ -809,7 +814,7 @@ Vue.component('my-game', {
 					attrs: {
 						"id": "input-chat",
 						type: "text",
-						placeholder: "Type here",
+						placeholder: translations["Type here"],
 					},
 					on: { keyup: this.trySendChat }, //if key is 'enter'
 				}
@@ -818,7 +823,7 @@ Vue.component('my-game', {
 				{
 					attrs: { id: "sendChatBtn"},
 					on: { click: this.sendChat },
-					domProps: { innerHTML: "Send" },
+					domProps: { innerHTML: translations["Send"] },
 				}
 			)
 		]);
@@ -878,7 +883,7 @@ Vue.component('my-game', {
 							{
 								attrs: { "id": "downloadBtn" },
 								on: { click: this.download },
-								domProps: { innerHTML: "Download game" },
+								domProps: { innerHTML: translations["Download game"] },
 							}
 						),
 					]
@@ -900,7 +905,7 @@ Vue.component('my-game', {
 							h('h3',
 								{
 									"class": { clickable: true },
-									domProps: { innerHTML: "Show solution" },
+									domProps: { innerHTML: translations["Show solution"] },
 									on: { click: this.toggleShowSolution },
 								}
 							),
@@ -962,13 +967,13 @@ Vue.component('my-game', {
 			switch (this.score)
 			{
 				case "1-0":
-					eogMessage = "White win";
+					eogMessage = translations["White win"];
 					break;
 				case "0-1":
-					eogMessage = "Black win";
+					eogMessage = translations["Black win"];
 					break;
 				case "1/2":
-					eogMessage = "Draw";
+					eogMessage = translations["Draw"];
 					break;
 			}
 			return eogMessage;
diff --git a/public/javascripts/components/problemSummary.js b/public/javascripts/components/problemSummary.js
index 31e9e545..5f0fd44b 100644
--- a/public/javascripts/components/problemSummary.js
+++ b/public/javascripts/components/problemSummary.js
@@ -10,11 +10,14 @@ Vue.component('my-problem-summary', {
 				<p v-html="prob.instructions"></p>
 				<p v-if="preview" v-html="prob.solution"></p>
 				<p v-else class="problem-time">{{ timestamp2date(prob.added) }}</p>
-				<button v-if="!preview" @click="showProblem()">Show</button>
+				<button v-if="!preview" @click="showProblem()">{{ translate("Solve") }}</button>
 			</div>
 		</div>
 	`,
 	methods: {
+		translate: function(text) {
+			return translations[text];
+		},
 		getDiagram: function(fen) {
 			const fenParsed = V.ParseFen(fen);
 			return getDiagram({
diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js
index b67203a3..9288a393 100644
--- a/public/javascripts/components/problems.js
+++ b/public/javascripts/components/problems.js
@@ -13,15 +13,15 @@ Vue.component('my-problems', {
 	template: `
 		<div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
 			<div id="problemControls" class="button-group">
-				<button aria-label="Load previous problems" class="tooltip"
+				<button :aria-label='translate("Load previous problems")' class="tooltip"
 						@click="fetchProblems('backward')">
 					<i class="material-icons">skip_previous</i>
 				</button>
-				<button aria-label="Add a problem" class="tooltip"
+				<button :aria-label='translate("Add a problem")' class="tooltip"
 						@click="showNewproblemModal">
-					New
+					{{ translate("New") }}
 				</button>
-				<button aria-label="Load next problems" class="tooltip"
+				<button :aria-label='translate("Load next problems")' class="tooltip"
 						@click="fetchProblems('forward')">
 					<i class="material-icons">skip_next</i>
 				</button>
@@ -34,22 +34,22 @@ Vue.component('my-problems', {
 			<div role="dialog" aria-labelledby="newProblemTxt">
 				<div v-show="newProblem.stage=='nothing'" class="card newproblem-form">
 					<label for="modal-newproblem" class="modal-close"></label>
-					<h3 id="newProblemTxt">Add problem</h3>
+					<h3 id="newProblemTxt">{{ translate("Add a problem") }}</h3>
 					<form @submit.prevent="previewNewProblem">
 						<fieldset>
-							<label for="newpbFen">Fen</label>
+							<label for="newpbFen">FEN</label>
 							<input id="newpbFen" type="text" v-model="newProblem.fen"
-								placeholder="Full FEN string"/>
+								:placeholder='translate("Full FEN string")'/>
 						</fieldset>
 						<fieldset>
-							<p class="emphasis">Safe HTML tags allowed</p>
-							<label for="newpbInstructions">Instructions</label>
+							<p class="emphasis">{{ translate("Safe HTML tags allowed") }}</p>
+							<label for="newpbInstructions">{{ translate("Instructions") }}</label>
 							<textarea id="newpbInstructions" v-model="newProblem.instructions"
 								placeholder="Explain the problem here"></textarea>
-							<label for="newpbSolution">Solution</label>
+							<label for="newpbSolution">{{ translate("Solution") }}</label>
 							<textarea id="newpbSolution" v-model="newProblem.solution"
 								placeholder="How to solve the problem?"></textarea>
-							<button class="center-btn">Preview</button>
+							<button class="center-btn">{{ translate("Preview") }}</button>
 						</fieldset>
 					</form>
 				</div>
@@ -58,8 +58,8 @@ Vue.component('my-problems', {
 					<my-problem-summary v-bind:prob="newProblem" v-bind:preview="true">
 					</my-problem-summary>
 					<div class="button-group">
-						<button @click="newProblem.stage='nothing'">Cancel</button>
-						<button @click="sendNewProblem()">Send</button>
+						<button @click="newProblem.stage='nothing'">{{ translate("Cancel") }}</button>
+						<button @click="sendNewProblem()">{{ translate("Send") }}</button>
 					</div>
 				</div>
 			</div>
@@ -72,6 +72,9 @@ Vue.component('my-problems', {
 		},
 	},
 	methods: {
+		translate: function(text) {
+			return translations[text];
+		},
 		// Propagate "show problem" event to parent component (my-variant)
 		bubbleUp: function(problem) {
 			this.$emit('show-problem', JSON.stringify(problem));
diff --git a/public/javascripts/components/variantSummary.js b/public/javascripts/components/variantSummary.js
index e6ec03aa..16b06ce4 100644
--- a/public/javascripts/components/variantSummary.js
+++ b/public/javascripts/components/variantSummary.js
@@ -12,7 +12,7 @@ Vue.component('my-variant-summary', {
 					</span>
 				</h4>
 				<p class="description text-center">
-					{{ vobj.desc }}
+					{{ translate(vobj.desc) }}
 				</p>
 			</a>
 		</div>
@@ -22,4 +22,9 @@ Vue.component('my-variant-summary', {
 			return "/" + this.vobj.name;
 		},
 	},
+	methods: {
+		translate: function(text) {
+			return translations[text];
+		},
+	},
 })
diff --git a/views/index.pug b/views/index.pug
index ba062e33..66c434a0 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -5,6 +5,20 @@ block css
 
 block content
 	.container#indexPage
+		// Modals
+		case lang
+			when "en"
+				include translations/en.pug
+				include langNames/en.pug
+				include welcome/en.pug
+				include modal-lang/en.pug
+				include modal-help/en.pug
+			when "fr"
+				include translations/fr.pug
+				include langNames/fr.pug
+				include welcome/fr.pug
+				include modal-lang/fr.pug
+				include modal-help/fr.pug
 		.row
 			#header.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
 				#mainTitle
@@ -18,26 +32,21 @@ block content
 				#helpMenu.clickable(
 						onClick="document.getElementById('modalHelp').checked=true")
 					.info-container
-						p Help
+						p= translations["Help"]
 		.row
 			my-variant-summary(v-for="(v,idx) in sortedCounts"
 				v-bind:vobj="v" v-bind:index="idx" v-bind:key="v.name")
-		// Modals:
-		include modal-lang.pug
-		include modal-help.pug
+		// Other modals:
 		input#modalB4welcome.modal(type="checkbox")
 		div(role="dialog")
 			#b4welcome.card.text-center.small-modal
-				h3.blue First visit?
-				p#readThis.clickable(@click="showWelcomeMsg") >>> Please read this <<<
-		case lang
-			when "en"
-				include welcome/en.pug
-			when "fr"
-				include welcome/fr.pug
+				h3.blue= translations["First visit?"]
+				p#readThis.clickable(@click="showWelcomeMsg")
+					=translations[">>> Please read this <<<"]
 
 block javascripts
 	script.
+		const translations = !{JSON.stringify(translations)};
 		const variantArray = !{JSON.stringify(variantArray)};
 	script(src="/javascripts/utils/misc.js")
 	script(src="/javascripts/socket_url.js")
diff --git a/views/langNames/en.pug b/views/langNames/en.pug
new file mode 100644
index 00000000..fc1f7e56
--- /dev/null
+++ b/views/langNames/en.pug
@@ -0,0 +1,5 @@
+-
+	var langName = {
+		"en": "English",
+		"fr": "French",
+	};
diff --git a/views/langNames/fr.pug b/views/langNames/fr.pug
new file mode 100644
index 00000000..cab58c80
--- /dev/null
+++ b/views/langNames/fr.pug
@@ -0,0 +1,5 @@
+-
+	var langName = {
+		"fr": "Français",
+		"en": "Anglais",
+	};
diff --git a/views/modal-help.pug b/views/modal-help/en.pug
similarity index 90%
rename from views/modal-help.pug
rename to views/modal-help/en.pug
index 6f1d40aa..73e166a3 100644
--- a/views/modal-help.pug
+++ b/views/modal-help/en.pug
@@ -12,7 +12,7 @@ div(role="dialog")
 		.section
 			h3.red Bug report
 			p
-				| Please send an email to 
+				| Please send an email (in English or French) to 
 				a(href="mailto:contact@vchess.club?subject=[vchess.club] bug report")
 					| contact@vchess.club
 				| .
diff --git a/views/modal-help/fr.pug b/views/modal-help/fr.pug
new file mode 100644
index 00000000..6e765abf
--- /dev/null
+++ b/views/modal-help/fr.pug
@@ -0,0 +1,18 @@
+input#modalHelp.modal(type="checkbox")
+div(role="dialog")
+	#help.card
+		label.modal-close(for="modalHelp")
+		.section
+			p.emphasis.bigfont D'abord: regardez la #[a(href="/demo.webm") vidéo démo] !
+			p Ensuite cliquez sur une variante... Rappel :
+			ul
+				li Toutes les parties démarrent avec une position aléatoire assymétrique.
+				li Les parties ne sont pas chronométrées, et jouée anonymemement..
+				li Pas de chat pendant la partie, pour se concentrer sur les coups.
+		.section
+			h3.red Rapport de bug
+			p
+				| SVP envoyez un email (en français ou anglais) à 
+				a(href="mailto:contact@vchess.club?subject=[vchess.club] bug report")
+					| contact@vchess.club
+				| .
diff --git a/views/modal-lang.pug b/views/modal-lang/en.pug
similarity index 69%
rename from views/modal-lang.pug
rename to views/modal-lang/en.pug
index f15d0634..53795729 100644
--- a/views/modal-lang.pug
+++ b/views/modal-lang/en.pug
@@ -4,11 +4,6 @@ div(role="dialog")
 		label.modal-close(for="modalLang")
 		.section
 			fieldset
-				-
-					var langName = {
-						"fr": "French",
-						"en": "English",
-					}
 				label(for="langSelect") Preferred language?
 				select#langSelect(onChange="setLanguage(event)")
 					each langCode in languages
@@ -20,8 +15,10 @@ div(role="dialog")
 				| Browse the 
 				a(href="https://github.com/yagu0/vchess/tree/master/views")
 					| github repository
-				| : welcome/en.pug and all files rules/*/en.pug
-				| should be translated. When it's done, send me the files: 
+				| : all files in the folders langNames/, modal-help/, modal-lang/,
+				| translations/ and welcome/ should be translated. Moreover, and
+				| it's the longest part, you would have to translate all the rules under 
+				| rules/. When it's done, send me the files: 
 				a(href="mailto:contact@vchess.club?subject=[vchess.club] translation")
 					| contact@vchess.club
 				| . Thanks!
diff --git a/views/modal-lang/fr.pug b/views/modal-lang/fr.pug
new file mode 100644
index 00000000..ffb25c40
--- /dev/null
+++ b/views/modal-lang/fr.pug
@@ -0,0 +1,25 @@
+input#modalLang.modal(type="checkbox")
+div(role="dialog")
+	#language.card
+		label.modal-close(for="modalLang")
+		.section
+			fieldset
+				label(for="langSelect") Langue préférée ?
+				select#langSelect(onChange="setLanguage(event)")
+					each langCode in languages
+						option(value=langCode selected=(lang==langCode))
+							=langName[langCode]
+		.section
+			h3.blue Contribuer
+			p
+				| Votre langue favorite n'est pas disponible ? Alors...
+				| Parcourez le 
+				a(href="https://github.com/yagu0/vchess/tree/master/views")
+					| dépôt github
+				| &nbsp;: il faut traduire tous les fichiers dans les dossiers langNames/,
+				| modal-help/, modal-lang/, translations/ et welcome/. De plus, et c'est l'essentiel
+				| du travail il faut traduire toutes les règles dans rules/.
+				| Une fois que c'est fait, envoyez-moi les fichiers : 
+				a(href="mailto:contact@vchess.club?subject=[vchess.club] translation")
+					| contact@vchess.club
+				| . Merci !
diff --git a/views/translations/en.pug b/views/translations/en.pug
new file mode 100644
index 00000000..9630efc3
--- /dev/null
+++ b/views/translations/en.pug
@@ -0,0 +1,73 @@
+-
+	var translations =
+	{
+		// 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",
+
+		// 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 online game": "New online game",
+		"New game versus computer": "New game versus computer",
+		"Analysis mode": "Analysis mode",
+		"Start chat": "Start chat",
+		"Clear game versus computer": "Clear game versus computer",
+		"Settings": "Settings",
+		"Resign": "Resign",
+		"Undo": "Undo",
+		"Play": "Play",
+		"Flip": "Flip",
+		"Position + flags (FEN):": "Position + flags (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",
+		"New game": "New game",
+		"All": "All",
+		"Chat with ": "Chat with ",
+		"Type here": "Type here",
+		"Send": "Send",
+		"Download game": "Download game",
+		"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 string": "Full FEN string",
+		"Safe HTML tags allowed": "Safe HTML tags allowed",
+		"Instructions": "Instructions",
+		"Solution": "Solution",
+		"Preview": "Preview",
+		"Cancel": "Cancel",
+		"Solve": "Solve",
+	};
diff --git a/views/translations/fr.pug b/views/translations/fr.pug
new file mode 100644
index 00000000..3ec7a509
--- /dev/null
+++ b/views/translations/fr.pug
@@ -0,0 +1,73 @@
+-
+	var translations =
+	{
+		// 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": "Captures ressucitées",
+		"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",
+
+		// 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 online 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 game versus computer": "Effacer la partie contre l'ordinateur",
+		"Settings": "Réglages",
+		"Resign": "Abandonner",
+		"Undo": "Annuler",
+		"Play": "Jouer",
+		"Flip": "Retourner",
+		"Position + flags (FEN):": "Position + drapeaux (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",
+		"New game": "Nouvelle partie",
+		"All": "Tous",
+		"Chat with ": "Discuter avec ",
+		"Type here": "Écrivez ici",
+		"Send": "Envoyer",
+		"Download game": "Télécharger la partie",
+		"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 string": "Chaîne FEN complète",
+		"Safe HTML tags allowed": "HTML 'sûr' autorisé",
+		"Instructions": "Instructions",
+		"Solution": "Solution",
+		"Preview": "Prévisualiser",
+		"Cancel": "Annuler",
+		"Solve": "Résoudre",
+	};
diff --git a/views/variant.pug b/views/variant.pug
index ff41740e..62e9d76d 100644
--- a/views/variant.pug
+++ b/views/variant.pug
@@ -6,6 +6,24 @@ block css
 
 block content
 	.container#variantPage
+		// Modals
+		case lang
+			when "en"
+				include translations/en.pug
+				include langNames/en.pug
+				include modal-lang/en.pug
+				include modal-help/en.pug
+			when "fr"
+				include translations/fr.pug
+				include langNames/fr.pug
+				include modal-lang/fr.pug
+				include modal-help/fr.pug
+		input#modal-newgame.modal(type="checkbox")
+		div(role="dialog" aria-labelledby="newGameTxt")
+			.card.smallpad.small-modal
+				label#close-newgame.modal-close(for="modal-newgame")
+				h3#newGameTxt= translations["New game"]
+				p= translations["Waiting for opponent..."]
 		.row
 			.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
 				label.drawer-toggle(for="drawer-control")
@@ -16,31 +34,22 @@ block content
 						i.material-icons home
 					.info-container
 						a(href="#rules" @click="setDisplay('rules')")
-							| Rules
+							=translations["Rules"]
 						a(href="#play" @click="setDisplay('play')")
-							| Play!
+							=translations["Play!"]
 						a(href="#problems" @click="setDisplay('problems')") 
-							| Problems
+							=translations["Problems"]
 					#flagMenu.clickable(
 							onClick="document.getElementById('modalLang').checked=true")
 						img(src="/images/flags/" + lang + ".svg")
 					#helpMenu.clickable(
 							onClick="document.getElementById('modalHelp').checked=true")
 						.info-container
-							p Help
+							p= translations["Help"]
 		.row
 			my-rules(v-show="display=='rules'")
 			my-game(v-show="display=='play'" v-bind:problem="problem")
 			my-problems(v-show="display=='problems'" v-on:show-problem="showProblem($event)")
-		// (Some) Modals:
-		include modal-help.pug
-		include modal-lang.pug
-		input#modal-newgame.modal(type="checkbox")
-		div(role="dialog" aria-labelledby="newGameTxt")
-			.card.smallpad.small-modal
-				label#close-newgame.modal-close(for="modal-newgame")
-				h3#newGameTxt New game
-				p Waiting for opponent...
 
 block javascripts
 	script(src="/javascripts/utils/misc.js")
@@ -56,6 +65,7 @@ block javascripts
 		const V = VariantRules; //because this variable is often used
 		const variant = "#{variant}";
 		const problemArray = !{JSON.stringify(problemArray)};
+		const translations = !{JSON.stringify(translations)};
 	script(src="/javascripts/components/rules.js")
 	script(src="/javascripts/components/game.js")
 	script(src="/javascripts/components/problemSummary.js")
-- 
2.44.0