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

16 KiB
Raw Blame History

Astro + React 项目国际化 (i18n) 指南

本文档旨在指导如何在基于 Astro 和 React 的项目中实现国际化功能,特别是针对 Astro 4.0+ 版本内置的 i18n 支持。

1. Astro i18n 配置

首先,需要在 astro.config.mjs 文件中配置国际化选项。

// 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 不加前缀,这个作为根目录的默认语言页面
  • 如果 prefixDefaultLocaletrue,则所有语言(包括默认语言)的页面都应放在各自的语言子目录中 (例如 src/pages/en/index.astro, src/pages/zh/index.astro)。
  • 如果 prefixDefaultLocalefalse,默认语言的页面可以直接放在 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:

{
  "site.title": "My Portfolio",
  "nav.home": "Home",
  "nav.about": "About",
  "nav.projects": "Projects",
  "button.viewDetails": "View Details"
}

示例 public/locales/zh/ui.json:

{
  "site.title": "我的作品集",
  "nav.home": "首页",
  "nav.about": "关于",
  "nav.projects": "项目",
  "button.viewDetails": "查看详情"
}

4. 创建翻译辅助函数 (可选但推荐)

为了方便在 Astro 组件和 React 组件中获取翻译文本,可以创建一个辅助函数。

src/i18n/utils.ts (如果目录不存在则创建) 中:

// 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。

// 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:

--- 
// 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 页面或组件中,根据当前语言动态导入对应的数据文件:

--- 
// src/pages/en/index.astro (或任何需要数据的页面)
import { personalData, experiences, projects } from '../../lib/data.en';
// ...
---
--- 
// src/pages/zh/index.astro
import { personalData, experiences, projects } from '../../lib/data.zh';
// ...
---

方式二:在同一个数据文件中使用嵌套结构

// 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: "开发了很棒的东西..." },
  ]
};
// ... 其他数据类似结构

然后在组件中根据当前语言选择对应的数据:

// 在 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 中获取当前语言,并加载对应的翻译文本。

---
// 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 组件。

// 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 文件。

// 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.astrosrc/pages/zh/index.astro)。如果 prefixDefaultLocalefalse,则默认语言页面保留在 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 项目构建一个完整的国际化解决方案。