Skip to content

Internacionalizacion ES EN

Mindset & Code edited this page Aug 18, 2026 · 3 revisions

Internacionalización 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.

The whole context

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.

Using it in a page

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.

Conventions

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

What is deliberately not translated

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.


🇪🇸 Español

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.

El contexto entero

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.

Cómo se usa en una página

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.

Convenciones

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

Lo que a propósito no se traduce

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.

Clone this wiki locally