From 5bcf71a37bcc5092ee0f7c87e64cfba1b024f505 Mon Sep 17 00:00:00 2001 From: adevopg Date: Thu, 16 Jul 2026 14:08:08 +0000 Subject: [PATCH] Foro: enlaces de wowhead en el idioma del lector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los enlaces de wowhead se guardan con el locale de quien los postea (subdominio, domain y nombre), así que un ítem posteado en /es se veía en español aunque lo leyeras en /en, y al revés. Ahora siguen el idioma de QUIEN LEE. Al renderizar el tema (server component, conoce el locale del lector) se localizan los enlaces: se reescribe el subdominio y el domain del data-wowhead y se vuelve a resolver el nombre en el idioma del lector (lib/forum-wowhead + lib/wowhead-resolve, compartido con la API del editor y cacheado). Es solo de presentación: no cambia lo guardado ni añade parpadeo (el nombre llega ya resuelto del servidor). Verificado: un post guardado con nombre inglés y domain=wotlk se lee como «Bolsa de tejido de Escarcha» + es.wotlk en /es, y «Frostweave Bag» + www en /en. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[locale]/forum/topic/[topicId]/page.tsx | 7 ++- web-next/app/api/forum/wowhead/route.ts | 34 +++-------- web-next/lib/forum-wowhead.ts | 60 +++++++++++++++++++ web-next/lib/wowhead-resolve.ts | 39 ++++++++++++ 4 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 web-next/lib/forum-wowhead.ts create mode 100644 web-next/lib/wowhead-resolve.ts diff --git a/web-next/app/[locale]/forum/topic/[topicId]/page.tsx b/web-next/app/[locale]/forum/topic/[topicId]/page.tsx index 57a79d4..39951a7 100644 --- a/web-next/app/[locale]/forum/topic/[topicId]/page.tsx +++ b/web-next/app/[locale]/forum/topic/[topicId]/page.tsx @@ -9,6 +9,7 @@ import { getUserPostCount, } from '@/lib/forum' import { formatForumDate } from '@/lib/forum-format' +import { localizeWowheadLinks } from '@/lib/forum-wowhead' import { getSession } from '@/lib/session' import { forumIsModerator, canEditPost } from '@/lib/forum-perm' import { PageShell } from '@/components/PageShell' @@ -43,7 +44,11 @@ export default async function TopicPage({ const total = await countPosts(id, isMod) const totalPages = Math.max(1, Math.ceil(total / PER_PAGE)) const page = Math.min(Math.max(1, Number((await searchParams).page) || 1), totalPages) - const posts = await getPosts(id, (page - 1) * PER_PAGE, PER_PAGE, isMod) + const rawPosts = await getPosts(id, (page - 1) * PER_PAGE, PER_PAGE, isMod) + // Enlaces de wowhead en el idioma de QUIEN LEE (no de quien posteó). + const posts = await Promise.all( + rawPosts.map(async (p) => ({ ...p, text: await localizeWowheadLinks(p.text, locale) })), + ) const posters = await resolvePosters(posts.map((p) => p.poster)) // Nº de posts por autor visible (para el panel lateral), en paralelo. diff --git a/web-next/app/api/forum/wowhead/route.ts b/web-next/app/api/forum/wowhead/route.ts index c0e164b..6d29f80 100644 --- a/web-next/app/api/forum/wowhead/route.ts +++ b/web-next/app/api/forum/wowhead/route.ts @@ -1,33 +1,15 @@ -// Resuelve nombre, calidad e icono de una entidad de wowhead (rama WotLK) para que -// el editor del foro pueda insertar el enlace ya con su nombre real y su color de -// calidad, sin el parpadeo de «item=xxx». Usa el endpoint de tooltip de wowhead -// (el mismo que consume tooltips.js) con dataEnv=8 = WotLK Classic. +import { fetchWowheadInfo } from '@/lib/wowhead-resolve' -const TYPES = new Set(['item', 'spell', 'quest', 'npc', 'achievement', 'object']) -// Código de locale de wowhead: 0 = inglés, 6 = español. -const WH_LOCALE: Record = { es: 6, en: 0 } +// Resuelve nombre, calidad e icono de una entidad de wowhead (rama WotLK) para que +// el editor del foro pueda insertar el enlace ya con su nombre real, sin el parpadeo +// de «item=xxx». La lógica vive en lib/wowhead-resolve (compartida con el render). export async function GET(request: Request) { const url = new URL(request.url) const type = String(url.searchParams.get('type') || 'item') - const id = String(url.searchParams.get('id') || '').replace(/\D/g, '') + const id = String(url.searchParams.get('id') || '') const locale = String(url.searchParams.get('locale') || 'es') - if (!TYPES.has(type) || !id) return Response.json({ name: null }, { status: 400 }) - - const wl = WH_LOCALE[locale] ?? 0 - try { - const r = await fetch(`https://nether.wowhead.com/tooltip/${type}/${id}?dataEnv=8&locale=${wl}`, { - // cache un día: los nombres/calidades no cambian - next: { revalidate: 86400 }, - }) - if (!r.ok) return Response.json({ name: null }) - const d = (await r.json()) as { name?: string; quality?: number; icon?: string } - return Response.json({ - name: d.name ?? null, - quality: typeof d.quality === 'number' ? d.quality : null, - icon: d.icon ?? null, - }) - } catch { - return Response.json({ name: null }) - } + const info = await fetchWowheadInfo(type, id, locale) + if (!info) return Response.json({ name: null }, { status: 400 }) + return Response.json(info) } diff --git a/web-next/lib/forum-wowhead.ts b/web-next/lib/forum-wowhead.ts new file mode 100644 index 0000000..ebd89e3 --- /dev/null +++ b/web-next/lib/forum-wowhead.ts @@ -0,0 +1,60 @@ +import { wowheadUrl, wowheadData, type WowheadType } from './wowhead' +import { fetchWowheadInfo } from './wowhead-resolve' + +// Localiza los enlaces de wowhead de un post al idioma de QUIEN LO LEE (no de quien +// lo posteó). Los enlaces se guardan con el locale del autor (subdominio, domain y +// nombre); aquí, al renderizar el tema, se reescriben al locale del lector: se ajusta +// el subdominio y el domain del tooltip, y se vuelve a resolver el nombre en su idioma. +// No toca lo guardado: es solo de presentación. + +const ANCHOR_RE = /]*)>([\s\S]*?)<\/a>/gi +const ENTITY_RE = /(item|spell|quest|npc|achievement|object)=(\d+)/i + +function attr(attrs: string, name: string): string | undefined { + return new RegExp(`${name}="([^"]*)"`, 'i').exec(attrs)?.[1] +} +function escAttr(s: string): string { + return s.replace(/&/g, '&').replace(/"/g, '"') +} +function escHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>') +} + +/** Extrae type=id de un enlace de wowhead (de data-wowhead o del href). */ +function entityOf(attrs: string): { type: WowheadType; id: string } | null { + if (!/wowhead\.com/i.test(attrs) && !/data-wowhead=/i.test(attrs)) return null + const src = attr(attrs, 'data-wowhead') || attr(attrs, 'href') || '' + const m = ENTITY_RE.exec(src) + return m ? { type: m[1].toLowerCase() as WowheadType, id: m[2] } : null +} + +export async function localizeWowheadLinks(html: string, locale: string): Promise { + if (!html || (!/wowhead\.com/i.test(html) && !/data-wowhead/i.test(html))) return html + + // 1) recopila las entidades únicas y resuelve su nombre en el locale del lector + const entities = new Map() + for (const m of html.matchAll(ANCHOR_RE)) { + const e = entityOf(m[1]) + if (e) entities.set(`${e.type}=${e.id}`, e) + } + if (entities.size === 0) return html + + const names = new Map() + await Promise.all( + [...entities.values()].map(async (e) => { + const info = await fetchWowheadInfo(e.type, e.id, locale) + if (info?.name) names.set(`${e.type}=${e.id}`, info.name) + }), + ) + + // 2) reescribe cada enlace de wowhead al locale del lector + return html.replace(ANCHOR_RE, (full, attrs: string, inner: string) => { + const e = entityOf(attrs) + if (!e) return full + const key = `${e.type}=${e.id}` + const href = wowheadUrl(e.type, e.id, locale) + const wh = wowheadData(e.type, e.id, locale) + const name = names.get(key) || inner // si no resuelve, se conserva el texto guardado + return `${escHtml(name)}` + }) +} diff --git a/web-next/lib/wowhead-resolve.ts b/web-next/lib/wowhead-resolve.ts new file mode 100644 index 0000000..1ff6fcc --- /dev/null +++ b/web-next/lib/wowhead-resolve.ts @@ -0,0 +1,39 @@ +// Resolución server-side de una entidad de wowhead (nombre, calidad, icono) en la +// rama WotLK y en un idioma concreto, usando el endpoint de tooltip de wowhead (el +// mismo que consume tooltips.js). Cacheado un día: nombres/calidades no cambian. + +export const WOWHEAD_ENTITY_TYPES = ['item', 'spell', 'quest', 'npc', 'achievement', 'object'] as const +export type WowheadEntityType = (typeof WOWHEAD_ENTITY_TYPES)[number] + +// Código de locale de wowhead: 0 = inglés, 6 = español. +const WH_LOCALE: Record = { es: 6, en: 0 } + +export interface WowheadInfo { + name: string | null + quality: number | null + icon: string | null +} + +export async function fetchWowheadInfo( + type: string, + id: string | number, + locale: string, +): Promise { + const cleanId = String(id).replace(/\D/g, '') + if (!WOWHEAD_ENTITY_TYPES.includes(type as WowheadEntityType) || !cleanId) return null + const wl = WH_LOCALE[locale] ?? 0 + try { + const r = await fetch(`https://nether.wowhead.com/tooltip/${type}/${cleanId}?dataEnv=8&locale=${wl}`, { + next: { revalidate: 86400 }, + }) + if (!r.ok) return null + const d = (await r.json()) as { name?: string; quality?: number; icon?: string } + return { + name: d.name ?? null, + quality: typeof d.quality === 'number' ? d.quality : null, + icon: d.icon ?? null, + } + } catch { + return null + } +}