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">
import type { CharacterWithRelations } from '$lib/server/daily-character';
import { onMount } from 'svelte';
import { t } from '$lib/i18n';
let {
characters,
@@ -138,13 +139,13 @@
</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">
<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 bind:this={state.searchContainer} class="relative w-full">
<input
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"
placeholder="Nom du personnage"
placeholder={$t.game.components.searchInput.placeholder}
type="text"
onkeydown={handleKeydown}
/>
@@ -193,7 +194,7 @@
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"
>
Valider
{$t.game.components.searchInput.submit}
</button>
</div>
</div>

View File

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

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character";
import { t } from '$lib/i18n';
export let dailyCharacter: CharacterWithRelations;
export let selectedCharacters: CharacterWithRelations[];
@@ -44,13 +45,13 @@
disabled={!isOriginAvailable}
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}
<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}
<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}
<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}
</button>
<button
@@ -59,13 +60,13 @@
disabled={!isFruitAvailable}
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}
<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}
<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}
<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}
</button>
<button
@@ -74,16 +75,16 @@
disabled={!isAffiliationAvailable}
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}
{@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}
<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}
<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}
<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}
</button>
</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">
import type { CharacterWithRelations } from "$lib/server/daily-character";
import { t } from '$lib/i18n';
export let selectedCharacter: CharacterWithRelations;
export let selectedCharacters: CharacterWithRelations[];
export let isGeckoMoriaWin: boolean = false;
const oneTryMessages = ['Tricheur 👀', '1 essai ? Avoue, tu avais la réponse 😏', 'Premier coup direct... suspect 🤨'];
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 pickMessage = (messages: readonly string[]) => messages[Math.floor(Math.random() * messages.length)];
const getAttemptMessage = (attempts: number): string => {
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) {
return pickMessage(oneTryMessages);
}
@@ -41,14 +33,17 @@
$: attempts = selectedCharacters.length;
$: attemptMessage = getAttemptMessage(attempts);
$: attemptWord = selectedCharacters.length > 1
? $t.game.components.winPanel.attemptPlural
: $t.game.components.winPanel.attemptSingular;
</script>
{#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="text-center">
<div class="text-3xl mb-2">🌑</div>
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<h2 class="text-xl font-bold text-slate-300 mb-1">{$t.game.components.winPanel.moriaTitle}</h2>
<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>
<div class="mt-3">
{#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="text-center">
<div class="text-3xl mb-2">🎉</div>
<h2 class="text-xl font-bold text-emerald-400 mb-1">Félicitations !</h2>
<p class="text-sm text-emerald-300">Vous avez trouvé le personnage en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<h2 class="text-xl font-bold text-emerald-400 mb-1">{$t.game.components.winPanel.winTitle}</h2>
<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>
<div class="mt-3">
{#if selectedCharacter.pictureUrl}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { CharacterWithRelations } from "$lib/server/daily-character";
import { t } from '$lib/i18n';
export let yesterdayCharacter: CharacterWithRelations | null;
</script>
@@ -15,11 +16,11 @@
/>
{: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">
Photo
{$t.game.components.yesterdayCharacter.photo}
</div>
{/if}
<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>
{#if yesterdayCharacter.epithets}
<p class="mt-1 text-sm text-slate-400">
@@ -35,18 +36,18 @@
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"
>
Voir la page
{$t.game.components.yesterdayCharacter.openPage}
</a>
</div>
{:else}
<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">
Photo
{$t.game.components.yesterdayCharacter.photo}
</div>
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</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">{$t.game.components.yesterdayCharacter.none}</p>
<p class="mt-1 text-sm text-slate-200">{$t.game.components.yesterdayCharacter.noneAvailable}</p>
</div>
</div>
{/if}