Insights / 技术解读

将 Astro 7 网站扩展至9种语言 ― 博客翻译与多语言架构

记录了将 Astro 7.1.3 + UnoCSS + Cloudflare Pages 架构的网站扩展至9种语言的过程。涵盖从UI国际化到博客文章翻译、Pages CMS多语言配置的全部流程。

  • 技术
  • Astro
  • i18n
  • 网站
将 Astro 7 网站扩展至9种语言 ― 博客翻译与多语言架构
本文目录
  1. 多语言化方针
  2. 范围确定
  3. URL 设计
  4. i18n 基础实现
  5. Astro i18n 配置
  6. 翻译工具
  7. View 组件模式
  8. 博客内容多语言化
  9. 目录结构
  10. 内容解析工具
  11. 翻译文件规则
  12. 翻译工作流
  13. View 组件的多语言支持
  14. BlogPostPage 实现
  15. 列表页面实现
  16. 构建注意事项
  17. YAML frontmatter 转义
  18. OG 图像路由过滤
  19. Pages CMS 多语言支持
  20. 语言切换 UI
  21. 标签多语言显示
  22. 作者数据多语言支持
  23. 多语言网站 SEO 优化
  24. 站点地图 hreflang 支持
  25. JSON-LD 结构化数据语言支持
  26. 多语言 RSS 订阅源
  27. 总结

我们将 Acecore 官方网站从仅支持日语升级为支持9种语言。首次上线时将21篇博客文章翻译为8种语言,共生成168个文件。截至2026年7月29日,仓库包含29篇日语文章和208篇翻译,共237个文章文件,构建会生成652个页面。只有存在翻译文件时才发布对应语言的文章 URL。

多语言化方针

范围确定

我们分三个阶段推进多语言化:

  1. i18n 基础搭建:Astro 内置 i18n 路由配置、翻译工具、9种语言的翻译 JSON 文件
  2. UI 文本翻译:页头、页脚、侧边栏及所有页面的组件文本
  3. 博客文章翻译:首次上线时将21篇文章翻译为8种语言(生成168个文件)

URL 设计

采用 Astro 的 prefixDefaultLocale: false,日语在根路径(/blog/...),其他语言带前缀(/en/blog/.../zh-cn/blog/... 等)。

# 日语(默认)
/blog/astro-performance-tuning/

# 英语
/en/blog/astro-performance-tuning/

# 简体中文
/zh-cn/blog/astro-performance-tuning/

所有语言使用相同的 slug,使得语言切换时的 URL 映射非常简单。

i18n 基础实现

Astro i18n 配置

astro.config.mjs 中配置 i18n 路由。

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: "ja",
    locales: ["ja", "en", "zh-cn", "es", "pt", "fr", "ko", "de", "ru"],
    routing: {
      prefixDefaultLocale: false,
    },
  },
});

翻译工具

配置文件、工具函数和翻译 JSON 文件集中在 src/i18n/ 目录下。

// src/i18n/utils.ts
export function t(locale: Locale, key: string): string {
  return translations[locale]?.[key] ?? translations[defaultLocale][key] ?? key;
}

翻译文件以 JSON 格式存放在 src/i18n/locales/ 下,管理着导航、页脚、博客 UI 和元数据等约100个键。

View 组件模式

页面实现采用 View 组件模式。布局和逻辑集中在 src/views/ 中,路由文件(src/pages/)仅作为传递 locale 的轻量包装器。

---
// src/pages/[locale]/about.astro(路由文件)
import AboutPage from "../../views/AboutPage.astro";
const { locale } = Astro.params;
---

<AboutPage locale={locale} />

这种设计消除了日语路由(/about)和多语言路由(/en/about)之间的逻辑重复。

博客内容多语言化

目录结构

翻译文章放置在语言代码子目录中。Astro 的 glob 加载器通过 **/*.md 模式自动递归检测。

src/content/blog/
  astro-performance-tuning.md          # 日语(基础)
  website-renewal.md
  en/
    astro-performance-tuning.md        # 英语版
    website-renewal.md
  zh-cn/
    astro-performance-tuning.md        # 简体中文版
    website-renewal.md
  es/
    ...

内容解析工具

src/utils/blog-i18n.ts 中实现了3个函数。

// 判断是否为基础文章(ID中无斜杠 = 基础)
export function isBasePost(post: CollectionEntry<"blog">): boolean {
  return !post.id.includes("/");
}

// 从ID中移除 locale 前缀获取基础 slug
export function getBaseSlug(postId: string): string {
  const idx = postId.indexOf("/");
  return idx !== -1 ? postId.slice(idx + 1) : postId;
}

// 获取基础文章的本地化版本(没有则回退到原文)
export function localizePost(
  post: CollectionEntry<"blog">,
  allPosts: CollectionEntry<"blog">[],
  locale: Locale,
): CollectionEntry<"blog"> {
  if (locale === defaultLocale) return post;
  return allPosts.find((p) => p.id === `${locale}/${post.id}`) ?? post;
}

localizePost() 本身仍会把日语原文作为安全回退,但公开路由和列表通过 isPostAvailableInLocale() 及过滤器只选择实际存在的翻译。缺少翻译时不会生成本地化文章 URL。

关键在于不修改现有的内容集合 schema。Astro 的 glob 加载器会自动识别子目录中的文件,生成类似 en/astro-performance-tuning 的 ID,无需更改配置。

翻译文件规则

翻译文件遵循以下规则生成:

  • frontmatter 键名保持英文(titledescriptiondate 等)
  • 标签值保持日语不变(['技術', 'Astro'] 等)
  • URL、图片路径、代码块和 HTML 不修改
  • 日期和作者保持不变
  • 正文和 frontmatter 文本值(title、description、callout、FAQ 等)进行翻译

翻译工作流

翻译流程如下:

  1. 创建英语作为中间语言:从日语原文翻译为英语
  2. 从英语翻译至各语言:以英语为起点扩展至7种语言
  3. 批量处理:使用 GitHub Copilot 每次处理5-6篇文章

日语→英语→目标语言的两阶段翻译减少了质量波动。通过英语作为中间语言,比直接从日语翻译至各语言能获得更稳定的质量。

View 组件的多语言支持

BlogPostPage 实现

博客文章页面使用 localizePost() 获取对应 locale 版本的内容,并赋值给模板变量。

---
// src/views/BlogPostPage.astro
const localizedPost = localizePost(basePost, allPosts, locale);
const post = localizedPost; // 现有模板引用直接可用
---

这种方法无需更改模板中对 post.data.titlepost.body 的任何引用即可实现多语言支持。

列表页面实现

博客列表、标签列表、作者列表和归档页面使用 isBasePost() 仅过滤基础文章,然后在显示时通过 localizePost() 替换为翻译版本。

---
const allPosts = await getCollection("blog");
const basePosts = allPosts.filter(isBasePost);
const displayPosts = basePosts.map((p) => localizePost(p, allPosts, locale));
---

构建注意事项

YAML frontmatter 转义

法语翻译中撇号(l'atelierqu'on 等)与 YAML 单引号冲突导致问题。

# NG:YAML 解析错误
title: 'Le métavers est plus proche qu'on ne le pense'

# OK:改用双引号
title: "Le métavers est plus proche qu'on ne le pense"

使用 Node.js 脚本批量修复了所有文件。英语文本如 Acecore's 也存在同样的问题,因此生成翻译文件时需注意引号类型。

OG 图像路由过滤

/blog/og/[slug].png.ts 也会捕获翻译文章的 slug(en/aceserver-hijacked 等),导致参数错误。通过 isBasePost() 过滤解决。

export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await getCollection("blog");
  const posts = allPosts.filter(isBasePost);
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { title: post.data.title },
  }));
};

Pages CMS 多语言支持

Pages CMS(.pages.yml)仅针对指定 path 目录下的直接文件,因此翻译子目录需作为独立集合注册。

content:
  - name: blog
    label: ブログ(日本語)
    path: src/content/blog
  - name: blog-en
    label: Blog(English)
    path: src/content/blog/en
  - name: blog-zh-cn
    label: 博客(简体中文)
    path: src/content/blog/zh-cn
  # ... 每种语言单独配置

标签使用各语言显示,以便在 CMS 中一目了然地识别每个集合对应的语言。

语言切换 UI

在页头添加了 LanguageSwitcher 组件,提供桌面端和移动端均可使用的语言切换 UI。切换语言时跳转到同一页面的对应语言版本,首次访问时检测浏览器的 navigator.language 进行自动重定向。

标签多语言显示

文章标签在 URL 中保持日语 slug,仅翻译显示名称。这避免了路由复杂化,同时让用户看到母语显示的标签。

标签定义集中在 src/content/tags/{tagId}.json 中,每个标签持有 i18n.name 字段。这将标签翻译的唯一真实来源从翻译 JSON 迁移至标签 collection。

{
  "id": "technology",
  "name": "技術",
  "i18n": {
    "en": { "name": "Technology" },
    "fr": { "name": "Technologie" }
  }
}

文章卡片、侧边栏、标签列表和文章详情页通过引用标签 collection 切换显示名称,确保所有标签显示统一为对应语言。

作者数据多语言支持

作者姓名、简介(bio)和技能列表也实现了按语言切换。在 src/content/authors/{authorId}.json 中添加了 i18n 字段,保存各语言的翻译。

{
  "id": "hatt",
  "name": "ハット",
  "bio": "代表取締役。Web制作・サーバー運用…",
  "skills": ["TypeScript", "Astro", "..."]
  "i18n": {
    "en": {
      "name": "Hatt",
      "bio": "CEO and representative director. Web development...",
      "skills": ["TypeScript", "Astro", "..."]
    }
  }
}

getLocalizedAuthor() 工具函数根据 locale 获取对应的作者信息。

// src/utils/blog-i18n.ts
export function getLocalizedAuthor(author: Author, locale: Locale) {
  const localized = author.i18n?.[locale];
  return localized ? { ...author, ...localized } : author;
}

多语言网站 SEO 优化

为了最大化多语言化的 SEO 优势,我们建立了让搜索引擎正确识别和索引各语言版本的机制。

站点地图 hreflang 支持

@astrojs/sitemapi18n 选项与检查翻译文件是否存在的过滤器配合使用。sitemap 只收录实际存在的语言版本,并自动输出对应的 xhtml:link rel="alternate" 标签。

// astro.config.mjs
sitemap({
  filter(page) {
    return !isMissingLocalizedBlogPost(page);
  },
  i18n: {
    defaultLocale: "ja",
    locales: {
      ja: "ja",
      en: "en",
      "zh-cn": "zh-CN",
      es: "es",
      pt: "pt",
      fr: "fr",
      ko: "ko",
      de: "de",
      ru: "ru",
    },
  },
});

同时存在9种语言的文章会生成包含9种语言的 hreflang 集群。只有日语版本的文章会保留为独立的日语 sitemap 条目,不会链接到不存在的本地化 URL。

JSON-LD 结构化数据语言支持

在博客文章的 BlogPosting 结构化数据中添加了 inLanguage 字段,告知搜索引擎每篇文章的语言。

// BlogPostPage.astro(JSON-LD 摘录)
{
  "@type": "BlogPosting",
  "inLanguage": htmlLangMap[locale],  // "ja"、"en"、"zh-CN" 等
  "headline": post.data.title,
  // ...
}

多语言 RSS 订阅源

除了日语版的 /rss.xml,还为每种语言生成了 RSS 订阅源(/en/rss.xml/zh-cn/rss.xml 等)。订阅源的标题和描述也翻译为各语言,<language> 标签输出符合 BCP47 标准的语言代码。

// src/pages/[locale]/rss.xml.ts
export const getStaticPaths = () =>
  locales
    .filter((l) => l !== defaultLocale)
    .map((l) => ({ params: { locale: l } }));

BaseLayout.astro 中的 <link rel="alternate" type="application/rss+xml"> 也会根据 locale 自动设置相应的 RSS URL。

总结

网站目前使用 Astro 7.1.3 的内置 i18n 功能生成多语言静态页面。

  • i18n 基础:使用 Astro 的 prefixDefaultLocale: false,日语无前缀
  • UI 翻译:通过 View 组件模式实现零逻辑重复
  • 内容翻译:子目录方式,无需修改现有 schema
  • 标签翻译:URL 保持日语 slug,仅翻译显示名称
  • 作者数据翻译:bio 和 skills 按语言切换
  • SEO 优化:站点地图 hreflang、JSON-LD inLanguage、多语言 RSS 订阅源
  • 缺少翻译时:不生成本地化文章 URL,日语文章继续在原 URL 发布
  • CMS 支持:各语言的文章可在 Pages CMS 中独立编辑

翻译文件今后仍会逐步添加。在翻译存在之前只发布日语文章;添加 locale 文件后再启用该语言的文章 URL、sitemap 条目和 hreflang 关系。

多语言化流程

  1. i18n 基础搭建

    搭建 Astro 内置 i18n 路由和翻译工具。

  2. UI 文本翻译

    将页头、页脚及所有组件的显示文本多语言化。

  3. 博客文章翻译

    首次上线时生成168个翻译文件(21篇文章 × 8种语言)。

  4. CMS 与构建验证

    Pages CMS 多语言配置及全部页面的构建验证。

多语言化前后对比

仅日语

  • 仅日语1种语言
  • 23篇博客文章
  • 生成523个页面(UI多语言化后)
  • Pages CMS 仅1个博客集合
  • 标签和作者数据仅日语
  • 仅1个 RSS 订阅源

支持9种语言(首次上线时)

  • 日语 + 8种语言(en、zh-cn、es、pt、fr、ko、de、ru)
  • 23篇博客文章 + 168篇翻译 = 共191篇
  • 首次上线时生成621个页面
  • Pages CMS 包含9个语言集合
  • 25种标签和作者数据翻译至各语言
  • 多语言 RSS 订阅源(9种语言)
支持语言数
9
翻译文章数(截至2026年7月29日)
208
生成页面数(截至2026年7月29日)
652

常见问题

为什么选择了9种语言?

为了最大化全球覆盖范围,我们选择了全球主要语言市场。英语、中文、西班牙语和葡萄牙语覆盖了大部分互联网用户,法语、德语、俄语和韩语则补充了其余主要市场。

如何保证翻译质量?

我们使用 GitHub Copilot 进行 AI 翻译。先创建英语版作为中间语言,再从英语翻译至各目标语言,以减少质量波动。frontmatter 中的标签值保持日语不变,URL、代码块和图片路径也保持不变。

如果翻译文章不存在会怎样?

某个 locale 缺少翻译文件时,不会生成该语言的文章 URL。日语文章继续在原 URL 发布,语言切换器会链接到目标 locale 的博客首页。

添加新文章时需要翻译吗?

发布日语文章不要求同时翻译。在对应语言目录中添加同名 Markdown 文件后,该 locale 的文章 URL、sitemap 条目和 hreflang 关系才会进入生成范围。