# 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 (
); }; 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: '中文' }, ] }; ---
``` **注意**: `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 }; --- {/* ...其他内容... */} ``` **React 组件 (.tsx)** 将翻译函数 `t` 或翻译好的文本作为 props 传递给 React 组件。 ```typescript // src/components/HeroSection.tsx interface HeroSectionProps { title: string; subtitle: string; // ...其他 props } const HeroSection = ({ title, subtitle }: HeroSectionProps) => { return (

{title}

{subtitle}

); }; 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 (

{t('some.dynamic.text')}

); }; 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 项目构建一个完整的国际化解决方案。