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
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
.trae/
|
||||||
455
i18n_guide.md
Normal file
455
i18n_guide.md
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
# 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 项目构建一个完整的国际化解决方案。
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
"astro": "^5.5.3",
|
"astro": "^5.5.3",
|
||||||
|
"astro-i18next": "1.0.0-beta.21",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.5.0",
|
"framer-motion": "^12.5.0",
|
||||||
|
|||||||
344
pnpm-lock.yaml
generated
344
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
astro:
|
astro:
|
||||||
specifier: ^5.5.3
|
specifier: ^5.5.3
|
||||||
version: 5.9.2(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.43.0)(typescript@5.8.3)
|
version: 5.9.2(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.43.0)(typescript@5.8.3)
|
||||||
|
astro-i18next:
|
||||||
|
specifier: 1.0.0-beta.21
|
||||||
|
version: 1.0.0-beta.21(astro@5.9.2(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.43.0)(typescript@5.8.3))
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -156,6 +159,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@babel/runtime@7.27.6':
|
||||||
|
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -186,6 +193,12 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.15.18':
|
||||||
|
resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/android-arm@0.25.5':
|
'@esbuild/android-arm@0.25.5':
|
||||||
resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==}
|
resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -240,6 +253,12 @@ packages:
|
|||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.15.18':
|
||||||
|
resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.25.5':
|
'@esbuild/linux-loong64@0.25.5':
|
||||||
resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==}
|
resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -454,6 +473,14 @@ packages:
|
|||||||
'@oslojs/encoding@1.1.0':
|
'@oslojs/encoding@1.1.0':
|
||||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||||
|
|
||||||
|
'@proload/core@0.3.3':
|
||||||
|
resolution: {integrity: sha512-7dAFWsIK84C90AMl24+N/ProHKm4iw0akcnoKjRvbfHifJZBLhaDsDus1QJmhG12lXj4e/uB/8mB/0aduCW+NQ==}
|
||||||
|
|
||||||
|
'@proload/plugin-tsm@0.2.1':
|
||||||
|
resolution: {integrity: sha512-Ex1sL2BxU+g8MHdAdq9SZKz+pU34o8Zcl9PHWo2WaG9hrnlZme607PU6gnpoAYsDBpHX327+eu60wWUk+d/b+A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@proload/core': ^0.3.2
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2':
|
'@radix-ui/react-compose-refs@1.1.2':
|
||||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -794,6 +821,12 @@ packages:
|
|||||||
array-iterate@2.0.1:
|
array-iterate@2.0.1:
|
||||||
resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==}
|
resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==}
|
||||||
|
|
||||||
|
astro-i18next@1.0.0-beta.21:
|
||||||
|
resolution: {integrity: sha512-1YPqwexumHpK/d9afEoi52CBFTu6k4MYv/oHjsaAasZDvFClU6U5VPttC/OgZcXRYggCM6ee2LOnyHqlmXOeLA==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
astro: '>=1.0.0'
|
||||||
|
|
||||||
astro@5.9.2:
|
astro@5.9.2:
|
||||||
resolution: {integrity: sha512-K/zZlQOWMpamfLDOls5jvG7lrsjH1gkk3ESRZyZDCkVBtKHMF4LbjwCicm/iBb3mX3V/PerqRYzLbOy3/4JLCQ==}
|
resolution: {integrity: sha512-K/zZlQOWMpamfLDOls5jvG7lrsjH1gkk3ESRZyZDCkVBtKHMF4LbjwCicm/iBb3mX3V/PerqRYzLbOy3/4JLCQ==}
|
||||||
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
|
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
|
||||||
@@ -913,6 +946,9 @@ packages:
|
|||||||
cross-fetch@3.2.0:
|
cross-fetch@3.2.0:
|
||||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
||||||
|
|
||||||
|
cross-fetch@4.0.0:
|
||||||
|
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
|
||||||
|
|
||||||
crossws@0.3.5:
|
crossws@0.3.5:
|
||||||
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
|
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
|
||||||
|
|
||||||
@@ -940,6 +976,10 @@ packages:
|
|||||||
decode-named-character-reference@1.1.0:
|
decode-named-character-reference@1.1.0:
|
||||||
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
|
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
|
||||||
|
|
||||||
|
deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
defu@6.1.4:
|
defu@6.1.4:
|
||||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
@@ -998,6 +1038,131 @@ packages:
|
|||||||
es-module-lexer@1.7.0:
|
es-module-lexer@1.7.0:
|
||||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||||
|
|
||||||
|
esbuild-android-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
esbuild-android-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
esbuild-darwin-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
esbuild-darwin-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
esbuild-freebsd-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
esbuild-freebsd-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
esbuild-linux-32@0.15.18:
|
||||||
|
resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-arm@0.15.18:
|
||||||
|
resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-mips64le@0.15.18:
|
||||||
|
resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-ppc64le@0.15.18:
|
||||||
|
resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-riscv64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-s390x@0.15.18:
|
||||||
|
resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-netbsd-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
esbuild-openbsd-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
esbuild-sunos-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
esbuild-windows-32@0.15.18:
|
||||||
|
resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
esbuild-windows-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
esbuild-windows-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
esbuild@0.15.18:
|
||||||
|
resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
esbuild@0.25.5:
|
esbuild@0.25.5:
|
||||||
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
|
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1123,6 +1288,18 @@ packages:
|
|||||||
http-cache-semantics@4.2.0:
|
http-cache-semantics@4.2.0:
|
||||||
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
||||||
|
|
||||||
|
i18next-browser-languagedetector@7.2.2:
|
||||||
|
resolution: {integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==}
|
||||||
|
|
||||||
|
i18next-fs-backend@2.6.0:
|
||||||
|
resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==}
|
||||||
|
|
||||||
|
i18next-http-backend@2.7.3:
|
||||||
|
resolution: {integrity: sha512-FgZxrXdRA5u44xfYsJlEBL4/KH3f2IluBpgV/7riW0YW2VEyM8FzVt2XHAOi6id0Ppj7vZvCZVpp5LrGXnc8Ig==}
|
||||||
|
|
||||||
|
i18next@22.5.1:
|
||||||
|
resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==}
|
||||||
|
|
||||||
import-meta-resolve@4.1.0:
|
import-meta-resolve@4.1.0:
|
||||||
resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
|
resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
|
||||||
|
|
||||||
@@ -1154,6 +1331,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
iso-639-1@2.1.15:
|
||||||
|
resolution: {integrity: sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
|
||||||
jiti@2.4.2:
|
jiti@2.4.2:
|
||||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1247,6 +1428,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
locale-emoji@0.3.0:
|
||||||
|
resolution: {integrity: sha512-JGm8+naU49CBDnH1jksS3LecPdfWQLxFgkLN6ZhYONKa850pJ0Xt8DPGJnYK0ZuJI8jTuiDDPCDtSL3nyacXwg==}
|
||||||
|
|
||||||
lodash.debounce@4.0.8:
|
lodash.debounce@4.0.8:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
|
|
||||||
@@ -1498,6 +1682,9 @@ packages:
|
|||||||
parse5@7.3.0:
|
parse5@7.3.0:
|
||||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||||
|
|
||||||
|
pathe@1.1.2:
|
||||||
|
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -1705,6 +1892,11 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
tsm@2.3.0:
|
||||||
|
resolution: {integrity: sha512-++0HFnmmR+gMpDtKTnW3XJ4yv9kVGi20n+NfyQWB9qwJvTaIWY9kBmzek2YUQK5APTQ/1DTrXmm4QtFPmW9Rzw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tw-animate-css@1.3.4:
|
tw-animate-css@1.3.4:
|
||||||
resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==}
|
resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==}
|
||||||
|
|
||||||
@@ -2117,6 +2309,8 @@ snapshots:
|
|||||||
'@babel/core': 7.27.4
|
'@babel/core': 7.27.4
|
||||||
'@babel/helper-plugin-utils': 7.27.1
|
'@babel/helper-plugin-utils': 7.27.1
|
||||||
|
|
||||||
|
'@babel/runtime@7.27.6': {}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@@ -2159,6 +2353,9 @@ snapshots:
|
|||||||
'@esbuild/android-arm64@0.25.5':
|
'@esbuild/android-arm64@0.25.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.15.18':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm@0.25.5':
|
'@esbuild/android-arm@0.25.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2186,6 +2383,9 @@ snapshots:
|
|||||||
'@esbuild/linux-ia32@0.25.5':
|
'@esbuild/linux-ia32@0.25.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.15.18':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.25.5':
|
'@esbuild/linux-loong64@0.25.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2326,6 +2526,16 @@ snapshots:
|
|||||||
|
|
||||||
'@oslojs/encoding@1.1.0': {}
|
'@oslojs/encoding@1.1.0': {}
|
||||||
|
|
||||||
|
'@proload/core@0.3.3':
|
||||||
|
dependencies:
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
escalade: 3.2.0
|
||||||
|
|
||||||
|
'@proload/plugin-tsm@0.2.1(@proload/core@0.3.3)':
|
||||||
|
dependencies:
|
||||||
|
'@proload/core': 0.3.3
|
||||||
|
tsm: 2.3.0
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
|
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -2621,6 +2831,21 @@ snapshots:
|
|||||||
|
|
||||||
array-iterate@2.0.1: {}
|
array-iterate@2.0.1: {}
|
||||||
|
|
||||||
|
astro-i18next@1.0.0-beta.21(astro@5.9.2(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.43.0)(typescript@5.8.3)):
|
||||||
|
dependencies:
|
||||||
|
'@proload/core': 0.3.3
|
||||||
|
'@proload/plugin-tsm': 0.2.1(@proload/core@0.3.3)
|
||||||
|
astro: 5.9.2(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.43.0)(typescript@5.8.3)
|
||||||
|
i18next: 22.5.1
|
||||||
|
i18next-browser-languagedetector: 7.2.2
|
||||||
|
i18next-fs-backend: 2.6.0
|
||||||
|
i18next-http-backend: 2.7.3
|
||||||
|
iso-639-1: 2.1.15
|
||||||
|
locale-emoji: 0.3.0
|
||||||
|
pathe: 1.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
astro@5.9.2(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.43.0)(typescript@5.8.3):
|
astro@5.9.2(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.43.0)(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 2.12.2
|
'@astrojs/compiler': 2.12.2
|
||||||
@@ -2823,6 +3048,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
cross-fetch@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
crossws@0.3.5:
|
crossws@0.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
uncrypto: 0.1.3
|
uncrypto: 0.1.3
|
||||||
@@ -2844,6 +3075,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
|
|
||||||
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
defu@6.1.4: {}
|
defu@6.1.4: {}
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
dequal@2.0.3: {}
|
||||||
@@ -2885,6 +3118,91 @@ snapshots:
|
|||||||
|
|
||||||
es-module-lexer@1.7.0: {}
|
es-module-lexer@1.7.0: {}
|
||||||
|
|
||||||
|
esbuild-android-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-android-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-darwin-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-darwin-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-freebsd-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-freebsd-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-32@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-arm@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-mips64le@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-ppc64le@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-riscv64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-s390x@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-netbsd-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-openbsd-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-sunos-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-windows-32@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-windows-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-windows-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild@0.15.18:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/android-arm': 0.15.18
|
||||||
|
'@esbuild/linux-loong64': 0.15.18
|
||||||
|
esbuild-android-64: 0.15.18
|
||||||
|
esbuild-android-arm64: 0.15.18
|
||||||
|
esbuild-darwin-64: 0.15.18
|
||||||
|
esbuild-darwin-arm64: 0.15.18
|
||||||
|
esbuild-freebsd-64: 0.15.18
|
||||||
|
esbuild-freebsd-arm64: 0.15.18
|
||||||
|
esbuild-linux-32: 0.15.18
|
||||||
|
esbuild-linux-64: 0.15.18
|
||||||
|
esbuild-linux-arm: 0.15.18
|
||||||
|
esbuild-linux-arm64: 0.15.18
|
||||||
|
esbuild-linux-mips64le: 0.15.18
|
||||||
|
esbuild-linux-ppc64le: 0.15.18
|
||||||
|
esbuild-linux-riscv64: 0.15.18
|
||||||
|
esbuild-linux-s390x: 0.15.18
|
||||||
|
esbuild-netbsd-64: 0.15.18
|
||||||
|
esbuild-openbsd-64: 0.15.18
|
||||||
|
esbuild-sunos-64: 0.15.18
|
||||||
|
esbuild-windows-32: 0.15.18
|
||||||
|
esbuild-windows-64: 0.15.18
|
||||||
|
esbuild-windows-arm64: 0.15.18
|
||||||
|
|
||||||
esbuild@0.25.5:
|
esbuild@0.25.5:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.25.5
|
'@esbuild/aix-ppc64': 0.25.5
|
||||||
@@ -3079,6 +3397,22 @@ snapshots:
|
|||||||
|
|
||||||
http-cache-semantics@4.2.0: {}
|
http-cache-semantics@4.2.0: {}
|
||||||
|
|
||||||
|
i18next-browser-languagedetector@7.2.2:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.27.6
|
||||||
|
|
||||||
|
i18next-fs-backend@2.6.0: {}
|
||||||
|
|
||||||
|
i18next-http-backend@2.7.3:
|
||||||
|
dependencies:
|
||||||
|
cross-fetch: 4.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
|
i18next@22.5.1:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.27.6
|
||||||
|
|
||||||
import-meta-resolve@4.1.0: {}
|
import-meta-resolve@4.1.0: {}
|
||||||
|
|
||||||
iron-webcrypto@1.2.1: {}
|
iron-webcrypto@1.2.1: {}
|
||||||
@@ -3100,6 +3434,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-inside-container: 1.0.0
|
is-inside-container: 1.0.0
|
||||||
|
|
||||||
|
iso-639-1@2.1.15: {}
|
||||||
|
|
||||||
jiti@2.4.2: {}
|
jiti@2.4.2: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@@ -3161,6 +3497,8 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.30.1
|
lightningcss-win32-arm64-msvc: 1.30.1
|
||||||
lightningcss-win32-x64-msvc: 1.30.1
|
lightningcss-win32-x64-msvc: 1.30.1
|
||||||
|
|
||||||
|
locale-emoji@0.3.0: {}
|
||||||
|
|
||||||
lodash.debounce@4.0.8: {}
|
lodash.debounce@4.0.8: {}
|
||||||
|
|
||||||
lodash.throttle@4.1.1: {}
|
lodash.throttle@4.1.1: {}
|
||||||
@@ -3584,6 +3922,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
entities: 6.0.1
|
entities: 6.0.1
|
||||||
|
|
||||||
|
pathe@1.1.2: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@@ -3867,6 +4207,10 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
tsm@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.15.18
|
||||||
|
|
||||||
tw-animate-css@1.3.4: {}
|
tw-animate-css@1.3.4: {}
|
||||||
|
|
||||||
type-fest@4.41.0: {}
|
type-fest@4.41.0: {}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ThemeToggle from "./ui/theme-toggle";
|
import ThemeToggle from "./ui/theme-toggle";
|
||||||
|
import LanguageSwitcher from "./LanguageSwitcher"; // Added import for LanguageSwitcher
|
||||||
import { personalInfo } from "@/lib/data";
|
import { personalInfo } from "@/lib/data";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Menu, X } from "lucide-react";
|
import { Menu, X } from "lucide-react";
|
||||||
@@ -46,6 +47,8 @@ export default function GlassHeader() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
{/* Language Switcher added here */}
|
||||||
|
<LanguageSwitcher />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
|
|||||||
64
src/components/LanguageSwitcher.tsx
Normal file
64
src/components/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Languages, Check } from "lucide-react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ code: "en", name: "English", icon: "🇬🇧" }, // Added icon for English
|
||||||
|
{ code: "zh", name: "中文", icon: "🇨🇳" }, // Added icon for Chinese
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LanguageSwitcher() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
// TODO: Implement actual language switching logic, e.g., using a context or a library
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||||
|
|
||||||
|
const toggleOpen = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
const handleSelectLanguage = (lang: typeof languages[0]) => {
|
||||||
|
setSelectedLanguage(lang);
|
||||||
|
setIsOpen(false);
|
||||||
|
// Here you would typically call a function to change the language of the application
|
||||||
|
console.log(`Language changed to: ${lang.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<motion.button
|
||||||
|
onClick={toggleOpen}
|
||||||
|
className="p-2 rounded-md hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
|
aria-label="Change language"
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Languages size={20} />
|
||||||
|
</motion.button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20"
|
||||||
|
>
|
||||||
|
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang.code}
|
||||||
|
onClick={() => handleSelectLanguage(lang)}
|
||||||
|
className="w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between cursor-pointer focus:bg-accent focus:outline-none"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<span className="mr-2">{lang.icon}</span> {/* Display icon */}
|
||||||
|
{lang.name}
|
||||||
|
</span>
|
||||||
|
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user