Dynamic Routing Documentation with i18n

Since Astro doesn’t natively support URL localization out of the box, a different approach was needed to enable multilingual pages. This project uses dynamic parameters ([...pages]) to implement localized paths.

πŸ—οΈ Project Architecture Overview

The project implements an internationalization (i18n) system that enables:

πŸ“ File Structure

src/
β”œβ”€β”€ components/           # Components (Header, Footer, LanguagePicker)
β”œβ”€β”€ content/              # Content (blogs, authors)
β”‚   β”œβ”€β”€ blog/
β”‚   β”‚   β”œβ”€β”€ en/           # English posts
β”‚   β”‚   └── sl/           # Slovenian posts
β”‚   └── authors/          # Author data
β”œβ”€β”€ data/                 # Navigation data
β”‚   └── navigationData.ts
β”œβ”€β”€ i18n/                 # Internationalization system
β”‚   β”œβ”€β”€ routes.ts         # Path translations
β”‚   β”œβ”€β”€ ui.ts             # Language configuration
β”‚   └── utils.ts          # Utility functions
β”œβ”€β”€ layouts/              # Base layouts
β”œβ”€β”€ locales/              # Translation files
β”‚   β”œβ”€β”€ en/               # English translations
β”‚   └── sl/               # Slovenian translations
β”œβ”€β”€ pages/                # Pages with dynamic routing
β”‚   β”œβ”€β”€ [about]/          # About pages
β”‚   β”‚   β”œβ”€β”€ [...index].astro
β”‚   β”‚   β”œβ”€β”€ _about-en.mdx
β”‚   β”‚   └── _about-sl.mdx
β”‚   β”œβ”€β”€ [blog]/           # Blog detail pages
β”‚   β”‚   └── [...slug].astro
β”‚   β”œβ”€β”€ [dyn_routing]/    # Dynamic routing examples
β”‚   β”‚   β”œβ”€β”€ [subpage2]/
β”‚   β”‚   β”‚   └── [...index].astro
β”‚   β”‚   └── [...subpage1].astro
β”‚   β”œβ”€β”€ [pages]/          # General pages
β”‚   β”‚   β”œβ”€β”€ [...index].astro
β”‚   β”‚   β”œβ”€β”€ _pages-en.mdx
β”‚   β”‚   └── _pages-sl.mdx
β”‚   β”œβ”€β”€ [pagination]/     # Pagination examples
β”‚   β”‚   └── [...page].astro
β”‚   β”œβ”€β”€ [...blog].astro   # Blog listing page
β”‚   β”œβ”€β”€ [...contact].astro # Contact page
β”‚   β”œβ”€β”€ [...index].astro  # Home page
β”‚   └── 404.astro         # Error page
└── styles/              # Style files

πŸ”€ How Dynamic Routing Works

1. URL Structure

Page PatternEnglish URLSlovenian URLDescription
[...index].astro//sl/Home page
[about]/[...index].astro/about/sl/o-projektuAbout
[...contact].astro/contact/sl/kontaktContact
[blog]/[...slug].astro/blog/post/sl/spletni-dnevnik/objavaBlog posts

2. Visual Routing Flow

File: [about]/[...index].astro
β”‚
β”œβ”€β”€ English path
β”‚   β”œβ”€β”€ URL: /about
β”‚   β”œβ”€β”€ Parameters: { about: "about", index: undefined }
β”‚   β”œβ”€β”€ Props: { lang: "en" }
β”‚   └── Content: _about-en.md
β”‚
└── Slovenian path
    β”œβ”€β”€ URL: /sl/o-projektu
    β”œβ”€β”€ Parameters: { about: "sl", index: "o-projektu" }
    β”œβ”€β”€ Props: { lang: "sl" }
    └── Content: _about-sl.md

3. Page Structures

There are two main patterns for implementing dynamic pages:

Pattern A: [...pages].astro

export function getStaticPaths() {
    return [
        // English path: /pages
        // [...pages] = "/pages" captures the entire path segment
        // Creates URL: /pages
        { params: { pages: "/pages" }, props: { lang: "en" } },
        // Slovenian path: /sl/strani
        // [...pages] = "sl/strani" captures language prefix and localized path
        // Creates URL: /sl/strani ("strani" = "pages" in Slovenian)
        { params: { pages: `/sl/strani` }, props: { lang: "sl" } },
    ];
}

Pattern B: [pages]/[...index].astro

export function getStaticPaths() {
    return [
        // English path: /pages
        // [pages]/[...index.astro] captures the entire path segment
        // English: { params: { pages: "pages", index: undefined }, props: { lang: "en" } } β†’ /pages
        { params: { pages: "pages", index: undefined }, props: { lang: "en" } },
        // Slovenian path: sl/strani
        // [pages]/[...index.astro] captures the entire path segment
        // Slovenian: { params: { pages: "sl", index: "strani" }, props: { lang: "sl" } } β†’ /sl/strani
        { params: { pages: "sl", index: "strani" }, props: { lang: "sl" } },
    ];
}

βš™οΈ Key System Components

1. src/i18n/routes.ts - Path Translations

This file defines mappings between English and localized paths:

export const routes: Record<string, Record<string, string>> = {
    sl: {
        about: "o-projektu",
        blog: "spletni-dnevnik",
        services: "storitve",
        pages: "strani",
        contact: "contact",
    },
};

Important: Keys must match parameters in getStaticPaths() functions!

2. src/i18n/ui.ts - Language Configuration

This file contains key configurations for language support:

Note: The defaultLang and showDefaultLang functionalities are not fully implemented and represent opportunities for future development.

export const languages = {
    en: "English",
    sl: "Slovenian",
};

export const defaultLang = "en";
export const showDefaultLang = false;

3. src/i18n/utils.ts - Utility Functions

getLangFromUrl(url: URL)

Extracts the current language from the URL:

const lang = getLangFromUrl(Astro.url); // "sl" or "en"

useTranslations(lang: string)

Returns a function for translating texts:

const t = useTranslations(lang);
const title = t("main:head.title");
const navHome = t("menu.list.home"); // Uses "common" namespace

Supported key formats:

useTranslatedPath(lang: string)

Returns a function for translating paths:

const translatePath = useTranslatedPath(lang);
<a href={translatePath("/about")}>About</a>; // "/o-projektu" for Slovenian

switchLanguageUrl(currentUrl: URL, targetLang: string)

Enables language switching while preserving current page context:

const newUrl = await switchLanguageUrl(Astro.url, "sl");

4. src/data/navigationData.ts - Navigation

const navigationData = [
    {
        label: "menu.list.home", // Translation key
        href: "/", // English path (default)
        children: [],
    },
    {
        label: "menu.list.service",
        href: "/services",
        children: [
            {
                label: "menu.list.subpage-1",
                href: "/services/service-1",
            },
        ],
    },
];

Note: URLs in navigation are always in English because they’re automatically localized via translatePath().

πŸ—£οΈ Translation System

1. Translation File Structure

src/locales/
β”œβ”€β”€ en/
β”‚   β”œβ”€β”€ common.json    # Shared translations (navigation, footer)
β”‚   β”œβ”€β”€ main.json      # Main page
β”‚   β”œβ”€β”€ about.json     # About page
β”‚   └── blog.json      # Blog page
└── sl/
    β”œβ”€β”€ common.json
    β”œβ”€β”€ main.json
    β”œβ”€β”€ about.json
    └── blog.json

2. Translation File Example (common.json)

{
    "menu": {
        "list": {
            "home": "Home",
            "about": "About",
            "blog": "Blog"
        }
    },
    "footer": {
        "made": "Made with {{what}}"
    }
}

Note: For common.json files, you don’t need to prefix with common:. Use t("menu.list.home") directly, not t("common:menu.list.home").

3. Using Translations in Components

---
import { useTranslations } from "@i18n/utils";
const t = useTranslations(lang);
---

<h1>{t("main:hero.title")}</h1>
<p>{t("menu.list.home")}</p>
<footer>{t("footer.made", { what: "Astro" })}</footer>

πŸ“ Blog System with linkedContent

1. Linking Content Between Languages

The blog system enables linking posts between different languages. The key question is: how does the system know the user is still on the same post when switching languages?

πŸ”— Solution: Each blog post must have a linkedContent identifier in the frontmatter. This identifier connects posts in different languages that cover the same topic. When users switch languages, the system uses this identifier to find the corresponding post in the target language.

Each blog post must have a linkedContent identifier in the frontmatter:

English post (en/security-trends.md):

---
title: "Top Security Trends for 2025"
linkedContent: "security-trends-2025"
author: "Nik Klemenc"
---

Slovenian post (sl/varnostni-trendi-2025.md):

---
title: "Glavni varnostni trendi za leto 2025"
linkedContent: "security-trends-2025" # Same identifier!
author: "Nik Klemenc"
---

2. Authors with Multilingual Data

The system enables linking authors with blog posts. In this case, a JSON format is used where each author has multilingual data. Under the β€œposition” key, positions are defined in English and Slovenian, which are then dynamically displayed based on the selected language.

{
    "nik-klemenc": {
        "name": "Nik Klemenc",
        "image": "./nik.jpg",
        "position": {
            "en": "Full-stack Developer",
            "sl": "Full-stack razvijalec"
        }
    }
}

πŸ“„ Pagination

The project includes an example of pagination in [pagination]/[...page].astro, implemented using Astro’s default paginate() and adapted for localized URLs.

// Pagination with Astro's paginate()
export const getStaticPaths = async ({ paginate }) => {
    const posts = /* fetch posts and sort per language */ [];
    return paginate(posts, {
        pageSize: 4,
        params: { pagination: "blog-pagination" },
        props: {
            /* lang, authors, totals */
        },
    });
};

πŸ’» Usage Examples

1. Basic Page with Dynamic Routing

---
import { useTranslations } from "@i18n/utils";
export function getStaticPaths() {
    return [
        { params: { pages: "/services" }, props: { lang: "en" } },
        { params: { pages: "/sl/storitve" }, props: { lang: "sl" } }
    ];
}

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

<Base title={t("services:head.title")}>
    <h1>{t("services:title")}</h1>
</Base>

2. Language Switching

---
// LanguagePicker.astro - actual implementation
import { switchLanguageUrl, getLangFromUrl, useTranslations } from "@i18n/utils";
import { languages } from "@i18n/ui";

// Get current language
const currentLang = getLangFromUrl(Astro.url);
const t = useTranslations(currentLang);

// Prepare URLs for all languages
const languageUrls = await Promise.all(
    Object.entries(languages).map(async ([lang, label]) => {
        const targetUrl = await switchLanguageUrl(Astro.url, lang);
        const translatedLabel = t(`menu.languages.${lang}`);
        return { lang, label: translatedLabel, targetUrl };
    })
);
---

<!-- Dropdown selector -->
<select
    name="language"
    onchange="window.location.href = this.value"
    aria-label={t("menu.languagesText.selectLanguage")}
>
    {languageUrls.map(({ lang, label, targetUrl }) => (
        <option
            value={targetUrl}
            selected={lang === currentLang}
        >
            {label}
        </option>
    ))}
</select>
---
import { useTranslatedPath } from "@i18n/utils";

const translatePath = useTranslatedPath(lang);
---

<a href={translatePath("/about")}>
    {t("menu.list.about")}
</a>

✨ Best Practices

1. File Naming

2. Translation Keys

3. Content Linking

4. SEO Optimization

❓ Frequently Asked Questions

Why don’t you use Astro’s built-in i18n functionality?

Although Astro supports internationalization, it doesn’t support β€œout of the box” URL localization (e.g., /about β†’ /sl/o-projektu). This project implements fully localized URLs with dynamic parameters, enabling complete control over path structure and SEO optimization.

How do I add a new language?

  1. Add the language to src/i18n/ui.ts
  2. Create a translation folder in src/locales/[lang]/
  3. Add path mappings in src/i18n/routes.ts
  4. Update getStaticPaths() in all page files

How does language switching work for blog posts?

The system uses linkedContent identifiers to link posts between languages. The switchLanguageUrl() function automatically finds the corresponding post in the target language.

Can I use relative paths?

No, always use absolute paths with a leading slash (/about, not about). The translation system relies on consistent path structure.

🎯 This approach enables complete localization of Astro applications while maintaining static generation speed and SEO optimization.