i18n con next-intl: raíz multilenguaje (es por defecto, /en) + textos externalizados
- next-intl 4 (compatible Next 16): routing (es default en RAÍZ sin prefijo, en con /en, localePrefix 'as-needed'), middleware, request config, navigation helpers. - Estructura app/[locale]/ (layout con <html lang>, NextIntlClientProvider, metadata SEO por idioma vía generateMetadata; page home con getTranslations). - Catálogos messages/es.json y en.json: TODOS los textos de la UI (nada hardcodeado en los .tsx, se usan con t()). Verificado: / -> español (lang=es, título ES), /en -> inglés (lang=en, título EN), datos SSR desde MySQL. Añadir idioma = locale en routing + messages/<loc>.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { NextIntlClientProvider, hasLocale } from 'next-intl'
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { routing } from '@/i18n/routing'
|
||||
import '../globals.css'
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }))
|
||||
}
|
||||
|
||||
// SEO por idioma (SSR) vía Metadata API.
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' })
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
openGraph: {
|
||||
siteName: 'Nova WoW',
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
type: 'website',
|
||||
locale: locale === 'es' ? 'es_ES' : 'en_US',
|
||||
url: 'https://www.nightspire.gg/',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
if (!hasLocale(routing.locales, locale)) notFound()
|
||||
setRequestLocale(locale)
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { getNoticias, getServerStatus, type Noticia, type ServerStatus } from '@/lib/home'
|
||||
|
||||
// SSR por petición: los datos se leen DIRECTAMENTE de MySQL desde el servidor Next
|
||||
// (sin Django). force-dynamic para renderizar por petición.
|
||||
// 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 }> {
|
||||
@@ -12,47 +12,54 @@ async function getData(): Promise<{ noticias: Noticia[]; status: ServerStatus |
|
||||
}
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations('Home')
|
||||
const { noticias, status } = await getData()
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-8">
|
||||
<h1 className="text-3xl font-bold text-amber-500">Nova WoW</h1>
|
||||
<h1 className="text-3xl font-bold text-amber-500">{t('brand')}</h1>
|
||||
|
||||
<section className="my-6 rounded-lg border border-amber-900/60 bg-[#241812] p-4">
|
||||
<h2 className="mb-3 text-lg font-semibold">Estado del servidor</h2>
|
||||
<h2 className="mb-3 text-lg font-semibold">{t('serverStatus')}</h2>
|
||||
{status ? (
|
||||
<ul className="space-y-1 text-sm">
|
||||
<li>
|
||||
Servidor: <span className="text-amber-400">{status.server_name ?? '—'}</span> ({status.expansion})
|
||||
{t('server')}: <span className="text-amber-400">{status.server_name ?? '—'}</span> ({status.expansion})
|
||||
</li>
|
||||
<li>Personajes en línea: {status.online_characters}</li>
|
||||
<li>
|
||||
Login:{' '}
|
||||
{t('onlineCharacters')}: {status.online_characters}
|
||||
</li>
|
||||
<li>
|
||||
{t('login')}:{' '}
|
||||
<span className={status.status === 'Online' ? 'text-green-400' : 'text-red-400'}>{status.status}</span>
|
||||
</li>
|
||||
<li>Dirección: {status.address ?? '—'}</li>
|
||||
<li>
|
||||
{t('address')}: {status.address ?? '—'}
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<p>No disponible.</p>
|
||||
<p>{t('notAvailable')}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-lg font-semibold">Noticias</h2>
|
||||
<h2 className="mb-3 text-lg font-semibold">{t('news')}</h2>
|
||||
{noticias.length === 0 ? (
|
||||
<p className="text-amber-200/70">No hay noticias disponibles en este momento.</p>
|
||||
<p className="text-amber-200/70">{t('noNews')}</p>
|
||||
) : (
|
||||
noticias.map((n) => (
|
||||
<article key={n.id} className="mb-4 border-b border-amber-900/60 pb-4">
|
||||
<h3 className="text-xl font-semibold">{n.titulo}</h3>
|
||||
{n.fecha && <small className="text-amber-200/60">{new Date(n.fecha).toLocaleString('es-ES')}</small>}
|
||||
{n.fecha && <small className="text-amber-200/60">{new Date(n.fecha).toLocaleString(locale)}</small>}
|
||||
<div className="prose prose-invert mt-2 max-w-none" dangerouslySetInnerHTML={{ __html: n.contenido }} />
|
||||
{n.enlace && (
|
||||
<p className="mt-2">
|
||||
Enlace:{' '}
|
||||
{t('link')}:{' '}
|
||||
<a className="text-sky-400 underline" href={n.enlace} target="_blank" rel="noopener noreferrer">
|
||||
aquí
|
||||
{t('here')}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
const SERVER = 'Nova WoW'
|
||||
|
||||
// SEO renderizado en servidor (SSR) — equivale a las meta de la antigua head.html.
|
||||
export const metadata: Metadata = {
|
||||
title: `${SERVER} Server de WoW WotLK 3.4.3 Latino y Español 💎`,
|
||||
description: `Descarga y juega gratis a WotLK Classic en ${SERVER} - Server de WoW WotLK 3.4.3 Latino y Español - El mejor server de WoW 🔥`,
|
||||
keywords: [
|
||||
'server wow', 'server wotlk', 'server 3.4.3', 'wotlk classic',
|
||||
'servidor de wow', 'servidor wotlk', 'wow 3.4.3 latino', 'wow 3.4.3 español',
|
||||
'server wow en español', 'el mejor server de wow', SERVER,
|
||||
],
|
||||
openGraph: {
|
||||
siteName: SERVER,
|
||||
title: `${SERVER} Server de WoW WotLK 3.4.3 Latino y Español 💎`,
|
||||
description: `Juega gratis WotLK Classic en ${SERVER} - Server de WoW 3.4.3 Latino y Español`,
|
||||
type: 'website',
|
||||
locale: 'es_ES',
|
||||
url: 'https://www.nightspire.gg/',
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="es">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user