How to internationalize an AstroJS website while maintaining good SEO ?
Preparation
Setup the languages and translations
First, we need to create a /src/i18n
folder which contains the languages and the translations. Here is my current implementation:
export const EN: Array<{ id: string; translation: string }> = [ { id: "hello", translation: "hello" }, { id: "world", translation: "world" },];
export const FR: Array<{ id: string; translation: string }> = [ { id: "hello", translation: "bonjour" }, { id: "world", translation: "monde" },];
export { EN } from "./en";export { FR } from "./fr";
import { EN } from "./en";import { FR } from "./fr";
export type Language = "fr" | "en";
export const TRANSLATIONS: Map<Language, Map<string, string>> = new Map([ ["fr", new Map(FR.map((obj) => [obj.id, obj.translation]))], ["en", new Map(EN.map((obj) => [obj.id, obj.translation]))],]);
Create the pages
/src/pages/[lang]/index.astro
---import type { Languages } from "src/i18n";
export function getStaticPaths() { return [{ params: { lang: "fr" } }, { params: { lang: "en" } }];}
const lang = Astro.params.lang as Languages;---
<h1>Homepage {lang}</h1>
<ComponentUsingI18n />
Problem
AstroJS renders ComponentUsingI18n
twice :
- One time on server (to generate HTML files)
- One time on client (to hydrate it and make it dynamic)
So, we will need to pass 2 times the lang
variable.
⚠️ The JS Components have to render the same DOM elements, else the hydratation won’t work. And we don’t have the Astro.params.lang
variable in JSX/TSX components.
If HTML server side and client side is different, we would have errors like:
Warning: An error occurred during hydration. The server HTML was replaced with client content in <astro-island>.
How my solution works
A schema is better than 1000 words:
Loading graph...
Implementation with code
Render the components server side
I created a SessionStorage
for the server Astro:
const _sessionStorage: any = {};
export const sessionStorageServer = { setItem: (key: string, value: string) => (_sessionStorage[key] = value), getItem: (key: string): string | undefined => _sessionStorage[key], removeItem: (key: string) => delete _sessionStorage[key],};
Then, I called on every astro pages:
// sessionStorageServer.setItem("lang", "en");// sessionStorageServer.setItem("lang", "fr");sessionStorageServer.setItem("lang", Astro.params.lang);
const lang = sessionStorageServer.getItem("lang");
Render the components client side
When the frontend is rendered by the client, we do:
const lang = document.documentElement.lang;
And we won’t forget to add in Astro layout :
---const lang = sessionStorageServer.getItem(SESSION_STORAGE_KEY);---
<!DOCTYPE html><html lang={lang}>...</html>
Refactoring : useTranslation()
hook
Implementation
To create a common hook, first, we need to find the runtime. I use this function:
export type Runtime = "client" | "server";
export function detectRuntime(): Runtime { if (typeof window === "undefined") { return "server"; } else { return "client"; }}
My hook looks like this:
import { detectRuntime, Runtime, sessionStorageServer } from "src/functions";import { Languages, TRANSLATIONS } from "src/i18n";
// export type Languages = "fr" | "en" | "de";
const SESSION_STORAGE_KEY = "lang";
/** * * To initialize the hook, please put a * * Server side: * const { lang, t } = useTranslation(Astro.params.lang as Languages); * <!DOCTYPE html><html lang={lang}>...</html> * * Or: * * Client side: * const { ... } = useTranslation(); */export const useTranslation = (setupLang: Languages | null = null) => { let lang: Languages | null = setupLang;
let runtime: Runtime = detectRuntime();
switch (runtime) { case "server": if (setupLang != null) { sessionStorageServer.setItem(SESSION_STORAGE_KEY, setupLang); }
lang = sessionStorageServer.getItem(SESSION_STORAGE_KEY) as Languages; break;
case "client": if (setupLang != null) { throw new Error( "The client can't init the language. Please pass the language as <html> `lang` attribute" ); }
lang = document.documentElement.lang as Languages;
break;
default: throw new Error(`Unknown runtime "${runtime}" found`); }
return { lang, t: (key: string) => { const translation = TRANSLATIONS.get(lang as Languages)?.get(key);
if (translation === undefined) { throw new Error( `Translation with lang=${lang} and key=${key} does not exist` ); }
return translation; }, };};
Usage
To ensure i18n is properly set up on every page, you should include this code at the beginning of each file.
---import { ReactComponent } from "src/components/Atoms";
const { lang, t } = useTranslation(Astro.params.lang as Languages);---
<!DOCTYPE html><html lang={lang} class="..."> <head></head> <body> <span>{t("hello")}</span>
<ReactComponent /> </body></html>
And to write a component using i18n, we will do something like:
import { useState } from "react";import { useTranslation } from "src/hooks/useTranslation";
export function ReactComponent() { const [count, setCount] = useState(0); const increment = () => setCount((c) => c + 1);
const { lang, t } = useTranslation();
return ( <div className="bg-orange-200 text-center"> <div>Language : {lang}</div> <div>{t("world")} ...</div>
<button onClick={increment} className="border-2 border-black bg-blue-400 p-4" > {count} </button> </div> );}
On my side, I have no problems to render and hydrate this component !
Thank you for reading my blog post !
Practice code with the "Quick Sort" algorithm
Enhance your coding skills by learning how the Quick Sort algorithm works!
The SOLID/STUPID principles
Learn what are the SOLID and STUPID principles with examples
Create a Docker Swarm playground
Let's create Docker Swarm playground on your local machine
Create an Ansible playground with Docker
Let's create an Ansible playground with Docker
Setup a Kubernetes cluster with K3S, Traefik, CertManager and Kubernetes Dashboard
Let's setup step by step our own K3S cluster !
HashiCorp Vault - Technological watch
Learn what is HashiCorp Vault in less than 5 minutes !
Database ACID/BASE - Understanding the CAP Theorem
Learn what is the CAP Theorem in less than 5 minutes !
LFTP - Deploy an application in command line
Here we will see how to automatically deploy an application with lftp in command line.