Thumbnail of Thai flag.

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

  1. As I already planned to add Thai language my previous post was already sitting in en folder, I then creat a th folder in src/pages and copied all the files from en folder to th folder.

  2. Create src/pages/index.astro (site root) and put meta redirect to en 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.

  1. 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
  1. 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,
};
  1. 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/>
  1. Update post list in the root page to use getCollection('blog') instead of Astro.glob<Frontmatter>("./posts/*.md"). I also move them to src/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

  1. 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;
  1. 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.

  1. 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

  1. 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>
  1. 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.

No comments yet.