2293 字
11 分钟

Astro i18n 多语言配置 30分钟实战指南

第一次配 Astro i18n 的时候,花了整整一下午研究 prefixDefaultLocale 到底该选 true 还是 false。后来才发现,Astro 的 i18n 配置其实没那么复杂,只是官方文档写得太技术化了。

Astro i18n 基础配置#

astro.config.mjs 配置#

Astro v4.0 开始内置了 i18n 支持。打开你的 astro.config.mjs

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
// 告诉 Astro 你的网站支持哪些语言
locales: ['en', 'zh-cn', 'ja'],
// 默认语言(必须是 locales 里的某一个)
defaultLocale: 'en',
// 是否给默认语言加路径前缀
prefixDefaultLocale: false,
}
});

locales - 网站支持的所有语言。用标准的语言代码,比如 'en'(英文)、'zh-cn'(简体中文)、'ja'(日文)。如果想做更细的语言区分,也可以写成 'en-US''en-GB' 这样。

defaultLocale - 默认语言,访客第一次来你网站时看到的语言。这个值必须是 locales 数组里的某一个,不然 Astro 会报错。

prefixDefaultLocale - 设置成 false 的话,默认语言的 URL 不带语言前缀(比如 /about),其他语言才带(比如 /zh-cn/about/ja/about)。设置成 true 的话,所有语言都带前缀(/en/about/zh-cn/about)。

大多数情况下用 false 就够了,这样默认语言的 URL 更简洁。

三种路由策略对比#

策略配置方式URL 形式示例适用场景优缺点
策略 1:默认语言不带前缀prefixDefaultLocale: false/about
/zh-cn/about
/ja/about
大多数网站(推荐)✅ 默认语言 URL 简洁
❌ URL 格式不统一
策略 2:所有语言都带前缀prefixDefaultLocale: true/en/about
/zh-cn/about
/ja/about
需要 URL 统一性
或特殊 SEO 需求
✅ URL 格式统一
✅ 语言切换逻辑简单
❌ 默认语言 URL 略长
策略 3:手动模式routing: 'manual'完全自定义复杂的多语言需求
需要完全控制路由
✅ 灵活度高
❌ 配置复杂,需要自己处理很多逻辑

如果你的项目比较简单(比如博客、文档站),直接用策略 1 就好。想要 URL 整齐一点就用策略 2。策略 3 是给那些有特殊需求的项目准备的。

多语言内容组织#

方案 1:按语言分文件夹#

这是最直观的方式,每个语言一个文件夹:

src/pages/
├── about.astro # 默认语言(假设是中文)
├── blog.astro
├── index.astro
├── en/ # 英文版本
│ ├── about.astro
│ ├── blog.astro
│ └── index.astro
└── ja/ # 日文版本
├── about.astro
├── blog.astro
└── index.astro

如果你用的是 prefixDefaultLocale: false,默认语言的文件就直接放在 pages 根目录下。其他语言才需要创建对应的子文件夹。

这种方案的好处是结构清晰,每个语言的页面独立,改起来不会互相影响。缺点是如果页面多了,会有大量重复的文件和代码。比如你有 20 个页面、支持 5 种语言,那就得维护 100 个文件。

方案 2:动态路由#

如果你觉得方案 1 太繁琐,可以试试动态路由:

src/pages/
└── [lang]/
└── [...slug].astro

然后在 [...slug].astro 里根据 lang 参数动态渲染内容:

src/pages/[lang]/[...slug].astro
---
export function getStaticPaths() {
const locales = ['zh-cn', 'en', 'ja'];
const slugs = ['about', 'blog', 'contact'];
return locales.flatMap((lang) =>
slugs.map((slug) => ({
params: { lang, slug },
}))
);
}
const { lang, slug } = Astro.params;
// 根据 lang 和 slug 加载对应的内容
---

这种方案代码复用性高,维护成本低,但需要你理解 Astro 的动态路由逻辑。如果你是 Astro 新手,建议先用方案 1。

Content Collections 多语言方案#

对博客来说,Content Collections 才是重点。

目录结构:

src/content/
└── blog/
├── en/
│ ├── post-1.md
│ └── post-2.md
├── zh-cn/
│ ├── post-1.md
│ └── post-2.md
└── ja/
├── post-1.md
└── post-2.md

src/content/config.ts 里定义 schema:

src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
author: z.string(),
date: z.date(),
lang: z.enum(['en', 'zh-cn', 'ja']), // 语言字段
}),
});
export const collections = {
blog: blogCollection,
};

在页面里获取对应语言的文章:

src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';
const currentLang = Astro.currentLocale; // 获取当前语言
const posts = await getCollection('blog', ({ data }) => {
return data.lang === currentLang; // 只获取当前语言的文章
});
---

UI 翻译文件管理#

创建一个翻译字典:

src/i18n/ui.ts
export const ui = {
'en': {
'nav.home': 'Home',
'nav.about': 'About',
'nav.blog': 'Blog',
'btn.readMore': 'Read More',
},
'zh-cn': {
'nav.home': '首页',
'nav.about': '关于',
'nav.blog': '博客',
'btn.readMore': '阅读更多',
},
'ja': {
'nav.home': 'ホーム',
'nav.about': '概要',
'nav.blog': 'ブログ',
'btn.readMore': '続きを読む',
},
} as const;

两个辅助函数:

src/i18n/utils.ts
import { ui } from './ui';
// 从 URL 获取当前语言
export function getLangFromUrl(url: URL) {
const [, lang] = url.pathname.split('/');
if (lang in ui) return lang as keyof typeof ui;
return 'zh-cn'; // 默认语言
}
// 获取翻译函数
export function useTranslations(lang: keyof typeof ui) {
return function t(key: keyof typeof ui[typeof lang]) {
return ui[lang][key] || ui['zh-cn'][key];
}
}

在组件里用:

---
// 某个组件
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
---
<nav>
<a href="/">{t('nav.home')}</a>
<a href="/about">{t('nav.about')}</a>
<a href="/blog">{t('nav.blog')}</a>
</nav>

实现语言切换器#

Astro i18n helper 函数#

getRelativeLocaleUrl(locale, path) - 获取某个语言的相对路径:

import { getRelativeLocaleUrl } from 'astro:i18n';
const url = getRelativeLocaleUrl('en', 'about');
// 返回:'/en/about' 或 '/about'(取决于你的配置)

getAbsoluteLocaleUrl(locale, path) - 获取绝对路径:

import { getAbsoluteLocaleUrl } from 'astro:i18n';
const url = getAbsoluteLocaleUrl('en', 'about');
// 返回:'https://example.com/en/about'

Astro.currentLocale - 获取当前页面的语言:

---
const currentLang = Astro.currentLocale;
// 返回:'zh-cn'、'en' 等
---

Astro.preferredLocale - 获取用户浏览器的首选语言:

---
const browserLang = Astro.preferredLocale;
// 返回:用户浏览器设置的语言(如果在你的 locales 里)
---

语言切换组件#

src/components/LanguageSwitcher.astro
---
import { getRelativeLocaleUrl } from 'astro:i18n';
const locales = {
'zh-cn': '简体中文',
'en': 'English',
'ja': '日本語',
};
const currentLang = Astro.currentLocale || 'zh-cn';
const currentPath = Astro.url.pathname
.replace(`/${currentLang}/`, '/')
.replace(/^\//, '');
---
<div class="language-switcher">
<button class="lang-button">
{locales[currentLang]}
</button>
<div class="lang-dropdown">
{Object.entries(locales).map(([lang, label]) => {
const url = getRelativeLocaleUrl(lang, currentPath);
return (
<a
href={url}
class={lang === currentLang ? 'active' : ''}
>
{label}
</a>
);
})}
</div>
</div>
<style>
.language-switcher {
position: relative;
display: inline-block;
}
.lang-button {
padding: 8px 16px;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
}
.lang-button:hover {
background: #e5e7eb;
}
.lang-dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: white;
border: 1px solid #d1d5db;
border-radius: 6px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
min-width: 150px;
}
.language-switcher:hover .lang-dropdown {
display: block;
}
.lang-dropdown a {
display: block;
padding: 10px 16px;
color: #374151;
text-decoration: none;
transition: background 0.2s;
}
.lang-dropdown a:hover {
background: #f3f4f6;
}
.lang-dropdown a.active {
background: #dbeafe;
color: #1e40af;
font-weight: 500;
}
</style>
<script>
document.querySelector('.lang-button')?.addEventListener('click', (e) => {
e.stopPropagation();
const dropdown = document.querySelector('.lang-dropdown');
dropdown?.classList.toggle('show');
});
document.addEventListener('click', () => {
document.querySelector('.lang-dropdown')?.classList.remove('show');
});
</script>

使用的时候,在你的 Layout 或导航栏里引入:

src/layouts/Layout.astro
---
import LanguageSwitcher from '@/components/LanguageSwitcher.astro';
---
<header>
<nav>
<LanguageSwitcher />
</nav>
</header>

这个组件的核心逻辑是:

  1. 获取当前语言和当前路径
  2. 为每种支持的语言生成对应的 URL
  3. 高亮显示当前语言
  4. 点击切换到对应语言的页面

浏览器语言检测#

src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware((context, next) => {
const url = context.url;
const currentLocale = context.currentLocale;
const preferredLocale = context.preferredLocale;
if (url.pathname === '/' && preferredLocale && preferredLocale !== currentLocale) {
return context.redirect(`/${preferredLocale}/`);
}
return next();
});

更好的方式是结合 Cookie 记住用户的选择:

export const onRequest = defineMiddleware((context, next) => {
const url = context.url;
const currentLocale = context.currentLocale;
const preferredLocale = context.preferredLocale;
const savedLang = context.cookies.get('user-lang')?.value;
if (savedLang && savedLang !== currentLocale && url.pathname === '/') {
return context.redirect(`/${savedLang}/`);
}
if (!savedLang && url.pathname === '/' && preferredLocale && preferredLocale !== currentLocale) {
context.cookies.set('user-lang', preferredLocale, {
path: '/',
maxAge: 31536000,
});
return context.redirect(`/${preferredLocale}/`);
}
return next();
});

然后在语言切换器里,切换语言时也更新 Cookie:

<script>
document.querySelectorAll('.lang-dropdown a').forEach((link) => {
link.addEventListener('click', (e) => {
const lang = e.target.getAttribute('data-lang');
document.cookie = `user-lang=${lang}; path=/; max-age=31536000`;
});
});
</script>

进阶技巧#

Fallback 策略配置#

假设你在逐步翻译网站内容,有些页面的日文版还没翻译完:

astro.config.mjs
export default defineConfig({
i18n: {
locales: ['en', 'zh-cn', 'ja'],
defaultLocale: 'en',
fallback: {
ja: 'en',
'zh-cn': 'en',
},
}
});

这样配置后,如果 /ja/some-page 不存在,Astro 会自动显示 /en/some-page 的内容,而不是 404 页面。

自定义域名映射#

astro.config.mjs
export default defineConfig({
output: 'server', // 必须启用 SSR
adapter: node(),
i18n: {
locales: ['en', 'zh-cn', 'ja'],
defaultLocale: 'en',
domains: {
'zh-cn': 'https://example.cn',
ja: 'https://example.jp',
},
}
});

域名映射只在 SSR 模式下可用。静态网站用不了这个功能。

SEO 优化要点#

在 Layout 中添加 hreflang 标签

src/layouts/Layout.astro
---
import { getAbsoluteLocaleUrl } from 'astro:i18n';
const locales = ['en', 'zh-cn', 'ja'];
const currentPath = Astro.url.pathname
.replace(/^\/(en|zh-cn|ja)\//, '')
.replace(/^\//, '');
---
<html>
<head>
{locales.map((lang) => (
<link
rel="alternate"
hreflang={lang}
href={getAbsoluteLocaleUrl(lang, currentPath)}
/>
))}
<link
rel="alternate"
hreflang="x-default"
href={getAbsoluteLocaleUrl('en', currentPath)}
/>
<link rel="canonical" href={getAbsoluteLocaleUrl(currentLang, currentPath)} />
</head>
</html>

sitemap.xml 多语言配置

import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [sitemap()],
});

本地化 meta 信息

---
const meta = {
'en': {
title: 'Welcome to My Blog',
description: 'A blog about web development',
},
'zh-cn': {
title: '欢迎来到我的博客',
description: '一个关于 Web 开发的博客',
},
};
const currentLang = Astro.currentLocale || 'en';
---
<head>
<title>{meta[currentLang].title}</title>
<meta name="description" content={meta[currentLang].description} />
</head>

我的踩坑记录#

第一次配置时,prefixDefaultLocale 设置错了,导致默认语言的 URL 变成了 /en/about 而不是 /about。后来才发现应该设成 false

还有一次忘记在 Content Collections 的 schema 里加 lang 字段,结果 getCollection 取不到当前语言的文章。排查了半天才发现 schema 没定义好。

语言切换器也踩过坑。一开始用的是硬编码的路径,切换语言后总是跳回首页。后来才改用 getRelativeLocaleUrl(lang, currentPath),这样就能保持在同一个页面切换语言了。

SEO 这块也容易忘。加了多语言但忘记加 hreflang 标签,Google 搜不同语言时总会显示错误的语言版本。后来记得在每个页面的 <head> 里加上完整的 hreflang 标签就好了。

Astro 的 i18n 配置其实挺方便的,配置简单,helper 函数好用,性能也不错(所有语言的路由都会在构建时预生成)。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Astro i18n 多语言配置 30分钟实战指南
https://blog.moewah.com/posts/astro-i18n-multilingual-setup-30-minute-guide/
作者
GoWah
发布于
2025-12-28
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
GoWah
Hello, I'm GoWah.
分类
标签
站点统计
文章
160
分类
9
标签
350
总字数
301,106
运行时长
0
最后活动
0 天前

目录