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>
|
||||
<html lang="en">
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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_COLORS,
|
||||
} from "@/lib/pokeapi"
|
||||
import { TYPE_FR } from "@/lib/locale-fr"
|
||||
import { useFavorites } from "@/composables/useFavorites"
|
||||
import PokemonCard from "@/components/PokemonCard.vue"
|
||||
import PokemonModal from "@/components/PokemonModal.vue"
|
||||
@@ -34,6 +35,14 @@ const detailError = ref<string | null>(null)
|
||||
const detailCache = new Map<number, PokemonDetail>()
|
||||
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(() => {
|
||||
let items = list.value
|
||||
if (showFavoritesOnly.value) items = items.filter((p) => isFavorite(p.id))
|
||||
@@ -41,10 +50,12 @@ const filtered = computed(() => {
|
||||
const ids = typeIds.value
|
||||
items = items.filter((p) => ids.has(p.id))
|
||||
}
|
||||
const q = search.value.trim().toLowerCase().replace(/^#/, "")
|
||||
if (q) {
|
||||
const raw = search.value.trim().toLowerCase().replace(/^#/, "")
|
||||
if (raw) {
|
||||
const q = norm(raw)
|
||||
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
|
||||
@@ -58,7 +69,7 @@ async function load(): Promise<void> {
|
||||
try {
|
||||
list.value = await fetchPokedex()
|
||||
} 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 {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -105,7 +116,7 @@ async function openDetail(id: number): Promise<void> {
|
||||
detailCache.set(id, d)
|
||||
detail.value = d
|
||||
} 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 {
|
||||
detailLoading.value = false
|
||||
}
|
||||
@@ -154,18 +165,18 @@ onUnmounted(() => window.removeEventListener("keydown", onKeydown))
|
||||
<input
|
||||
v-model="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"
|
||||
aria-label="Search Pokémon"
|
||||
aria-label="Rechercher un Pokémon"
|
||||
/>
|
||||
<select
|
||||
:value="selectedType ?? ''"
|
||||
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)"
|
||||
>
|
||||
<option value="">All types</option>
|
||||
<option v-for="t in TYPES" :key="t" :value="t" class="capitalize">{{ t }}</option>
|
||||
<option value="">Tous les types</option>
|
||||
<option v-for="t in TYPES" :key="t" :value="t">{{ TYPE_FR[t] ?? t }}</option>
|
||||
</select>
|
||||
<button
|
||||
class="btn rounded-none border-2 text-lg"
|
||||
@@ -173,20 +184,20 @@ onUnmounted(() => window.removeEventListener("keydown", onKeydown))
|
||||
:aria-pressed="showFavoritesOnly"
|
||||
@click="showFavoritesOnly = !showFavoritesOnly"
|
||||
>
|
||||
★ Favorites
|
||||
★ Favoris
|
||||
</button>
|
||||
</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">
|
||||
Booting Pokédex…
|
||||
Démarrage du Pokédex…
|
||||
</div>
|
||||
|
||||
<div v-else-if="loadError" class="text-center py-20 space-y-4">
|
||||
<p class="text-xl text-error">{{ loadError }}</p>
|
||||
<button class="btn btn-primary rounded-none font-display text-xs" @click="load">
|
||||
Retry
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +205,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeydown))
|
||||
v-else-if="filtered.length === 0"
|
||||
class="text-center py-20 text-2xl text-base-content/60"
|
||||
>
|
||||
No Pokémon found.
|
||||
Aucun Pokémon trouvé.
|
||||
</div>
|
||||
|
||||
<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
|
||||
class="absolute top-1 right-1 z-10 leading-none text-xl p-1"
|
||||
: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"
|
||||
@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="capitalize text-xl leading-tight text-center">{{ name }}</span>
|
||||
<span class="text-xl leading-tight text-center">{{ name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { type PokemonDetail, TYPE_COLORS } from "@/lib/pokeapi"
|
||||
import { TYPE_FR } from "@/lib/locale-fr"
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -15,12 +16,12 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const STAT_LABELS: Record<string, string> = {
|
||||
hp: "HP",
|
||||
attack: "ATK",
|
||||
defense: "DEF",
|
||||
"special-attack": "SP.ATK",
|
||||
"special-defense": "SP.DEF",
|
||||
speed: "SPD",
|
||||
hp: "PV",
|
||||
attack: "ATT",
|
||||
defense: "DÉF",
|
||||
"special-attack": "A.SPÉ",
|
||||
"special-defense": "D.SPÉ",
|
||||
speed: "VIT",
|
||||
}
|
||||
|
||||
// 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
|
||||
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">
|
||||
<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 v-else-if="detail" class="space-y-5">
|
||||
<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">
|
||||
<span class="text-base-content/50 text-xl">{{ dexNo(detail.id) }}</span>
|
||||
<button
|
||||
class="text-2xl leading-none"
|
||||
:class="favorite ? 'text-primary' : 'text-base-300 hover:text-primary'"
|
||||
: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)"
|
||||
>
|
||||
{{ favorite ? "★" : "☆" }}
|
||||
@@ -80,25 +81,25 @@ function barWidth(value: number): string {
|
||||
class="px-2 py-1 text-sm uppercase text-white rounded-none"
|
||||
:style="{ backgroundColor: TYPE_COLORS[t] ?? '#777' }"
|
||||
>
|
||||
{{ t }}
|
||||
{{ TYPE_FR[t] ?? t }}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
<dt class="text-base-content/50">Weight</dt>
|
||||
<dt class="text-base-content/50">Poids</dt>
|
||||
<dd>{{ (detail.weight / 10).toFixed(1) }} kg</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base-content/50 text-lg mb-1">Abilities</h3>
|
||||
<p class="capitalize text-xl">{{ detail.abilities.join(", ").replace(/-/g, " ") }}</p>
|
||||
<h3 class="text-base-content/50 text-lg mb-1">Talents</h3>
|
||||
<p class="text-xl">{{ detail.abilities.join(", ") }}</p>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<span class="w-16 shrink-0 text-base-content/70">{{
|
||||
STAT_LABELS[s.name] ?? s.name
|
||||
@@ -112,7 +113,7 @@ function barWidth(value: number): string {
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn rounded-none font-display text-xs" @click="emit('close')">
|
||||
Close
|
||||
Fermer
|
||||
</button>
|
||||
</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).
|
||||
// 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 GEN1_LIMIT = 151
|
||||
@@ -81,7 +82,10 @@ export async function fetchPokedex(): Promise<PokemonListItem[]> {
|
||||
const res = await fetch(`${API}/pokemon?limit=${GEN1_LIMIT}`)
|
||||
if (!res.ok) throw new Error(`PokeAPI list failed (${res.status})`)
|
||||
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> {
|
||||
@@ -90,12 +94,12 @@ export async function fetchPokemon(id: number): Promise<PokemonDetail> {
|
||||
const d = (await res.json()) as RawDetail
|
||||
return {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
name: POKEMON_FR[d.id] ?? d.name,
|
||||
height: d.height,
|
||||
weight: d.weight,
|
||||
types: d.types.map((t) => t.type.name),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user