Overview
The portfolio uses Astro’s built-in i18n routing combined with a custom translation system. It currently supports Spanish (default) and English, with the ability to easily add more languages.
How i18n Works
The internationalization system consists of three main parts:
Astro i18n routing - Handles URL structure and locale detection
Translation utilities - Provides translation functions and helpers
Language-specific pages - Separate routes for each language
URL Structure
All routes are prefixed with the language code:
Spanish: https://yoursite.com/es/
English: https://yoursite.com/en/
Root redirect: https://yoursite.com/ → https://yoursite.com/es/
Configuration
Astro i18n Config
The i18n configuration is defined in astro.config.mjs:
export default defineConfig ({
i18n: {
defaultLocale: 'es' ,
locales: [ 'es' , 'en' ],
routing: {
prefixDefaultLocale: true ,
},
} ,
}) ;
prefixDefaultLocale: true ensures even the default language (Spanish) has a /es/ prefix in the URL.
Translation System
Translations are managed in src/i18n/utils.ts:
export const languages = {
es: 'Español' ,
en: 'English' ,
};
export const defaultLang = 'es' ;
export const translations = {
es: {
'nav.home' : 'Inicio' ,
'nav.about' : 'Sobre mí' ,
'hero.name' : 'Kevin Maximiliano Palma Romero' ,
// ... more translations
},
en: {
'nav.home' : 'Home' ,
'nav.about' : 'About' ,
'hero.name' : 'Kevin Maximiliano Palma Romero' ,
// ... more translations
},
} as const ;
Using Translations
In Astro Components
To use translations in your components:
Get the current language
Use getLangFromUrl() to detect the language from the URL: ---
import { getLangFromUrl , useTranslations } from '../i18n/utils' ;
const lang = getLangFromUrl ( Astro . url );
---
Create translation function
Use useTranslations() to create a t() function: ---
const t = useTranslations ( lang );
---
Use translations in markup
Call t() with translation keys: < h1 > { t ( 'hero.name' ) } </ h1 >
< p > { t ( 'hero.bio' ) } </ p >
Complete Example
src/components/Hero.astro
---
import { getLangFromUrl , useTranslations } from '../i18n/utils' ;
const lang = getLangFromUrl ( Astro . url );
const t = useTranslations ( lang );
---
< section >
< h1 class = "text-4xl font-bold" >
{ t ( 'hero.greeting' ) } { t ( 'hero.name' ) }
</ h1 >
< p class = "text-xl text-slate-600 dark:text-slate-400" >
{ t ( 'hero.role' ) }
</ p >
< p class = "mt-4" >
{ t ( 'hero.bio' ) }
</ p >
</ section >
Translation Utilities
The src/i18n/utils.ts file exports several helper functions:
getLangFromUrl(url: URL)
Extracts the language code from the URL pathname:
export function getLangFromUrl ( url : URL ) {
const [, lang ] = url . pathname . split ( '/' );
if ( lang in translations ) return lang as keyof typeof translations ;
return defaultLang ;
}
Usage:
const lang = getLangFromUrl ( Astro . url ); // 'es' or 'en'
useTranslations(lang)
Returns a translation function for the specified language:
export function useTranslations ( lang : keyof typeof translations ) {
return function t ( key : TranslationKey ) {
return translations [ lang ][ key ] || translations [ defaultLang ][ key ];
};
}
Features:
Type-safe translation keys
Automatic fallback to default language
Returns the translation string
getLocalizedPath(lang, hash)
Generates a localized path with optional hash:
export function getLocalizedPath ( lang : string , hash : string = '' ) {
return `/ ${ lang } / ${ hash } ` ;
}
Usage:
getLocalizedPath ( 'en' , '#contact' ) // '/en/#contact'
Adding New Languages
Follow these steps to add a new language (e.g., French):
Update Astro config
Add the new locale to astro.config.mjs: i18n : {
defaultLocale : 'es' ,
locales : [ 'es' , 'en' , 'fr' ],
routing : {
prefixDefaultLocale : true ,
},
}
Add to languages object
Update the languages object in src/i18n/utils.ts: export const languages = {
es: 'Español' ,
en: 'English' ,
fr: 'Français' ,
};
Add translations
Create a new translation object: export const translations = {
es: { /* Spanish translations */ },
en: { /* English translations */ },
fr: {
'nav.home' : 'Accueil' ,
'nav.about' : 'À propos' ,
'hero.greeting' : 'Bonjour, je suis' ,
// ... add all translation keys
},
} as const ;
Create language pages
Create src/pages/fr/index.astro: ---
import Layout from '../../layouts/Layout.astro' ;
import Hero from '../../components/Hero.astro' ;
import About from '../../components/About.astro' ;
// ... other components
const pageTitle = 'Kevin Palma - Azure Cloud Engineer | Portfolio' ;
---
< Layout title = { pageTitle } >
< Hero />
< About />
<!-- ... other sections -->
</ Layout >
Add CV/Resume
Create public/cv/fr/ directory and add French CV file.
Ensure ALL translation keys are present in the new language, otherwise the system will fall back to the default language for missing keys.
Managing Translations
Translation Keys Structure
Translation keys follow a hierarchical naming convention:
'section.element' : 'Translation'
Examples:
nav.home - Navigation: Home link
hero.greeting - Hero section: Greeting text
about.title - About section: Title
contact.email - Contact section: Email label
Adding New Translation Keys
When adding new content:
Choose a descriptive key following the naming convention: 'section.element.variation' : 'Value'
Add the key to EVERY language in the translations object: es : {
'hero.cta' : 'Descargar CV' ,
},
en : {
'hero.cta' : 'Download CV' ,
}
Reference the key in your component: < button > { t ( 'hero.cta' ) } </ button >
Type Safety
The translation system is fully type-safe thanks to TypeScript:
export type TranslationKey = keyof typeof translations . es ;
This means:
✅ Autocomplete for translation keys
✅ Compile-time error for invalid keys
✅ Refactoring support
Language Selector
The language selector is implemented in src/components/LanguageSelector.astro:
src/components/LanguageSelector.astro
---
import { getLangFromUrl , languages } from '../i18n/utils' ;
const lang = getLangFromUrl ( Astro . url );
const otherLang = lang === 'es' ? 'en' : 'es' ;
const hash = Astro . url . hash || '' ;
---
< a
href = { `/ ${ otherLang } / ${ hash } ` }
class = "flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium"
aria-label = { `Switch to ${ languages [ otherLang ] } ` }
>
< svg class = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
<!-- Globe icon -->
</ svg >
{ languages [ otherLang ] }
</ a >
Features
Preserves URL hash when switching languages
Shows the name of the target language
Accessible with proper aria-label
Smooth transitions
Supporting More Than Two Languages
For 3+ languages, replace the toggle with a dropdown:
---
import { getLangFromUrl , languages } from '../i18n/utils' ;
const currentLang = getLangFromUrl ( Astro . url );
const hash = Astro . url . hash || '' ;
---
< div class = "relative" >
< button id = "lang-dropdown" class = "flex items-center gap-2" >
< span > { languages [ currentLang ] } </ span >
< svg > <!-- Chevron icon --> </ svg >
</ button >
< div id = "lang-menu" class = "hidden absolute" >
{ Object . entries ( languages ). map (([ code , name ]) => (
< a href = { `/ ${ code } / ${ hash } ` } class = "block px-4 py-2" >
{ name }
</ a >
)) }
</ div >
</ div >
SEO Considerations
Ensure proper SEO by setting the HTML lang attribute:
---
const lang = getLangFromUrl ( Astro . url );
---
< html lang = { lang } >
Alternate Links
Add alternate links for better SEO:
< link rel = "alternate" hreflang = "es" href = "https://yoursite.com/es/" />
< link rel = "alternate" hreflang = "en" href = "https://yoursite.com/en/" />
< link rel = "alternate" hreflang = "x-default" href = "https://yoursite.com/es/" />
Best Practices
Keep translations synchronized
Always add new keys to all languages at the same time to avoid missing translations.
Make translation keys self-documenting: hero.greeting is better than text1.
All user-facing text should come from the translation system, even if it seems unlikely to be translated.
Always test your site in all supported languages to ensure nothing breaks with longer/shorter translations.
Some languages (like German) can be 30% longer than English. Design with flexibility in mind.
Next Steps
Customization Learn how to customize your portfolio’s appearance
Deployment Deploy your multilingual portfolio to production