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:
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user