Foro: enlaces de wowhead en el idioma del lector
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<string, number> = { 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)
|
||||
}
|
||||
|
||||
@@ -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 = /<a\b([^>]*)>([\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, '<').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<string> {
|
||||
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<string, { type: WowheadType; id: string }>()
|
||||
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<string, string>()
|
||||
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 `<a href="${href}" data-wowhead="${escAttr(wh)}" target="_blank" rel="noopener noreferrer">${escHtml(name)}</a>`
|
||||
})
|
||||
}
|
||||
@@ -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<string, number> = { 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<WowheadInfo | null> {
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user