Astro i18n 多语言配置 30分钟实战指南
第一次配 Astro i18n 的时候,花了整整一下午研究 prefixDefaultLocale 到底该选 true 还是 false。后来才发现,Astro 的 i18n 配置其实没那么复杂,只是官方文档写得太技术化了。
Astro i18n 基础配置
astro.config.mjs 配置
Astro v4.0 开始内置了 i18n 支持。打开你的 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 参数动态渲染内容:
---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:
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,};在页面里获取对应语言的文章:
---import { getCollection } from 'astro:content';
const currentLang = Astro.currentLocale; // 获取当前语言const posts = await getCollection('blog', ({ data }) => { return data.lang === currentLang; // 只获取当前语言的文章});---UI 翻译文件管理
创建一个翻译字典:
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;两个辅助函数:
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 里)---语言切换组件
---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 或导航栏里引入:
---import LanguageSwitcher from '@/components/LanguageSwitcher.astro';---
<header> <nav> <LanguageSwitcher /> </nav></header>这个组件的核心逻辑是:
- 获取当前语言和当前路径
- 为每种支持的语言生成对应的 URL
- 高亮显示当前语言
- 点击切换到对应语言的页面
浏览器语言检测
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 策略配置
假设你在逐步翻译网站内容,有些页面的日文版还没翻译完:
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 页面。
自定义域名映射
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 标签:
---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 函数好用,性能也不错(所有语言的路由都会在构建时预生成)。
推荐文章
基于标签匹配 · 智能推荐支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
喵斯基部落