i18n: traducir toda la app (ES + EN) — páginas y componentes con texto hardcodeado

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>
This commit is contained in:
2026-07-14 18:58:54 +00:00
parent e457a43f05
commit 651d8deafc
57 changed files with 3516 additions and 1190 deletions
+22 -20
View File
@@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
import { Turnstile } from '@/components/Turnstile'
@@ -13,26 +14,27 @@ interface DeletedItem {
count: number
}
function itemsError(error?: string, minutes?: number): string {
function itemsError(t: ReturnType<typeof useTranslations>, error?: string, minutes?: number): string {
switch (error) {
case 'characterNotOwned':
return 'El personaje seleccionado no es válido.'
return t('restoreItems.errors.characterNotOwned')
case 'cooldown':
return `Solo puedes consultar los ítems de este personaje cada 8 horas (faltan ${minutes ?? 0} min).`
return t('restoreItems.errors.cooldown', { minutes: minutes ?? 0 })
case 'soapError':
return 'No se pudo conectar con el servidor de juego. Inténtalo más tarde.'
return t('restoreItems.errors.soapError')
case 'insufficientPoints':
return 'No tienes suficientes PD (se requieren 100 por ítem).'
return t('restoreItems.errors.insufficientPoints')
case 'captcha':
return 'Verifica que no eres un robot.'
return t('restoreItems.errors.captcha')
case 'notAuthenticated':
return 'Tu sesión ha expirado. Inicia sesión de nuevo.'
return t('restoreItems.errors.notAuthenticated')
default:
return 'Algo ha salido mal. Inténtalo de nuevo.'
return t('restoreItems.errors.default')
}
}
export function RestoreItemsForm({ characters, price }: { characters: CharOption[]; price: number }) {
const t = useTranslations('CharService')
const [character, setCharacter] = useState('')
const [captcha, setCaptcha] = useState('')
const [captchaKey, setCaptchaKey] = useState(0)
@@ -55,12 +57,12 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
const data: { success?: boolean; error?: string; minutes?: number; items?: DeletedItem[] } = await res.json()
if (data.success) {
setItems(data.items ?? [])
if ((data.items ?? []).length === 0) setMessage({ ok: true, text: 'Este personaje no tiene ítems borrados recuperables.' })
if ((data.items ?? []).length === 0) setMessage({ ok: true, text: t('restoreItems.noDeletedItemsMsg') })
} else {
setMessage({ ok: false, text: itemsError(data.error, data.minutes) })
setMessage({ ok: false, text: itemsError(t, data.error, data.minutes) })
}
} catch {
setMessage({ ok: false, text: itemsError() })
setMessage({ ok: false, text: itemsError(t) })
} finally {
setBusy(false)
setCaptcha('')
@@ -70,7 +72,7 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
async function restore(item: DeletedItem) {
if (busy) return
if (!window.confirm(`¿Recuperar "${item.name}" por ${price} € (SumUp)?`)) return
if (!window.confirm(t('restoreItems.confirm', { name: item.name, price }))) return
setBusy(true)
setMessage(null)
try {
@@ -85,10 +87,10 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
window.location.assign(data.url) // checkout de SumUp
return
}
setMessage({ ok: false, text: data.message || itemsError(data.error) })
setMessage({ ok: false, text: data.message || itemsError(t, data.error) })
setBusy(false)
} catch {
setMessage({ ok: false, text: itemsError() })
setMessage({ ok: false, text: itemsError(t) })
setBusy(false)
}
}
@@ -106,19 +108,19 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
{items === null ? (
<div id="char-selection">
<form id="uw-search-items-form" onSubmit={search} acceptCharset="utf-8">
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder="Selecciona un personaje" />
<CharacterSelect characters={characters} value={character} onChange={setCharacter} placeholder={t('restoreItems.selectPlaceholder')} />
<br />
<Turnstile key={captchaKey} onVerify={setCaptcha} />
<button type="submit" className="search-items-button" disabled={busy || !character}>
{busy ? 'Buscando' : 'BUSCAR ÍTEMS'}
{busy ? t('restoreItems.searching') : t('restoreItems.search')}
</button>
</form>
</div>
) : (
<div id="search-items-response">
<p className="yellow-info">Ítems borrados de {character}</p>
<p className="yellow-info">{t('restoreItems.deletedItemsOf', { character })}</p>
{items.length === 0 ? (
<p className="second-brown">No hay ítems borrados recuperables.</p>
<p className="second-brown">{t('restoreItems.noDeletedItems')}</p>
) : (
<table className="restore-item-table">
<tbody>
@@ -130,7 +132,7 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
</td>
<td className="real-info-box">
<button type="button" className="restore-item-button" value={it.recoverId} data-char={character} onClick={() => restore(it)} disabled={busy}>
Recuperar ({price} )
{t('restoreItems.restoreItem', { price })}
</button>
</td>
</tr>
@@ -139,7 +141,7 @@ export function RestoreItemsForm({ characters, price }: { characters: CharOption
</table>
)}
<br />
<p><a id="a-select-other" href="#" onClick={(e) => { e.preventDefault(); reset() }}>Consultar otro personaje</a></p>
<p><a id="a-select-other" href="#" onClick={(e) => { e.preventDefault(); reset() }}>{t('restoreItems.queryOther')}</a></p>
</div>
)}
<hr />