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 !
Understanding XSS vulnerabilities
In this article, we will explore how XSS vulnerabilities work and how to prevent them
Understanding CSRF vulnerabilities
In this article, we will explore how CSRF vulnerabilities work and how to prevent them
Understanding SQL injection vulnerabilities
In this article, we will explore how SQL injection vulnerabilities work and how to prevent them
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 !