Skip to main content

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:
  1. Astro i18n routing - Handles URL structure and locale detection
  2. Translation utilities - Provides translation functions and helpers
  3. 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:
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:
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:
1

Get the current language

Use getLangFromUrl() to detect the language from the URL:
---
import { getLangFromUrl, useTranslations } from '../i18n/utils';

const lang = getLangFromUrl(Astro.url);
---
2

Create translation function

Use useTranslations() to create a t() function:
---
const t = useTranslations(lang);
---
3

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):
1

Update Astro config

Add the new locale to astro.config.mjs:
astro.config.mjs
i18n: {
  defaultLocale: 'es',
  locales: ['es', 'en', 'fr'],
  routing: {
    prefixDefaultLocale: true,
  },
}
2

Add to languages object

Update the languages object in src/i18n/utils.ts:
src/i18n/utils.ts
export const languages = {
  es: 'Español',
  en: 'English',
  fr: 'Français',
};
3

Add translations

Create a new translation object:
src/i18n/utils.ts
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;
4

Create language pages

Create src/pages/fr/index.astro:
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>
5

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'

Type Safety

The translation system is fully type-safe thanks to TypeScript:
src/i18n/utils.ts
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:
Multi-language Selector
---
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

Language Meta Tags

Ensure proper SEO by setting the HTML lang attribute:
src/layouts/Layout.astro
---
const lang = getLangFromUrl(Astro.url);
---

<html lang={lang}>
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

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

Build docs developers (and LLMs) love