Skip to content

Commit 0cbb97d

Browse files
itziarZGctrl-alt-dfrancescarpi
authored
feat: add sponsors translations (#47)
This PR implements Internationalization (i18n) for the PyConES 2026 website, specifically focusing on the Sponsors page and landing components. It introduces support for English (en), Spanish (es), and Catalan (ca). Key Changes - i18n Infrastructure: * Added translation files (.ts) for all sponsor-related content (tiers, benefits, testimonials, and past sponsors). Implemented a middleware to handle automatic language detection based on the user's browser settings (Accept-Language). - Routing & Structure: Refactored page structure to use dynamic routing: moved pages to src/pages/[lang]/. Updated astro.config.mjs to configure the i18n routing strategy. - UI/UX Improvements: Localized the landing page elements (Hero title, subtitle, and CTA buttons). Updated the Layout.astro to dynamically set the lang attribute in the HTML tag for better accessibility and SEO. - Technical Cleanup: Centralized text constants to improve maintainability. Refactored the main index and sponsors pages to consume translated strings via a helper. - Languages Added: Spanish es English en Catalan ca --------- Co-authored-by: dani herrera <[email protected]> Co-authored-by: Francesc Arpí Roca <[email protected]>
1 parent a0b656a commit 0cbb97d

File tree

16 files changed

+1927
-846
lines changed

16 files changed

+1927
-846
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ dist/
1111

1212
# macOS-specific files
1313
.DS_Store
14+
15+
# vscode settings
16+
.vscode/

astro.config.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import { defineConfig } from 'astro/config';
2-
import tailwindcss from "@tailwindcss/vite";
1+
import { defineConfig } from 'astro/config'
2+
import tailwindcss from '@tailwindcss/vite'
33

44
export default defineConfig({
55
site: 'https://2026.es.pycon.org',
66
base: '/',
77
vite: {
88
plugins: [tailwindcss()],
99
},
10-
});
11-
10+
i18n: {
11+
defaultLocale: 'es',
12+
locales: ['es', 'en', 'ca'],
13+
routing: {
14+
prefixDefaultLocale: true,
15+
redirectToDefaultLocale: false
16+
},
17+
},
18+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
import { texts } from '../i18n/components/LanguagePicker'
3+
4+
// Idiomas disponibles
5+
const languages = [
6+
{ code: 'es', label: 'es', ariadesc: 'Idioma español' },
7+
{ code: 'en', label: 'en', ariadesc: 'English language' },
8+
{ code: 'ca', label: 'ca', ariadesc: 'Idioma català' },
9+
]
10+
11+
interface Props {
12+
lang: string
13+
}
14+
15+
const { lang = 'es' } = Astro.params
16+
const t = texts[lang as keyof typeof texts]
17+
18+
// Idioma actual detectado desde la URL
19+
const pathname = Astro.url.pathname
20+
const currentLang = pathname.split('/')[1] || 'es'
21+
22+
// Función para construir la URL destino
23+
const getPathForLang = (lang) => {
24+
const parts = pathname.split('/').filter(Boolean)
25+
26+
// Si ya hay idioma, lo reemplaza
27+
if (languages.some((l) => l.code === parts[0])) {
28+
parts[0] = lang
29+
} else {
30+
parts.unshift(lang)
31+
}
32+
33+
return '/' + parts.join('/')
34+
}
35+
---
36+
37+
<ul aria-label={t['alttext']} class="flex gap-6 p-2 pr-6 justify-end text-green-400 font-mono">
38+
{
39+
languages.map((lang) => (
40+
<li>
41+
<a
42+
href={getPathForLang(lang.code)}
43+
hreflang={lang.code}
44+
aria-current={lang.code === currentLang ? 'page' : undefined}
45+
aria-label={lang.ariadesc}
46+
lang={lang.code}
47+
class={lang.code === currentLang ? 'font-bold underline underline-offset-4' : ''}
48+
>
49+
{lang.label}
50+
</a>
51+
</li>
52+
))
53+
}
54+
</ul>

src/components/index.astro

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
---
2+
import Layout from '../layouts/Layout.astro'
3+
import '@fontsource-variable/jetbrains-mono'
4+
import { texts } from '../i18n/home'
5+
import { getRelativeLocaleUrl } from 'astro:i18n'
6+
7+
interface Props {
8+
lang: string
9+
}
10+
11+
const { lang } = Astro.props
12+
const t = texts[lang as keyof typeof texts]
13+
---
14+
15+
<Layout title="PyConES 2026">
16+
<main class="min-h-screen flex flex-col justify-center items-center text-center px-4">
17+
<div class="relative z-10 max-w-3xl">
18+
<h1
19+
class="text-4xl md:text-7xl font-bold text-white font-mono tracking-tighter cursor-default"
20+
aria-label="PyConES 2026"
21+
>
22+
<span aria-hidden="true" data-value="PyConES 2026" id="matrix-text"> PyConES 2026 </span>
23+
</h1>
24+
25+
<h2
26+
id="subtitle-container"
27+
class="mt-4 text-xl md:text-2xl text-green-400 font-mono h-8 flex justify-center items-center gap-2"
28+
aria-live="polite"
29+
aria-busy="true"
30+
>
31+
<span aria-hidden="true" class="select-none">&gt;</span>
32+
<span id="subtitle" data-final-text={t['index.subtitle']}>{t['index.initializing']}</span>
33+
<span class="animate-pulse motion-reduce:animate-none" aria-hidden="true">_</span>
34+
</h2>
35+
36+
<div
37+
id="actions"
38+
class="mt-12 flex flex-col md:flex-row gap-6 justify-center items-center opacity-0 transition-opacity duration-1000 ease-in"
39+
>
40+
<a href={getRelativeLocaleUrl(lang, 'sponsors')}>
41+
<button
42+
id="sponsor-btn"
43+
type="button"
44+
aria-describedby="sponsor-hint"
45+
class="group relative px-7 py-2 bg-green-500 text-black font-mono font-bold text-lg hover:bg-green-400 transition-all duration-300 shadow-[0_0_15px_rgba(34,197,94,0.5)] hover:shadow-[0_0_25px_rgba(34,197,94,0.8)] cursor-pointer focus:outline-none focus:ring-4 focus:ring-green-300/50 rounded-sm"
46+
>
47+
<span
48+
class="absolute inset-0 w-full h-full bg-white opacity-0 group-hover:opacity-20 transition-opacity pointer-events-none"
49+
></span>
50+
51+
<span id="sponsor-text">
52+
<span aria-hidden="true">&lt; </span><span lang="en">{t['index.sponsor_btn']}</span><span
53+
aria-hidden="true"
54+
>
55+
/&gt;</span
56+
>
57+
</span>
58+
</button>
59+
</a>
60+
</div>
61+
</div>
62+
63+
<div
64+
aria-hidden="true"
65+
class="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"
66+
>
67+
</div>
68+
</main>
69+
</Layout>
70+
71+
<script>
72+
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+'
73+
74+
let interval: number | null = null
75+
76+
const runMatrixEffect = (element: HTMLElement) => {
77+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
78+
if (prefersReducedMotion) return
79+
80+
let iteration = 0
81+
clearInterval(interval as number)
82+
const originalText = element.dataset.value || ''
83+
84+
interval = window.setInterval(() => {
85+
element.innerText = originalText
86+
.split('')
87+
.map((_, index) => {
88+
if (index < iteration) {
89+
return originalText[index]
90+
}
91+
return letters[Math.floor(Math.random() * letters.length)]
92+
})
93+
.join('')
94+
95+
if (iteration >= originalText.length) {
96+
clearInterval(interval as number)
97+
}
98+
99+
iteration += 1 / 3
100+
}, 30)
101+
}
102+
103+
const init = () => {
104+
const matrixText = document.getElementById('matrix-text')
105+
const h1 = document.querySelector('h1')
106+
const subtitle = document.getElementById('subtitle')
107+
const actions = document.querySelector('#actions')
108+
109+
if (matrixText && h1) {
110+
runMatrixEffect(matrixText)
111+
h1.onmouseover = () => runMatrixEffect(matrixText)
112+
}
113+
114+
if (subtitle) {
115+
setTimeout(() => {
116+
const finalText = subtitle.dataset.finalText
117+
if (finalText) {
118+
subtitle.textContent = finalText
119+
}
120+
const container = document.getElementById('subtitle-container')
121+
if (container) {
122+
container.setAttribute('aria-busy', 'false')
123+
}
124+
if (actions) {
125+
actions.classList.remove('opacity-0')
126+
}
127+
}, 1500)
128+
}
129+
}
130+
131+
//not needed now
132+
document.addEventListener('astro:page-load', init)
133+
init()
134+
</script>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const texts = {
2+
es: {
3+
alttext: 'Selector de idioma',
4+
},
5+
en: {
6+
alttext: 'Language selector',
7+
},
8+
ca: {
9+
alttext: "Selector d'idioma",
10+
},
11+
} as const

src/i18n/home.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export const texts = {
2+
es: {
3+
'index.initializing': 'Inicializando sistema...',
4+
'index.subtitle': 'Sede UB Barcelona | 6-8 Nov 2026',
5+
'index.sponsor_btn': 'PATROCINA',
6+
},
7+
en: {
8+
'index.initializing': 'Initialising system...',
9+
'index.subtitle': 'UB Barcelona Venue | Nov 6-8, 2026',
10+
'index.sponsor_btn': 'BECOME A SPONSOR',
11+
},
12+
ca: {
13+
'index.initializing': 'Inicialitzant sistema...',
14+
'index.subtitle': 'Seu UB Barcelona | 6-8 Nov 2026',
15+
'index.sponsor_btn': 'PATROCINA',
16+
},
17+
} as const

0 commit comments

Comments
 (0)