feat(i18n): integrate internationalization for game pages

- Added translation support for various game-related texts in the home, daily, infinite, login, and profile pages.
- Replaced hardcoded French strings with translation keys using the `$t` function.
- Updated titles, descriptions, and button texts to enhance localization.
This commit is contained in:
2026-03-15 20:19:26 +01:00
parent 6d2dccd47f
commit bd121b7d85
15 changed files with 805 additions and 191 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from '$lib/server/daily-character'; import type { CharacterWithRelations } from '$lib/server/daily-character';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from '$lib/i18n';
let { let {
characters, characters,
@@ -138,13 +139,13 @@
</script> </script>
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur z-10"> <div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur z-10">
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Entrer une supposition</h2> <h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">{$t.game.components.searchInput.title}</h2>
<div class="mt-4 flex flex-col gap-3 sm:flex-row"> <div class="mt-4 flex flex-col gap-3 sm:flex-row">
<div bind:this={state.searchContainer} class="relative w-full"> <div bind:this={state.searchContainer} class="relative w-full">
<input <input
bind:value={state.searchInput} bind:value={state.searchInput}
class="w-full rounded-full border border-amber-200/30 bg-slate-900/60 px-5 py-3 text-sm text-slate-100 placeholder:text-slate-400 focus:border-amber-200/70 focus:outline-none" class="w-full rounded-full border border-amber-200/30 bg-slate-900/60 px-5 py-3 text-sm text-slate-100 placeholder:text-slate-400 focus:border-amber-200/70 focus:outline-none"
placeholder="Nom du personnage" placeholder={$t.game.components.searchInput.placeholder}
type="text" type="text"
onkeydown={handleKeydown} onkeydown={handleKeydown}
/> />
@@ -193,7 +194,7 @@
disabled={state.searchInput.length === 0 || filteredCharacters.length === 0} disabled={state.searchInput.length === 0 || filteredCharacters.length === 0}
class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200 disabled:cursor-not-allowed disabled:opacity-50" class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200 disabled:cursor-not-allowed disabled:opacity-50"
> >
Valider {$t.game.components.searchInput.submit}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { formatBounty } from '$lib'; import { formatBounty } from '$lib';
import type { CharacterWithRelations } from '$lib/server/daily-character'; import type { CharacterWithRelations } from '$lib/server/daily-character';
import { t } from '$lib/i18n';
export let selectedCharacters: CharacterWithRelations[]; export let selectedCharacters: CharacterWithRelations[];
export let dailyCharacter: CharacterWithRelations; export let dailyCharacter: CharacterWithRelations;
@@ -68,10 +69,10 @@
> >
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col items-center gap-4 text-center"> <div class="flex flex-col items-center gap-4 text-center">
<p class="text-xs font-semibold tracking-[0.28em] text-amber-100 uppercase">Historique</p> <p class="text-xs font-semibold tracking-[0.28em] text-amber-100 uppercase">{$t.game.components.guessHistory.title}</p>
</div> </div>
{#if selectedCharacters.length === 0} {#if selectedCharacters.length === 0}
<p class="text-center text-sm text-slate-200">Aucune tentative pour le moment.</p> <p class="text-center text-sm text-slate-200">{$t.game.components.guessHistory.empty}</p>
{:else} {:else}
<div class="-mx-6 overflow-x-auto px-6 pb-2 sm:mx-0 sm:px-0"> <div class="-mx-6 overflow-x-auto px-6 pb-2 sm:mx-0 sm:px-0">
<div class="mx-auto w-max min-w-max"> <div class="mx-auto w-max min-w-max">
@@ -83,7 +84,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Personnage {$t.game.components.guessHistory.character}
</p> </p>
</div> </div>
{#if columnVisibility.status !== false} {#if columnVisibility.status !== false}
@@ -93,7 +94,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Statut {$t.game.components.guessHistory.status}
</p> </p>
</div> </div>
{/if} {/if}
@@ -104,7 +105,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Genre {$t.game.components.guessHistory.gender}
</p> </p>
</div> </div>
{/if} {/if}
@@ -115,7 +116,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Affiliations {$t.game.components.guessHistory.affiliations}
</p> </p>
</div> </div>
{/if} {/if}
@@ -126,7 +127,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Fruit {$t.game.components.guessHistory.fruit}
</p> </p>
</div> </div>
{/if} {/if}
@@ -137,7 +138,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Haki {$t.game.components.guessHistory.haki}
</p> </p>
</div> </div>
{/if} {/if}
@@ -148,7 +149,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Prime {$t.game.components.guessHistory.bounty}
</p> </p>
</div> </div>
{/if} {/if}
@@ -159,7 +160,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Taille {$t.game.components.guessHistory.height}
</p> </p>
</div> </div>
{/if} {/if}
@@ -170,7 +171,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Origine {$t.game.components.guessHistory.origin}
</p> </p>
</div> </div>
{/if} {/if}
@@ -181,7 +182,7 @@
<p <p
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs" class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
> >
Arc {$t.game.components.guessHistory.arc}
</p> </p>
</div> </div>
{/if} {/if}
@@ -229,14 +230,14 @@
> >
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"> <p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm">
{character.status === 'Alive' {character.status === 'Alive'
? 'Vivant' ? $t.game.components.guessHistory.alive
: character.status === 'Dead' : character.status === 'Dead'
? 'Mort' ? $t.game.components.guessHistory.dead
: character.status === 'Unknown' : character.status === 'Unknown'
? 'Inconnu' ? $t.game.components.guessHistory.unknown
: character.status === null : character.status === null
? '-' ? '-'
: character.status || 'Inconnu'} : character.status || $t.game.components.guessHistory.unknown}
</p> </p>
</div> </div>
{/if} {/if}
@@ -251,10 +252,10 @@
> >
<p class="text-center text-xs font-bold text-white sm:text-sm md:text-base"> <p class="text-center text-xs font-bold text-white sm:text-sm md:text-base">
{character.gender === 'Male' {character.gender === 'Male'
? 'Homme' ? $t.game.components.guessHistory.male
: character.gender === 'Female' : character.gender === 'Female'
? 'Femme' ? $t.game.components.guessHistory.female
: character.gender || 'Inconnu'} : character.gender || $t.game.components.guessHistory.unknown}
</p> </p>
</div> </div>
{/if} {/if}
@@ -322,10 +323,10 @@
})()} flex items-center justify-center p-1 sm:p-2" })()} flex items-center justify-center p-1 sm:p-2"
> >
<p class="text-center text-sm font-bold text-white sm:text-lg md:text-2xl"> <p class="text-center text-sm font-bold text-white sm:text-lg md:text-2xl">
{#if character.hakiObservation}<span title="Haki de l'Observation">👁️</span {#if character.hakiObservation}<span title={$t.game.components.guessHistory.obsHakiTitle}>👁️</span
>{/if} >{/if}
{#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if} {#if character.hakiArmament}<span title={$t.game.components.guessHistory.armHakiTitle}>🦾</span>{/if}
{#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if} {#if character.hakiConqueror}<span title={$t.game.components.guessHistory.kingHakiTitle}>👑</span>{/if}
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror} {#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
<span class="text-2xl sm:text-3xl md:text-5xl"></span> <span class="text-2xl sm:text-3xl md:text-5xl"></span>
{/if} {/if}
@@ -362,7 +363,7 @@
<p <p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm" class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
> >
Inconnue {$t.game.components.guessHistory.unknown}
</p> </p>
{/if} {/if}
</div> </div>
@@ -397,7 +398,7 @@
<p <p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm" class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
> >
Inconnue {$t.game.components.guessHistory.unknown}
</p> </p>
{/if} {/if}
</div> </div>
@@ -412,7 +413,7 @@
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2" : 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2"
> >
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"> <p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm">
{character.origin || 'Inconnue'} {character.origin || $t.game.components.guessHistory.unknown}
</p> </p>
</div> </div>
{/if} {/if}
@@ -439,7 +440,7 @@
<p <p
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm" class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
> >
{character.arcName || 'Inconnu'} {character.arcName || $t.game.components.guessHistory.unknown}
</p> </p>
</div> </div>
{/if} {/if}

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character"; import type { CharacterWithRelations } from "$lib/server/daily-character";
import { t } from '$lib/i18n';
export let dailyCharacter: CharacterWithRelations; export let dailyCharacter: CharacterWithRelations;
export let selectedCharacters: CharacterWithRelations[]; export let selectedCharacters: CharacterWithRelations[];
@@ -44,13 +45,13 @@
disabled={!isOriginAvailable} disabled={!isOriginAvailable}
onclick={() => showHintOrigin = !showHintOrigin} onclick={() => showHintOrigin = !showHintOrigin}
> >
<p class="text-sm font-medium text-amber-100">Origine</p> <p class="text-sm font-medium text-amber-100">{$t.game.components.hints.origin}</p>
{#if showHintOrigin} {#if showHintOrigin}
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.origin || 'Inconnue'}</p> <p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.origin || $t.game.components.hints.unknown}</p>
{:else if Math.max(0, 5 - selectedCharacters.length) > 0} {:else if Math.max(0, 5 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} essais avant déblocage</p> <p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
{:else} {:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p> <p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
{/if} {/if}
</button> </button>
<button <button
@@ -59,13 +60,13 @@
disabled={!isFruitAvailable} disabled={!isFruitAvailable}
onclick={() => showHintFruit = !showHintFruit} onclick={() => showHintFruit = !showHintFruit}
> >
<p class="text-sm font-medium text-amber-100">Fruit du démon</p> <p class="text-sm font-medium text-amber-100">{$t.game.components.hints.devilFruit}</p>
{#if showHintFruit} {#if showHintFruit}
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || 'Aucun'}</p> <p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || $t.game.components.hints.none}</p>
{:else if Math.max(0, 10 - selectedCharacters.length) > 0} {:else if Math.max(0, 10 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} essais avant déblocage</p> <p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
{:else} {:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p> <p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
{/if} {/if}
</button> </button>
<button <button
@@ -74,16 +75,16 @@
disabled={!isAffiliationAvailable} disabled={!isAffiliationAvailable}
onclick={() => showHintAffiliation = !showHintAffiliation} onclick={() => showHintAffiliation = !showHintAffiliation}
> >
<p class="text-sm font-medium text-amber-100">Affiliation</p> <p class="text-sm font-medium text-amber-100">{$t.game.components.hints.affiliation}</p>
{#if showHintAffiliation} {#if showHintAffiliation}
{@const affiliations = typeof dailyCharacter.affiliations === 'string' {@const affiliations = typeof dailyCharacter.affiliations === 'string'
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim())) ? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
: dailyCharacter.affiliations} : dailyCharacter.affiliations}
<p class="mt-2 text-xs text-white font-semibold">{Array.isArray(affiliations) ? affiliations[0] : affiliations || 'Inconnue'}</p> <p class="mt-2 text-xs text-white font-semibold">{Array.isArray(affiliations) ? affiliations[0] : affiliations || $t.game.components.hints.unknown}</p>
{:else if Math.max(0, 15 - selectedCharacters.length) > 0} {:else if Math.max(0, 15 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} essais avant déblocage</p> <p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
{:else} {:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p> <p class="mt-2 text-xs text-slate-400">{$t.game.components.hints.available}</p>
{/if} {/if}
</button> </button>
</div> </div>

View File

@@ -0,0 +1,104 @@
<script lang="ts">
import { onMount } from 'svelte';
import { availableLanguages, language, setLanguage } from '$lib/i18n';
let isOpen = false;
let rootElement: HTMLDivElement | undefined;
const languageLabels: Record<string, string> = {
en: 'English',
fr: 'Francais'
};
const languageFlags: Record<string, string> = {
en: 'GB',
fr: 'FR'
};
function getLanguageLabel(lang: string): string {
return languageLabels[lang] || lang.toUpperCase();
}
function getFlagCode(lang: string): string {
return languageFlags[lang] || 'UN';
}
function toFlagEmoji(code: string): string {
const normalized = code.toUpperCase();
if (normalized.length !== 2) {
return 'UN';
}
const first = normalized.codePointAt(0);
const second = normalized.codePointAt(1);
if (!first || !second) {
return 'UN';
}
return String.fromCodePoint(127397 + first, 127397 + second);
}
function toggleMenu() {
isOpen = !isOpen;
}
function selectLanguage(lang: string) {
setLanguage(lang);
isOpen = false;
}
onMount(() => {
const onDocumentClick = (event: MouseEvent) => {
if (!rootElement) {
return;
}
if (!rootElement.contains(event.target as Node)) {
isOpen = false;
}
};
document.addEventListener('click', onDocumentClick);
return () => document.removeEventListener('click', onDocumentClick);
});
</script>
<div bind:this={rootElement} class="relative">
<button
type="button"
onclick={toggleMenu}
class="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-2 text-sm font-semibold text-slate-100 transition hover:border-amber-300/50 hover:bg-white/10"
aria-haspopup="true"
aria-expanded={isOpen}
aria-label="Change language"
>
<span class="text-base" aria-hidden="true">{toFlagEmoji(getFlagCode($language))}</span>
<span class="uppercase text-xs tracking-wider">{$language}</span>
<svg
class="h-3.5 w-3.5 transition-transform {isOpen ? 'rotate-180' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{#if isOpen}
<div class="absolute right-0 top-full z-20 mt-2 w-44 rounded-xl border border-white/10 bg-slate-900/95 p-1 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
{#each availableLanguages as lang (lang)}
<button
type="button"
onclick={() => selectLanguage(lang)}
class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition {lang === $language ? 'bg-amber-300 text-slate-900' : 'text-slate-100 hover:bg-white/5'}"
>
<span class="flex items-center gap-2">
<span class="text-base" aria-hidden="true">{toFlagEmoji(getFlagCode(lang))}</span>
<span>{getLanguageLabel(lang)}</span>
</span>
<span class="text-xs uppercase tracking-wide opacity-70">{lang}</span>
</button>
{/each}
</div>
{/if}
</div>

View File

@@ -1,28 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character"; import type { CharacterWithRelations } from "$lib/server/daily-character";
import { t } from '$lib/i18n';
export let selectedCharacter: CharacterWithRelations; export let selectedCharacter: CharacterWithRelations;
export let selectedCharacters: CharacterWithRelations[]; export let selectedCharacters: CharacterWithRelations[];
export let isGeckoMoriaWin: boolean = false; export let isGeckoMoriaWin: boolean = false;
const oneTryMessages = ['Tricheur 👀', '1 essai ? Avoue, tu avais la réponse 😏', 'Premier coup direct... suspect 🤨']; const pickMessage = (messages: readonly string[]) => messages[Math.floor(Math.random() * messages.length)];
const twoTryMessages = ['Bien joué ! ⚡', 'Deux essais, propre ! 👏', 'Tu chauffes vite, bien joué 🔥'];
const tenPlusMessages = [
'${attempts} essais... même un escargophone aurait trouvé plus vite 📞',
'${attempts} tentatives ? Le Grand Line est moins long que ça 😵',
'${attempts} essais : performance légendaire... dans le mauvais sens 🫠'
];
const fivePlusMessages = [
"${attempts} essais ? On va dire que c'était pour le suspense 😅",
'Ça en fait des essais... mais au moins tu y es arrivé 😬',
'Tu ne lâches rien, même après plusieurs essais 😂'
];
const defaultMessages = ['Pas mal du tout !', 'Bien tenté, bon rythme 👍', 'Ça se passe bien, continue comme ça ✨'];
const pickMessage = (messages: string[]) => messages[Math.floor(Math.random() * messages.length)];
const getAttemptMessage = (attempts: number): string => { const getAttemptMessage = (attempts: number): string => {
if (attempts <= 0) return ''; if (attempts <= 0) return '';
const oneTryMessages = $t.game.components.winPanel.oneTryMessages;
const twoTryMessages = $t.game.components.winPanel.twoTryMessages;
const tenPlusMessages = $t.game.components.winPanel.tenPlusMessages;
const fivePlusMessages = $t.game.components.winPanel.fivePlusMessages;
const defaultMessages = $t.game.components.winPanel.defaultMessages;
if (attempts === 1) { if (attempts === 1) {
return pickMessage(oneTryMessages); return pickMessage(oneTryMessages);
} }
@@ -41,14 +33,17 @@
$: attempts = selectedCharacters.length; $: attempts = selectedCharacters.length;
$: attemptMessage = getAttemptMessage(attempts); $: attemptMessage = getAttemptMessage(attempts);
$: attemptWord = selectedCharacters.length > 1
? $t.game.components.winPanel.attemptPlural
: $t.game.components.winPanel.attemptSingular;
</script> </script>
{#if isGeckoMoriaWin} {#if isGeckoMoriaWin}
<div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect"> <div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect">
<div class="text-center"> <div class="text-center">
<div class="text-3xl mb-2">🌑</div> <div class="text-3xl mb-2">🌑</div>
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2> <h2 class="text-xl font-bold text-slate-300 mb-1">{$t.game.components.winPanel.moriaTitle}</h2>
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p> <p class="text-sm text-slate-400">{$t.game.components.winPanel.moriaPrefix} {selectedCharacters.length} {attemptWord} !</p>
<p class="text-xs text-slate-300 mt-1">{attemptMessage}</p> <p class="text-xs text-slate-300 mt-1">{attemptMessage}</p>
<div class="mt-3"> <div class="mt-3">
{#if selectedCharacter.pictureUrl} {#if selectedCharacter.pictureUrl}
@@ -73,8 +68,8 @@
<div class="rounded-3xl border border-emerald-500/50 bg-emerald-500/10 p-4 shadow-[0_24px_60px_rgba(16,185,129,0.3)] backdrop-blur"> <div class="rounded-3xl border border-emerald-500/50 bg-emerald-500/10 p-4 shadow-[0_24px_60px_rgba(16,185,129,0.3)] backdrop-blur">
<div class="text-center"> <div class="text-center">
<div class="text-3xl mb-2">🎉</div> <div class="text-3xl mb-2">🎉</div>
<h2 class="text-xl font-bold text-emerald-400 mb-1">Félicitations !</h2> <h2 class="text-xl font-bold text-emerald-400 mb-1">{$t.game.components.winPanel.winTitle}</h2>
<p class="text-sm text-emerald-300">Vous avez trouvé le personnage en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p> <p class="text-sm text-emerald-300">{$t.game.components.winPanel.winPrefix} {selectedCharacters.length} {attemptWord} !</p>
<p class="text-xs text-emerald-200 mt-1">{attemptMessage}</p> <p class="text-xs text-emerald-200 mt-1">{attemptMessage}</p>
<div class="mt-3"> <div class="mt-3">
{#if selectedCharacter.pictureUrl} {#if selectedCharacter.pictureUrl}

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character"; import type { CharacterWithRelations } from "$lib/server/daily-character";
import { t } from '$lib/i18n';
export let yesterdayCharacter: CharacterWithRelations | null; export let yesterdayCharacter: CharacterWithRelations | null;
</script> </script>
@@ -15,11 +16,11 @@
/> />
{:else} {:else}
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100"> <div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo {$t.game.components.yesterdayCharacter.photo}
</div> </div>
{/if} {/if}
<div class="flex-1"> <div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p> <p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.components.yesterdayCharacter.title}</p>
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p> <p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if yesterdayCharacter.epithets} {#if yesterdayCharacter.epithets}
<p class="mt-1 text-sm text-slate-400"> <p class="mt-1 text-sm text-slate-400">
@@ -35,18 +36,18 @@
rel="noopener noreferrer" rel="noopener noreferrer"
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto" class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
> >
Voir la page {$t.game.components.yesterdayCharacter.openPage}
</a> </a>
</div> </div>
{:else} {:else}
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left"> <div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100"> <div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo {$t.game.components.yesterdayCharacter.photo}
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p> <p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.components.yesterdayCharacter.title}</p>
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p> <p class="mt-2 text-lg font-semibold text-white">{$t.game.components.yesterdayCharacter.none}</p>
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p> <p class="mt-1 text-sm text-slate-200">{$t.game.components.yesterdayCharacter.noneAvailable}</p>
</div> </div>
</div> </div>
{/if} {/if}

227
src/lib/i18n/en.json Normal file
View File

@@ -0,0 +1,227 @@
{
"common": {
"language": "Language",
"selectLanguage": "Select Language",
"english": "English",
"french": "Français",
"german": "Deutsch",
"spanish": "Español"
},
"game": {
"home": {
"heroDescription": "Guess the character from pirate crews, marines, or the wider world. Every hint brings you closer to the treasure.",
"dailyTitle": "Daily Character",
"dailySubtitle": "A new mystery every 24 hours",
"dailyDescription": "Compare your guesses, unlock hints, and keep your streak alive.",
"dailyCta": "Start",
"infiniteTitle": "Infinite Mode",
"infiniteSubtitle": "Endless challenges",
"infiniteDescription": "Chain characters and chase your score. No limits, only fun.",
"infiniteCta": "Play",
"photoFallback": "Photo",
"yesterdayCharacter": "Yesterday's character",
"openPage": "Open page",
"noCharacter": "No character",
"noYesterdayCharacter": "No character from yesterday available"
},
"login": {
"titleSignUp": "Sign Up",
"titleSignIn": "Sign In",
"headerSignUp": "Create your account",
"headerSignIn": "Welcome, pirate",
"nameLabel": "Name",
"namePlaceholder": "Your name",
"usernameLabel": "Username",
"usernamePlaceholder": "e.g. luffy_gear5",
"identifierLabelSignUp": "Email",
"identifierLabelSignIn": "Email or username",
"identifierPlaceholderSignUp": "yourmail@email.com",
"identifierPlaceholderSignIn": "yourmail@email.com or luffy_gear5",
"passwordLabel": "Password",
"confirmPasswordLabel": "Confirm password",
"loading": "Loading...",
"submitSignUp": "Create an account",
"submitSignIn": "Log in",
"togglePromptSignUp": "Already have an account?",
"togglePromptSignIn": "Don't have an account?",
"toggleActionSignUp": "Log in",
"toggleActionSignIn": "Sign up",
"backHome": "Back to home"
},
"profile": {
"pageTitle": "My Profile",
"headerTitle": "My Profile",
"headerSubtitle": "Edit your profile information",
"tabProfile": "Profile",
"tabPassword": "Password",
"tabDaily": "Daily History",
"tabSessions": "Sessions",
"tabFriends": "Friends",
"avatarFallbackAlt": "Profile",
"email": "Email",
"displayName": "Display name",
"displayNamePlaceholder": "Your name",
"profileUpdateSuccess": "Profile updated successfully!",
"updating": "Updating...",
"saveChanges": "Save changes",
"friendsTitle": "Friends System",
"addFriendByUsername": "Add a friend by username",
"friendUsernamePlaceholder": "e.g. luffy_gear5",
"sending": "Sending...",
"send": "Send",
"incomingRequests": "Incoming requests",
"noIncomingRequests": "No incoming requests.",
"accept": "Accept",
"decline": "Decline",
"outgoingRequests": "Outgoing requests",
"noOutgoingRequests": "No outgoing requests.",
"cancel": "Cancel",
"myFriends": "My friends",
"noFriends": "You don't have any friends yet.",
"remove": "Remove",
"changePasswordTitle": "Change password",
"currentPassword": "Current password",
"newPassword": "New password",
"confirmPassword": "Confirm password",
"passwordChangeSuccess": "Password changed successfully!",
"changing": "Changing...",
"changePassword": "Change password",
"dailyHistoryTitle": "Daily history",
"noDailyHistory": "No history available",
"noImage": "N/A",
"trySingular": "try",
"tryPlural": "tries",
"activeSessionsTitle": "Active sessions",
"noActiveSessions": "No active session",
"unknownDevice": "Unknown device",
"unknown": "Unknown",
"ip": "IP",
"created": "Created",
"terminate": "Terminate",
"backHome": "Back to home"
},
"daily": {
"metaTitle": "OnePieceDle - Daily Mode",
"title": "Daily Character",
"winsPeopleSingular": "person",
"winsPeoplePlural": "people",
"winsVerbSingular": "has",
"winsVerbPlural": "have",
"winsSuffix": "found it today 🎉",
"reset": "Play again",
"description": "Guess the character. Each hint unlocks after a certain number of guesses. Good luck!",
"friendsToday": "Your friends today",
"friendTrySingular": "try",
"friendTryPlural": "tries"
},
"infinite": {
"metaTitle": "OnePieceDle - Infinite Mode",
"title": "Infinite Mode",
"score": "Score",
"resetScore": "Reset",
"description": "Guess characters endlessly. Each hint unlocks after a certain number of guesses. Good luck!",
"nextCharacter": "Play again",
"revealAnswer": "Reveal answer",
"loadingCharacter": "Loading character...",
"filtersTitle": "Character filters",
"clearFilters": "Reset",
"filterGender": "Gender",
"filterStatus": "Status",
"filterAbilities": "Abilities",
"filterInformation": "Information",
"filterArcs": "Arcs",
"male": "Male",
"female": "Female",
"alive": "Alive",
"dead": "Dead",
"unknown": "Unknown",
"hasHaki": "Has Haki",
"fruitAll": "Fruit (All)",
"withFruit": "With Fruit",
"withoutFruit": "Without Fruit",
"heightDefined": "Height defined",
"originDefined": "Origin defined",
"availableCharactersSingular": "character available",
"availableCharactersPlural": "characters available",
"columnsTitle": "Columns"
},
"components": {
"searchInput": {
"title": "Enter a guess",
"placeholder": "Character name",
"submit": "Submit"
},
"hints": {
"origin": "Origin",
"devilFruit": "Devil fruit",
"affiliation": "Affiliation",
"unknown": "Unknown",
"none": "None",
"beforeUnlock": "guesses before unlock",
"available": "Hint available!"
},
"guessHistory": {
"title": "History",
"empty": "No guesses yet.",
"character": "Character",
"status": "Status",
"gender": "Gender",
"affiliations": "Affiliations",
"fruit": "Fruit",
"haki": "Haki",
"bounty": "Bounty",
"height": "Height",
"origin": "Origin",
"arc": "Arc",
"alive": "Alive",
"dead": "Dead",
"unknown": "Unknown",
"male": "Male",
"female": "Female",
"obsHakiTitle": "Observation Haki",
"armHakiTitle": "Armament Haki",
"kingHakiTitle": "Conqueror's Haki"
},
"winPanel": {
"attemptSingular": "attempt",
"attemptPlural": "attempts",
"moriaTitle": "Moria controls you...",
"moriaPrefix": "You succumbed to the shadows in",
"winTitle": "Congratulations!",
"winPrefix": "You found the character in",
"oneTryMessages": [
"Cheater 👀",
"1 guess? Admit it, you already knew 😏",
"First try... suspicious 🤨"
],
"twoTryMessages": [
"Well played! ⚡",
"Two guesses, clean! 👏",
"You warmed up fast, nice 🔥"
],
"tenPlusMessages": [
"${attempts} guesses... even a transponder snail would be faster 📞",
"${attempts} attempts? The Grand Line is shorter than that 😵",
"${attempts} guesses: legendary performance... in the wrong direction 🫠"
],
"fivePlusMessages": [
"${attempts} guesses? Let's say it was for suspense 😅",
"That is a lot of guesses... but you made it 😬",
"You never give up, even after several guesses 😂"
],
"defaultMessages": [
"Not bad at all!",
"Nice try, good pace 👍",
"Things are going well, keep it up ✨"
]
},
"yesterdayCharacter": {
"photo": "Photo",
"title": "Yesterday's character",
"openPage": "Open page",
"none": "No character",
"noneAvailable": "No character from yesterday available"
}
}
}
}

227
src/lib/i18n/fr.json Normal file
View File

@@ -0,0 +1,227 @@
{
"common": {
"language": "Langue",
"selectLanguage": "Sélectionnez la Langue",
"english": "English",
"french": "Français",
"german": "Deutsch",
"spanish": "Español"
},
"game": {
"home": {
"heroDescription": "Devine le personnage de l'equipage, des marines ou du vaste monde. Chaque indice te rapproche du tresor.",
"dailyTitle": "Personnage du jour",
"dailySubtitle": "Nouveau mystere toutes les 24 heures",
"dailyDescription": "Compare tes essais, debloque des indices et garde ta serie.",
"dailyCta": "Commencer",
"infiniteTitle": "Mode Infini",
"infiniteSubtitle": "Des defis sans fin",
"infiniteDescription": "Enchaine les personnages et croise ton score. Pas de limite, que du plaisir.",
"infiniteCta": "Jouer",
"photoFallback": "Photo",
"yesterdayCharacter": "Personnage d'hier",
"openPage": "Voir la page",
"noCharacter": "Aucun personnage",
"noYesterdayCharacter": "Aucun personnage d'hier disponible"
},
"login": {
"titleSignUp": "Inscription",
"titleSignIn": "Connexion",
"headerSignUp": "Creer votre compte",
"headerSignIn": "Bienvenue, pirate",
"nameLabel": "Nom",
"namePlaceholder": "Votre nom",
"usernameLabel": "Nom d'utilisateur",
"usernamePlaceholder": "ex: luffy_gear5",
"identifierLabelSignUp": "E-mail",
"identifierLabelSignIn": "E-mail ou nom d'utilisateur",
"identifierPlaceholderSignUp": "votremail@email.com",
"identifierPlaceholderSignIn": "votremail@email.com ou luffy_gear5",
"passwordLabel": "Mot de passe",
"confirmPasswordLabel": "Confirmer le mot de passe",
"loading": "Chargement...",
"submitSignUp": "Creer un compte",
"submitSignIn": "Se connecter",
"togglePromptSignUp": "Vous avez deja un compte ?",
"togglePromptSignIn": "Vous n'avez pas de compte ?",
"toggleActionSignUp": "Se connecter",
"toggleActionSignIn": "S'inscrire",
"backHome": "Retour a l'accueil"
},
"profile": {
"pageTitle": "Mon Profil",
"headerTitle": "Mon Profil",
"headerSubtitle": "Modifie les informations de ton profil",
"tabProfile": "Profil",
"tabPassword": "Mot de passe",
"tabDaily": "Historique Daily",
"tabSessions": "Sessions",
"tabFriends": "Amis",
"avatarFallbackAlt": "Profil",
"email": "Email",
"displayName": "Nom d'affichage",
"displayNamePlaceholder": "Ton nom",
"profileUpdateSuccess": "Profil mis a jour avec succes !",
"updating": "Mise a jour...",
"saveChanges": "Enregistrer les modifications",
"friendsTitle": "Systeme d'amis",
"addFriendByUsername": "Ajouter un ami par nom d'utilisateur",
"friendUsernamePlaceholder": "ex: luffy_gear5",
"sending": "Envoi...",
"send": "Envoyer",
"incomingRequests": "Demandes recues",
"noIncomingRequests": "Aucune demande recue.",
"accept": "Accepter",
"decline": "Refuser",
"outgoingRequests": "Demandes envoyees",
"noOutgoingRequests": "Aucune demande envoyee.",
"cancel": "Annuler",
"myFriends": "Mes amis",
"noFriends": "Tu n'as pas encore d'amis.",
"remove": "Supprimer",
"changePasswordTitle": "Changer le mot de passe",
"currentPassword": "Mot de passe actuel",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"passwordChangeSuccess": "Mot de passe change avec succes !",
"changing": "Changement en cours...",
"changePassword": "Changer le mot de passe",
"dailyHistoryTitle": "Historique des Daily",
"noDailyHistory": "Aucun historique disponible",
"noImage": "N/A",
"trySingular": "tentative",
"tryPlural": "tentatives",
"activeSessionsTitle": "Sessions actives",
"noActiveSessions": "Aucune session active",
"unknownDevice": "Appareil inconnu",
"unknown": "Inconnue",
"ip": "IP",
"created": "Creee",
"terminate": "Terminer",
"backHome": "Retour a l'accueil"
},
"daily": {
"metaTitle": "OnePieceDle - Mode du jour",
"title": "Personnage du jour",
"winsPeopleSingular": "personne",
"winsPeoplePlural": "personnes",
"winsVerbSingular": "a",
"winsVerbPlural": "ont",
"winsSuffix": "trouve aujourd'hui 🎉",
"reset": "Recommencer",
"description": "Devine le personnage. Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
"friendsToday": "Tes amis aujourd'hui",
"friendTrySingular": "coup",
"friendTryPlural": "coups"
},
"infinite": {
"metaTitle": "OnePieceDle - Mode Infini",
"title": "Mode Infini",
"score": "Score",
"resetScore": "Reinitialiser",
"description": "Devine des personnages a l'infini ! Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
"nextCharacter": "Recommencer",
"revealAnswer": "Reveler la reponse",
"loadingCharacter": "Chargement du personnage...",
"filtersTitle": "Filtres de personnages",
"clearFilters": "Reinitialiser",
"filterGender": "Genre",
"filterStatus": "Statut",
"filterAbilities": "Capacites",
"filterInformation": "Informations",
"filterArcs": "Arcs",
"male": "Homme",
"female": "Femme",
"alive": "Vivant",
"dead": "Mort",
"unknown": "Inconnu",
"hasHaki": "A du Haki",
"fruitAll": "Fruit (Tous)",
"withFruit": "Avec Fruit",
"withoutFruit": "Sans Fruit",
"heightDefined": "Taille definie",
"originDefined": "Origine definie",
"availableCharactersSingular": "personnage disponible",
"availableCharactersPlural": "personnages disponibles",
"columnsTitle": "Colonnes"
},
"components": {
"searchInput": {
"title": "Entrer une supposition",
"placeholder": "Nom du personnage",
"submit": "Valider"
},
"hints": {
"origin": "Origine",
"devilFruit": "Fruit du demon",
"affiliation": "Affiliation",
"unknown": "Inconnue",
"none": "Aucun",
"beforeUnlock": "essais avant deblocage",
"available": "Indice disponible !"
},
"guessHistory": {
"title": "Historique",
"empty": "Aucune tentative pour le moment.",
"character": "Personnage",
"status": "Statut",
"gender": "Genre",
"affiliations": "Affiliations",
"fruit": "Fruit",
"haki": "Haki",
"bounty": "Prime",
"height": "Taille",
"origin": "Origine",
"arc": "Arc",
"alive": "Vivant",
"dead": "Mort",
"unknown": "Inconnu",
"male": "Homme",
"female": "Femme",
"obsHakiTitle": "Haki de l'Observation",
"armHakiTitle": "Haki de l'Armement",
"kingHakiTitle": "Haki des Rois"
},
"winPanel": {
"attemptSingular": "tentative",
"attemptPlural": "tentatives",
"moriaTitle": "Moria vous controle...",
"moriaPrefix": "Vous avez succombe a l'ombre en",
"winTitle": "Felicitations !",
"winPrefix": "Vous avez trouve le personnage en",
"oneTryMessages": [
"Tricheur 👀",
"1 essai ? Avoue, tu avais la reponse 😏",
"Premier coup direct... suspect 🤨"
],
"twoTryMessages": [
"Bien joue ! ⚡",
"Deux essais, propre ! 👏",
"Tu chauffes vite, bien joue 🔥"
],
"tenPlusMessages": [
"${attempts} essais... meme un escargophone aurait trouve plus vite 📞",
"${attempts} tentatives ? Le Grand Line est moins long que ca 😵",
"${attempts} essais : performance legendaire... dans le mauvais sens 🫠"
],
"fivePlusMessages": [
"${attempts} essais ? On va dire que c'etait pour le suspense 😅",
"Ca en fait des essais... mais au moins tu y es arrive 😬",
"Tu ne laches rien, meme apres plusieurs essais 😂"
],
"defaultMessages": [
"Pas mal du tout !",
"Bien tente, bon rythme 👍",
"Ca se passe bien, continue comme ca ✨"
]
},
"yesterdayCharacter": {
"photo": "Photo",
"title": "Personnage d'hier",
"openPage": "Voir la page",
"none": "Aucun personnage",
"noneAvailable": "Aucun personnage d'hier disponible"
}
}
}
}

51
src/lib/i18n/index.ts Normal file
View File

@@ -0,0 +1,51 @@
import { writable, derived } from 'svelte/store';
import type { Writable, Readable } from 'svelte/store';
import en from './en.json';
import fr from './fr.json';
type Messages = typeof en;
const translations: Record<string, Messages> = { en, fr };
// Get initial language
function getInitialLanguage(): string {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('language');
if (stored && stored in translations) {
return stored;
}
const browserLang = navigator.language.split('-')[0];
if (browserLang in translations) {
return browserLang;
}
}
return 'en';
}
// Create writable store for the current language
export const language: Writable<string> = writable(getInitialLanguage());
// Create derived store for the current messages
export const t: Readable<Messages> = derived(language, ($language) => {
return translations[$language] || translations['en'];
});
export function setLanguage(lang: string) {
if (lang in translations) {
if (typeof window !== 'undefined') {
localStorage.setItem('language', lang);
}
language.set(lang);
}
}
export function getLanguage(): string {
let currentLang = 'en';
language.subscribe((lang) => {
currentLang = lang;
})();
return currentLang;
}
export const availableLanguages = Object.keys(translations);

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import ProfileButton from '$lib/components/ProfileButton.svelte'; import ProfileButton from '$lib/components/ProfileButton.svelte';
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
let { children, data } = $props(); let { children, data } = $props();
@@ -11,8 +12,11 @@
<a href={resolve("/")} class="text-lg font-black uppercase tracking-[0.15em] text-amber-50 transition hover:text-amber-100"> <a href={resolve("/")} class="text-lg font-black uppercase tracking-[0.15em] text-amber-50 transition hover:text-amber-100">
OnePieceDle OnePieceDle
</a> </a>
<div class="flex items-center gap-3">
<LanguageSwitcher />
<ProfileButton user={data.user} /> <ProfileButton user={data.user} />
</div> </div>
</div>
</header> </header>
<main class="pt-20"> <main class="pt-20">
{@render children()} {@render children()}

View File

@@ -2,6 +2,7 @@
export let data; export let data;
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { t } from '$lib/i18n';
$: yesterdayCharacter = data.yesterdayCharacter; $: yesterdayCharacter = data.yesterdayCharacter;
</script> </script>
@@ -23,30 +24,30 @@
OnePieceDle OnePieceDle
</h1> </h1>
<p class="mt-4 max-w-2xl text-base text-slate-200 sm:text-lg"> <p class="mt-4 max-w-2xl text-base text-slate-200 sm:text-lg">
Devine le personnage de l'equipage, des marines ou du vaste monde. Chaque indice te rapproche du tresor. {$t.game.home.heroDescription}
</p> </p>
</div> </div>
<div class="grid w-full gap-4 sm:grid-cols-2"> <div class="grid w-full gap-4 sm:grid-cols-2">
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"> <div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Personnage du jour</h2> <h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">{$t.game.home.dailyTitle}</h2>
<p class="mt-3 text-lg font-semibold text-white">Nouveau mystere toutes les 24 heures</p> <p class="mt-3 text-lg font-semibold text-white">{$t.game.home.dailySubtitle}</p>
<p class="mt-2 text-sm text-slate-200">Compare tes essais, debloque des indices et garde ta serie.</p> <p class="mt-2 text-sm text-slate-200">{$t.game.home.dailyDescription}</p>
<a <a
href={resolve("/daily")} href={resolve("/daily")}
class="mt-5 inline-flex w-full items-center justify-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200" class="mt-5 inline-flex w-full items-center justify-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200"
> >
Commencer {$t.game.home.dailyCta}
</a> </a>
</div> </div>
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"> <div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Mode Infini</h2> <h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">{$t.game.home.infiniteTitle}</h2>
<p class="mt-3 text-lg font-semibold text-white">Des defis sans fin</p> <p class="mt-3 text-lg font-semibold text-white">{$t.game.home.infiniteSubtitle}</p>
<p class="mt-2 text-sm text-slate-200">Enchaine les personnages et croise ton score. Pas de limite, que du plaisir.</p> <p class="mt-2 text-sm text-slate-200">{$t.game.home.infiniteDescription}</p>
<a <a
href={resolve("/infinite")} href={resolve("/infinite")}
class="mt-5 inline-flex w-full items-center justify-center rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50" class="mt-5 inline-flex w-full items-center justify-center rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50"
> >
Jouer {$t.game.home.infiniteCta}
</a> </a>
</div> </div>
</div> </div>
@@ -61,11 +62,11 @@
/> />
{:else} {:else}
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100"> <div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo {$t.game.home.photoFallback}
</div> </div>
{/if} {/if}
<div class="flex-1"> <div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p> <p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.home.yesterdayCharacter}</p>
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p> <p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if yesterdayCharacter.epithets} {#if yesterdayCharacter.epithets}
<p class="mt-1 text-sm text-slate-400"> <p class="mt-1 text-sm text-slate-400">
@@ -81,18 +82,18 @@
rel="noopener noreferrer" rel="noopener noreferrer"
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto" class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
> >
Voir la page {$t.game.home.openPage}
</a> </a>
</div> </div>
{:else} {:else}
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left"> <div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100"> <div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo {$t.game.home.photoFallback}
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p> <p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">{$t.game.home.yesterdayCharacter}</p>
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p> <p class="mt-2 text-lg font-semibold text-white">{$t.game.home.noCharacter}</p>
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p> <p class="mt-1 text-sm text-slate-200">{$t.game.home.noYesterdayCharacter}</p>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -6,6 +6,7 @@
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte'; import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
import WinPanel from '$lib/components/WinPanel.svelte'; import WinPanel from '$lib/components/WinPanel.svelte';
import type { CharacterWithRelations } from '$lib/server/daily-character.js'; import type { CharacterWithRelations } from '$lib/server/daily-character.js';
import { t } from '$lib/i18n';
export let data; export let data;
@@ -186,7 +187,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>OnePieceDle - Mode du jour</title> <title>{$t.game.daily.metaTitle}</title>
<style> <style>
@keyframes shadow-pulse { @keyframes shadow-pulse {
0% { 0% {
@@ -264,10 +265,10 @@
<div class="flex w-full items-center justify-between gap-4"> <div class="flex w-full items-center justify-between gap-4">
<div> <div>
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 sm:text-5xl"> <h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 sm:text-5xl">
Personnage du jour {$t.game.daily.title}
</h1> </h1>
<p class="mt-2 text-sm text-amber-300"> <p class="mt-2 text-sm text-amber-300">
{data.winCount} {data.winCount > 1 ? 'personnes' : 'personne'} {data.winCount > 1 ? 'ont' : 'a'} trouvé aujourd'hui 🎉 {data.winCount} {data.winCount > 1 ? $t.game.daily.winsPeoplePlural : $t.game.daily.winsPeopleSingular} {data.winCount > 1 ? $t.game.daily.winsVerbPlural : $t.game.daily.winsVerbSingular} {$t.game.daily.winsSuffix}
</p> </p>
</div> </div>
{#if hasWon} {#if hasWon}
@@ -275,12 +276,12 @@
class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50" class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50"
onclick={resetHistory} onclick={resetHistory}
> >
Recommencer {$t.game.daily.reset}
</button> </button>
{/if} {/if}
</div> </div>
<p class="max-w-2xl text-base text-slate-200 sm:text-lg"> <p class="max-w-2xl text-base text-slate-200 sm:text-lg">
Devine le personnage. Chaque indice se débloque après un certain nombre de tentatives. Bonne chance ! {$t.game.daily.description}
</p> </p>
</header> </header>
@@ -313,7 +314,7 @@
{#if hasWon && data.friendsTodayResults && data.friendsTodayResults.length > 0} {#if hasWon && data.friendsTodayResults && data.friendsTodayResults.length > 0}
<section class="mt-6 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"> <section class="mt-6 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">Tes amis aujourd'hui</p> <p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">{$t.game.daily.friendsToday}</p>
<div class="mt-4 space-y-2"> <div class="mt-4 space-y-2">
{#each data.friendsTodayResults as friendResult (friendResult.userId)} {#each data.friendsTodayResults as friendResult (friendResult.userId)}
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-slate-950/50 px-4 py-2"> <div class="flex items-center justify-between rounded-lg border border-white/10 bg-slate-950/50 px-4 py-2">
@@ -332,7 +333,7 @@
<p class="text-sm font-semibold text-slate-100">{friendResult.name}</p> <p class="text-sm font-semibold text-slate-100">{friendResult.name}</p>
</div> </div>
<p class="text-sm text-amber-300"> <p class="text-sm text-amber-300">
{friendResult.tryCount} {friendResult.tryCount > 1 ? 'coups' : 'coup'} {friendResult.tryCount} {friendResult.tryCount > 1 ? $t.game.daily.friendTryPlural : $t.game.daily.friendTrySingular}
</p> </p>
</div> </div>
{/each} {/each}

View File

@@ -5,6 +5,7 @@
import WinPanel from '$lib/components/WinPanel.svelte'; import WinPanel from '$lib/components/WinPanel.svelte';
import HintsPanel from '$lib/components/HintsPanel.svelte'; import HintsPanel from '$lib/components/HintsPanel.svelte';
import type { CharacterWithRelations } from '$lib/server/daily-character.js'; import type { CharacterWithRelations } from '$lib/server/daily-character.js';
import { t } from '$lib/i18n';
export let data; export let data;
@@ -18,17 +19,7 @@
let availableArcs: ArcFilterOption[] = []; let availableArcs: ArcFilterOption[] = [];
let hasWon = false; let hasWon = false;
let columnVisibility: Record<string, boolean> = {}; let columnVisibility: Record<string, boolean> = {};
const columnDisplayNames: Record<string, string> = { let columnDisplayNames: Record<string, string> = {};
status: 'Statut',
gender: 'Genre',
affiliations: 'Affiliations',
devilFruitType: 'Fruit',
haki: 'Haki',
bounty: 'Prime',
height: 'Taille',
origin: 'Origine',
arc: 'Arc'
};
// Character filters // Character filters
let characterFilters = { let characterFilters = {
@@ -291,6 +282,17 @@
} else if (!hasWon) { } else if (!hasWon) {
isGeckoMoriaWin = false; isGeckoMoriaWin = false;
} }
$: columnDisplayNames = {
status: $t.game.components.guessHistory.status,
gender: $t.game.components.guessHistory.gender,
affiliations: $t.game.components.guessHistory.affiliations,
devilFruitType: $t.game.components.guessHistory.fruit,
haki: $t.game.components.guessHistory.haki,
bounty: $t.game.components.guessHistory.bounty,
height: $t.game.components.guessHistory.height,
origin: $t.game.components.guessHistory.origin,
arc: $t.game.components.guessHistory.arc
};
function generateNewCharacter() { function generateNewCharacter() {
if (characters.length === 0) return; if (characters.length === 0) return;
@@ -473,7 +475,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>OnePieceDle - Mode Infini</title> <title>{$t.game.infinite.metaTitle}</title>
<style> <style>
@keyframes shadow-pulse { @keyframes shadow-pulse {
0% { 0% {
@@ -564,22 +566,21 @@
<div class="flex w-full items-center justify-between gap-4"> <div class="flex w-full items-center justify-between gap-4">
<div> <div>
<h1 class="text-3xl font-black tracking-[0.25em] text-amber-50 uppercase sm:text-5xl"> <h1 class="text-3xl font-black tracking-[0.25em] text-amber-50 uppercase sm:text-5xl">
Mode Infini {$t.game.infinite.title}
</h1> </h1>
<p class="mt-2 text-2xl font-bold text-amber-300">Score: {score}</p> <p class="mt-2 text-2xl font-bold text-amber-300">{$t.game.infinite.score}: {score}</p>
</div> </div>
{#if score > 0} {#if score > 0}
<button <button
class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50" class="rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50"
onclick={resetScore} onclick={resetScore}
> >
Réinitialiser {$t.game.infinite.resetScore}
</button> </button>
{/if} {/if}
</div> </div>
<p class="max-w-2xl text-base text-slate-200 sm:text-lg"> <p class="max-w-2xl text-base text-slate-200 sm:text-lg">
Devine des personnages à l'infini ! Chaque indice se débloque après un certain nombre de {$t.game.infinite.description}
tentatives. Bonne chance !
</p> </p>
</header> </header>
@@ -593,7 +594,7 @@
onclick={nextCharacter} onclick={nextCharacter}
class="mt-4 w-full rounded-full bg-emerald-500 px-6 py-2 text-sm font-semibold text-white transition hover:bg-emerald-600" class="mt-4 w-full rounded-full bg-emerald-500 px-6 py-2 text-sm font-semibold text-white transition hover:bg-emerald-600"
> >
Recommencer {$t.game.infinite.nextCharacter}
</button> </button>
</div> </div>
{:else} {:else}
@@ -611,7 +612,7 @@
onclick={revealAnswer} onclick={revealAnswer}
class="rounded-lg border border-red-600/40 bg-red-900/20 px-4 py-2 text-sm text-red-300 transition hover:border-red-500 hover:bg-red-900/40 hover:text-red-200" class="rounded-lg border border-red-600/40 bg-red-900/20 px-4 py-2 text-sm text-red-300 transition hover:border-red-500 hover:bg-red-900/40 hover:text-red-200"
> >
Révéler la réponse {$t.game.infinite.revealAnswer}
</button> </button>
</div> </div>
{/if} {/if}
@@ -625,7 +626,7 @@
<div <div
class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur" class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"
> >
<p class="text-center text-slate-300">Chargement du personnage...</p> <p class="text-center text-slate-300">{$t.game.infinite.loadingCharacter}</p>
</div> </div>
{/if} {/if}
</section> </section>
@@ -642,7 +643,7 @@
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4"> <div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4">
<div class="mb-3 flex items-center justify-between gap-3"> <div class="mb-3 flex items-center justify-between gap-3">
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase"> <h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
Filtres de personnages {$t.game.infinite.filtersTitle}
</h3> </h3>
{#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasOrigin || characterFilters.arcs.length > 0} {#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasOrigin || characterFilters.arcs.length > 0}
<button <button
@@ -650,7 +651,7 @@
onclick={clearAllFilters} onclick={clearAllFilters}
class="text-xs text-red-300 transition hover:text-red-200" class="text-xs text-red-300 transition hover:text-red-200"
> >
Réinitialiser {$t.game.infinite.clearFilters}
</button> </button>
{/if} {/if}
</div> </div>
@@ -658,7 +659,7 @@
<div class="space-y-3"> <div class="space-y-3">
<!-- Gender Filter --> <!-- Gender Filter -->
<div> <div>
<p class="mb-2 text-xs text-slate-400">Genre</p> <p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterGender}</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each ['Male', 'Female'] as gender (gender)} {#each ['Male', 'Female'] as gender (gender)}
<button <button
@@ -670,7 +671,7 @@
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20' ? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}" : 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
> >
{gender === 'Male' ? 'Homme' : 'Femme'} {gender === 'Male' ? $t.game.infinite.male : $t.game.infinite.female}
</button> </button>
{/each} {/each}
</div> </div>
@@ -678,7 +679,7 @@
<!-- Status Filter --> <!-- Status Filter -->
<div> <div>
<p class="mb-2 text-xs text-slate-400">Statut</p> <p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterStatus}</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each ['Alive', 'Dead', 'Unknown'] as status (status)} {#each ['Alive', 'Dead', 'Unknown'] as status (status)}
<button <button
@@ -690,7 +691,7 @@
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20' ? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}" : 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
> >
{status === 'Alive' ? 'Vivant' : status === 'Dead' ? 'Mort' : 'Inconnu'} {status === 'Alive' ? $t.game.infinite.alive : status === 'Dead' ? $t.game.infinite.dead : $t.game.infinite.unknown}
</button> </button>
{/each} {/each}
</div> </div>
@@ -698,7 +699,7 @@
<!-- Haki Filter --> <!-- Haki Filter -->
<div> <div>
<p class="mb-2 text-xs text-slate-400">Capacités</p> <p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterAbilities}</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<button <button
type="button" type="button"
@@ -707,7 +708,7 @@
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20' ? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}" : 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
> >
A du Haki {$t.game.infinite.hasHaki}
</button> </button>
<button <button
type="button" type="button"
@@ -720,17 +721,17 @@
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}" : 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
> >
{characterFilters.hasDevilFruit === null {characterFilters.hasDevilFruit === null
? 'Fruit (Tous)' ? $t.game.infinite.fruitAll
: characterFilters.hasDevilFruit : characterFilters.hasDevilFruit
? 'Avec Fruit' ? $t.game.infinite.withFruit
: 'Sans Fruit'} : $t.game.infinite.withoutFruit}
</button> </button>
</div> </div>
</div> </div>
<!-- Informations Filter --> <!-- Informations Filter -->
<div> <div>
<p class="mb-2 text-xs text-slate-400">Informations</p> <p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterInformation}</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<button <button
type="button" type="button"
@@ -739,7 +740,7 @@
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20' ? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}" : 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
> >
Taille définie {$t.game.infinite.heightDefined}
</button> </button>
<button <button
type="button" type="button"
@@ -748,14 +749,14 @@
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20' ? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}" : 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
> >
Origine définie {$t.game.infinite.originDefined}
</button> </button>
</div> </div>
</div> </div>
<!-- Arc Filter --> <!-- Arc Filter -->
<div> <div>
<p class="mb-2 text-xs text-slate-400">Arcs</p> <p class="mb-2 text-xs text-slate-400">{$t.game.infinite.filterArcs}</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each availableArcs as arc (arc.id)} {#each availableArcs as arc (arc.id)}
<button <button
@@ -774,10 +775,7 @@
</div> </div>
<p class="mt-2 text-xs text-slate-500"> <p class="mt-2 text-xs text-slate-500">
{characters.length} personnage{characters.length > 1 ? 's' : ''} disponible{characters.length > {characters.length} {characters.length > 1 ? $t.game.infinite.availableCharactersPlural : $t.game.infinite.availableCharactersSingular}
1
? 's'
: ''}
</p> </p>
</div> </div>
</div> </div>
@@ -788,7 +786,7 @@
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4"> <div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4">
<div class="mb-3 flex items-center justify-between gap-3"> <div class="mb-3 flex items-center justify-between gap-3">
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase"> <h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
Colonnes {$t.game.infinite.columnsTitle}
</h3> </h3>
<p class="text-xs text-slate-400"> <p class="text-xs text-slate-400">
{Object.values(columnVisibility).filter(Boolean).length}/{Object.keys( {Object.values(columnVisibility).filter(Boolean).length}/{Object.keys(

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { t } from '$lib/i18n';
import type { ActionData } from './$types'; import type { ActionData } from './$types';
export let form: ActionData; export let form: ActionData;
@@ -25,7 +26,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>OnePieceDle - {isSignUp ? 'Inscription' : 'Connexion'}</title> <title>OnePieceDle - {isSignUp ? $t.game.login.titleSignUp : $t.game.login.titleSignIn}</title>
</svelte:head> </svelte:head>
<main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100"> <main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100">
@@ -42,7 +43,7 @@
OnePieceDle OnePieceDle
</h1> </h1>
<p class="mt-4 text-slate-300"> <p class="mt-4 text-slate-300">
{isSignUp ? 'Créer votre compte' : 'Bienvenue, pirate'} {isSignUp ? $t.game.login.headerSignUp : $t.game.login.headerSignIn}
</p> </p>
</div> </div>
@@ -64,7 +65,7 @@
{#if isSignUp} {#if isSignUp}
<div> <div>
<label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Nom {$t.game.login.nameLabel}
</label> </label>
<input <input
id="name" id="name"
@@ -72,7 +73,7 @@
name="name" name="name"
bind:value={name} bind:value={name}
required required
placeholder="Votre nom" placeholder={$t.game.login.namePlaceholder}
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
</div> </div>
@@ -82,7 +83,7 @@
{#if isSignUp} {#if isSignUp}
<div> <div>
<label for="username" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="username" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Nom d'utilisateur {$t.game.login.usernameLabel}
</label> </label>
<input <input
id="username" id="username"
@@ -90,7 +91,7 @@
name="username" name="username"
bind:value={username} bind:value={username}
required required
placeholder="ex: luffy_gear5" placeholder={$t.game.login.usernamePlaceholder}
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
</div> </div>
@@ -99,7 +100,7 @@
<!-- Email / Username Field --> <!-- Email / Username Field -->
<div> <div>
<label for="identifier" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="identifier" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
{isSignUp ? 'E-mail' : 'E-mail ou nom d\'utilisateur'} {isSignUp ? $t.game.login.identifierLabelSignUp : $t.game.login.identifierLabelSignIn}
</label> </label>
<input <input
id="identifier" id="identifier"
@@ -107,7 +108,7 @@
name={isSignUp ? 'email' : 'identifier'} name={isSignUp ? 'email' : 'identifier'}
bind:value={email} bind:value={email}
required required
placeholder={isSignUp ? 'votremail@email.com' : 'votremail@email.com ou luffy_gear5'} placeholder={isSignUp ? $t.game.login.identifierPlaceholderSignUp : $t.game.login.identifierPlaceholderSignIn}
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
</div> </div>
@@ -115,7 +116,7 @@
<!-- Password Field --> <!-- Password Field -->
<div> <div>
<label for="password" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="password" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Mot de passe {$t.game.login.passwordLabel}
</label> </label>
<input <input
id="password" id="password"
@@ -135,7 +136,7 @@
for="confirmPassword" for="confirmPassword"
class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"
> >
Confirmer le mot de passe {$t.game.login.confirmPasswordLabel}
</label> </label>
<input <input
id="confirmPassword" id="confirmPassword"
@@ -162,20 +163,20 @@
disabled={isLoading} disabled={isLoading}
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200" class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
> >
{isLoading ? 'Chargement...' : isSignUp ? 'Créer un compte' : 'Se connecter'} {isLoading ? $t.game.login.loading : isSignUp ? $t.game.login.submitSignUp : $t.game.login.submitSignIn}
</button> </button>
</form> </form>
<!-- Toggle Sign Up / Login --> <!-- Toggle Sign Up / Login -->
<div class="mt-6 border-t border-white/10 pt-6"> <div class="mt-6 border-t border-white/10 pt-6">
<p class="text-center text-sm text-slate-400"> <p class="text-center text-sm text-slate-400">
{isSignUp ? 'Vous avez déjà un compte ?' : "Vous n'avez pas de compte ?"} {isSignUp ? $t.game.login.togglePromptSignUp : $t.game.login.togglePromptSignIn}
<button <button
type="button" type="button"
on:click={handleToggle} on:click={handleToggle}
class="text-amber-300 transition hover:text-amber-200" class="text-amber-300 transition hover:text-amber-200"
> >
{isSignUp ? 'Se connecter' : "S'inscrire"} {isSignUp ? $t.game.login.toggleActionSignUp : $t.game.login.toggleActionSignIn}
</button> </button>
</p> </p>
</div> </div>
@@ -184,7 +185,7 @@
<!-- Back to Home --> <!-- Back to Home -->
<div class="text-center"> <div class="text-center">
<a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300"> <a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300">
Retour à l'accueil {$t.game.login.backHome}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { t, language } from '$lib/i18n';
interface Props { interface Props {
data: PageData; data: PageData;
@@ -56,7 +57,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Mon Profil - OnePieceDle</title> <title>{$t.game.profile.pageTitle} - OnePieceDle</title>
</svelte:head> </svelte:head>
<main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100"> <main class="relative min-h-[calc(100vh-5rem)] bg-slate-950 text-slate-100">
@@ -68,10 +69,10 @@
<!-- Header --> <!-- Header -->
<div class="text-center"> <div class="text-center">
<h1 class="text-3xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-4xl"> <h1 class="text-3xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-4xl">
Mon Profil {$t.game.profile.headerTitle}
</h1> </h1>
<p class="mt-2 text-sm text-slate-300"> <p class="mt-2 text-sm text-slate-300">
Modifie les informations de ton profil {$t.game.profile.headerSubtitle}
</p> </p>
</div> </div>
@@ -83,7 +84,7 @@
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
Profil {$t.game.profile.tabProfile}
</button> </button>
<button <button
onclick={() => handleTabChange('password')} onclick={() => handleTabChange('password')}
@@ -91,7 +92,7 @@
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
Mot de passe {$t.game.profile.tabPassword}
</button> </button>
<button <button
onclick={() => handleTabChange('daily')} onclick={() => handleTabChange('daily')}
@@ -99,7 +100,7 @@
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
Historique Daily {$t.game.profile.tabDaily}
</button> </button>
<button <button
onclick={() => handleTabChange('sessions')} onclick={() => handleTabChange('sessions')}
@@ -107,7 +108,7 @@
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
Sessions {$t.game.profile.tabSessions}
</button> </button>
<button <button
onclick={() => handleTabChange('friends')} onclick={() => handleTabChange('friends')}
@@ -115,7 +116,7 @@
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
Amis {$t.game.profile.tabFriends}
</button> </button>
</div> </div>
@@ -127,7 +128,7 @@
{#if data.user.image} {#if data.user.image}
<img <img
src={data.user.image} src={data.user.image}
alt={data.user.name || 'Profil'} alt={data.user.name || $t.game.profile.avatarFallbackAlt}
class="h-24 w-24 rounded-full border-2 border-amber-300 object-cover" class="h-24 w-24 rounded-full border-2 border-amber-300 object-cover"
/> />
{:else} {:else}
@@ -136,7 +137,7 @@
</div> </div>
{/if} {/if}
<div class="text-center"> <div class="text-center">
<p class="text-sm text-slate-400">Email</p> <p class="text-sm text-slate-400">{$t.game.profile.email}</p>
<p class="font-semibold text-white">{data.user.email}</p> <p class="font-semibold text-white">{data.user.email}</p>
</div> </div>
</div> </div>
@@ -158,7 +159,7 @@
<!-- Name Field --> <!-- Name Field -->
<div> <div>
<label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Nom d'affichage {$t.game.profile.displayName}
</label> </label>
<input <input
id="name" id="name"
@@ -166,7 +167,7 @@
name="name" name="name"
bind:value={name} bind:value={name}
required required
placeholder="Ton nom" placeholder={$t.game.profile.displayNamePlaceholder}
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
</div> </div>
@@ -181,7 +182,7 @@
<!-- Success Message --> <!-- Success Message -->
{#if showSuccess} {#if showSuccess}
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200"> <div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
Profil mis à jour avec succès ! {$t.game.profile.profileUpdateSuccess}
</div> </div>
{/if} {/if}
@@ -191,7 +192,7 @@
disabled={isLoading} disabled={isLoading}
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200" class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
> >
{isLoading ? 'Mise à jour...' : 'Enregistrer les modifications'} {isLoading ? $t.game.profile.updating : $t.game.profile.saveChanges}
</button> </button>
</form> </form>
</div> </div>
@@ -201,7 +202,7 @@
{#if activeTab === 'friends'} {#if activeTab === 'friends'}
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8"> <div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50"> <h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
Système d'amis {$t.game.profile.friendsTitle}
</h2> </h2>
<form <form
@@ -218,7 +219,7 @@
class="mb-8 space-y-3" class="mb-8 space-y-3"
> >
<label for="friendUsername" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="friendUsername" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Ajouter un ami par nom d'utilisateur {$t.game.profile.addFriendByUsername}
</label> </label>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
@@ -227,7 +228,7 @@
name="friendUsername" name="friendUsername"
required required
bind:value={friendUsername} bind:value={friendUsername}
placeholder="ex: luffy_gear5" placeholder={$t.game.profile.friendUsernamePlaceholder}
class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30" class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
/> />
<button <button
@@ -235,7 +236,7 @@
disabled={isLoading} disabled={isLoading}
class="rounded-full bg-amber-300 px-4 py-2 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200" class="rounded-full bg-amber-300 px-4 py-2 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
> >
{isLoading ? 'Envoi...' : 'Envoyer'} {isLoading ? $t.game.profile.sending : $t.game.profile.send}
</button> </button>
</div> </div>
{#if form?.message} {#if form?.message}
@@ -245,9 +246,9 @@
<div class="space-y-8"> <div class="space-y-8">
<div> <div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">Demandes reçues</h3> <h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">{$t.game.profile.incomingRequests}</h3>
{#if incomingRequests.length === 0} {#if incomingRequests.length === 0}
<p class="text-sm text-slate-400">Aucune demande reçue.</p> <p class="text-sm text-slate-400">{$t.game.profile.noIncomingRequests}</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each incomingRequests as req (req.id)} {#each incomingRequests as req (req.id)}
@@ -259,11 +260,11 @@
<div class="flex gap-2"> <div class="flex gap-2">
<form method="POST" action="?/acceptFriendRequest" use:enhance> <form method="POST" action="?/acceptFriendRequest" use:enhance>
<input type="hidden" name="friendshipId" value={req.id} /> <input type="hidden" name="friendshipId" value={req.id} />
<button type="submit" class="rounded-lg border border-emerald-400/50 bg-emerald-900/20 px-3 py-1.5 text-xs font-semibold text-emerald-300 transition hover:bg-emerald-900/40">Accepter</button> <button type="submit" class="rounded-lg border border-emerald-400/50 bg-emerald-900/20 px-3 py-1.5 text-xs font-semibold text-emerald-300 transition hover:bg-emerald-900/40">{$t.game.profile.accept}</button>
</form> </form>
<form method="POST" action="?/declineFriendRequest" use:enhance> <form method="POST" action="?/declineFriendRequest" use:enhance>
<input type="hidden" name="friendshipId" value={req.id} /> <input type="hidden" name="friendshipId" value={req.id} />
<button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">Refuser</button> <button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">{$t.game.profile.decline}</button>
</form> </form>
</div> </div>
</div> </div>
@@ -273,9 +274,9 @@
</div> </div>
<div> <div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">Demandes envoyées</h3> <h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">{$t.game.profile.outgoingRequests}</h3>
{#if outgoingRequests.length === 0} {#if outgoingRequests.length === 0}
<p class="text-sm text-slate-400">Aucune demande envoyée.</p> <p class="text-sm text-slate-400">{$t.game.profile.noOutgoingRequests}</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each outgoingRequests as req (req.id)} {#each outgoingRequests as req (req.id)}
@@ -286,7 +287,7 @@
</div> </div>
<form method="POST" action="?/cancelFriendRequest" use:enhance> <form method="POST" action="?/cancelFriendRequest" use:enhance>
<input type="hidden" name="friendshipId" value={req.id} /> <input type="hidden" name="friendshipId" value={req.id} />
<button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">Annuler</button> <button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">{$t.game.profile.cancel}</button>
</form> </form>
</div> </div>
{/each} {/each}
@@ -295,9 +296,9 @@
</div> </div>
<div> <div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">Mes amis</h3> <h3 class="mb-3 text-sm font-semibold uppercase tracking-[0.15em] text-amber-100">{$t.game.profile.myFriends}</h3>
{#if friends.length === 0} {#if friends.length === 0}
<p class="text-sm text-slate-400">Tu n'as pas encore d'amis.</p> <p class="text-sm text-slate-400">{$t.game.profile.noFriends}</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each friends as friend (friend.id)} {#each friends as friend (friend.id)}
@@ -308,7 +309,7 @@
</div> </div>
<form method="POST" action="?/removeFriend" use:enhance> <form method="POST" action="?/removeFriend" use:enhance>
<input type="hidden" name="friendshipId" value={friend.id} /> <input type="hidden" name="friendshipId" value={friend.id} />
<button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">Supprimer</button> <button type="submit" class="rounded-lg border border-red-500/50 bg-red-900/20 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-900/40">{$t.game.profile.remove}</button>
</form> </form>
</div> </div>
{/each} {/each}
@@ -323,7 +324,7 @@
{#if activeTab === 'password'} {#if activeTab === 'password'}
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8"> <div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50"> <h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
Changer le mot de passe {$t.game.profile.changePasswordTitle}
</h2> </h2>
<!-- Form --> <!-- Form -->
@@ -345,7 +346,7 @@
<!-- Old Password Field --> <!-- Old Password Field -->
<div> <div>
<label for="oldPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="oldPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Mot de passe actuel {$t.game.profile.currentPassword}
</label> </label>
<input <input
id="oldPassword" id="oldPassword"
@@ -361,7 +362,7 @@
<!-- New Password Field --> <!-- New Password Field -->
<div> <div>
<label for="newPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="newPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Nouveau mot de passe {$t.game.profile.newPassword}
</label> </label>
<input <input
id="newPassword" id="newPassword"
@@ -377,7 +378,7 @@
<!-- Confirm Password Field --> <!-- Confirm Password Field -->
<div> <div>
<label for="confirmPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100"> <label for="confirmPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
Confirmer le mot de passe {$t.game.profile.confirmPassword}
</label> </label>
<input <input
id="confirmPassword" id="confirmPassword"
@@ -400,7 +401,7 @@
<!-- Success Message --> <!-- Success Message -->
{#if showSuccess} {#if showSuccess}
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200"> <div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
Mot de passe changé avec succès ! {$t.game.profile.passwordChangeSuccess}
</div> </div>
{/if} {/if}
@@ -410,7 +411,7 @@
disabled={isLoading} disabled={isLoading}
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200" class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
> >
{isLoading ? 'Changement en cours...' : 'Changer le mot de passe'} {isLoading ? $t.game.profile.changing : $t.game.profile.changePassword}
</button> </button>
</form> </form>
</div> </div>
@@ -420,11 +421,11 @@
{#if activeTab === 'daily'} {#if activeTab === 'daily'}
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8"> <div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50"> <h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
Historique des Daily {$t.game.profile.dailyHistoryTitle}
</h2> </h2>
{#if dailyHistory.length === 0} {#if dailyHistory.length === 0}
<p class="text-center text-slate-400">Aucun historique disponible</p> <p class="text-center text-slate-400">{$t.game.profile.noDailyHistory}</p>
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">
{#each dailyHistory as day (day.id)} {#each dailyHistory as day (day.id)}
@@ -439,7 +440,7 @@
/> />
{:else} {:else}
<div class="flex h-16 w-16 items-center justify-center rounded-lg border border-white/20 bg-slate-700"> <div class="flex h-16 w-16 items-center justify-center rounded-lg border border-white/20 bg-slate-700">
<span class="text-xs text-slate-400">N/A</span> <span class="text-xs text-slate-400">{$t.game.profile.noImage}</span>
</div> </div>
{/if} {/if}
</div> </div>
@@ -448,7 +449,7 @@
<div class="flex-1"> <div class="flex-1">
<p class="font-semibold text-white">{day.characterName}</p> <p class="font-semibold text-white">{day.characterName}</p>
<p class="text-xs text-slate-400"> <p class="text-xs text-slate-400">
{new Date(day.date).toLocaleDateString('fr-FR', { {new Date(day.date).toLocaleDateString($language === 'fr' ? 'fr-FR' : 'en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric' day: 'numeric'
@@ -459,7 +460,7 @@
<!-- Tries --> <!-- Tries -->
<div class="flex flex-col items-end"> <div class="flex flex-col items-end">
<p class="text-xs text-slate-400"> <p class="text-xs text-slate-400">
{day.tryCount} {day.tryCount === 1 ? 'tentative' : 'tentatives'} {day.tryCount} {day.tryCount === 1 ? $t.game.profile.trySingular : $t.game.profile.tryPlural}
</p> </p>
</div> </div>
</div> </div>
@@ -473,24 +474,24 @@
{#if activeTab === 'sessions'} {#if activeTab === 'sessions'}
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8"> <div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
<h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50"> <h2 class="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
Sessions actives {$t.game.profile.activeSessionsTitle}
</h2> </h2>
{#if sessions.length === 0} {#if sessions.length === 0}
<p class="text-center text-slate-400">Aucune session active</p> <p class="text-center text-slate-400">{$t.game.profile.noActiveSessions}</p>
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">
{#each sessions as sess (sess.id)} {#each sessions as sess (sess.id)}
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-4 py-4"> <div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-4 py-4">
<div class="flex-1"> <div class="flex-1">
<p class="font-semibold text-white"> <p class="font-semibold text-white">
{sess.userAgent || 'Appareil inconnu'} {sess.userAgent || $t.game.profile.unknownDevice}
</p> </p>
<p class="text-xs text-slate-400"> <p class="text-xs text-slate-400">
IP: {sess.ipAddress || 'Inconnue'} {$t.game.profile.ip}: {sess.ipAddress || $t.game.profile.unknown}
</p> </p>
<p class="mt-1 text-xs text-slate-500"> <p class="mt-1 text-xs text-slate-500">
Créée: {new Date(sess.createdAt).toLocaleDateString('fr-FR', { {$t.game.profile.created}: {new Date(sess.createdAt).toLocaleDateString($language === 'fr' ? 'fr-FR' : 'en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
@@ -514,7 +515,7 @@
type="submit" type="submit"
class="rounded-lg border border-red-500/50 bg-red-900/20 px-4 py-2 text-xs font-semibold text-red-300 transition hover:border-red-500 hover:bg-red-900/40" class="rounded-lg border border-red-500/50 bg-red-900/20 px-4 py-2 text-xs font-semibold text-red-300 transition hover:border-red-500 hover:bg-red-900/40"
> >
Terminer {$t.game.profile.terminate}
</button> </button>
</form> </form>
</div> </div>
@@ -527,7 +528,7 @@
<!-- Back to Home --> <!-- Back to Home -->
<div class="text-center"> <div class="text-center">
<a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300"> <a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300">
Retour à l'accueil {$t.game.profile.backHome}
</a> </a>
</div> </div>
</div> </div>