feat: add French localization support for character attributes and improve character display logic
Build Docker Image / build (push) Successful in 1m18s

- Added optional French names, affiliations, origins, and epithets to character records.
- Updated character import logic to handle new French fields.
- Enhanced character search and display components to show French names and epithets based on selected language.
- Modified database schema to include French fields for characters.
- Improved error handling in daily character setup to check for existing characters.
- Refactored components to utilize helper functions for displaying names and attributes based on language.
This commit is contained in:
2026-03-15 22:00:19 +01:00
parent bd121b7d85
commit 997b2f1781
15 changed files with 655 additions and 236 deletions
+73 -15
View File
@@ -2,9 +2,58 @@
export let data;
import { resolve } from '$app/paths';
import { t } from '$lib/i18n';
import { language, t } from '$lib/i18n';
import type { CharacterWithRelations } from '$lib/server/daily-character';
$: yesterdayCharacter = data.yesterdayCharacter;
$: isFrench = $language === 'fr';
function parseEpithets(value: unknown): string[] {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
}
} catch {
if (value.length > 0) {
return [value];
}
}
}
return [];
}
function getDisplayName(character: CharacterWithRelations | null): string {
if (isFrench && typeof character?.frName === 'string' && character.frName.length > 0) {
return character.frName;
}
return character?.name || '';
}
function getDisplayEpithets(character: CharacterWithRelations | null): string[] {
const frenchEpithets = parseEpithets(character?.frEpithets);
if (isFrench && frenchEpithets.length > 0) {
return frenchEpithets;
}
return parseEpithets(character?.epithets);
}
function getWikiUrl(character: CharacterWithRelations | null): string {
if (isFrench && typeof character?.frUrl === 'string' && character.frUrl.length > 0) {
return character.frUrl;
}
return character?.url || '';
}
</script>
<svelte:head>
@@ -57,7 +106,7 @@
{#if yesterdayCharacter.pictureUrl}
<img
src={yesterdayCharacter.pictureUrl}
alt={yesterdayCharacter.name}
alt={getDisplayName(yesterdayCharacter)}
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
/>
{:else}
@@ -67,23 +116,32 @@
{/if}
<div class="flex-1">
<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>
{#if yesterdayCharacter.epithets}
<p class="mt-2 text-lg font-semibold text-white">{getDisplayName(yesterdayCharacter)}</p>
{#if getDisplayEpithets(yesterdayCharacter).length > 0}
<p class="mt-1 text-sm text-slate-400">
{typeof yesterdayCharacter.epithets === 'string'
? JSON.parse(yesterdayCharacter.epithets).join(', ')
: (yesterdayCharacter.epithets as string[]).join(', ')}
{getDisplayEpithets(yesterdayCharacter).join(', ')}
</p>
{/if}
</div>
<a
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
target="_blank"
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"
>
{$t.game.home.openPage}
</a>
{#if isFrench}
<a
href="https://onepiece.fandom.com/fr/wiki/{getWikiUrl(yesterdayCharacter)}"
target="_blank"
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"
>
{$t.game.home.openPage}
</a>
{:else}
<a
href="https://onepiece.fandom.com/wiki/{getWikiUrl(yesterdayCharacter)}"
target="_blank"
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"
>
{$t.game.home.openPage}
</a>
{/if}
</div>
{:else}
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
+20 -4
View File
@@ -5,7 +5,7 @@
import WinPanel from '$lib/components/WinPanel.svelte';
import HintsPanel from '$lib/components/HintsPanel.svelte';
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
import { t } from '$lib/i18n';
import { language, t } from '$lib/i18n';
export let data;
@@ -213,16 +213,32 @@
}
$: allCharacters = data.characters || [];
$: isFrench = $language === 'fr';
function getDisplayArcName(character: CharacterWithRelations, useFrench: boolean): string | null {
if (useFrench && typeof character.frArcName === 'string' && character.frArcName.length > 0) {
return character.frArcName;
}
return character.arcName;
}
// Extract unique arcs from all characters
$: {
const useFrench = isFrench;
const arcMap = new Map<string, ArcFilterOption>(
allCharacters
.filter(
(char: CharacterWithRelations): char is CharacterWithRelations & { arcId: string; arcName: string } =>
typeof char.arcId === 'string' && char.arcId.length > 0 && typeof char.arcName === 'string' && char.arcName.length > 0
(char: CharacterWithRelations): char is CharacterWithRelations & { arcId: string } =>
typeof char.arcId === 'string' &&
char.arcId.length > 0 &&
typeof getDisplayArcName(char, useFrench) === 'string' &&
getDisplayArcName(char, useFrench)!.length > 0
)
.map((char: CharacterWithRelations & { arcId: string; arcName: string }) => [char.arcId, { id: char.arcId, name: char.arcName }])
.map((char: CharacterWithRelations & { arcId: string }) => [
char.arcId,
{ id: char.arcId, name: getDisplayArcName(char, useFrench) as string }
])
);
availableArcs = [...arcMap.values()].sort((a, b) => a.name.localeCompare(b.name));