From eeec686cf42b5beb4985d8891bd953c339945658 Mon Sep 17 00:00:00 2001
From: Brieuc Dubois <git@bhasher.com>
Date: Sun, 17 Mar 2024 00:20:46 +0100
Subject: [PATCH] frontend: internationalization

---
 frontend/package-lock.json                    | 323 ++++++++++++++++--
 frontend/package.json                         |   2 +
 frontend/rollup.config.js                     |   5 +
 frontend/src/lib/components/header.svelte     |   9 +-
 .../components/header/localSelector.svelte    |  20 ++
 frontend/src/lib/services/i18n.ts             |  82 +++++
 frontend/src/routes/+layout.svelte            |  15 +-
 frontend/static/lang/en.json                  |   6 +
 frontend/static/lang/fr.json                  |   6 +
 9 files changed, 430 insertions(+), 38 deletions(-)
 create mode 100644 frontend/rollup.config.js
 create mode 100644 frontend/src/lib/components/header/localSelector.svelte
 create mode 100644 frontend/src/lib/services/i18n.ts
 create mode 100644 frontend/static/lang/en.json
 create mode 100644 frontend/static/lang/fr.json

diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1d111cde..f0ca6d69 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -11,10 +11,12 @@
 				"axios": "^1.6.7",
 				"axios-jwt": "^4.0.2",
 				"jwt-decode": "^4.0.0",
+				"svelte-i18n": "^4.0.0",
 				"svelte-material-icons": "^3.0.5",
 				"svelte-select": "^5.8.3"
 			},
 			"devDependencies": {
+				"@rollup/plugin-json": "^6.1.0",
 				"@sveltejs/adapter-auto": "^3.0.0",
 				"@sveltejs/adapter-static": "^3.0.1",
 				"@sveltejs/kit": "^2.0.0",
@@ -81,7 +83,6 @@
 			"cpu": [
 				"ppc64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"aix"
@@ -97,7 +98,6 @@
 			"cpu": [
 				"arm"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"android"
@@ -113,7 +113,6 @@
 			"cpu": [
 				"arm64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"android"
@@ -129,7 +128,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"android"
@@ -145,7 +143,6 @@
 			"cpu": [
 				"arm64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"darwin"
@@ -161,7 +158,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"darwin"
@@ -177,7 +173,6 @@
 			"cpu": [
 				"arm64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"freebsd"
@@ -193,7 +188,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"freebsd"
@@ -209,7 +203,6 @@
 			"cpu": [
 				"arm"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -225,7 +218,6 @@
 			"cpu": [
 				"arm64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -241,7 +233,6 @@
 			"cpu": [
 				"ia32"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -257,7 +248,6 @@
 			"cpu": [
 				"loong64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -273,7 +263,6 @@
 			"cpu": [
 				"mips64el"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -289,7 +278,6 @@
 			"cpu": [
 				"ppc64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -305,7 +293,6 @@
 			"cpu": [
 				"riscv64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -321,7 +308,6 @@
 			"cpu": [
 				"s390x"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -337,7 +323,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"linux"
@@ -353,7 +338,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"netbsd"
@@ -369,7 +353,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"openbsd"
@@ -385,7 +368,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"sunos"
@@ -401,7 +383,6 @@
 			"cpu": [
 				"arm64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"win32"
@@ -417,7 +398,6 @@
 			"cpu": [
 				"ia32"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"win32"
@@ -433,7 +413,6 @@
 			"cpu": [
 				"x64"
 			],
-			"dev": true,
 			"optional": true,
 			"os": [
 				"win32"
@@ -542,6 +521,50 @@
 			"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
 			"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
 		},
+		"node_modules/@formatjs/ecma402-abstract": {
+			"version": "1.18.2",
+			"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
+			"integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==",
+			"dependencies": {
+				"@formatjs/intl-localematcher": "0.5.4",
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@formatjs/fast-memoize": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
+			"integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
+			"dependencies": {
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@formatjs/icu-messageformat-parser": {
+			"version": "2.7.6",
+			"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz",
+			"integrity": "sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==",
+			"dependencies": {
+				"@formatjs/ecma402-abstract": "1.18.2",
+				"@formatjs/icu-skeleton-parser": "1.8.0",
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@formatjs/icu-skeleton-parser": {
+			"version": "1.8.0",
+			"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz",
+			"integrity": "sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==",
+			"dependencies": {
+				"@formatjs/ecma402-abstract": "1.18.2",
+				"tslib": "^2.4.0"
+			}
+		},
+		"node_modules/@formatjs/intl-localematcher": {
+			"version": "0.5.4",
+			"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
+			"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
+			"dependencies": {
+				"tslib": "^2.4.0"
+			}
+		},
 		"node_modules/@humanwhocodes/config-array": {
 			"version": "0.11.14",
 			"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -735,6 +758,54 @@
 			"integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==",
 			"dev": true
 		},
+		"node_modules/@rollup/plugin-json": {
+			"version": "6.1.0",
+			"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
+			"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
+			"dev": true,
+			"dependencies": {
+				"@rollup/pluginutils": "^5.1.0"
+			},
+			"engines": {
+				"node": ">=14.0.0"
+			},
+			"peerDependencies": {
+				"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+			},
+			"peerDependenciesMeta": {
+				"rollup": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@rollup/pluginutils": {
+			"version": "5.1.0",
+			"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
+			"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
+			"dev": true,
+			"dependencies": {
+				"@types/estree": "^1.0.0",
+				"estree-walker": "^2.0.2",
+				"picomatch": "^2.3.1"
+			},
+			"engines": {
+				"node": ">=14.0.0"
+			},
+			"peerDependencies": {
+				"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+			},
+			"peerDependenciesMeta": {
+				"rollup": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@rollup/pluginutils/node_modules/estree-walker": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+			"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+			"dev": true
+		},
 		"node_modules/@rollup/rollup-android-arm-eabi": {
 			"version": "4.12.0",
 			"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz",
@@ -1618,6 +1689,21 @@
 				"node": ">= 6"
 			}
 		},
+		"node_modules/cli-color": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz",
+			"integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==",
+			"dependencies": {
+				"d": "^1.0.1",
+				"es5-ext": "^0.10.64",
+				"es6-iterator": "^2.0.3",
+				"memoizee": "^0.4.15",
+				"timers-ext": "^0.1.7"
+			},
+			"engines": {
+				"node": ">=0.10"
+			}
+		},
 		"node_modules/code-red": {
 			"version": "1.0.4",
 			"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
@@ -1733,6 +1819,18 @@
 				"node": ">=4"
 			}
 		},
+		"node_modules/d": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+			"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+			"dependencies": {
+				"es5-ext": "^0.10.64",
+				"type": "^2.7.2"
+			},
+			"engines": {
+				"node": ">=0.12"
+			}
+		},
 		"node_modules/debug": {
 			"version": "4.3.4",
 			"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -1760,7 +1858,6 @@
 			"version": "4.3.1",
 			"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
 			"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
-			"dev": true,
 			"engines": {
 				"node": ">=0.10.0"
 			}
@@ -1863,17 +1960,64 @@
 				"errno": "cli.js"
 			}
 		},
+		"node_modules/es5-ext": {
+			"version": "0.10.64",
+			"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+			"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+			"hasInstallScript": true,
+			"dependencies": {
+				"es6-iterator": "^2.0.3",
+				"es6-symbol": "^3.1.3",
+				"esniff": "^2.0.1",
+				"next-tick": "^1.1.0"
+			},
+			"engines": {
+				"node": ">=0.10"
+			}
+		},
+		"node_modules/es6-iterator": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+			"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+			"dependencies": {
+				"d": "1",
+				"es5-ext": "^0.10.35",
+				"es6-symbol": "^3.1.1"
+			}
+		},
 		"node_modules/es6-promise": {
 			"version": "3.3.1",
 			"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
 			"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
 			"dev": true
 		},
+		"node_modules/es6-symbol": {
+			"version": "3.1.4",
+			"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+			"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+			"dependencies": {
+				"d": "^1.0.2",
+				"ext": "^1.7.0"
+			},
+			"engines": {
+				"node": ">=0.12"
+			}
+		},
+		"node_modules/es6-weak-map": {
+			"version": "2.0.3",
+			"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+			"integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+			"dependencies": {
+				"d": "1",
+				"es5-ext": "^0.10.46",
+				"es6-iterator": "^2.0.3",
+				"es6-symbol": "^3.1.1"
+			}
+		},
 		"node_modules/esbuild": {
 			"version": "0.19.12",
 			"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
 			"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
-			"dev": true,
 			"hasInstallScript": true,
 			"bin": {
 				"esbuild": "bin/esbuild"
@@ -2098,6 +2242,20 @@
 			"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
 			"dev": true
 		},
+		"node_modules/esniff": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+			"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+			"dependencies": {
+				"d": "^1.0.1",
+				"es5-ext": "^0.10.62",
+				"event-emitter": "^0.3.5",
+				"type": "^2.7.2"
+			},
+			"engines": {
+				"node": ">=0.10"
+			}
+		},
 		"node_modules/espree": {
 			"version": "9.6.1",
 			"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -2165,6 +2323,23 @@
 				"node": ">=0.10.0"
 			}
 		},
+		"node_modules/event-emitter": {
+			"version": "0.3.5",
+			"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+			"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+			"dependencies": {
+				"d": "1",
+				"es5-ext": "~0.10.14"
+			}
+		},
+		"node_modules/ext": {
+			"version": "1.7.0",
+			"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+			"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+			"dependencies": {
+				"type": "^2.7.2"
+			}
+		},
 		"node_modules/fast-deep-equal": {
 			"version": "3.1.3",
 			"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2442,8 +2617,7 @@
 		"node_modules/globalyzer": {
 			"version": "0.1.0",
 			"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
-			"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
-			"dev": true
+			"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q=="
 		},
 		"node_modules/globby": {
 			"version": "11.1.0",
@@ -2468,8 +2642,7 @@
 		"node_modules/globrex": {
 			"version": "0.1.2",
 			"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
-			"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
-			"dev": true
+			"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
 		},
 		"node_modules/graceful-fs": {
 			"version": "4.2.11",
@@ -2590,6 +2763,17 @@
 			"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
 			"dev": true
 		},
+		"node_modules/intl-messageformat": {
+			"version": "10.5.11",
+			"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz",
+			"integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==",
+			"dependencies": {
+				"@formatjs/ecma402-abstract": "1.18.2",
+				"@formatjs/fast-memoize": "2.2.0",
+				"@formatjs/icu-messageformat-parser": "2.7.6",
+				"tslib": "^2.4.0"
+			}
+		},
 		"node_modules/is-binary-path": {
 			"version": "2.1.0",
 			"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -2662,6 +2846,11 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/is-promise": {
+			"version": "2.2.2",
+			"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+			"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
+		},
 		"node_modules/is-reference": {
 			"version": "3.0.2",
 			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
@@ -2863,6 +3052,14 @@
 				"node": ">=10"
 			}
 		},
+		"node_modules/lru-queue": {
+			"version": "0.1.0",
+			"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
+			"integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==",
+			"dependencies": {
+				"es5-ext": "~0.10.2"
+			}
+		},
 		"node_modules/magic-string": {
 			"version": "0.30.8",
 			"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
@@ -2903,6 +3100,21 @@
 			"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
 			"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
 		},
+		"node_modules/memoizee": {
+			"version": "0.4.15",
+			"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz",
+			"integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==",
+			"dependencies": {
+				"d": "^1.0.1",
+				"es5-ext": "^0.10.53",
+				"es6-weak-map": "^2.0.3",
+				"event-emitter": "^0.3.5",
+				"is-promise": "^2.2.2",
+				"lru-queue": "^0.1.0",
+				"next-tick": "^1.1.0",
+				"timers-ext": "^0.1.7"
+			}
+		},
 		"node_modules/merge2": {
 			"version": "1.4.1",
 			"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3015,7 +3227,6 @@
 			"version": "1.2.0",
 			"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
 			"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
-			"dev": true,
 			"engines": {
 				"node": ">=4"
 			}
@@ -3087,6 +3298,11 @@
 				"node": ">= 4.4.x"
 			}
 		},
+		"node_modules/next-tick": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+			"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+		},
 		"node_modules/node-releases": {
 			"version": "2.0.14",
 			"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -3708,7 +3924,6 @@
 			"version": "1.8.1",
 			"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
 			"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
-			"dev": true,
 			"dependencies": {
 				"mri": "^1.1.0"
 			},
@@ -4155,6 +4370,34 @@
 				"svelte": "^3.19.0 || ^4.0.0"
 			}
 		},
+		"node_modules/svelte-i18n": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.0.tgz",
+			"integrity": "sha512-4vivjKZADUMRIhTs38JuBNy3unbnh9AFRxWFLxq62P4NHic+/BaIZZlAsvqsCdnp7IdJf5EoSiH6TNdItcjA6g==",
+			"dependencies": {
+				"cli-color": "^2.0.3",
+				"deepmerge": "^4.2.2",
+				"esbuild": "^0.19.2",
+				"estree-walker": "^2",
+				"intl-messageformat": "^10.5.3",
+				"sade": "^1.8.1",
+				"tiny-glob": "^0.2.9"
+			},
+			"bin": {
+				"svelte-i18n": "dist/cli.js"
+			},
+			"engines": {
+				"node": ">= 16"
+			},
+			"peerDependencies": {
+				"svelte": "^3 || ^4"
+			}
+		},
+		"node_modules/svelte-i18n/node_modules/estree-walker": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+			"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+		},
 		"node_modules/svelte-material-icons": {
 			"version": "3.0.5",
 			"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-3.0.5.tgz",
@@ -4357,11 +4600,19 @@
 				"node": ">=0.8"
 			}
 		},
+		"node_modules/timers-ext": {
+			"version": "0.1.7",
+			"resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
+			"integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
+			"dependencies": {
+				"es5-ext": "~0.10.46",
+				"next-tick": "1"
+			}
+		},
 		"node_modules/tiny-glob": {
 			"version": "0.2.9",
 			"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
 			"integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
-			"dev": true,
 			"dependencies": {
 				"globalyzer": "0.1.0",
 				"globrex": "^0.1.2"
@@ -4409,8 +4660,12 @@
 		"node_modules/tslib": {
 			"version": "2.6.2",
 			"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-			"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
-			"dev": true
+			"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+		},
+		"node_modules/type": {
+			"version": "2.7.2",
+			"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+			"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
 		},
 		"node_modules/type-check": {
 			"version": "0.4.0",
diff --git a/frontend/package.json b/frontend/package.json
index 6f0f517d..e9ff5b31 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
 		"format": "prettier --write ."
 	},
 	"devDependencies": {
+		"@rollup/plugin-json": "^6.1.0",
 		"@sveltejs/adapter-auto": "^3.0.0",
 		"@sveltejs/adapter-static": "^3.0.1",
 		"@sveltejs/kit": "^2.0.0",
@@ -42,6 +43,7 @@
 		"axios": "^1.6.7",
 		"axios-jwt": "^4.0.2",
 		"jwt-decode": "^4.0.0",
+		"svelte-i18n": "^4.0.0",
 		"svelte-material-icons": "^3.0.5",
 		"svelte-select": "^5.8.3"
 	}
diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js
new file mode 100644
index 00000000..983b1302
--- /dev/null
+++ b/frontend/rollup.config.js
@@ -0,0 +1,5 @@
+import json from '@rollup/plugin-json';
+
+export default {
+	plugins: [json(), svelte({})]
+};
diff --git a/frontend/src/lib/components/header.svelte b/frontend/src/lib/components/header.svelte
index c191440b..dd7e6c23 100644
--- a/frontend/src/lib/components/header.svelte
+++ b/frontend/src/lib/components/header.svelte
@@ -1,15 +1,17 @@
-<script>
+<script lang="ts">
 	import session from '$lib/stores/JWTSession';
 	import { get } from 'svelte/store';
 	import Logout from 'svelte-material-icons/Logout.svelte';
 	import Login from 'svelte-material-icons/Login.svelte';
+	import LocalSelector from './header/localSelector.svelte';
+	import { _ } from '$lib/services/i18n';
 </script>
 
 <header class="bg-secondary text-white flex align-middle justify-between px-4 py-2">
-	<h1 class="font-bold text-2xl"><a href="/">LanguageLab</a></h1>
+	<h1 class="font-bold text-2xl"><a href="/">{$_('header.appName')}</a></h1>
 	<div class="flex align-middle">
 		{#if session.isLoggedIn()}
-			<span class="pr-2">Connecté en tant que <strong>{get(session.username)}</strong></span>
+			<span class="pr-2">{$_('header.connectedAs')} <strong>{get(session.username)}</strong></span>
 			<a href="/logout"><Logout class="h-4/5" /></a>
 		{:else}
 			<a
@@ -18,5 +20,6 @@
 				)}"><Login /></a
 			>
 		{/if}
+		<LocalSelector />
 	</div>
 </header>
diff --git a/frontend/src/lib/components/header/localSelector.svelte b/frontend/src/lib/components/header/localSelector.svelte
new file mode 100644
index 00000000..08926bd5
--- /dev/null
+++ b/frontend/src/lib/components/header/localSelector.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { _, _activeLocale, locales, setupI18n } from '../../services/i18n';
+
+	let value: string = _activeLocale;
+
+	function onChange(event: Event) {
+		const target = event.target as HTMLSelectElement;
+		setupI18n({
+			withLocale: target.value
+		});
+	}
+</script>
+
+<div class="ml-2">
+	<select {value} on:change={onChange} class="bg-transparent">
+		{#each Object.entries(locales) as [locale, name] (locale)}
+			<option value={locale}>{name}</option>
+		{/each}
+	</select>
+</div>
diff --git a/frontend/src/lib/services/i18n.ts b/frontend/src/lib/services/i18n.ts
new file mode 100644
index 00000000..a410c02e
--- /dev/null
+++ b/frontend/src/lib/services/i18n.ts
@@ -0,0 +1,82 @@
+/**
+ * Inspired by https://github.com/PhraseApp-Blog/svelte-i18n-2020-06/tree/master
+ */
+
+import { get, derived, writable } from 'svelte/store';
+import {
+	_,
+	date,
+	init,
+	locale,
+	number,
+	dictionary,
+	addMessages,
+	getLocaleFromNavigator
+} from 'svelte-i18n';
+
+export const locales = {
+	en: 'En',
+	fr: 'Fr'
+};
+
+const fallbackLocale = 'fr';
+
+const MESSAGE_FILE_URL_TEMPLATE = '/lang/{locale}.json';
+
+export let _activeLocale: string;
+
+const isDownloading = writable(false);
+
+function setupI18n(options = { withLocale: fallbackLocale }) {
+	const locale_ = supported(options.withLocale || language(getLocaleFromNavigator() ?? ''));
+
+	init({ initialLocale: locale_, fallbackLocale: fallbackLocale });
+
+	if (!hasLoadedLocale(locale_)) {
+		isDownloading.set(true);
+
+		const messagesFileUrl = MESSAGE_FILE_URL_TEMPLATE.replace('{locale}', locale_);
+
+		return loadJson(messagesFileUrl).then((messages) => {
+			_activeLocale = locale_;
+
+			addMessages(locale_, messages);
+
+			locale.set(locale_);
+
+			isDownloading.set(false);
+		});
+	}
+}
+
+const isLocaleLoaded = derived(
+	[isDownloading, dictionary],
+	([$isDownloading, $dictionary]) =>
+		!$isDownloading &&
+		$dictionary[_activeLocale] &&
+		Object.keys($dictionary[_activeLocale]).length > 0
+);
+
+const dir = derived(locale, ($locale) => ($locale === 'ar' ? 'rtl' : 'ltr'));
+
+function loadJson(url: string) {
+	return fetch(url).then((response) => response.json());
+}
+
+function hasLoadedLocale(locale: string) {
+	return get(dictionary)[locale];
+}
+
+function language(locale: string): string {
+	return locale.replace('_', '-').split('-')[0];
+}
+
+function supported(locale: string): string {
+	if (Object.keys(locales).includes(locale)) {
+		return locale;
+	} else {
+		return fallbackLocale;
+	}
+}
+
+export { _, setupI18n, isLocaleLoaded, locale, dir, date, number };
diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte
index 83ce6cef..880468e1 100644
--- a/frontend/src/routes/+layout.svelte
+++ b/frontend/src/routes/+layout.svelte
@@ -1,8 +1,21 @@
 <script>
 	import { SvelteToast } from '@zerodevx/svelte-toast';
 	import '../app.css';
+	import { setupI18n, isLocaleLoaded, dir, _ } from '$lib/services/i18n';
+
+	setupI18n();
+
+	$: if (document.dir !== $dir) {
+		document.dir = $dir;
+	}
+
+	$: if ($isLocaleLoaded) {
+		document.title = $_('header.appName');
+	}
 </script>
 
-<slot />
+{#if $isLocaleLoaded}
+	<slot />
+{/if}
 
 <SvelteToast />
diff --git a/frontend/static/lang/en.json b/frontend/static/lang/en.json
new file mode 100644
index 00000000..0cc241b2
--- /dev/null
+++ b/frontend/static/lang/en.json
@@ -0,0 +1,6 @@
+{
+	"header": {
+		"appName": "LanguageLab",
+		"connectedAs": "Connected as"
+	}
+}
diff --git a/frontend/static/lang/fr.json b/frontend/static/lang/fr.json
new file mode 100644
index 00000000..76b694ac
--- /dev/null
+++ b/frontend/static/lang/fr.json
@@ -0,0 +1,6 @@
+{
+	"header": {
+		"appName": "LanguageLab",
+		"connectedAs": "Connecté en tant que"
+	}
+}
-- 
GitLab