Vojtěch Struhár

Posted on October 9, 2024 | #astro , #web

Localization in Astro

If you want multiple languages on a single website (domain), check out Astro’s localization routing. In this guide, I’ll describe how to build an Astro website in different languages for different domains.

Let’s get started.

Define translations in TypeScript

I want all my translations to have an english fallback. That means, the en translation has to cover every translation key that appears on my website, so let’s define english first:

const english = {
  "home.coming-soon": "COMING SOON",
  "newsletter.email-placeholder": "Enter your email",
  "newsletter.sign-up": "Sign-up"
}

That could be enough for a simple Coming soon website with a newsletter sign up.

Because I now have all possible translation keys, I can type check my translations to other languages.

/** All localizations are a subset of the english translation */
export type TranslationKey = keyof typeof english;
type OtherTranslation = Partial<Record<TranslationKey, string>>;

I make use of Partial type here, because other translations don’t have to override everything that english defines.

Now I can type annotate each following language object as OtherTranslation. Typescript will give me hints when filling in the keys and will prevent me from making typos.

Here is a (partial!) translation to italian:

const italian: OtherTranslation = {
  "newsletter.email-placeholder": "Inserisci la tua email",
  "newsletter.sign-up": "Iscriviti",
};

Notice that the italian translation doesn’t override the home.coming-soon key, so that one will display the english version!

Finally I export all the languages in a single object. I also explicitly mark the english version as the fallback translation.

export const fallbackTranslation: Record<TranslationKey, string> = english
export const translations: Record<string, OtherTranslation> = {
  en: english,
  it: italian
};

Localize strings

Now I need to make use of all the hard work I put into my translations. I want to use my translations like this, so let’s go from there:

Localize("newsletter.sign-up")

For that I’ll create a separate file, localization.ts. Here I’ll just check that the specified translation really exists and then define my simple function:

import { fallbackTranslation, translations, type TranslationKey } from "./translations";

const language = import.meta.env.LANGUAGE;
if (language !== undefined && !(language in translations)) {
  throw new Error("INVALID LANGUAGE: " + language);
}

const preferredLocalization = translations[language] || {};

export function Localize(key: TranslationKey): string {
  let preferredText = preferredLocalization[key]
  return preferredText || fallbackTranslation[key];
}

If the LANGUAGE env variable is not specified, it won’t complain and just use the english version for everything.

Use in Astro component

---
import Layout from "../layouts/Layout.astro";
import { Localize } from "../utils/localization";
---

<Layout>
    <main>
        <h1>{Localize("home.coming-soon")}</h1>
    </main>
</Layout>

Typescript gives autocompletion on the translation key, which prevents typos. Very nice!

One caveat that I found with this method is that you can’t really use the Localize function in an inline <script> tag. Astro would import the function and send all translations to the client in a bundle. See how to pass frontmatter variables to scripts, maybe that will work for you!

Build it, ship it

There are multiple ways to deal with environment variables in Astro. But since each website build is going to have a different env variable, I find it easiest to just supply it directly with the build command:

LANGUAGE=it npm run build --outDir dist-it

This command will build the italian version of the website. The vanilla npm run build will produce the default english version.

You can configure this env variable in your CI/CD or (as in my case) just zip it and upload it to your web server. It’s just static HTML files, the way Astro intended ❤️

Read next: