Skip to main content
The Chapinismos project supports multiple languages (Spanish and English) through a custom internationalization system built with Astro’s dynamic routing.

Translation System

Translations are managed in a centralized file at src/utils/i18n.ts. The system uses a simple key-value object structure for each language.

Translation Object Structure

export const translations = {
  es: {
    "home.title": "Chapinismos - Diccionario de Palabras Guatemaltecas",
    "home.description": "Diccionario completo de chapinismos...",
    "search.title": "Buscar",
    "nav.home": "Inicio",
    // ... more translations
  },
  en: {
    "home.title": "Chapinismos - Guatemalan Slang Dictionary",
    "home.description": "Complete dictionary of Guatemalan slang...",
    "search.title": "Search",
    "nav.home": "Home",
    // ... more translations
  },
};

Core i18n Functions

getLangFromUrl()

Extracts the language from the URL pathname.
export function getLangFromUrl(url: URL): "es" | "en" {
  const [, lang] = url.pathname.split("/");
  if (lang === "en") return "en";
  return "es";
}
Usage:
const lang = getLangFromUrl(Astro.url);
// Returns "es" or "en" based on URL like /es/buscar or /en/search

useTranslations()

Returns a translation function for the specified language with support for variable interpolation.
export function useTranslations(lang: string | undefined) {
  return (key: string, vars?: Record<string, string | number>) => {
    const locale = (lang === "en" ? "en" : "es") as "es" | "en";
    let text = translations[locale][key as keyof typeof translations.es] || key;

    if (vars) {
      Object.entries(vars).forEach(([varKey, value]) => {
        text = text.replace(`{${varKey}}`, String(value));
      });
    }

    return text;
  };
}
Usage in Astro components:
---
import { useTranslations } from "../../utils/i18n";
const { lang } = Astro.params;
const t = useTranslations(lang);
---

<h1>{t("search.title")}</h1>
<p>{t("index.subtitle", { count: 150 })}</p>

getLocalizedPath()

Generates localized URL paths with language prefixes.
export function getLocalizedPath(path: string, lang: string): string {
  const prefix = lang === "en" ? "/en" : "/es";
  return `${prefix}${path}`;
}
Usage:
const searchPath = getLocalizedPath("/buscar/", "en");
// Returns: "/en/buscar/"

Dynamic Routing

Pages use Astro’s [lang] dynamic parameter to generate static routes for each language. Example from src/pages/[lang]/buscar.astro:
---
export const prerender = true;

export async function getStaticPaths() {
  return [{ params: { lang: "es" } }, { params: { lang: "en" } }];
}

const { lang } = Astro.params;
const t = useTranslations(lang);

const collectionName = lang === "es" ? "words-es" : "words-en";
const allWords = await getCollection(collectionName);
---
This generates:
  • /es/buscar/ (Spanish)
  • /en/buscar/ (English)

Translation Categories

Translations are organized by feature area:
CategoryKeysPurpose
home.*Title, description, search, about sectionsHomepage content
search.*Search UI, results, categoriesSearch functionality
word.*Definition, examples, synonymsWord detail pages
nav.*Navigation linksSite navigation
schema.*SEO metadataStructured data

Variable Interpolation

Translation keys support variable interpolation using {variableName} syntax. Translation definition:
"index.subtitle": "Explora las {count} palabras del diccionario"
Usage:
{t("index.subtitle", { count: allWords.length })}
// Output: "Explora las 150 palabras del diccionario"

Adding New Languages

1

Add translations object

Add a new language object to the translations export in src/utils/i18n.ts:
export const translations = {
  es: { /* ... */ },
  en: { /* ... */ },
  pt: { // Portuguese example
    "home.title": "Chapinismos - Dicionário de Gírias Guatemaltecas",
    // ... all other keys
  },
};
2

Update type definitions

Update the return type of getLangFromUrl() to include the new language:
export function getLangFromUrl(url: URL): "es" | "en" | "pt" {
  const [, lang] = url.pathname.split("/");
  if (lang === "en") return "en";
  if (lang === "pt") return "pt";
  return "es";
}
3

Add to getStaticPaths()

Update each page’s getStaticPaths() function to include the new language:
export async function getStaticPaths() {
  return [
    { params: { lang: "es" } },
    { params: { lang: "en" } },
    { params: { lang: "pt" } },
  ];
}
4

Create content collections

Create a new content collection for the language:
src/content/words-pt/

SEO Integration

Translations are used in meta tags and structured data:
<Base
  title={t("search.title") + " — " + t("schema.site.name")}
  description={t("search.description")}
  keywords={t("search.keywords")}
  lang={lang}
  alternateUrls={{
    es: "/es/buscar/",
    en: "/en/buscar/",
    xDefault: "/es/buscar/",
  }}
/>
This generates proper hreflang tags and localized Open Graph metadata.

Client-Side Translations

For client-side JavaScript, translations are passed via define:vars:
<script
  is:inline
  define:vars={{
    searchTranslations: JSON.stringify({
      noResults: t("search.noResults"),
      tryAnother: t("search.tryAnother"),
      categories: {
        sustantivo: t("search.category.sustantivo"),
        verbo: t("search.category.verbo"),
      },
    }),
  }}
>
  const t = JSON.parse(searchTranslations);
  console.log(t.noResults); // Accessible in client JS
</script>

Best Practices

Follow the pattern section.subsection.key for organization:
  • home.search.placeholder
  • search.category.sustantivo
  • word.definition
The system defaults to Spanish (es) if an invalid language is detected, ensuring content is always displayed.
Ensure every user-facing string has entries in both es and en objects to prevent missing translations.
For counts, names, or other dynamic values, use variable interpolation instead of string concatenation:
// Good
t("index.subtitle", { count: 150 })

// Avoid
"Explora las " + count + " palabras"

Build docs developers (and LLMs) love