DEV Community

Mete ARSLAN
Mete ARSLAN

Posted on

Astro Internationalization With Dynamic Routing

Table of Contents

  1. About The Project
  2. How To Implement
  3. Resources
  4. GitHub Link

About The Project

This project demonstrates an alternative approach to internationalization in Astro using dynamic routing and cookies. While disabling prerendering is not generally recommended, it is necessary on pages that rely on cookies for language detection. This method also doesn't prevent use of alternative translation methods.

You can also check libraries like astro-i18next, paraglide.

(back to top)

Built With

(back to top)

How To Implement

Project Structure

Project Structure

Configure Astro

First lets start with configuration. We have to define i18n settings. I used Turkish and English languages for my project and default locale for my website was Turkish. Also for this project Turkish language have been designated as fallback language.

Routing is set to manual. While options like prefixDefaultLocale are available, manual routing combined with middleware is easier to manage in this setup.

astro.config.mjs

import { defineConfig } from "astro/config";
import vue from "@astrojs/vue";
import tailwind from "@astrojs/tailwind";

// https://astro.build/config
export default defineConfig({
  i18n: {
    locales: ["tr", "en"],
    defaultLocale: "tr",
    fallback: {
      en: "tr",
    },
    routing: "manual",
  },
  integrations: [
    vue(),
    tailwind({
      applyBaseStyles: false,
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Translation Files

After some configuration we have to create our translation methods and files. The functions are straightforward. We get translations from json files and from the url we decide which language to use.

src/translate/index.ts

import tr from "./tr.json";
import en from "./en.json";

export const supportedLangues = ["en", "tr"];
export const defaultLang = "tr";

export const translations = {
  en,
  tr,
} as const;

export function getLangFromUrl(url: URL) {
  const lang = url.pathname.split("/").at(1)!;
  if (lang in translations) return lang as keyof typeof translations;
  return defaultLang;
}

function useTranslations(lang: keyof typeof translations) {
  return function t(key: keyof (typeof translations)[typeof defaultLang]) {
    return key in translations[lang]
      ? (translations[lang] as any)[key]
      : translations[defaultLang][key];
  };
}

export function getTranslation(url: URL) {
  const lang = getLangFromUrl(url);
  return useTranslations(lang);
}
Enter fullscreen mode Exit fullscreen mode

src/translate/en.json

{
  "hello": "Hello world"
}
Enter fullscreen mode Exit fullscreen mode

Set Language Endpoint

To hold user language preference I used cookies. Create an endpoint to set language cookie.

src/pages/api/set-language.ts

import type { APIRoute } from "astro";

export const prerender = false;

export const POST: APIRoute = async ({ cookies, request }) => {
  const language = await request.text();

  cookies.set("language", language, {
    httpOnly: true,
    sameSite: "strict",
    path: "/",
  });

  return new Response("", { status: 200 });
};
Enter fullscreen mode Exit fullscreen mode

Language Middleware

The middleware determines which endpoint the user should be redirected to. First we check language cookie to find if user has set a language. If cookie is undefined than we check preferredLocale from the request. If both language cookie and preferredLocale is undefined user will be redirected to default language.

You should also not interrupt the requests that are not require language redirection (Like API request, asset requests etc.). So we added ignorPath function.

src/middleware/index.ts

import { sequence } from "astro:middleware";
import { languageMiddleware } from "./language.middleware";

export const onRequest = sequence(languageMiddleware);
Enter fullscreen mode Exit fullscreen mode

src/middleware/language.middleware.ts

import { defineMiddleware } from "astro:middleware";
import { redirectToDefaultLocale } from "astro:i18n"; // function available with `manual` routing
import { supportedLangues } from "../translate";

export const languageMiddleware = defineMiddleware((ctx, next) => {
  const pathName = ctx.url.pathname;

  if (ignorePath(pathName)) {
    return next();
  }

  let cookieLang = ctx.cookies.get("language")?.value;

  if (!cookieLang && ctx.preferredLocale) {
    cookieLang = ctx.preferredLocale;
  }

  if (cookieLang && supportedLangues.includes(cookieLang)) {
    return ctx.redirect(`/${cookieLang}/${pathName}`, 302);
  }

  return redirectToDefaultLocale(ctx, 302);
});

function ignorePath(pathName: string) {
  const ignoredPaths = [
    ...supportedLangues.map((lang) => `/${lang}`),
    "/api",
    "/assets",
    "/static",
    "/.",
  ];

  return ignoredPaths.some((ignoredPath) => pathName.startsWith(ignoredPath));
}
Enter fullscreen mode Exit fullscreen mode

Disable Prerendering

You have to disable prerending in pages that you want to get language preference from cookies.

src/pages/index.astro

---
export const prerender = false;
---
Enter fullscreen mode Exit fullscreen mode

Add Dynamic Routing

Finally we have to add dynamic routing for creating multiple versions of pages. Create a [locale] folder under the pages and add your other pages that requires translation.

src/pages/[locale]/index.astro

---
import Layout from "@pw/layouts/Layout.astro";

export const prerender = false;

export async function getStaticPaths() {
  return [{ params: { locale: "en" } }, { params: { locale: "tr" } }];
}
---

<Layout title="Mete ARSLAN" />
Enter fullscreen mode Exit fullscreen mode

Add Components

I also added simple Layout.astro component for this project. There is a default slot for rendering page content and named slots for header, footer components.

src/layouts/Layout.astro

---
import DefaultFooter from "@pw/components/astro/footers/DefaultFooter.astro";
import DefaultHeader from "@pw/components/astro/headers/DefaultHeader.astro";
import "@pw/styles/main.scss";

interface Props {
  title: string;
}

const { title } = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="description" content="Astro description" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <title>{title}</title>
  </head>
  <body>
    <header>
      <slot name="header">
        <DefaultHeader />
      </slot>
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <slot name="footer">
        <DefaultFooter />
      </slot>
    </footer>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

To get translation call getTranslation function that we have created and give page's url to it. For this example I also passed targetLanguage as a parameter to a vue component for being able to swap between the languages. You can also use other frameworks or simply add inline javascript to Astro components.

src/components/astro/headers/DefaultHeader.astro

---
import { LanguageSwapButton } from "@pw/vue-components";
import { getTranslation } from "@pw/translate";

const translate = getTranslation(Astro.url);

const currentLanguage =
  Astro.cookies.get("language")?.value || Astro.preferredLocale || "tr"; // Default to "tr" if cookie is not set
const targetLanguage = currentLanguage === "en" ? "tr" : "en";
---

<div>
  <LanguageSwapButton client:load targetLanguage={targetLanguage}>
    <pre>{translate('hello')}</pre>
  </LanguageSwapButton>
</div>
Enter fullscreen mode Exit fullscreen mode

Call Set Language Endpoint

To change the language, send a request to /api/set-language.

src/components/vue/buttons/LanguageSwapButton.vue

<script lang="ts" setup>
import Button from "../shadcn/buttons/Button.vue";

const { targetLanguage } = defineProps({
  targetLanguage: {
    type: String,
    required: true,
  },
});

async function onClick() {
  await fetch(`/api/set-language`, {
    method: "POST",
    headers: { "Content-Type": "application/text" },
    body: targetLanguage,
  });

  window.location.href = "/" + targetLanguage;
}
</script>

<template>
  <Button variant="outline" size="sm" @click="onClick()">
    <slot />
  </Button>
</template>
Enter fullscreen mode Exit fullscreen mode

(back to top)

Resources

(back to top)

GitHub Link

Top comments (0)