feat: localize Pokédex to French (UI, names, types, abilities)
- lib/locale-fr.ts: baked FR names for 151 Pokémon, 18 types, 114 abilities (generated once from PokeAPI — no extra runtime locale requests) - pokeapi.ts: map Pokémon + ability names to FR in the data layer - accent-insensitive search (evoli matches Évoli) - all UI strings, stat labels (PV/ATT/DÉF…) and html lang=fr
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|||||||
41
src/App.vue
41
src/App.vue
@@ -8,6 +8,7 @@ import {
|
|||||||
type PokemonListItem,
|
type PokemonListItem,
|
||||||
TYPE_COLORS,
|
TYPE_COLORS,
|
||||||
} from "@/lib/pokeapi"
|
} from "@/lib/pokeapi"
|
||||||
|
import { TYPE_FR } from "@/lib/locale-fr"
|
||||||
import { useFavorites } from "@/composables/useFavorites"
|
import { useFavorites } from "@/composables/useFavorites"
|
||||||
import PokemonCard from "@/components/PokemonCard.vue"
|
import PokemonCard from "@/components/PokemonCard.vue"
|
||||||
import PokemonModal from "@/components/PokemonModal.vue"
|
import PokemonModal from "@/components/PokemonModal.vue"
|
||||||
@@ -34,6 +35,14 @@ const detailError = ref<string | null>(null)
|
|||||||
const detailCache = new Map<number, PokemonDetail>()
|
const detailCache = new Map<number, PokemonDetail>()
|
||||||
const typeCache = new Map<string, Set<number>>()
|
const typeCache = new Map<string, Set<number>>()
|
||||||
|
|
||||||
|
// Lower-case and strip accents so "evoli" matches "Évoli".
|
||||||
|
function norm(s: string): string {
|
||||||
|
return s
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/\p{Diacritic}/gu, "")
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = computed(() => {
|
const filtered = computed(() => {
|
||||||
let items = list.value
|
let items = list.value
|
||||||
if (showFavoritesOnly.value) items = items.filter((p) => isFavorite(p.id))
|
if (showFavoritesOnly.value) items = items.filter((p) => isFavorite(p.id))
|
||||||
@@ -41,10 +50,12 @@ const filtered = computed(() => {
|
|||||||
const ids = typeIds.value
|
const ids = typeIds.value
|
||||||
items = items.filter((p) => ids.has(p.id))
|
items = items.filter((p) => ids.has(p.id))
|
||||||
}
|
}
|
||||||
const q = search.value.trim().toLowerCase().replace(/^#/, "")
|
const raw = search.value.trim().toLowerCase().replace(/^#/, "")
|
||||||
if (q) {
|
if (raw) {
|
||||||
|
const q = norm(raw)
|
||||||
items = items.filter(
|
items = items.filter(
|
||||||
(p) => p.name.includes(q) || String(p.id) === q || String(p.id).padStart(3, "0") === q,
|
(p) =>
|
||||||
|
norm(p.name).includes(q) || String(p.id) === raw || String(p.id).padStart(3, "0") === raw,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
@@ -58,7 +69,7 @@ async function load(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
list.value = await fetchPokedex()
|
list.value = await fetchPokedex()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadError.value = e instanceof Error ? e.message : "Failed to load Pokédex"
|
loadError.value = e instanceof Error ? e.message : "Échec du chargement du Pokédex"
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -105,7 +116,7 @@ async function openDetail(id: number): Promise<void> {
|
|||||||
detailCache.set(id, d)
|
detailCache.set(id, d)
|
||||||
detail.value = d
|
detail.value = d
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
detailError.value = e instanceof Error ? e.message : "Failed to load this Pokémon"
|
detailError.value = e instanceof Error ? e.message : "Échec du chargement de ce Pokémon"
|
||||||
} finally {
|
} finally {
|
||||||
detailLoading.value = false
|
detailLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -154,18 +165,18 @@ onUnmounted(() => window.removeEventListener("keydown", onKeydown))
|
|||||||
<input
|
<input
|
||||||
v-model="search"
|
v-model="search"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search name or #001…"
|
placeholder="Rechercher un nom ou #001…"
|
||||||
class="input input-bordered rounded-none border-2 border-base-300 focus:border-primary grow text-lg"
|
class="input input-bordered rounded-none border-2 border-base-300 focus:border-primary grow text-lg"
|
||||||
aria-label="Search Pokémon"
|
aria-label="Rechercher un Pokémon"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
:value="selectedType ?? ''"
|
:value="selectedType ?? ''"
|
||||||
class="select select-bordered rounded-none border-2 border-base-300 text-lg"
|
class="select select-bordered rounded-none border-2 border-base-300 text-lg"
|
||||||
aria-label="Filter by type"
|
aria-label="Filtrer par type"
|
||||||
@change="selectType(($event.target as HTMLSelectElement).value)"
|
@change="selectType(($event.target as HTMLSelectElement).value)"
|
||||||
>
|
>
|
||||||
<option value="">All types</option>
|
<option value="">Tous les types</option>
|
||||||
<option v-for="t in TYPES" :key="t" :value="t" class="capitalize">{{ t }}</option>
|
<option v-for="t in TYPES" :key="t" :value="t">{{ TYPE_FR[t] ?? t }}</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
class="btn rounded-none border-2 text-lg"
|
class="btn rounded-none border-2 text-lg"
|
||||||
@@ -173,20 +184,20 @@ onUnmounted(() => window.removeEventListener("keydown", onKeydown))
|
|||||||
:aria-pressed="showFavoritesOnly"
|
:aria-pressed="showFavoritesOnly"
|
||||||
@click="showFavoritesOnly = !showFavoritesOnly"
|
@click="showFavoritesOnly = !showFavoritesOnly"
|
||||||
>
|
>
|
||||||
★ Favorites
|
★ Favoris
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="typeLoading" class="text-center text-lg text-base-content/50 py-2">Filtering…</p>
|
<p v-if="typeLoading" class="text-center text-lg text-base-content/50 py-2">Filtrage…</p>
|
||||||
|
|
||||||
<div v-if="loading" class="text-center py-20 text-2xl text-base-content/60">
|
<div v-if="loading" class="text-center py-20 text-2xl text-base-content/60">
|
||||||
Booting Pokédex…
|
Démarrage du Pokédex…
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="loadError" class="text-center py-20 space-y-4">
|
<div v-else-if="loadError" class="text-center py-20 space-y-4">
|
||||||
<p class="text-xl text-error">{{ loadError }}</p>
|
<p class="text-xl text-error">{{ loadError }}</p>
|
||||||
<button class="btn btn-primary rounded-none font-display text-xs" @click="load">
|
<button class="btn btn-primary rounded-none font-display text-xs" @click="load">
|
||||||
Retry
|
Réessayer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -194,7 +205,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeydown))
|
|||||||
v-else-if="filtered.length === 0"
|
v-else-if="filtered.length === 0"
|
||||||
class="text-center py-20 text-2xl text-base-content/60"
|
class="text-center py-20 text-2xl text-base-content/60"
|
||||||
>
|
>
|
||||||
No Pokémon found.
|
Aucun Pokémon trouvé.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
<div v-else class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const dexNo = `#${String(props.id).padStart(3, "0")}`
|
|||||||
<button
|
<button
|
||||||
class="absolute top-1 right-1 z-10 leading-none text-xl p-1"
|
class="absolute top-1 right-1 z-10 leading-none text-xl p-1"
|
||||||
:class="favorite ? 'text-primary' : 'text-base-300 hover:text-primary'"
|
:class="favorite ? 'text-primary' : 'text-base-300 hover:text-primary'"
|
||||||
:aria-label="favorite ? `Remove ${name} from favorites` : `Add ${name} to favorites`"
|
:aria-label="favorite ? `Retirer ${name} des favoris` : `Ajouter ${name} aux favoris`"
|
||||||
:aria-pressed="favorite"
|
:aria-pressed="favorite"
|
||||||
@click.stop="emit('toggle-favorite', id)"
|
@click.stop="emit('toggle-favorite', id)"
|
||||||
>
|
>
|
||||||
@@ -40,6 +40,6 @@ const dexNo = `#${String(props.id).padStart(3, "0")}`
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="text-base-content/50 text-lg leading-none">{{ dexNo }}</span>
|
<span class="text-base-content/50 text-lg leading-none">{{ dexNo }}</span>
|
||||||
<span class="capitalize text-xl leading-tight text-center">{{ name }}</span>
|
<span class="text-xl leading-tight text-center">{{ name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type PokemonDetail, TYPE_COLORS } from "@/lib/pokeapi"
|
import { type PokemonDetail, TYPE_COLORS } from "@/lib/pokeapi"
|
||||||
|
import { TYPE_FR } from "@/lib/locale-fr"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -15,12 +16,12 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const STAT_LABELS: Record<string, string> = {
|
const STAT_LABELS: Record<string, string> = {
|
||||||
hp: "HP",
|
hp: "PV",
|
||||||
attack: "ATK",
|
attack: "ATT",
|
||||||
defense: "DEF",
|
defense: "DÉF",
|
||||||
"special-attack": "SP.ATK",
|
"special-attack": "A.SPÉ",
|
||||||
"special-defense": "SP.DEF",
|
"special-defense": "D.SPÉ",
|
||||||
speed: "SPD",
|
speed: "VIT",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highest base stat in Gen I (Chansey's HP, 250) — round up for the bar scale.
|
// Highest base stat in Gen I (Chansey's HP, 250) — round up for the bar scale.
|
||||||
@@ -40,23 +41,23 @@ function barWidth(value: number): string {
|
|||||||
<div
|
<div
|
||||||
class="modal-box rounded-none border-4 border-primary shadow-[8px_8px_0_0_#1a1a1a] max-w-lg"
|
class="modal-box rounded-none border-4 border-primary shadow-[8px_8px_0_0_#1a1a1a] max-w-lg"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="py-16 text-center text-xl">Loading…</div>
|
<div v-if="loading" class="py-16 text-center text-xl">Chargement…</div>
|
||||||
|
|
||||||
<div v-else-if="error" class="py-16 text-center space-y-4">
|
<div v-else-if="error" class="py-16 text-center space-y-4">
|
||||||
<p class="text-xl text-error">{{ error }}</p>
|
<p class="text-xl text-error">{{ error }}</p>
|
||||||
<button class="btn btn-sm rounded-none" @click="emit('close')">Close</button>
|
<button class="btn btn-sm rounded-none" @click="emit('close')">Fermer</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="detail" class="space-y-5">
|
<div v-else-if="detail" class="space-y-5">
|
||||||
<header class="flex items-center justify-between gap-3">
|
<header class="flex items-center justify-between gap-3">
|
||||||
<h2 class="font-display text-base text-primary capitalize">{{ detail.name }}</h2>
|
<h2 class="font-display text-base text-primary">{{ detail.name }}</h2>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-base-content/50 text-xl">{{ dexNo(detail.id) }}</span>
|
<span class="text-base-content/50 text-xl">{{ dexNo(detail.id) }}</span>
|
||||||
<button
|
<button
|
||||||
class="text-2xl leading-none"
|
class="text-2xl leading-none"
|
||||||
:class="favorite ? 'text-primary' : 'text-base-300 hover:text-primary'"
|
:class="favorite ? 'text-primary' : 'text-base-300 hover:text-primary'"
|
||||||
:aria-pressed="favorite"
|
:aria-pressed="favorite"
|
||||||
:aria-label="favorite ? 'Remove from favorites' : 'Add to favorites'"
|
:aria-label="favorite ? 'Retirer des favoris' : 'Ajouter aux favoris'"
|
||||||
@click="emit('toggle-favorite', detail.id)"
|
@click="emit('toggle-favorite', detail.id)"
|
||||||
>
|
>
|
||||||
{{ favorite ? "★" : "☆" }}
|
{{ favorite ? "★" : "☆" }}
|
||||||
@@ -80,25 +81,25 @@ function barWidth(value: number): string {
|
|||||||
class="px-2 py-1 text-sm uppercase text-white rounded-none"
|
class="px-2 py-1 text-sm uppercase text-white rounded-none"
|
||||||
:style="{ backgroundColor: TYPE_COLORS[t] ?? '#777' }"
|
:style="{ backgroundColor: TYPE_COLORS[t] ?? '#777' }"
|
||||||
>
|
>
|
||||||
{{ t }}
|
{{ TYPE_FR[t] ?? t }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<dl class="text-lg grid grid-cols-2 gap-x-4">
|
<dl class="text-lg grid grid-cols-2 gap-x-4">
|
||||||
<dt class="text-base-content/50">Height</dt>
|
<dt class="text-base-content/50">Taille</dt>
|
||||||
<dd>{{ (detail.height / 10).toFixed(1) }} m</dd>
|
<dd>{{ (detail.height / 10).toFixed(1) }} m</dd>
|
||||||
<dt class="text-base-content/50">Weight</dt>
|
<dt class="text-base-content/50">Poids</dt>
|
||||||
<dd>{{ (detail.weight / 10).toFixed(1) }} kg</dd>
|
<dd>{{ (detail.weight / 10).toFixed(1) }} kg</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 class="text-base-content/50 text-lg mb-1">Abilities</h3>
|
<h3 class="text-base-content/50 text-lg mb-1">Talents</h3>
|
||||||
<p class="capitalize text-xl">{{ detail.abilities.join(", ").replace(/-/g, " ") }}</p>
|
<p class="text-xl">{{ detail.abilities.join(", ") }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="space-y-1.5">
|
<section class="space-y-1.5">
|
||||||
<h3 class="text-base-content/50 text-lg">Base stats</h3>
|
<h3 class="text-base-content/50 text-lg">Statistiques</h3>
|
||||||
<div v-for="s in detail.stats" :key="s.name" class="flex items-center gap-2 text-lg">
|
<div v-for="s in detail.stats" :key="s.name" class="flex items-center gap-2 text-lg">
|
||||||
<span class="w-16 shrink-0 text-base-content/70">{{
|
<span class="w-16 shrink-0 text-base-content/70">{{
|
||||||
STAT_LABELS[s.name] ?? s.name
|
STAT_LABELS[s.name] ?? s.name
|
||||||
@@ -112,7 +113,7 @@ function barWidth(value: number): string {
|
|||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button class="btn rounded-none font-display text-xs" @click="emit('close')">
|
<button class="btn rounded-none font-display text-xs" @click="emit('close')">
|
||||||
Close
|
Fermer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
295
src/lib/locale-fr.ts
Normal file
295
src/lib/locale-fr.ts
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
// French (fr) display names for Gen-I Pokémon, types, and abilities.
|
||||||
|
// Generated once from PokeAPI (https://pokeapi.co) and baked in so the UI needs
|
||||||
|
// no extra locale requests at runtime. To refresh, re-run /tmp/gen-fr.mjs.
|
||||||
|
|
||||||
|
export const POKEMON_FR: Record<number, string> = {
|
||||||
|
1: "Bulbizarre",
|
||||||
|
2: "Herbizarre",
|
||||||
|
3: "Florizarre",
|
||||||
|
4: "Salamèche",
|
||||||
|
5: "Reptincel",
|
||||||
|
6: "Dracaufeu",
|
||||||
|
7: "Carapuce",
|
||||||
|
8: "Carabaffe",
|
||||||
|
9: "Tortank",
|
||||||
|
10: "Chenipan",
|
||||||
|
11: "Chrysacier",
|
||||||
|
12: "Papilusion",
|
||||||
|
13: "Aspicot",
|
||||||
|
14: "Coconfort",
|
||||||
|
15: "Dardargnan",
|
||||||
|
16: "Roucool",
|
||||||
|
17: "Roucoups",
|
||||||
|
18: "Roucarnage",
|
||||||
|
19: "Rattata",
|
||||||
|
20: "Rattatac",
|
||||||
|
21: "Piafabec",
|
||||||
|
22: "Rapasdepic",
|
||||||
|
23: "Abo",
|
||||||
|
24: "Arbok",
|
||||||
|
25: "Pikachu",
|
||||||
|
26: "Raichu",
|
||||||
|
27: "Sabelette",
|
||||||
|
28: "Sablaireau",
|
||||||
|
29: "Nidoran♀",
|
||||||
|
30: "Nidorina",
|
||||||
|
31: "Nidoqueen",
|
||||||
|
32: "Nidoran♂",
|
||||||
|
33: "Nidorino",
|
||||||
|
34: "Nidoking",
|
||||||
|
35: "Mélofée",
|
||||||
|
36: "Mélodelfe",
|
||||||
|
37: "Goupix",
|
||||||
|
38: "Feunard",
|
||||||
|
39: "Rondoudou",
|
||||||
|
40: "Grodoudou",
|
||||||
|
41: "Nosferapti",
|
||||||
|
42: "Nosferalto",
|
||||||
|
43: "Mystherbe",
|
||||||
|
44: "Ortide",
|
||||||
|
45: "Rafflesia",
|
||||||
|
46: "Paras",
|
||||||
|
47: "Parasect",
|
||||||
|
48: "Mimitoss",
|
||||||
|
49: "Aéromite",
|
||||||
|
50: "Taupiqueur",
|
||||||
|
51: "Triopikeur",
|
||||||
|
52: "Miaouss",
|
||||||
|
53: "Persian",
|
||||||
|
54: "Psykokwak",
|
||||||
|
55: "Akwakwak",
|
||||||
|
56: "Férosinge",
|
||||||
|
57: "Colossinge",
|
||||||
|
58: "Caninos",
|
||||||
|
59: "Arcanin",
|
||||||
|
60: "Ptitard",
|
||||||
|
61: "Têtarte",
|
||||||
|
62: "Tartard",
|
||||||
|
63: "Abra",
|
||||||
|
64: "Kadabra",
|
||||||
|
65: "Alakazam",
|
||||||
|
66: "Machoc",
|
||||||
|
67: "Machopeur",
|
||||||
|
68: "Mackogneur",
|
||||||
|
69: "Chétiflor",
|
||||||
|
70: "Boustiflor",
|
||||||
|
71: "Empiflor",
|
||||||
|
72: "Tentacool",
|
||||||
|
73: "Tentacruel",
|
||||||
|
74: "Racaillou",
|
||||||
|
75: "Gravalanch",
|
||||||
|
76: "Grolem",
|
||||||
|
77: "Ponyta",
|
||||||
|
78: "Galopa",
|
||||||
|
79: "Ramoloss",
|
||||||
|
80: "Flagadoss",
|
||||||
|
81: "Magnéti",
|
||||||
|
82: "Magnéton",
|
||||||
|
83: "Canarticho",
|
||||||
|
84: "Doduo",
|
||||||
|
85: "Dodrio",
|
||||||
|
86: "Otaria",
|
||||||
|
87: "Lamantine",
|
||||||
|
88: "Tadmorv",
|
||||||
|
89: "Grotadmorv",
|
||||||
|
90: "Kokiyas",
|
||||||
|
91: "Crustabri",
|
||||||
|
92: "Fantominus",
|
||||||
|
93: "Spectrum",
|
||||||
|
94: "Ectoplasma",
|
||||||
|
95: "Onix",
|
||||||
|
96: "Soporifik",
|
||||||
|
97: "Hypnomade",
|
||||||
|
98: "Krabby",
|
||||||
|
99: "Krabboss",
|
||||||
|
100: "Voltorbe",
|
||||||
|
101: "Électrode",
|
||||||
|
102: "Noeunoeuf",
|
||||||
|
103: "Noadkoko",
|
||||||
|
104: "Osselait",
|
||||||
|
105: "Ossatueur",
|
||||||
|
106: "Kicklee",
|
||||||
|
107: "Tygnon",
|
||||||
|
108: "Excelangue",
|
||||||
|
109: "Smogo",
|
||||||
|
110: "Smogogo",
|
||||||
|
111: "Rhinocorne",
|
||||||
|
112: "Rhinoféros",
|
||||||
|
113: "Leveinard",
|
||||||
|
114: "Saquedeneu",
|
||||||
|
115: "Kangourex",
|
||||||
|
116: "Hypotrempe",
|
||||||
|
117: "Hypocéan",
|
||||||
|
118: "Poissirène",
|
||||||
|
119: "Poissoroy",
|
||||||
|
120: "Stari",
|
||||||
|
121: "Staross",
|
||||||
|
122: "M. Mime",
|
||||||
|
123: "Insécateur",
|
||||||
|
124: "Lippoutou",
|
||||||
|
125: "Élektek",
|
||||||
|
126: "Magmar",
|
||||||
|
127: "Scarabrute",
|
||||||
|
128: "Tauros",
|
||||||
|
129: "Magicarpe",
|
||||||
|
130: "Léviator",
|
||||||
|
131: "Lokhlass",
|
||||||
|
132: "Métamorph",
|
||||||
|
133: "Évoli",
|
||||||
|
134: "Aquali",
|
||||||
|
135: "Voltali",
|
||||||
|
136: "Pyroli",
|
||||||
|
137: "Porygon",
|
||||||
|
138: "Amonita",
|
||||||
|
139: "Amonistar",
|
||||||
|
140: "Kabuto",
|
||||||
|
141: "Kabutops",
|
||||||
|
142: "Ptéra",
|
||||||
|
143: "Ronflex",
|
||||||
|
144: "Artikodin",
|
||||||
|
145: "Électhor",
|
||||||
|
146: "Sulfura",
|
||||||
|
147: "Minidraco",
|
||||||
|
148: "Draco",
|
||||||
|
149: "Dracolosse",
|
||||||
|
150: "Mewtwo",
|
||||||
|
151: "Mew",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TYPE_FR: Record<string, string> = {
|
||||||
|
electric: "Électrik",
|
||||||
|
fire: "Feu",
|
||||||
|
ice: "Glace",
|
||||||
|
poison: "Poison",
|
||||||
|
water: "Eau",
|
||||||
|
grass: "Plante",
|
||||||
|
normal: "Normal",
|
||||||
|
fighting: "Combat",
|
||||||
|
ground: "Sol",
|
||||||
|
psychic: "Psy",
|
||||||
|
flying: "Vol",
|
||||||
|
dragon: "Dragon",
|
||||||
|
rock: "Roche",
|
||||||
|
dark: "Ténèbres",
|
||||||
|
bug: "Insecte",
|
||||||
|
ghost: "Spectre",
|
||||||
|
steel: "Acier",
|
||||||
|
fairy: "Fée",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ABILITY_FR: Record<string, string> = {
|
||||||
|
adaptability: "Adaptabilité",
|
||||||
|
aftermath: "Boom Final",
|
||||||
|
analytic: "Analyste",
|
||||||
|
"anger-point": "Colérique",
|
||||||
|
anticipation: "Anticipation",
|
||||||
|
"arena-trap": "Piège Sable",
|
||||||
|
"battle-armor": "Armurbaston",
|
||||||
|
"big-pecks": "Cœur de Coq",
|
||||||
|
blaze: "Brasier",
|
||||||
|
chlorophyll: "Chlorophylle",
|
||||||
|
"clear-body": "Corps Sain",
|
||||||
|
"cloud-nine": "Ciel Gris",
|
||||||
|
competitive: "Battant",
|
||||||
|
"compound-eyes": "Œil Composé",
|
||||||
|
"cursed-body": "Corps Maudit",
|
||||||
|
"cute-charm": "Joli Sourire",
|
||||||
|
damp: "Moiteur",
|
||||||
|
defiant: "Acharné",
|
||||||
|
download: "Télécharge",
|
||||||
|
drought: "Sécheresse",
|
||||||
|
"dry-skin": "Peau Sèche",
|
||||||
|
"early-bird": "Matinal",
|
||||||
|
"effect-spore": "Pose Spore",
|
||||||
|
filter: "Filtre",
|
||||||
|
"flame-body": "Corps Ardent",
|
||||||
|
"flash-fire": "Torche",
|
||||||
|
forewarn: "Prédiction",
|
||||||
|
"friend-guard": "Garde-Ami",
|
||||||
|
frisk: "Fouille",
|
||||||
|
gluttony: "Gloutonnerie",
|
||||||
|
guts: "Cran",
|
||||||
|
harvest: "Récolte",
|
||||||
|
healer: "Cœur Soin",
|
||||||
|
hustle: "Agitation",
|
||||||
|
hydration: "Hydratation",
|
||||||
|
"hyper-cutter": "Hyper Cutter",
|
||||||
|
"ice-body": "Corps Gel",
|
||||||
|
illuminate: "Lumiattirance",
|
||||||
|
immunity: "Vaccin",
|
||||||
|
imposter: "Imposteur",
|
||||||
|
infiltrator: "Infiltration",
|
||||||
|
"inner-focus": "Attention",
|
||||||
|
insomnia: "Insomnia",
|
||||||
|
intimidate: "Intimidation",
|
||||||
|
"iron-fist": "Poing de Fer",
|
||||||
|
justified: "Cœur Noble",
|
||||||
|
"keen-eye": "Regard Vif",
|
||||||
|
"leaf-guard": "Feuille Garde",
|
||||||
|
levitate: "Lévitation",
|
||||||
|
"lightning-rod": "Paratonnerre",
|
||||||
|
limber: "Échauffement",
|
||||||
|
"liquid-ooze": "Suintement",
|
||||||
|
"magic-guard": "Garde Magik",
|
||||||
|
"magnet-pull": "Magnépiège",
|
||||||
|
"marvel-scale": "Écaille Spéciale",
|
||||||
|
"mold-breaker": "Brise Moule",
|
||||||
|
moxie: "Impudence",
|
||||||
|
multiscale: "Multiécaille",
|
||||||
|
"natural-cure": "Médic Nature",
|
||||||
|
"neutralizing-gas": "Gaz Inhibiteur",
|
||||||
|
"no-guard": "Annule Garde",
|
||||||
|
oblivious: "Benêt",
|
||||||
|
overcoat: "Envelocape",
|
||||||
|
overgrow: "Engrais",
|
||||||
|
"own-tempo": "Tempo Perso",
|
||||||
|
pickup: "Ramassage",
|
||||||
|
"poison-point": "Point Poison",
|
||||||
|
"poison-touch": "Toxitouche",
|
||||||
|
pressure: "Pression",
|
||||||
|
"quick-feet": "Pied Véloce",
|
||||||
|
"rain-dish": "Cuvette",
|
||||||
|
rattled: "Phobique",
|
||||||
|
reckless: "Téméraire",
|
||||||
|
regenerator: "Régé-Force",
|
||||||
|
rivalry: "Rivalité",
|
||||||
|
"rock-head": "Tête de Roc",
|
||||||
|
"run-away": "Fuite",
|
||||||
|
"sand-force": "Force Sable",
|
||||||
|
"sand-rush": "Baigne Sable",
|
||||||
|
"sand-veil": "Voile Sable",
|
||||||
|
scrappy: "Querelleur",
|
||||||
|
"serene-grace": "Sérénité",
|
||||||
|
"shed-skin": "Mue",
|
||||||
|
"sheer-force": "Sans Limite",
|
||||||
|
"shell-armor": "Coque Armure",
|
||||||
|
"shield-dust": "Écran Poudre",
|
||||||
|
"skill-link": "Multi-Coups",
|
||||||
|
sniper: "Sniper",
|
||||||
|
"snow-cloak": "Rideau Neige",
|
||||||
|
"solar-power": "Force Soleil",
|
||||||
|
soundproof: "Anti-Bruit",
|
||||||
|
static: "Statik",
|
||||||
|
steadfast: "Impassible",
|
||||||
|
stench: "Puanteur",
|
||||||
|
"sticky-hold": "Glu",
|
||||||
|
sturdy: "Fermeté",
|
||||||
|
swarm: "Essaim",
|
||||||
|
"swift-swim": "Glissade",
|
||||||
|
synchronize: "Synchro",
|
||||||
|
"tangled-feet": "Pieds Confus",
|
||||||
|
technician: "Technicien",
|
||||||
|
"thick-fat": "Isograisse",
|
||||||
|
"tinted-lens": "Lentiteintée",
|
||||||
|
torrent: "Torrent",
|
||||||
|
trace: "Calque",
|
||||||
|
unaware: "Inconscient",
|
||||||
|
unburden: "Délestage",
|
||||||
|
unnerve: "Tension",
|
||||||
|
"vital-spirit": "Esprit Vital",
|
||||||
|
"volt-absorb": "Absorbe-Volt",
|
||||||
|
"water-absorb": "Absorbe-Eau",
|
||||||
|
"water-veil": "Ignifu-Voile",
|
||||||
|
"weak-armor": "Armurouillée",
|
||||||
|
"wonder-skin": "Peau Miracle",
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Thin typed client over the public PokeAPI (https://pokeapi.co).
|
// Thin typed client over the public PokeAPI (https://pokeapi.co).
|
||||||
// Scoped to Generation I (#001–#151) — the classic 8-bit Red/Blue dex.
|
// Scoped to Generation I (#001–#151) — the classic 8-bit Red/Blue dex.
|
||||||
|
import { ABILITY_FR, POKEMON_FR } from "./locale-fr"
|
||||||
|
|
||||||
const API = "https://pokeapi.co/api/v2"
|
const API = "https://pokeapi.co/api/v2"
|
||||||
const GEN1_LIMIT = 151
|
const GEN1_LIMIT = 151
|
||||||
@@ -81,7 +82,10 @@ export async function fetchPokedex(): Promise<PokemonListItem[]> {
|
|||||||
const res = await fetch(`${API}/pokemon?limit=${GEN1_LIMIT}`)
|
const res = await fetch(`${API}/pokemon?limit=${GEN1_LIMIT}`)
|
||||||
if (!res.ok) throw new Error(`PokeAPI list failed (${res.status})`)
|
if (!res.ok) throw new Error(`PokeAPI list failed (${res.status})`)
|
||||||
const data = (await res.json()) as RawList
|
const data = (await res.json()) as RawList
|
||||||
return data.results.map((r) => ({ id: idFromUrl(r.url), name: r.name }))
|
return data.results.map((r) => {
|
||||||
|
const id = idFromUrl(r.url)
|
||||||
|
return { id, name: POKEMON_FR[id] ?? r.name }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPokemon(id: number): Promise<PokemonDetail> {
|
export async function fetchPokemon(id: number): Promise<PokemonDetail> {
|
||||||
@@ -90,12 +94,12 @@ export async function fetchPokemon(id: number): Promise<PokemonDetail> {
|
|||||||
const d = (await res.json()) as RawDetail
|
const d = (await res.json()) as RawDetail
|
||||||
return {
|
return {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
name: d.name,
|
name: POKEMON_FR[d.id] ?? d.name,
|
||||||
height: d.height,
|
height: d.height,
|
||||||
weight: d.weight,
|
weight: d.weight,
|
||||||
types: d.types.map((t) => t.type.name),
|
types: d.types.map((t) => t.type.name),
|
||||||
stats: d.stats.map((s) => ({ name: s.stat.name, value: s.base_stat })),
|
stats: d.stats.map((s) => ({ name: s.stat.name, value: s.base_stat })),
|
||||||
abilities: d.abilities.map((a) => a.ability.name),
|
abilities: d.abilities.map((a) => ABILITY_FR[a.ability.name] ?? a.ability.name),
|
||||||
sprite: d.sprites.front_default ?? spriteUrl(d.id),
|
sprite: d.sprites.front_default ?? spriteUrl(d.id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user