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 # New2. 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 noteEnexport 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 executenpm 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:
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:
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.