Documentation Index Fetch the complete documentation index at: https://mintlify.com/lewis-kori/astro-portfolio-v3/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Astro Portfolio v3 includes built-in internationalization (i18n) support, allowing the site to be available in multiple languages. The system uses Astro’s native i18n configuration combined with a custom translation utility.
The i18n configuration is defined in ~/workspace/source/astro.config.mjs:48-54 and translation utilities are in ~/workspace/source/src/utils/i18n.ts.
Supported Languages
The portfolio supports four languages:
English (en) Default language - no URL prefix required
Español (es) Spanish - accessible at /es/
Français (fr) French - accessible at /fr/
Deutsch (de) German - accessible at /de/
Configuration
Astro Config
The i18n setup in astro.config.mjs:
// astro.config.mjs:48-54
export default defineConfig ({
i18n: {
defaultLocale: 'en' ,
locales: [ 'en' , 'es' , 'fr' , 'de' ],
routing: {
prefixDefaultLocale: false ,
},
} ,
}) ;
Key settings:
defaultLocale: 'en' - English is the default language
prefixDefaultLocale: false - English pages don’t have /en/ prefix
All other languages use a prefix (/es/, /fr/, /de/)
Translation Definitions
Translations are defined in src/utils/i18n.ts:
// src/utils/i18n.ts:8-13
export const languages = {
en: 'English' ,
es: 'Español' ,
fr: 'Français' ,
de: 'Deutsch' ,
};
export const defaultLang = 'en' ;
Translation System
Translation Keys
The ui object contains all translations organized by language:
// src/utils/i18n.ts:17-66
export const ui = {
en: {
'nav.home' : 'Home' ,
'nav.about' : 'About' ,
'nav.projects' : 'Projects' ,
'nav.contact' : 'Contact' ,
'hero.title' : "Hi, I'm Lewis Kori" ,
'hero.subtitle' : 'Full-stack Developer & Tech Enthusiast' ,
'hero.cta' : 'View My Work' ,
'about.title' : 'About Me' ,
'projects.title' : 'My Projects' ,
'contact.title' : 'Get In Touch' ,
},
es: {
'nav.home' : 'Inicio' ,
'nav.about' : 'Sobre Mí' ,
'nav.projects' : 'Proyectos' ,
'nav.contact' : 'Contacto' ,
'hero.title' : 'Hola, soy Lewis Kori' ,
'hero.subtitle' : 'Desarrollador Full-stack y Entusiasta de la Tecnología' ,
'hero.cta' : 'Ver Mi Trabajo' ,
'about.title' : 'Sobre Mí' ,
'projects.title' : 'Mis Proyectos' ,
'contact.title' : 'Contactar' ,
},
fr: {
'nav.home' : 'Accueil' ,
'nav.about' : 'À Propos' ,
'nav.projects' : 'Projets' ,
'nav.contact' : 'Contact' ,
'hero.title' : 'Bonjour, je suis Lewis Kori' ,
'hero.subtitle' : 'Développeur Full-stack et Passionné de Technologie' ,
'hero.cta' : 'Voir Mon Travail' ,
'about.title' : 'À Propos' ,
'projects.title' : 'Mes Projets' ,
'contact.title' : 'Me Contacter' ,
},
de: {
'nav.home' : 'Startseite' ,
'nav.about' : 'Über Mich' ,
'nav.projects' : 'Projekte' ,
'nav.contact' : 'Kontakt' ,
'hero.title' : 'Hallo, ich bin Lewis Kori' ,
'hero.subtitle' : 'Full-stack Entwickler und Technik-Enthusiast' ,
'hero.cta' : 'Meine Arbeit Ansehen' ,
'about.title' : 'Über Mich' ,
'projects.title' : 'Meine Projekte' ,
'contact.title' : 'Kontaktieren' ,
},
} as const ;
Utility Functions
Getting Current Language
Extract the language from the URL:
// src/utils/i18n.ts:68-72
export function getLangFromUrl ( url : URL ) {
const [, lang ] = url . pathname . split ( '/' );
if ( lang in ui ) return lang as keyof typeof ui ;
return defaultLang ;
}
Usage:
---
import { getLangFromUrl } from '@/utils/i18n' ;
const currentLang = getLangFromUrl ( Astro . url );
// currentLang will be 'en', 'es', 'fr', or 'de'
---
Using Translations
Create a translation function for a specific language:
// src/utils/i18n.ts:74-78
export function useTranslations ( lang : keyof typeof ui ) {
return function t ( key : keyof ( typeof ui )[ typeof defaultLang ]) {
return ui [ lang ][ key ] || ui [ defaultLang ][ key ];
};
}
Usage in components:
---
import { getLangFromUrl , useTranslations } from '@/utils/i18n' ;
const lang = getLangFromUrl ( Astro . url );
const t = useTranslations ( lang );
---
< nav >
< a href = "/" > { t ( 'nav.home' ) } </ a >
< a href = "/about" > { t ( 'nav.about' ) } </ a >
< a href = "/projects" > { t ( 'nav.projects' ) } </ a >
< a href = "/contact" > { t ( 'nav.contact' ) } </ a >
</ nav >
< section >
< h1 > { t ( 'hero.title' ) } </ h1 >
< p > { t ( 'hero.subtitle' ) } </ p >
< button > { t ( 'hero.cta' ) } </ button >
</ section >
Language Switcher Component
The LanguageSwitcher component allows users to change languages:
---
// src/components/shared/LanguageSwitcher.astro:1-7
import { languages , getLangFromUrl } from '@/utils/i18n' ;
const currentLang = getLangFromUrl ( Astro . url );
const currentPath =
Astro . url . pathname . replace ( `/ ${ currentLang } ` , '' ). replace ( / ^ \/ / , '' ) || '/' ;
---
< div class = 'relative group' >
< button
class = 'p-2 rounded-md hover:bg-accent transition-colors'
aria-label = 'Change language'
>
<!-- Globe icon -->
</ button >
< div class = 'hidden group-hover:block absolute right-0 mt-2' >
{ Object . entries ( languages ). map (([ lang , name ]) => (
< a
href = { lang === 'en' ? `/ ${ currentPath } ` : `/ ${ lang } / ${ currentPath } ` }
class : list = { [
'block px-4 py-2 hover:bg-accent transition-colors' ,
{ 'bg-accent' : currentLang === lang }
] }
>
{ name }
</ a >
)) }
</ div >
</ div >
Features:
Shows current language with highlighting
Preserves the current path when switching languages
Handles the default locale (no prefix for English)
Location: src/components/shared/LanguageSwitcher.astro:1
URL Structure
Default Language (English)
/ → Homepage
/about → About page
/projects → Projects page
/blog/my-post → Blog post
Other Languages
/es/ → Spanish homepage
/es/about → Spanish about page
/fr/projects → French projects page
/de/blog/my-post → German blog post
Adding New Translations
Step 1: Add Translation Keys
Edit src/utils/i18n.ts and add new keys to all language objects:
export const ui = {
en: {
// Existing keys...
'footer.copyright' : '© 2026 All rights reserved' ,
'button.learn-more' : 'Learn More' ,
},
es: {
// Existing keys...
'footer.copyright' : '© 2026 Todos los derechos reservados' ,
'button.learn-more' : 'Aprende Más' ,
},
fr: {
// Existing keys...
'footer.copyright' : '© 2026 Tous droits réservés' ,
'button.learn-more' : 'En Savoir Plus' ,
},
de: {
// Existing keys...
'footer.copyright' : '© 2026 Alle Rechte vorbehalten' ,
'button.learn-more' : 'Mehr Erfahren' ,
},
} as const ;
Step 2: Use in Components
---
import { getLangFromUrl , useTranslations } from '@/utils/i18n' ;
const lang = getLangFromUrl ( Astro . url );
const t = useTranslations ( lang );
---
< footer >
< p > { t ( 'footer.copyright' ) } </ p >
< button > { t ( 'button.learn-more' ) } </ button >
</ footer >
Adding a New Language
To add a new language (e.g., Italian):
Step 1: Update Astro Config
// astro.config.mjs
export default defineConfig ({
i18n: {
defaultLocale: 'en' ,
locales: [ 'en' , 'es' , 'fr' , 'de' , 'it' ], // Add 'it'
routing: {
prefixDefaultLocale: false ,
},
} ,
}) ;
Step 2: Add Language to i18n Utilities
// src/utils/i18n.ts
export const languages = {
en: 'English' ,
es: 'Español' ,
fr: 'Français' ,
de: 'Deutsch' ,
it: 'Italiano' , // Add Italian
};
export const ui = {
en: { /* existing translations */ },
es: { /* existing translations */ },
fr: { /* existing translations */ },
de: { /* existing translations */ },
it: { // Add Italian translations
'nav.home' : 'Home' ,
'nav.about' : 'Chi Sono' ,
'nav.projects' : 'Progetti' ,
'nav.contact' : 'Contatto' ,
// ... all other keys
},
} as const ;
Step 3: Test the New Language
Visit /it/ to see the Italian version of your site.
Localized Content
For fully localized content (not just UI translations), you can organize content by language:
Content Structure
Querying by Language
Alternative Approach
src/content/blog/
├── en/
│ ├── post-1.md
│ └── post-2.md
├── es/
│ ├── post-1.md
│ └── post-2.md
└── fr/
├── post-1.md
└── post-2.md
---
import { getCollection } from 'astro:content' ;
import { getLangFromUrl } from '@/utils/i18n' ;
const lang = getLangFromUrl ( Astro . url );
// Filter blog posts by language
const posts = await getCollection ( 'blog' , ( entry ) => {
return entry . id . startsWith ( ` ${ lang } /` );
});
---
{ posts . map ( post => (
< article >
< h2 > { post . data . title } </ h2 >
</ article >
)) }
Add a lang field to your content schema: const blog = defineCollection ({
type: 'content' ,
schema: z . object ({
title: z . string (),
lang: z . enum ([ 'en' , 'es' , 'fr' , 'de' ]),
// other fields...
}),
});
Then filter by the field: ---
const posts = await getCollection ( 'blog' , ( entry ) => {
return entry . data . lang === lang ;
});
---
Type Safety
The i18n system is fully type-safe:
import { useTranslations } from '@/utils/i18n' ;
const t = useTranslations ( 'en' );
t ( 'nav.home' ); // ✓ Valid key
t ( 'nav.invalid' ); // ✗ TypeScript error
t ( 'nav.about' ); // ✓ Valid key
Best Practices
Use semantic keys - nav.home instead of home_button
Keep keys organized - Group by component or section
Provide fallbacks - The system falls back to English if a translation is missing
Translate all keys - Ensure all languages have all keys defined
Test all languages - Verify translations in context
Use proper Unicode - Include accented characters correctly
Consider RTL languages - Plan for right-to-left languages if needed
Keep translations short - UI space is limited, especially in mobile views
Advanced Features
Custom Language Detection
Add browser language detection:
export function detectLanguage () : keyof typeof ui {
if ( typeof window !== 'undefined' ) {
const browserLang = navigator . language . split ( '-' )[ 0 ];
if ( browserLang in ui ) {
return browserLang as keyof typeof ui ;
}
}
return defaultLang ;
}
Translation Pluralization
For more complex translations with plurals:
export function pluralize (
count : number ,
singular : string ,
plural : string
) : string {
return count === 1 ? singular : plural ;
}
// Usage
const t = useTranslations ( 'en' );
const count = 5 ;
const message = ` ${ count } ${ pluralize ( count , 'project' , 'projects' ) } ` ;
Localize dates using the Intl API:
---
const lang = getLangFromUrl ( Astro . url );
const date = new Date ( '2026-03-05' );
const formattedDate = new Intl . DateTimeFormat ( lang , {
year: 'numeric' ,
month: 'long' ,
day: 'numeric'
}). format ( date );
---
< time datetime = { date . toISOString () } >
{ formattedDate }
</ time >
Troubleshooting
Issue: Language switcher doesn’t preserve pathSolution: Make sure to remove the language prefix before constructing the new URL:const currentPath = Astro . url . pathname
. replace ( `/ ${ currentLang } ` , '' )
. replace ( / ^ \/ / , '' ) || '/' ;
Issue: Translations showing English fallbackSolution: Verify all translation keys exist in all language objects.Issue: TypeScript errors with translation keysSolution: The ui object uses as const for type inference. Make sure it’s applied.
Related Pages
Components See how LanguageSwitcher component works
Content Collections Learn about organizing localized content
Project Structure Understand where i18n files are located