Skip to content
Open
280 changes: 155 additions & 125 deletions App.tsx

Large diffs are not rendered by default.

88 changes: 88 additions & 0 deletions I18N_USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# ACE-Step UI 国际化使用指南

## 概述

本项目已实现中英文双语支持,默认语言为中文。

## 架构

```
ace-step-ui/
├── i18n/
│ └── translations.ts # 翻译文件
├── context/
│ └── I18nContext.tsx # i18n Context Provider
└── components/ # 已支持国际化的组件
```

## 如何使用

### 1. 在组件中使用翻译

```tsx
import { useI18n } from '../context/I18nContext';

function YourComponent() {
const { t } = useI18n();

return <div>{t('yourTranslationKey')}</div>;
}
```

### 2. 切换语言

在设置面板中可以切换语言,或通过代码:

```tsx
const { language, setLanguage } = useI18n();

// 切换到英文
setLanguage('en');

// 切换到中文
setLanguage('zh');
```

### 3. 添加新的翻译键

在 `i18n/translations.ts` 中同时添加英文和中文翻译:

```typescript
export const translations = {
en: {
// 添加英文
yourNewKey: 'Your English Text',
},
zh: {
// 添加中文
yourNewKey: '你的中文文本',
}
};
```

## 已翻译的组件

- ✅ App.tsx (主应用、错误消息、Toast提示)
- ✅ Sidebar.tsx (导航栏)
- ✅ UsernameModal.tsx (用户名设置弹窗)
- ✅ SettingsModal.tsx (设置面板,包含语言切换)

## 翻译覆盖范围

- 导航菜单 (创作/音乐库/搜索)
- 用户认证 (登录/登出)
- 主题切换 (浅色/深色模式)
- 错误和成功提示
- 设置界面
- 移动端按钮

## 语言持久化

用户选择的语言会自动保存到 `localStorage`,下次访问时会自动应用。

## 注意事项

1. 所有翻译键必须同时在 `en` 和 `zh` 中定义
2. 使用 TypeScript 类型 `TranslationKey` 确保类型安全
3. 默认语言为中文 (zh)
4. 如果翻译键不存在,会返回键名本身
90 changes: 90 additions & 0 deletions components/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useEffect, useRef } from 'react';
import { AlertTriangle } from 'lucide-react';
import { useI18n } from '../context/I18nContext';

interface ConfirmDialogProps {
isOpen: boolean;
title: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
danger?: boolean;
onConfirm: () => void;
onCancel: () => void;
}

export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
title,
message,
confirmLabel,
cancelLabel,
danger = true,
onConfirm,
onCancel,
}) => {
const { t } = useI18n();
const dialogRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!isOpen) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onCancel();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, onCancel]);

if (!isOpen) return null;

return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-150"
onClick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
>
<div
ref={dialogRef}
className="bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-zinc-200 dark:border-white/10 w-full max-w-sm mx-4 p-6 animate-in zoom-in-95 fade-in duration-200"
>
<div className="flex items-start gap-3 mb-4">
{danger && (
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-red-500/10 flex items-center justify-center">
<AlertTriangle size={20} className="text-red-500" />
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="text-base font-semibold text-zinc-900 dark:text-white">
{title}
</h3>
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400 leading-relaxed">
{message}
</p>
</div>
</div>

<div className="flex gap-3 justify-end mt-6">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium rounded-lg
text-zinc-700 dark:text-zinc-300
bg-zinc-100 dark:bg-zinc-800
hover:bg-zinc-200 dark:hover:bg-zinc-700
transition-colors"
>
{cancelLabel || t('cancel')}
</button>
<button
onClick={onConfirm}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors
${danger
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{confirmLabel || t('delete')}
</button>
</div>
</div>
</div>
);
};
Loading