
May 13, 2023
Adding Thai language
The process of adding i18n to my blog.
As most of my viewer from my YouTube channel are Thai, I think it is a good idea to add Thai language to my blog. I’ll write mainly in English, but I’ll try to translate it to Thai as well. (It is weird that I am Thai but I write in English first lol)
I am following the Add i18n features recipe from Astro documentation. It is pretty straight forward, but I have to make some changes to make it work with my blog.
Translate blog posts
-
As I already planned to add Thai language my previous post was already sitting in
en
folder, I then creat ath
folder insrc/pages
and copied all the files fromen
folder toth
folder. -
Create
src/pages/index.astro
(site root) and put meta redirect toen
folder.
---
---
<meta http-equiv="refresh" content="0;url=/en/" />
I may store the language code in LocalStorage and redirect to the last visited language in the future.
- After some read I found out that Astro have Content Collections feature that was not part of the tutorial I follow to create this blog. I think it is a good idea to use it to store the post data as provide automatic TypeScript type-safety for all of your content and is recommended by Astro. So I moved all the post data to
src/content/blog
folder.
My src/content/blog
folder structure now looks like this:
- src
- content
- blog
- en
- post-1.md
- post-2.md
- th
- post-1.md
- post-2.md
- Create a
src/content/config.ts
file and export a collection for each type of content.
import { defineCollection, z } from "astro:content";
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
author: z.string().default("Anonymous"),
lang: z.enum(["en", "th"]),
tags: z.array(z.string()),
pubDate: z.date(),
image: z
.object({
src: z.string(),
alt: z.string(),
})
.optional(),
isDraft: z.boolean(),
}),
});
export const collections = {
blog: blogCollection,
};
- Use dynamic routes to generate page content for each posts.
src/pages/[lang]/blog/[…slug].astro
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const pages = await getCollection('blog')
const paths = pages.map(page => {
const [lang, ...slug] = page.slug.split('/');
return { params: { lang, slug: slug.join('/') || undefined }, props: page }
})
return paths;
}
const { lang, slug } = Astro.params;
const page = Astro.props;
const formattedDate = page.data.pubDate.toLocaleString(lang);
const { Content } = await page.render();
---
<Content/>
- Update post list in the root page to use
getCollection('blog')
instead ofAstro.glob<Frontmatter>("./posts/*.md")
. I also move them tosrc/pages/[lang]
folder.
src/pages/[lang]/index.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import BlogCard from '../../components/BlogCard.svelte';
import { getCollection } from 'astro:content';
import { languages } from '../../i18n/ui';
export async function getStaticPaths() {
let paths = [];
for (const lang of Object.keys(languages)) {
paths.push({
params: {
lang,
},
});
}
return paths;
}
const { lang } = Astro.params;
const blogPosts = await getCollection('blog');
const pageTitle = "XiaZ.TV";
---
<BaseLayout pageTitle={pageTitle} description="XiaZ.TV -- Blog">
<div class="max-w-7xl mx-auto px-8 py-4">
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{
blogPosts.map(post => {
const [_lang, ...slug] = post.slug.split('/');
if (_lang !== lang) {
return;
}
const url = "/" + lang + "/blog/" + slug.join('/');
return <BlogCard
title={post.data.title}
description={post.data.description}
url={url}
imageSrc={post.data.image.src}
imageAlt={post.data.image.alt}
pubDate={post.data.pubDate}
/>
})
}
</div>
</div>
</BaseLayout>
Translation other UI elements around the site
- Create
src/i18n/ui.ts
to store translation string.
export const languages = {
en: "English",
th: "ไทย",
};
export const defaultLang = "en";
export const ui = {
en: {
"nav.home": "Home",
"nav.build": "Build",
"nav.tool": "Tool",
"nav.community": "Community",
"nav.about": "About",
},
th: {
"nav.home": "หน้าหลัก",
"nav.build": "บิลด์",
"nav.tool": "เครื่องมือ",
"nav.community": "ชุมชน",
"nav.about": "เกี่ยวกับ",
},
} as const;
- Create two helper functions: one to detect the page language based on the current URL, and one to get translations strings for different parts of the UI in
src/i18n/utils.ts
:
import { languages, ui, defaultLang } from "./ui";
export function getLangFromUrl(url: URL) {
const [, lang] = url.pathname.split("/");
if (lang in ui) return lang as keyof typeof ui;
return defaultLang;
}
export function useTranslations(lang: keyof typeof ui) {
return function t(key: keyof (typeof ui)[typeof defaultLang]) {
return ui[lang][key] || ui[defaultLang][key];
};
}
export type Language = keyof typeof languages;
Note: I also export
Language
type to use in other files.
- Use helpers to translate the following files:
src/layouts/BaseLayout.astro
---
+ import { getLangFromUrl } from '../i18n/utils';
+ const lang = getLangFromUrl(Astro.url);
---
- <html lang="en" class="bg-stone-950">
+ <html lang={lang} class="bg-stone-950">
- <Header currentPage={currentPage} client:load />
+ <Header currentPage={currentPage} lang={lang} client:load />
src/components/Header.svelte
<script lang="ts">
+ import { type Language, useTranslations } from '../i18n/utils';
+ export let lang: Language = 'en'
+ const t = useTranslations(lang)
const routes = [
- { name: 'Build', path: 'build' },
+ { name: t('nav.build'), path: 'build' },
Let users switch between languages
- Create a component to show a link for each language
src/components/LanguagePicker.svelte
<script lang="ts">
import { languages } from '../i18n/ui';
export let url: URL | null = null
const [, langFromUrl, ...rest] = url?.pathname.split('/') ?? []
const urlWithoutLang = rest.join('/')
</script>
<div>
{#each Object.entries(languages) as [_lang, label]}
<a
class=""
href={"/" + _lang + "/" + urlWithoutLang}
>
<img src="/flags/{_lang}.svg" alt={label} class="inline-block w-6 h-4 shadow hover:ring-1">
</a>
{/each}
</div>
- Add the component to the header
src/components/Header.svelte
<script lang="ts">
import LanguagePicker from '../components/LanguagePicker.svelte';
</script>
<nav>
...
+ <LanguagePicker {url} />
...
</nav>
Conclusion
I think that’s it for now, I’ll try to write more posts in the future, and I’ll try to translate them to Thai as well. It’s time for some Path of Exile, be sure to catch me streaming on YouTube if you are interested in Path of Exile.