-
Notifications
You must be signed in to change notification settings - Fork 0
Internacionalizacion ES EN
🇬🇧 English first · 🇪🇸 Español más abajo.
Spanish and English, through src/contexts/LangContext.jsx — 33 lines, no i18n library,
no translation files, no route prefix. The URL does not change with the language.
const LangContext = createContext({ lang: 'es', toggleLang: () => {} })
export function LangProvider({ children }) {
const [lang, setLang] = useState(() => {
try { return localStorage.getItem('portfolio_lang') || 'es' } catch { return 'es' }
})
useEffect(() => { document.documentElement.lang = lang }, [lang])
function toggleLang() {
const next = lang === 'es' ? 'en' : 'es'
setLang(next)
try { localStorage.setItem('portfolio_lang', next) } catch { /* private mode */ }
}
return <LangContext.Provider value={{ lang, toggleLang }}>{children}</LangContext.Provider>
}
export function useLang() { return useContext(LangContext) }Four decisions worth reading out of those lines:
The choice survives the session. It is kept in localStorage under
portfolio_lang, not in memory, so a visitor who switched to English last week arrives
in English. Spanish is the default when there is nothing stored.
<html lang> follows the content. The useEffect is the important line:
useEffect(() => { document.documentElement.lang = lang }, [lang])A page whose markup declares lang="es" while showing English text is a page Chrome
offers to translate — and a machine translation of an already-translated page is how
technical terms come out mangled. Keeping the attribute honest is what stops the browser
from intervening.
localStorage can throw. In private-browsing modes it does, so both the read and
the write sit inside try blocks. Without them the whole app would fail to mount for a
visitor in an incognito window — from a preference, which is the least important thing
on the page.
The provider sits inside the router, wrapping Navbar and every route in App.jsx,
so the toggle is reachable from any page and switching does not navigate.
The pattern across the pages is a T object keyed by language, picked once at the top of
the component:
import { useLang } from '../contexts/LangContext'
const T = {
es: { title: 'Simulador de ingresos', abrir: 'Abrir en ventana completa' },
en: { title: 'Pricing Simulator', abrir: 'Open full-screen' },
}
export default function MiPagina() {
const { lang } = useLang()
const t = T[lang]
return <h1>{t.title}</h1>
}Declaring T outside the component — as RevenueSimulator.jsx does — keeps the object
from being rebuilt on every render. It is a small thing that costs nothing to do right.
| Rule | Detail |
|---|---|
| Visible text | goes in the page's T object, never inline |
| Proper nouns | not translated: Python, Firebase, Recharts, RevPAR |
| ES/EN parity | the same keys in both objects — add one, add the other |
| Toggle | in the Navbar, and it persists across visits |
The ETL dashboard labels. Total Revenue, Peak Month, Temperature vs Sales are
English literals inside the components, and amounts are formatted with
Intl.NumberFormat('en-US', …) in dollars. The underlying dataset is US retail, so
translating the labels while leaving the currency would be worse than leaving both.
The embedded simulator. /revenue serves the original English site inside an
iframe. Rather than hide that, the Spanish version of the page says so in a line that
only appears in Spanish:
idioma: 'El simulador está en inglés: se publicó así en su repositorio y se embebe tal
cual, sin una segunda copia que mantener.',In English that key is an empty string and the paragraph does not render. Explaining a limitation to the audience that notices it, and saying nothing to the audience that does not, is a better answer than a second copy of the simulator to keep in sync.
Castellano e inglés, a través de src/contexts/LangContext.jsx — 33 líneas, sin librería
de i18n, sin ficheros de traducción y sin prefijo en la ruta. La URL no cambia con el
idioma.
const LangContext = createContext({ lang: 'es', toggleLang: () => {} })
export function LangProvider({ children }) {
const [lang, setLang] = useState(() => {
try { return localStorage.getItem('portfolio_lang') || 'es' } catch { return 'es' }
})
useEffect(() => { document.documentElement.lang = lang }, [lang])
function toggleLang() {
const next = lang === 'es' ? 'en' : 'es'
setLang(next)
try { localStorage.setItem('portfolio_lang', next) } catch { /* modo privado */ }
}
return <LangContext.Provider value={{ lang, toggleLang }}>{children}</LangContext.Provider>
}
export function useLang() { return useContext(LangContext) }Cuatro decisiones que se leen en esas líneas:
La elección sobrevive a la sesión. Se guarda en localStorage bajo portfolio_lang
y no en memoria, así que quien se pasó al inglés la semana pasada llega en inglés. El
castellano es el valor por defecto cuando no hay nada guardado.
<html lang> sigue al contenido. El useEffect es la línea importante:
useEffect(() => { document.documentElement.lang = lang }, [lang])Una página cuyo marcado declara lang="es" mientras enseña texto en inglés es una página
que Chrome se ofrece a traducir — y una traducción automática de algo ya traducido es
justo como salen destrozados los términos técnicos. Mantener el atributo honesto es lo
que impide que el navegador intervenga.
localStorage puede lanzar. En modo de navegación privada lo hace, así que tanto la
lectura como la escritura van dentro de try. Sin ellos, la aplicación entera no
montaría para quien entre en una ventana de incógnito — y por una preferencia, que es lo
menos importante de la página.
El proveedor va dentro del router, envolviendo al Navbar y a todas las rutas de
App.jsx, así que el conmutador se alcanza desde cualquier página y cambiar de idioma no
navega.
El patrón en todas las páginas es un objeto T indexado por idioma, elegido una sola vez
en la cabecera del componente:
import { useLang } from '../contexts/LangContext'
const T = {
es: { title: 'Simulador de ingresos', abrir: 'Abrir en ventana completa' },
en: { title: 'Pricing Simulator', abrir: 'Open full-screen' },
}
export default function MiPagina() {
const { lang } = useLang()
const t = T[lang]
return <h1>{t.title}</h1>
}Declarar T fuera del componente —como hace RevenueSimulator.jsx— evita reconstruir el
objeto en cada render. Es una cosa pequeña que no cuesta nada hacer bien.
| Regla | Detalle |
|---|---|
| Texto visible | va en el objeto T de la página, nunca en línea |
| Nombres propios | no se traducen: Python, Firebase, Recharts, RevPAR |
| Paridad ES/EN | las mismas claves en los dos objetos — si añades una, añade la otra |
| Conmutador | en el Navbar, y persiste entre visitas |
Las etiquetas del dashboard del ETL. Total Revenue, Peak Month y Temperature vs
Sales son literales en inglés dentro de los componentes, y los importes se formatean con
Intl.NumberFormat('en-US', …) en dólares. El dataset de debajo es retail
estadounidense, así que traducir las etiquetas dejando la moneda sería peor que dejar las
dos.
El simulador embebido. /revenue sirve el sitio original en inglés dentro de un
iframe. En vez de esconderlo, la versión en castellano de la página lo dice en una línea
que solo aparece en castellano:
idioma: 'El simulador está en inglés: se publicó así en su repositorio y se embebe tal
cual, sin una segunda copia que mantener.',En inglés esa clave es una cadena vacía y el párrafo no se pinta. Explicarle una limitación al público que la nota, y no decirle nada al que no, es mejor respuesta que una segunda copia del simulador que mantener sincronizada.