diff --git a/backend/app/main.py b/backend/app/main.py
index 51a02aaa1363b58ebb8fae34dff3160861d695f5..50d657ef948b6dfde4c3387d6bfc38c72ed0c89e 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,5 +1,6 @@
 from collections import defaultdict
-from fastapi import APIRouter, FastAPI, status, Depends, HTTPException, BackgroundTasks
+from typing import Annotated
+from fastapi import APIRouter, FastAPI, Form, status, Depends, HTTPException, BackgroundTasks
 from sqlalchemy.orm import Session
 from fastapi.security import OAuth2PasswordRequestForm
 from fastapi.middleware.cors import CORSMiddleware
@@ -62,6 +63,18 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend
         "refresh_token": hashing.create_refresh_token(db_user),
     }   
 
+@authRouter.post("/register", status_code=status.HTTP_201_CREATED)
+def register(email: Annotated[str, Form()], password: Annotated[str, Form()], nickname: Annotated[str, Form()], db: Session = Depends(get_db)):
+    db_user = crud.get_user_by_email(db, email=email)
+    if db_user:
+        raise HTTPException(status_code=400, detail="User already registered")
+
+    user_data = schemas.UserCreate(email=email, password=password, nickname=nickname)
+        
+    user = crud.create_user(db=db, user=user_data)
+
+    return user.id
+
 @authRouter.post("/refresh", response_model=schemas.Token)
 def refresh_token(current_user: models.User = Depends(hashing.get_jwt_user_from_refresh_token)):
     return {
diff --git a/frontend/src/lib/api/auth.ts b/frontend/src/lib/api/auth.ts
index ca1f95ec445e9dba2e4a1e0398e6ec77dadfcba2..0c8a2735e1d66fe9006964a49c10f7e4d852ad4e 100644
--- a/frontend/src/lib/api/auth.ts
+++ b/frontend/src/lib/api/auth.ts
@@ -51,3 +51,39 @@ export async function loginAPI(email: string, password: string): Promise<string>
 			return error.toString();
 		});
 }
+
+export async function registerAPI(
+	email: string,
+	password: string,
+	nickname: string
+): Promise<string> {
+	return axiosPublicInstance
+		.post(
+			`/auth/register`,
+			{
+				email,
+				username: email,
+				password,
+				nickname
+			},
+			{
+				headers: {
+					'Content-Type': 'application/x-www-form-urlencoded'
+				}
+			}
+		)
+		.then((response) => {
+			if (response.status === 401) {
+				return response.data.detail ?? 'Unauthorized';
+			} else if (response.status === 422) {
+				return 'Invalid request';
+			} else if (response.status === 201) {
+				return 'OK';
+			}
+
+			return 'Error ' + response.status + ': ' + response.data.detail;
+		})
+		.catch((error) => {
+			return error.toString();
+		});
+}
diff --git a/frontend/src/lib/components/header.svelte b/frontend/src/lib/components/header.svelte
index c4de06dc7fd8bc076c7d83e2f84862c17090d3ae..7a4ff31776adf32090e85f21a03a7603ba01ced5 100644
--- a/frontend/src/lib/components/header.svelte
+++ b/frontend/src/lib/components/header.svelte
@@ -26,6 +26,6 @@
 				)}"><Login /></a
 			>
 		{/if}
-		<LocalSelector />
+		<LocalSelector class="ml-2" />
 	</div>
 </header>
diff --git a/frontend/src/lib/components/header/localSelector.svelte b/frontend/src/lib/components/header/localSelector.svelte
index 8d1b3cd62c82df9b0f33ee75a0ca42ae4bee09c4..43b5e40873ab1e058ce0f7989a1ac12415510719 100644
--- a/frontend/src/lib/components/header/localSelector.svelte
+++ b/frontend/src/lib/components/header/localSelector.svelte
@@ -1,7 +1,14 @@
 <script lang="ts">
 	import { _, _activeLocale, locales, setupI18n } from '../../services/i18n';
 
-	let value: string = _activeLocale;
+	let classes = '';
+	export { classes as class };
+
+	$: value = $_activeLocale;
+
+	_activeLocale.subscribe((locale) => {
+		value = locale;
+	});
 
 	function onChange(event: Event) {
 		const target = event.target as HTMLSelectElement;
@@ -12,8 +19,8 @@
 	}
 </script>
 
-<div class="ml-2">
-	<select {value} on:change={onChange} class="bg-transparent">
+<div class=" flex-1">
+	<select {value} on:change={onChange} class="bg-transparent {classes}">
 		{#each Object.entries(locales) as [locale, name] (locale)}
 			<option value={locale}>{name}</option>
 		{/each}
diff --git a/frontend/src/lib/services/i18n.ts b/frontend/src/lib/services/i18n.ts
index a410c02e994345beee0bbbb44e7924d69ef2e13f..f294944d11cad27807bfbde41b662c961cb614de 100644
--- a/frontend/src/lib/services/i18n.ts
+++ b/frontend/src/lib/services/i18n.ts
@@ -23,7 +23,7 @@ const fallbackLocale = 'fr';
 
 const MESSAGE_FILE_URL_TEMPLATE = '/lang/{locale}.json';
 
-export let _activeLocale: string;
+export let _activeLocale = writable(fallbackLocale);
 
 const isDownloading = writable(false);
 
@@ -32,13 +32,15 @@ function setupI18n(options = { withLocale: fallbackLocale }) {
 
 	init({ initialLocale: locale_, fallbackLocale: fallbackLocale });
 
+	_activeLocale.set(locale_);
+
 	if (!hasLoadedLocale(locale_)) {
 		isDownloading.set(true);
 
 		const messagesFileUrl = MESSAGE_FILE_URL_TEMPLATE.replace('{locale}', locale_);
 
 		return loadJson(messagesFileUrl).then((messages) => {
-			_activeLocale = locale_;
+			_activeLocale.set(locale_);
 
 			addMessages(locale_, messages);
 
@@ -53,8 +55,8 @@ const isLocaleLoaded = derived(
 	[isDownloading, dictionary],
 	([$isDownloading, $dictionary]) =>
 		!$isDownloading &&
-		$dictionary[_activeLocale] &&
-		Object.keys($dictionary[_activeLocale]).length > 0
+		$dictionary[get(_activeLocale)] &&
+		Object.keys($dictionary[get(_activeLocale)]).length > 0
 );
 
 const dir = derived(locale, ($locale) => ($locale === 'ar' ? 'rtl' : 'ltr'));
diff --git a/frontend/src/routes/register/+page.svelte b/frontend/src/routes/register/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..030dd3b67263d472af64ae99dc6d57ae37e3997e
--- /dev/null
+++ b/frontend/src/routes/register/+page.svelte
@@ -0,0 +1,164 @@
+<script lang="ts">
+	import Header from '$lib/components/header.svelte';
+	import JWTSession from '$lib/stores/JWTSession';
+	import { onMount } from 'svelte';
+	import { _ } from '$lib/services/i18n';
+	import LocalSelector from '$lib/components/header/localSelector.svelte';
+	import { CheckCircle, ExclamationTriangle, Icon } from 'svelte-hero-icons';
+	import { loginAPI, registerAPI } from '$lib/api/auth';
+	import { toastAlert } from '$lib/utils/toasts';
+
+	let checker: HTMLDivElement;
+
+	onMount(() => {
+		if (JWTSession.isLoggedIn()) {
+			const redirect = new URLSearchParams(window.location.search).get('redirect') ?? '/';
+			window.location.href = redirect;
+		}
+
+		checker.innerHTML =
+			'<input type="checkbox" id="humanCheck" required><label for="humanCheck">' +
+			$_('signup.humans') +
+			'</label>';
+	});
+
+	let message = '';
+
+	let nickname = '';
+	let email = '';
+	let password = '';
+	let confirmPassword = '';
+
+	const signup = async () => {
+		if (nickname == '' || email == '') {
+			message = $_('signup.emptyFields');
+			return;
+		}
+		if (password.length < 8) {
+			message = $_('signup.passwordRules');
+			return;
+		}
+		if (password != confirmPassword) {
+			message = $_('signup.differentPasswords');
+			return;
+		}
+		if (!checker.querySelector('input')?.checked) {
+			message = $_('signup.humanity');
+			return;
+		}
+
+		const result = await registerAPI(email, password, nickname);
+
+		if (result !== 'OK') {
+			message = result;
+			return;
+		}
+		message = '';
+
+		const loginRes = await loginAPI(email, password);
+
+		if (loginRes !== 'OK') {
+			toastAlert('Failed to login: ' + loginRes);
+			document.location.href = '/login';
+			return;
+		}
+
+		document.location.href = '/first-login';
+	};
+</script>
+
+<Header />
+
+<div class="flex items-center justify-center h-screen">
+	<form action="#" class="shadow-md w-1/2 min-w-fit max-w-2xl mb-7 flex items-center flex-col p-5">
+		<div class="text-xl mb-4 font-bold">
+			{$_('signup.title')}
+		</div>
+		{#if message}
+			<div class="w-full py-1 bg-red-600 text-white text-center font-bold rounded mb-4">
+				{message}
+			</div>
+		{/if}
+		<div class="flex w-full mb-4">
+			<label for="language">{$_('signup.language')}</label>
+			<LocalSelector class="w-full !bg-gray-200 py-2 px-4 rounded" />
+		</div>
+		<div class="flex w-full mb-4">
+			<label for="nickname">{$_('signup.nickname')}</label>
+			<input
+				class="w-1/2"
+				type="text"
+				id="nickname"
+				name="nickname"
+				bind:value={nickname}
+				required
+			/>
+		</div>
+		<div class="flex w-full mb-4">
+			<label for="email">{$_('signup.email')}</label>
+			<input class="w-1/2" type="email" id="email" name="email" bind:value={email} required />
+		</div>
+		<div class="flex w-full mb-4">
+			<label for="password">{$_('signup.password')}</label>
+			<div class="w-1/2 flex">
+				<div class="flex-grow">
+					<input
+						class="w-full"
+						type="password"
+						id="password"
+						name="password"
+						bind:value={password}
+						required
+					/>
+				</div>
+				<div class="w-12 ml-2 flex items-center justify-center">
+					{#if password.length < 8}
+						<div title={$_('signup.passwordRules')}>
+							<Icon src={ExclamationTriangle} class="w-8 text-orange-600" />
+						</div>
+					{:else}
+						<Icon src={CheckCircle} class="w-8 text-green-600" />
+					{/if}
+				</div>
+			</div>
+		</div>
+		<div class="flex w-full mb-4">
+			<label for="password">{$_('signup.password')}</label>
+			<div class="w-1/2 -ml-1 flex">
+				<div class="flex-grow">
+					<input
+						class="w-full"
+						type="password"
+						id="password"
+						name="password"
+						bind:value={confirmPassword}
+						required
+					/>
+				</div>
+				<div class="w-12 ml-2 flex items-center justify-center">
+					{#if confirmPassword == '' || password != confirmPassword}
+						<div title={$_('signup.differentPasswords')}>
+							<Icon src={ExclamationTriangle} class="w-8 text-orange-600" />
+						</div>
+					{:else}
+						<Icon src={CheckCircle} class="w-8 text-green-600" />
+					{/if}
+				</div>
+			</div>
+		</div>
+		<div bind:this={checker} class="mb-4 space-x-4"></div>
+		<button type="submit" on:click|preventDefault={signup} class="button"
+			>{$_('signup.signup')}</button
+		>
+	</form>
+</div>
+
+<style lang="postcss">
+	label {
+		@apply font-bold pr-4 w-1/2 flex items-center justify-end;
+	}
+
+	input {
+		@apply border-2 bg-gray-200 rounded py-2 px-4;
+	}
+</style>
diff --git a/frontend/static/lang/en.json b/frontend/static/lang/en.json
index 0931054a733d7af8a669f3c575bbc83e60486385..0bda99c8019ad1686b6c2005c102089a5f9094b3 100644
--- a/frontend/static/lang/en.json
+++ b/frontend/static/lang/en.json
@@ -18,6 +18,15 @@
 		"participantPlaceholder": "Please select",
 		"remainingDuration": "Remaining duration"
 	},
+	"signup": {
+		"title": "Register",
+		"language": "Language",
+		"nickname": "Nickname",
+		"email": "Email",
+		"password": "Password",
+		"confirmPassword": "Confirm password",
+		"signup": "Sign up"
+	},
 	"utils": {
 		"month": {
 			"january": "january",
diff --git a/frontend/static/lang/fr.json b/frontend/static/lang/fr.json
index 76a7d6d8f058f3af9edf316d45bfeb9da31ee115..2e529e65b9eb2f353144e83a5a8cef95a35a0aec 100644
--- a/frontend/static/lang/fr.json
+++ b/frontend/static/lang/fr.json
@@ -32,6 +32,20 @@
 		"actions": "Actions",
 		"passwordPrompt": "Veuillez entrer le mot de passe. Attention, celui ci ne sera plus récupérable."
 	},
+	"signup": {
+		"title": "Inscription",
+		"language": "Langue / Language",
+		"nickname": "Pseudo",
+		"email": "Email",
+		"password": "Mot de passe",
+		"confirmPassword": "Confirmer le mot de passe",
+		"signup": "S'inscrire",
+		"passwordRules": "Le mot de passe doit contenir au moins 8 caractères",
+		"differentPasswords": "Les mots de passe ne correspondent pas",
+		"emptyFields": "Veuillez remplir tous les champs",
+		"humans": "Je ne suis pas un robot",
+		"humanity": "Veuillez confirmer que vous n'êtes pas un robot"
+	},
 	"tests": {
 		"sendResults": "Envoyer les résultats",
 		"sendResultsDone": "Envoyé"