Skip to content

Commit a3a00b2

Browse files
devsart95claude
andcommitted
feat(i18n): selector de idioma ES/EN/JA con traducción completa del sitio
- Agrega LangContext con persistencia en localStorage y atributo lang en html - Agrega LangSelector (ES/EN/JA) posicionado junto al toggle de tema - Traduce todas las secciones: Hero, About, Stack, Skills, Connect, Footer, Nav - useTypewriter acepta frases dinámicas para reaccionar al cambio de idioma - Traducciones completas en español, inglés y japonés Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 41eabb5 commit a3a00b2

File tree

13 files changed

+442
-397
lines changed

13 files changed

+442
-397
lines changed

src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { LangProvider } from './context/LangContext'
12
import { StarfieldCanvas } from './components/StarfieldCanvas'
23
import { AuroraBlobs } from './components/AuroraBlobs'
34
import { SidebarNav } from './components/SidebarNav'
45
import { SectionDivider } from './components/SectionDivider'
56
import { ThemeToggle } from './components/ThemeToggle'
7+
import { LangSelector } from './components/LangSelector'
68
import { InterferenceScan } from './components/InterferenceScan'
79
import { Hero } from './components/sections/Hero'
810
import { About } from './components/sections/About'
@@ -13,12 +15,13 @@ import { Footer } from './components/Footer'
1315

1416
export default function App() {
1517
return (
16-
<>
18+
<LangProvider>
1719
<AuroraBlobs />
1820
<StarfieldCanvas />
1921
<InterferenceScan />
2022
<SidebarNav />
2123
<ThemeToggle />
24+
<LangSelector />
2225
<main>
2326
<Hero />
2427
<SectionDivider />
@@ -31,6 +34,6 @@ export default function App() {
3134
<Connect />
3235
</main>
3336
<Footer />
34-
</>
37+
</LangProvider>
3538
)
3639
}

src/components/Footer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { useLang } from '../context/LangContext'
2+
13
export function Footer() {
4+
const { t } = useLang()
25
return (
36
<footer>
47
<div className="wrap">
@@ -39,7 +42,7 @@ export function Footer() {
3942
&nbsp;·&nbsp; San Pedro, Paraguay 🇵🇾 &nbsp;·&nbsp; 2026
4043
</p>
4144
<p style={{ marginTop: '6px' }}>
42-
built with <span style={{ color: '#ec4899' }}></span> caffeine &amp; late nights
45+
{t('footer.built')}
4346
</p>
4447
</div>
4548
</footer>

src/components/LangSelector.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useLang } from '../context/LangContext'
2+
import type { Lang } from '../i18n/translations'
3+
4+
const LANGS: { code: Lang; label: string }[] = [
5+
{ code: 'es', label: 'ES' },
6+
{ code: 'en', label: 'EN' },
7+
{ code: 'ja', label: 'JA' },
8+
]
9+
10+
export function LangSelector() {
11+
const { lang, setLang } = useLang()
12+
13+
return (
14+
<div className="lang-selector" role="group" aria-label="Language selector">
15+
{LANGS.map(({ code, label }) => (
16+
<button
17+
key={code}
18+
onClick={() => setLang(code)}
19+
aria-pressed={lang === code}
20+
aria-label={`Switch to ${label}`}
21+
className={`lang-btn${lang === code ? ' lang-btn-active' : ''}`}
22+
>
23+
{label}
24+
</button>
25+
))}
26+
</div>
27+
)
28+
}

src/components/SidebarNav.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,38 @@
1-
const NAV_ITEMS = [
2-
{ num: '01_', label: 'sobre', href: '#sobre' },
3-
{ num: '02_', label: 'stack', href: '#stack' },
4-
{ num: '03_', label: 'skills', href: '#skills' },
5-
{ num: '04_', label: 'contacto', href: '#conectar' },
6-
]
1+
import { useLang } from '../context/LangContext'
72

83
export function SidebarNav() {
4+
const { t } = useLang()
5+
6+
const NAV_ITEMS = [
7+
{ num: '01_', labelKey: 'nav.about' as const, href: '#sobre' },
8+
{ num: '02_', labelKey: 'nav.stack' as const, href: '#stack' },
9+
{ num: '03_', labelKey: 'nav.skills' as const, href: '#skills' },
10+
{ num: '04_', labelKey: 'nav.contact' as const, href: '#conectar' },
11+
]
12+
913
return (
10-
<nav aria-label="Navegación principal">
14+
<nav aria-label={t('nav.aria')}>
1115
<div className="nav-line" />
1216
<div className="nav-inner">
13-
{/* signal bars en la parte superior */}
1417
<div className="signal-bars" style={{ marginBottom: 24, marginLeft: 2 }}>
1518
<div className="signal-bar" />
1619
<div className="signal-bar" />
1720
<div className="signal-bar" />
1821
<div className="signal-bar" />
1922
</div>
20-
{NAV_ITEMS.map((item) => (
21-
<a key={item.href} className="nav-item" href={item.href}>
22-
<span className="nav-num">{item.num}</span>
23-
<span className="nav-label">
24-
{item.label.split('').map((char, i) => (
25-
<span key={i}>{char}</span>
26-
))}
27-
</span>
28-
</a>
29-
))}
23+
{NAV_ITEMS.map((item) => {
24+
const label = t(item.labelKey)
25+
return (
26+
<a key={item.href} className="nav-item" href={item.href}>
27+
<span className="nav-num">{item.num}</span>
28+
<span className="nav-label">
29+
{label.split('').map((char, i) => (
30+
<span key={i}>{char}</span>
31+
))}
32+
</span>
33+
</a>
34+
)
35+
})}
3036
</div>
3137
</nav>
3238
)

src/components/sections/About.tsx

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { useRef } from 'react'
22
import { useScrollReveal } from '../../hooks/useScrollReveal'
3+
import { useLang } from '../../context/LangContext'
34

45
export function About() {
56
const sectionRef = useRef<HTMLElement>(null)
67
useScrollReveal(sectionRef)
8+
const { t } = useLang()
79

810
return (
911
<section id="sobre" ref={sectionRef}>
@@ -13,52 +15,38 @@ export function About() {
1315
<circle cx="12" cy="8" r="4" />
1416
<path d="M20 21a8 8 0 10-16 0" />
1517
</svg>
16-
// whoami
18+
{t('about.tag')}
1719
</div>
18-
<h2>Sobre mí</h2>
20+
<h2>{t('about.h2')}</h2>
1921

2022
<div className="hud-card reveal">
2123
<div className="hud-corner-tr" />
2224
<div className="hud-corner-bl" />
2325
<div className="hud-scan" />
2426

25-
{/* circuit traces decorativas */}
2627
<svg className="hud-circuit" viewBox="0 0 800 320" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
27-
{/* trazos izquierda */}
2828
<path d="M0 40 H30 V80 H60 V40 H120" stroke="rgba(56,189,248,.07)" fill="none" strokeWidth="1" />
2929
<path d="M0 200 H50 V160 H90" stroke="rgba(56,189,248,.06)" fill="none" strokeWidth="1" />
3030
<path d="M0 280 H20 V240 H70 V260 H110" stroke="rgba(129,140,248,.05)" fill="none" strokeWidth="1" />
31-
{/* nodos izquierda */}
3231
<circle cx="30" cy="80" r="2" fill="rgba(56,189,248,.12)" />
3332
<circle cx="60" cy="40" r="2" fill="rgba(56,189,248,.12)" />
3433
<circle cx="50" cy="160" r="2" fill="rgba(56,189,248,.10)" />
3534
<circle cx="20" cy="240" r="2" fill="rgba(129,140,248,.10)" />
3635
<circle cx="70" cy="260" r="2" fill="rgba(129,140,248,.10)" />
37-
{/* trazos derecha */}
3836
<path d="M800 60 H770 V100 H730 V60 H680" stroke="rgba(129,140,248,.07)" fill="none" strokeWidth="1" />
3937
<path d="M800 180 H750 V220 H710" stroke="rgba(129,140,248,.06)" fill="none" strokeWidth="1" />
4038
<path d="M800 290 H780 V250 H730 V270 H690" stroke="rgba(56,189,248,.05)" fill="none" strokeWidth="1" />
41-
{/* nodos derecha */}
4239
<circle cx="770" cy="100" r="2" fill="rgba(129,140,248,.12)" />
4340
<circle cx="730" cy="60" r="2" fill="rgba(129,140,248,.12)" />
4441
<circle cx="750" cy="220" r="2" fill="rgba(129,140,248,.10)" />
4542
<circle cx="780" cy="250" r="2" fill="rgba(56,189,248,.10)" />
4643
<circle cx="730" cy="270" r="2" fill="rgba(56,189,248,.10)" />
47-
{/* dot matrix sutil fondo */}
4844
<circle cx="200" cy="60" r="1" fill="rgba(255,255,255,.04)" />
49-
<circle cx="250" cy="60" r="1" fill="rgba(255,255,255,.04)" />
5045
<circle cx="300" cy="60" r="1" fill="rgba(255,255,255,.04)" />
51-
<circle cx="350" cy="60" r="1" fill="rgba(255,255,255,.04)" />
5246
<circle cx="400" cy="60" r="1" fill="rgba(255,255,255,.04)" />
53-
<circle cx="450" cy="60" r="1" fill="rgba(255,255,255,.04)" />
5447
<circle cx="500" cy="60" r="1" fill="rgba(255,255,255,.04)" />
55-
<circle cx="550" cy="60" r="1" fill="rgba(255,255,255,.04)" />
56-
<circle cx="600" cy="60" r="1" fill="rgba(255,255,255,.04)" />
5748
<circle cx="200" cy="260" r="1" fill="rgba(255,255,255,.04)" />
58-
<circle cx="300" cy="260" r="1" fill="rgba(255,255,255,.04)" />
5949
<circle cx="400" cy="260" r="1" fill="rgba(255,255,255,.04)" />
60-
<circle cx="500" cy="260" r="1" fill="rgba(255,255,255,.04)" />
61-
<circle cx="600" cy="260" r="1" fill="rgba(255,255,255,.04)" />
6250
</svg>
6351

6452
<div className="hud-header">
@@ -69,38 +57,38 @@ export function About() {
6957

7058
<div className="hud-rows">
7159
<div className="hud-row">
72-
<span className="hud-key">nombre</span>
60+
<span className="hud-key">{t('about.key.name')}</span>
7361
<span className="hud-val"><span className="hud-val-accent">Justino Rojas Sartorio</span></span>
7462
</div>
7563
<div className="hud-row">
76-
<span className="hud-key">alias</span>
64+
<span className="hud-key">{t('about.key.alias')}</span>
7765
<span className="hud-val" style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '13px', color: 'var(--accent)' }}>devsart95</span>
7866
</div>
7967
<div className="hud-row">
80-
<span className="hud-key">ubicación</span>
68+
<span className="hud-key">{t('about.key.location')}</span>
8169
<span className="hud-val">San Pedro, Paraguay 🇵🇾</span>
8270
</div>
8371
<div className="hud-row">
84-
<span className="hud-key">rol</span>
72+
<span className="hud-key">{t('about.key.role')}</span>
8573
<span className="hud-val">
86-
<span className="hud-tag">fullstack dev</span>
87-
<span className="hud-tag">AI enthusiast</span>
88-
<span className="hud-tag">agro worker</span>
74+
<span className="hud-tag">{t('about.tag.fullstack')}</span>
75+
<span className="hud-tag">{t('about.tag.ai')}</span>
76+
<span className="hud-tag">{t('about.tag.agro')}</span>
8977
</span>
9078
</div>
9179
<div className="hud-row">
92-
<span className="hud-key">foco</span>
93-
<span className="hud-val" style={{ color: 'var(--muted2)', fontSize: '13px' }}>IA aplicada · SaaS para LATAM · herramientas reales</span>
80+
<span className="hud-key">{t('about.key.focus')}</span>
81+
<span className="hud-val" style={{ color: 'var(--muted2)', fontSize: '13px' }}>{t('about.focus.text')}</span>
9482
</div>
9583
<div className="hud-row">
96-
<span className="hud-key">modo</span>
97-
<span className="hud-val" style={{ fontSize: '13px', color: 'var(--muted2)', fontStyle: 'italic' }}>"Experimentando con IA. Construyendo cosas reales."</span>
84+
<span className="hud-key">{t('about.key.mode')}</span>
85+
<span className="hud-val" style={{ fontSize: '13px', color: 'var(--muted2)', fontStyle: 'italic' }}>{t('about.mode.text')}</span>
9886
</div>
9987
</div>
10088

10189
<div className="hud-footer">
10290
<div className="hud-footer-line" />
103-
<span className="hud-status">ONLINE · SAN PEDRO · PY</span>
91+
<span className="hud-status">{t('about.status')}</span>
10492
</div>
10593
</div>
10694
</div>

src/components/sections/Connect.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useRef, useState } from 'react'
22
import { useScrollReveal } from '../../hooks/useScrollReveal'
3+
import { useLang } from '../../context/LangContext'
34

45
type Status = 'idle' | 'sending' | 'success' | 'error'
56

@@ -8,6 +9,7 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
89
export function Connect() {
910
const sectionRef = useRef<HTMLElement>(null)
1011
useScrollReveal(sectionRef)
12+
const { t } = useLang()
1113

1214
const [status, setStatus] = useState<Status>('idle')
1315
const [form, setForm] = useState({ name: '', email: '', message: '' })
@@ -58,9 +60,9 @@ export function Connect() {
5860
<polyline points="15 3 21 3 21 9" />
5961
<line x1="10" y1="14" x2="21" y2="3" />
6062
</svg>
61-
// social
63+
{t('connect.tag')}
6264
</div>
63-
<h2>Conectar</h2>
65+
<h2>{t('connect.h2')}</h2>
6466

6567
<div className="connect-layout">
6668
{/* Social cards */}
@@ -105,7 +107,7 @@ export function Connect() {
105107
{/* Contact form */}
106108
<form className="contact-form reveal" onSubmit={handleSubmit} noValidate>
107109
<div className="cf-header">
108-
<span className="cf-title">// mensaje directo</span>
110+
<span className="cf-title">{t('connect.form.title')}</span>
109111
<div className="cf-title-line" />
110112
</div>
111113

@@ -115,22 +117,22 @@ export function Connect() {
115117
<circle cx="12" cy="12" r="10" stroke="var(--accent)" opacity=".4"/>
116118
<polyline points="20 6 9 17 4 12" stroke="#4ade80" strokeWidth="2.2"/>
117119
</svg>
118-
<p className="cf-thanks-title">¡Gracias por escribir!</p>
119-
<p className="cf-thanks-sub">Recibí tu mensaje y te respondo a la brevedad.</p>
120+
<p className="cf-thanks-title">{t('connect.form.ok.title')}</p>
121+
<p className="cf-thanks-sub">{t('connect.form.ok.sub')}</p>
120122
</div>
121123
) : (
122124
<>
123125
<div className="cf-fields">
124126
<div className="cf-field">
125-
<label className="cf-label" htmlFor="cf-name">nombre</label>
127+
<label className="cf-label" htmlFor="cf-name">{t('connect.form.name')}</label>
126128
<input
127129
id="cf-name"
128130
className="cf-input"
129131
type="text"
130132
name="name"
131133
value={form.name}
132134
onChange={handleChange}
133-
placeholder="Tu nombre"
135+
placeholder={t('connect.form.name.ph')}
134136
autoComplete="name"
135137
/>
136138
</div>
@@ -144,20 +146,20 @@ export function Connect() {
144146
name="email"
145147
value={form.email}
146148
onChange={handleChange}
147-
placeholder="tu@email.com"
149+
placeholder={t('connect.form.email.ph')}
148150
autoComplete="email"
149151
/>
150152
</div>
151153

152154
<div className="cf-field cf-field-full">
153-
<label className="cf-label" htmlFor="cf-message">mensaje</label>
155+
<label className="cf-label" htmlFor="cf-message">{t('connect.form.message')}</label>
154156
<textarea
155157
id="cf-message"
156158
className="cf-input cf-textarea"
157159
name="message"
158160
value={form.message}
159161
onChange={handleChange}
160-
placeholder="¿En qué puedo ayudarte?"
162+
placeholder={t('connect.form.message.ph')}
161163
rows={4}
162164
/>
163165
</div>
@@ -167,25 +169,25 @@ export function Connect() {
167169
{validErr && (
168170
<span className="cf-feedback cf-error">
169171
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
170-
Completá todos los campos
172+
{t('connect.form.err.fields')}
171173
</span>
172174
)}
173175
{status === 'error' && !validErr && (
174176
<span className="cf-feedback cf-error">
175177
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
176-
Error al enviar, intentá de nuevo
178+
{t('connect.form.err.send')}
177179
</span>
178180
)}
179181
<button className="btn btn-glow cf-submit" type="submit" disabled={status === 'sending'}>
180182
{status === 'sending' ? (
181183
<>
182184
<span className="cf-spinner" />
183-
enviando...
185+
{t('connect.form.sending')}
184186
</>
185187
) : (
186188
<>
187189
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
188-
enviar
190+
{t('connect.form.send')}
189191
</>
190192
)}
191193
</button>

0 commit comments

Comments
 (0)