651d8deafc
Se externaliza el texto español hardcodeado de ~55 archivos a next-intl y se añaden traducciones al inglés, con paridad de claves es/en. Nuevos namespaces: History, CharService, CharServiceB, Points, Legal, UI, Misc (+ altas en Common/Admin). - Historiales (PD/PV, transacciones, sanciones, seguridad), servicios de personaje (transfer, send-gift, quest, restore-*, change-*, customize, level-up, gold, rename), PD/pagos (d-points, trade, promo, transfer-dp, rename-guild, DPointsTabs), páginas legales (cookies, privacidad, términos, reembolsos, aviso legal, contacto), layout (cabecera, footer, cookies, 2FA, descargas, jugadores, recluta) y páginas varias (home, reino, ayuda, addons, 2falogin). - Textos con markup inline via t.rich; interpolación con ICU. - Componente <NoteLegend/> para la leyenda NOTA/NOTE compartida. - payLabel/confirmText de los servicios de pago traducidos. - Verificado: tsc OK, next build OK, todas las claves t() resuelven en es y en, todos los t.rich casan etiquetas, páginas 200 en /es/ y /en/ sin claves crudas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
159 lines
6.1 KiB
TypeScript
159 lines
6.1 KiB
TypeScript
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
|
import { Link } from '@/i18n/navigation'
|
|
import { ServerClock } from '@/components/ServerClock'
|
|
import { getNoticias, getServerStatus, type Noticia, type ServerStatus } from '@/lib/home'
|
|
import { getRealmName } from '@/lib/realm'
|
|
|
|
// SSR por petición: los datos se leen DIRECTAMENTE de MySQL desde el servidor Next.
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
async function getData(): Promise<{ noticias: Noticia[]; status: ServerStatus | null }> {
|
|
const [news, status] = await Promise.allSettled([getNoticias(), getServerStatus()])
|
|
return {
|
|
noticias: news.status === 'fulfilled' ? news.value : [],
|
|
status: status.status === 'fulfilled' ? status.value : null,
|
|
}
|
|
}
|
|
|
|
// Sustituye la marca «UltimoWoW»/«Nova WoW» por el nombre del reino SOLO en el
|
|
// texto visible (fuera de las etiquetas < >), para no romper las URLs de los
|
|
// enlaces; respeta MAYÚSCULAS y no toca dominios «...com».
|
|
function brandify(html: string, realm: string): string {
|
|
const re = /(?:ultimo\s?wow|nova\s?wow)(?!\.com)/gi
|
|
const repl = (m: string) => (m === m.toUpperCase() ? realm.toUpperCase() : realm)
|
|
return html.replace(/<[^>]*>|[^<]+/g, (seg) => (seg.startsWith('<') ? seg : seg.replace(re, repl)))
|
|
}
|
|
|
|
function formatFecha(iso: string | Date | null): string {
|
|
if (!iso) return ''
|
|
const d = new Date(iso)
|
|
if (isNaN(d.getTime())) return String(iso)
|
|
return d.toLocaleDateString('es-ES', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
}
|
|
|
|
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
|
|
const { locale } = await params
|
|
setRequestLocale(locale)
|
|
const t = await getTranslations('Misc')
|
|
const { noticias, status } = await getData()
|
|
const realmName = await getRealmName()
|
|
|
|
const worldStatus = status ? status.world_status : 'Offline'
|
|
const worldClass = worldStatus === 'Online' ? 'green-info' : 'red-info'
|
|
const loginStatus = status ? status.status : 'Offline'
|
|
const loginClass = loginStatus === 'Online' ? 'green-info' : 'red-info'
|
|
|
|
return (
|
|
<div className="main-page">
|
|
<div className="middle-content middle-content-index">
|
|
<div className="body-content">
|
|
<div className="title-content">
|
|
<h1>{realmName} WOTLK 3.4.3</h1>
|
|
</div>
|
|
<div className="box-content">
|
|
<div className="raf-index-holder">
|
|
<div className="right-content" id="right-content-min">
|
|
<div className="right-body">
|
|
{/* Estado del servidor + reloj en vivo (réplica de la isla React de Django) */}
|
|
<table className="max-center-table">
|
|
<tbody>
|
|
<tr>
|
|
<td className="yellow-info">
|
|
{realmName}{' '}
|
|
<span className="blue-info small-font">{status?.expansion ?? ''}</span>
|
|
</td>
|
|
<td className={`real-info-box ${worldClass}`}>{worldStatus}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Login</td>
|
|
<td className={`real-info-box ${loginClass}`}>{loginStatus}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{t('home.labelTime')}</td>
|
|
<td className="real-info-box">
|
|
<ServerClock />
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colSpan={2}>
|
|
<hr />
|
|
</td>
|
|
</tr>
|
|
<tr className="centered">
|
|
<td colSpan={2}>
|
|
<p>
|
|
<span>{status?.address ?? ''}</span>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="raf-index-responsive">
|
|
<Link href="/recruit-a-friend">{t('home.recruitFriend')}</Link>
|
|
</div>
|
|
</div>
|
|
<div className="raf-index">
|
|
<Link href="/recruit-a-friend">{t('home.recruitFriend')}</Link>
|
|
</div>
|
|
</div>
|
|
<div className="body-box-content">
|
|
<div className="body-box-content">
|
|
<p>{t('home.introP1', { realm: realmName })}</p>
|
|
<p>{t('home.introP2')}</p>
|
|
<br />
|
|
<p>{t('home.introP3')}</p>
|
|
<p>{t('home.introP4')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<br />
|
|
|
|
<div className="title-content-h3">
|
|
<h3 className="big-font">{t('home.newsHeading')}</h3>
|
|
</div>
|
|
<div className="box-content">
|
|
<div className="body-box-content">
|
|
{noticias.length === 0 ? (
|
|
<p>{t('home.noNews')}</p>
|
|
) : (
|
|
<>
|
|
{noticias.map((n) => (
|
|
<div key={n.id} className="noticia-item">
|
|
<h3>{brandify(n.titulo, realmName)}</h3>
|
|
<span className="date">{formatFecha(n.fecha)}</span>
|
|
<p
|
|
className="noticia-contenido"
|
|
dangerouslySetInnerHTML={{ __html: brandify(n.contenido, realmName) }}
|
|
/>
|
|
{n.enlace && (
|
|
<p>
|
|
{t('home.newsLinkLabel')}{' '}
|
|
<a href={n.enlace} target="_blank" rel="noopener noreferrer">
|
|
{t('home.here')}
|
|
</a>
|
|
</p>
|
|
)}
|
|
<br />
|
|
<hr />
|
|
<br />
|
|
</div>
|
|
))}
|
|
<p className="centered third-brown">
|
|
<i>{t('home.showingLatest')}</i>
|
|
</p>
|
|
<br />
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|