Files
zhaoguiyang.site/i18n_guide.md
joyzhao 4ab809ed94 feat(i18n): add internationalization support with astro-i18next
- Add astro-i18next package for i18n support
- Create LanguageSwitcher component with English and Chinese options
- Add i18n guide documentation
- Update .gitignore and package.json
2025-06-14 10:08:29 +08:00

455 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Astro + React 项目国际化 (i18n) 指南
本文档旨在指导如何在基于 Astro 和 React 的项目中实现国际化功能,特别是针对 Astro 4.0+ 版本内置的 i18n 支持。
## 1. Astro i18n 配置
首先,需要在 `astro.config.mjs` 文件中配置国际化选项。
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from "@astrojs/react";
import tailwind from "@tailwindcss/vite";
export default defineConfig({
integrations: [react(), tailwind()],
i18n: {
defaultLocale: "en", // 默认语言
locales: ["en", "zh"], // 支持的语言列表
routing: {
prefixDefaultLocale: true, // 是否为默认语言添加前缀 (例如 /en/blog)
// 其他路由配置,例如 fallback
}
}
});
```
**关键点**
- `defaultLocale`: 设置网站的默认语言。
- `locales`: 定义所有支持的语言。
- `routing.prefixDefaultLocale`: 决定默认语言的 URL 是否也带有语言前缀。设置为 `true` (例如 `/en/about`) 通常能提供更一致的 URL 结构,但如果希望默认语言的 URL 更简洁 (例如 `/about`),可以设置为 `false`
## 2. 组织多语言内容 (页面)
Astro 的 i18n 路由策略是基于文件系统的。你需要根据语言在 `src/pages/` 目录下组织你的页面文件。
创建对应语言的子目录,例如:
```
src/
└── pages/
├── en/
│ ├── index.astro
│ └── about.astro
├── zh/
│ ├── index.astro
│ └── about.astro
└── index.astro // (可选) 如果 defaultLocale 不加前缀,这个作为根目录的默认语言页面
```
- 如果 `prefixDefaultLocale``true`,则所有语言(包括默认语言)的页面都应放在各自的语言子目录中 (例如 `src/pages/en/index.astro`, `src/pages/zh/index.astro`)。
- 如果 `prefixDefaultLocale``false`,默认语言的页面可以直接放在 `src/pages/` 根目录下 (例如 `src/pages/index.astro` 对应默认语言),其他语言的页面放在各自的子目录中 (例如 `src/pages/zh/index.astro`)。
## 3. 存储 UI 翻译文本
对于 UI 界面中需要翻译的文本(例如按钮文字、标签、提示信息等),建议使用 JSON 文件来管理。
`public/locales/` 目录下为每种语言创建一个 JSON 文件:
```
public/
└── locales/
├── en/
│ └── ui.json
└── zh/
└── ui.json
```
**示例 `public/locales/en/ui.json`**:
```json
{
"site.title": "My Portfolio",
"nav.home": "Home",
"nav.about": "About",
"nav.projects": "Projects",
"button.viewDetails": "View Details"
}
```
**示例 `public/locales/zh/ui.json`**:
```json
{
"site.title": "我的作品集",
"nav.home": "首页",
"nav.about": "关于",
"nav.projects": "项目",
"button.viewDetails": "查看详情"
}
```
## 4. 创建翻译辅助函数 (可选但推荐)
为了方便在 Astro 组件和 React 组件中获取翻译文本,可以创建一个辅助函数。
`src/i18n/utils.ts` (如果目录不存在则创建) 中:
```typescript
// src/i18n/utils.ts
import { ui } from './ui'; // 假设 ui.ts 用于加载 JSON
// 这是一个简化的示例,实际项目中可能需要更完善的加载和错误处理机制
// 或者直接在组件中 fetch JSON 文件
export function useTranslations(lang: keyof typeof ui) {
return function t(key: keyof typeof ui[typeof lang]) {
return ui[lang][key] || key; // 如果翻译不存在,返回 key 本身
}
}
```
你还需要一个 `src/i18n/ui.ts` 来实际加载这些 JSON 文件。Astro 官方文档提供了一种方式,但对于客户端 React 组件,可能需要调整。
一个更通用的方法是在组件中根据当前语言动态 `fetch` 对应的 `ui.json` 文件。
**或者,更简单的方式是直接在 Astro 页面/布局中获取当前语言,然后将翻译对象传递给 React 组件。**
## 5. 更新 LanguageSwitcher 组件
`LanguageSwitcher.tsx` 组件需要能够检测当前语言、列出可选语言,并在用户选择新语言时导航到对应的本地化 URL。
```typescript
// src/components/LanguageSwitcher.tsx
import { useState, useEffect } from 'react';
// 假设我们从 Astro.locals.lang 获取当前语言,并通过 props 传入
// 或者通过 window.location.pathname 解析
interface LanguageSwitcherProps {
currentLang: string;
availableLangs: Array<{ code: string; name: string }>; // 例如 [{ code: 'en', name: 'English' }, { code: 'zh', name: '中文' }]
}
const LanguageSwitcher = ({ currentLang, availableLangs }: LanguageSwitcherProps) => {
const [selectedLang, setSelectedLang] = useState(currentLang);
useEffect(() => {
setSelectedLang(currentLang);
}, [currentLang]);
const handleChangeLanguage = (langCode: string) => {
if (langCode !== selectedLang) {
// 获取当前路径,但不包含语言前缀
const currentPathname = window.location.pathname;
let basePath = '';
// 移除已有的语言前缀 (如果存在)
const langRegex = new RegExp(`^/(${availableLangs.map(l => l.code).join('|')})`);
basePath = currentPathname.replace(langRegex, '');
if (!basePath.startsWith('/')) {
basePath = '/' + basePath;
}
// Astro 的 i18n 路由会自动处理
// 如果 prefixDefaultLocale 为 true则所有语言都有前缀
// 如果为 false默认语言没有前缀
// 这里我们假设 Astro 的配置会处理好目标 URL
// 对于 Astro 4.0+,可以直接使用 astro:i18n 的 getRelativeLocaleUrl
// 但在 React 组件中,我们通常通过 window.location.href 来跳转
// 简单的跳转逻辑,实际项目中可能需要更精确的 URL 构建
// 考虑 Astro.config.mjs 中的 prefixDefaultLocale 设置
const astroConfig = {
defaultLocale: 'en', // 需要与 astro.config.mjs 一致
locales: ['en', 'zh'], // 需要与 astro.config.mjs 一致
prefixDefaultLocale: true // 需要与 astro.config.mjs 一致
};
let newPath;
if (langCode === astroConfig.defaultLocale && !astroConfig.prefixDefaultLocale) {
newPath = basePath;
} else {
newPath = `/${langCode}${basePath}`;
}
// 确保路径是干净的,例如移除双斜杠
newPath = newPath.replace(/\/\/+/g, '/');
if (newPath === '') newPath = '/';
window.location.href = newPath;
}
};
return (
<div className="language-switcher">
<select
value={selectedLang}
onChange={(e) => handleChangeLanguage(e.target.value)}
className="bg-gray-800 text-white p-2 rounded"
>
{availableLangs.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
</div>
);
};
export default LanguageSwitcher;
```
**在 Astro 布局或页面中使用 `LanguageSwitcher`**:
```astro
---
// src/layouts/Layout.astro
import LanguageSwitcher from '../components/LanguageSwitcher.tsx';
import { getRelativeLocaleUrl } from 'astro:i18n'; // Astro 4.0+ i18n helper
const { currentLocale } = Astro;
const availableLangs = [
{ code: 'en', name: 'English', url: getRelativeLocaleUrl('en') },
{ code: 'zh', name: '中文', url: getRelativeLocaleUrl('zh') },
// ... 其他语言
];
// 传递给 React 组件的 props
const langSwitcherProps = {
currentLang: currentLocale || 'en', // Astro.currentLocale 可能为 undefined
availableLangs: [
{ code: 'en', name: 'English' },
{ code: 'zh', name: '中文' },
]
};
---
<html lang={currentLocale}>
<head>
<!-- ... -->
</head>
<body>
<header>
<LanguageSwitcher {...langSwitcherProps} client:load />
</header>
<slot />
</body>
</html>
```
**注意**: `getRelativeLocaleUrl` 是在 Astro 文件中使用的。在 React 组件内部进行路由切换时,通常是直接修改 `window.location.href`。确保生成的 URL 与 Astro 的路由配置(特别是 `prefixDefaultLocale`)相匹配。
## 6. 国际化静态数据 (例如 `src/lib/data.ts`)
对于 `src/lib/data.ts` 中的静态数据,有两种主要的处理方式:
**方式一:为每种语言创建单独的数据文件**
```
src/
└── lib/
├── data.en.ts
├── data.zh.ts
└── data.ts // (可选) 作为默认或共享数据
```
然后在你的 Astro 页面或组件中,根据当前语言动态导入对应的数据文件:
```astro
---
// src/pages/en/index.astro (或任何需要数据的页面)
import { personalData, experiences, projects } from '../../lib/data.en';
// ...
---
```
```astro
---
// src/pages/zh/index.astro
import { personalData, experiences, projects } from '../../lib/data.zh';
// ...
---
```
**方式二:在同一个数据文件中使用嵌套结构**
```typescript
// src/lib/data.ts
export const siteTitle = {
en: "My Awesome Portfolio",
zh: "我的炫酷作品集"
};
export const personalData = {
en: {
name: "Your Name",
tagline: "Full Stack Developer | Tech Enthusiast",
bio: "A short bio about yourself..."
},
zh: {
name: "你的名字",
tagline: "全栈开发者 | 技术爱好者",
bio: "一段关于你自己的简短介绍..."
}
};
export const experiences = {
en: [
{ title: "Software Engineer at Tech Corp", duration: "2020 - Present", description: "Developed amazing things..." },
],
zh: [
{ title: "软件工程师 @ 科技公司", duration: "2020 -至今", description: "开发了很棒的东西..." },
]
};
// ... 其他数据类似结构
```
然后在组件中根据当前语言选择对应的数据:
```typescript
// 在 React 组件中 (假设 lang 作为 prop 传入)
// const currentData = experiences[lang] || experiences.en;
// 在 Astro 页面中
// ---
// import { experiences } from '../lib/data';
// const lang = Astro.currentLocale || 'en';
// const currentExperiences = experiences[lang] || experiences.en;
// ---
```
选择哪种方式取决于你的偏好和数据复杂度。分离文件更清晰,但可能需要更多导入语句。嵌套结构更集中,但文件可能变得较大。
## 7. 在 Astro 页面和 React 组件中使用翻译
**Astro 页面/布局 (.astro)**
你可以直接在 Astro 的 frontmatter 中获取当前语言,并加载对应的翻译文本。
```astro
---
// src/pages/en/index.astro
import Layout from '../../layouts/Layout.astro';
import HeroSection from '../../components/HeroSection.tsx';
// 假设我们有一个方法来加载翻译
import { getTranslations } from '../../i18n/server'; // 这是一个假设的服务器端翻译加载函数
const lang = Astro.currentLocale || 'en';
const t = await getTranslations(lang, 'ui'); // 加载 public/locales/en/ui.json
const heroSectionProps = {
title: t['hero.title'], // 假设 ui.json 中有 hero.title
subtitle: t['hero.subtitle']
// ... 其他需要翻译的 props
};
---
<Layout title={t['site.title']}>
<HeroSection {...heroSectionProps} client:load />
{/* ...其他内容... */}
</Layout>
```
**React 组件 (.tsx)**
将翻译函数 `t` 或翻译好的文本作为 props 传递给 React 组件。
```typescript
// src/components/HeroSection.tsx
interface HeroSectionProps {
title: string;
subtitle: string;
// ...其他 props
}
const HeroSection = ({ title, subtitle }: HeroSectionProps) => {
return (
<section>
<h1>{title}</h1>
<p>{subtitle}</p>
</section>
);
};
export default HeroSection;
```
或者,如果 React 组件需要在客户端自行获取翻译(例如,不通过 Astro 预渲染的部分),可以 `fetch` 对应的 `ui.json` 文件。
```typescript
// src/components/SomeClientComponent.tsx
import { useState, useEffect } from 'react';
interface Translations {
[key: string]: string;
}
const SomeClientComponent = () => {
const [t, setT] = useState< (key: string) => string >(() => (key: string) => key);
const [lang, setLang] = useState('en'); // 需要确定当前语言
useEffect(() => {
// 确定当前语言,例如从 URL 或 localStorage
const currentPathLang = window.location.pathname.split('/')[1];
// 假设 'en', 'zh' 是支持的语言
if (['en', 'zh'].includes(currentPathLang)) {
setLang(currentPathLang);
}
}, []);
useEffect(() => {
async function fetchTranslations() {
try {
const response = await fetch(`/locales/${lang}/ui.json`);
if (!response.ok) throw new Error('Failed to load translations');
const translations: Translations = await response.json();
setT(() => (key: string) => translations[key] || key);
} catch (error) {
console.error("Error fetching translations:", error);
// 保留默认的 t 函数 (返回 key)
}
}
fetchTranslations();
}, [lang]);
return (
<div>
<p>{t('some.dynamic.text')}</p>
</div>
);
};
export default SomeClientComponent;
```
## 8. 重要注意事项和目标
在开始实施国际化之前,请务必考虑以下几点,以确保项目质量和未来的可维护性:
* **当前支持语言**: 初期主要支持 **英语 (en)****中文 (zh)**
* **保护现有文案**: 在迁移和翻译过程中,必须确保当前网站上已经正确展示的文案内容不被意外修改或破坏。所有现有的、在原始语言中准确无误的文本应保持原样,直到为其创建对应语言的翻译版本。
* **专业翻译**: 对于非目标语言的文案(例如,如果现有内容主要是中文,需要提供英文版本),应进行专业且准确的翻译,避免机器翻译带来的生硬和不准确问题。确保翻译质量符合专业标准。
* **清晰的目录结构**: 按照 Astro i18n 的推荐,使用 `src/pages/[locale]/` 结构组织页面,使用 `public/locales/[locale]/` 结构组织 UI 翻译文件。这有助于保持项目的整洁和直观。
* **规范的数据结构**: 对于 `src/lib/data.ts` 中的静态数据,无论是采用分离文件(如 `data.en.ts`, `data.zh.ts`)还是嵌套结构,都应保持一致性和清晰性。数据结构的设计应考虑到未来可能新增语言或数据项的扩展需求。
* **代码注释**: 在关键的国际化逻辑和数据结构处添加清晰的英文注释,方便团队协作和后续维护。
* **逐步实施与测试**: 建议分阶段进行国际化改造,每完成一部分(例如一个组件或一个页面)后进行充分测试,确保在不同语言环境下显示和功能均正常。
遵循这些原则将有助于您顺利完成国际化,并为项目未来的发展打下坚实的基础。
## 9. 实施步骤建议
1. **配置 Astro i18n**: 更新 `astro.config.mjs`
2. **创建翻译文件**: 在 `public/locales/` 下为每种语言创建 `ui.json` 并添加一些基础翻译。
3. **调整页面结构**: 根据 `prefixDefaultLocale` 的设置,在 `src/pages/` 下创建语言子目录,并将现有页面(如 `index.astro`)移动到对应的语言目录中(例如 `src/pages/en/index.astro``src/pages/zh/index.astro`)。如果 `prefixDefaultLocale``false`,则默认语言页面保留在 `src/pages/`
4. **更新 `LanguageSwitcher.tsx`**: 实现语言切换逻辑,确保它能正确导航到本地化 URL。
5. **国际化 `Layout.astro`**: 在布局中获取当前语言,加载基础翻译(如网站标题),并将语言信息和 `LanguageSwitcher` 集成进去。
6. **国际化 `src/lib/data.ts`**: 选择一种策略(分离文件或嵌套结构)来国际化静态数据。
7. **逐个国际化 React 组件**:
* 确定哪些 props 需要翻译。
* 从 Astro 页面将翻译后的文本或翻译函数传递给这些组件。
* 更新组件以使用这些 props。
* 例如,先从 `HeroSection.tsx` 开始。
8. **国际化 Astro 页面内容**: 对于直接在 `.astro` 文件中定义的文本内容,使用加载的翻译函数进行替换。
9. **测试**: 彻底测试所有语言的页面和功能,确保翻译正确显示,链接正常工作。
通过以上步骤,你可以为你的 Astro + React 项目构建一个完整的国际化解决方案。