skip to content
Logo I like chickens.
they're persistent.
Faust
you better try google.com...
中文

Multi-language Adaptation for Astro Blog

/

This article is machine-translated and may contain errors. Please refer to the original Chinese version if anything is unclear.

Recently, I added an English version to my blog. Since I didn’t want to introduce complex i18n frameworks, I adopted the simplest solution: dividing directories directly.

1. Directory Structure

Simple and crude, just create a corresponding en directory under content. post for Chinese, post-en for English.

src/content/
├── note
├── note-en # New
├── post
└── post-en # New

2. Collection Configuration

In src/content.config.ts, directly reuse the original schema.

// English translation article collection (reusing post schema)
const postEn = defineCollection({
loader: glob({ base: "./src/content/post-en", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
baseSchema.extend({
// ... configuration same as Chinese version
}),
});
// Same for noteEn
export const collections = { post, note, postEn, noteEn };

3. Routing and Utilities

I wrote an i18n.ts to handle paths. The core logic is simply checking if the URL starts with /en.

export function getLocaleFromPath(path: string): Locale {
if (path.startsWith("/en/") || path === "/en") {
return "en";
}
return "zh-CN";
}

When switching languages, it performs simple string replacement:

export function getLocalizedPath(path: string, targetLocale: Locale): string {
if (targetLocale === "en") {
return `/en${path}`;
}
return path.replace(/^\/en/, "") || "/";
}

4. Translation

For UI text (like “Home”, “About”), I used a simple dictionary object.

As for the article content, I was too lazy to translate it manually, so I wrote a script to call DeepLX. It runs automatically during deployment. If it detects a new Chinese article, it automatically generates an English version and puts it into post-en. Although it might prolong the build time (after all, it makes API requests one by one), it wins in being worry-free and fully automatic.

The implementation is split into two files:

  • src/integrations/astro-translate.ts: Astro integration entry point, runs automatically when you execute npm run build, handles translation and writing translated files.
  • src/utils/translate.ts: The utility class that does the work, encapsulating DeepLX API calls.

The integration script looks something like this:

src/integrations/astro-translate.ts
import type { AstroIntegration } from "astro";
import { translateMarkdown } from "../utils/translate"; // Core logic here
export function astroTranslate(options: { enabled?: boolean } = {}): AstroIntegration {
const { enabled = true } = options;
return {
name: "astro-translate",
hooks: {
// Run after config is done but before content sync to ensure generated files are properly loaded
"astro:config:done": async ({ logger }) => {
if (!enabled) return;
// Check environment variable
const apiKey = process.env.DEEPLX_API_KEY;
// ... Traverse files, call translateMarkdown, write files ...
},
},
};
}

The utility is purely a fetch request, with API URL format https://api.deeplx.org/<api-key>/translate:

src/utils/translate.ts
export async function translateText(text: string, options: TranslateOptions) {
const { apiKey } = options;
// API URL format: https://api.deeplx.org/<api-key>/translate
const apiUrl = `https://api.deeplx.org/${apiKey}/translate`;
const response = await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, source_lang: "ZH", target_lang: "EN" })
});
// ...
}

Just put a DEEPLX_API_KEY in the Vercel environment variables.


BTW, looking at /en/ in the address bar feels quite decent. Done.