feat(i18n): 添加播客生成任务的多语言支持

refactor(login): 改进登录后重定向逻辑
style(components): 统一加载中的文本显示
chore(docker): 为服务添加名称配置
This commit is contained in:
hex2077
2025-08-25 22:27:30 +08:00
parent 99fad315d0
commit bf314aa5b4
11 changed files with 41 additions and 21 deletions

View File

@@ -12,6 +12,7 @@ services:
- /opt/audio/output:/app/server/output
- /opt/audio/sqlite.db:/app/web/sqlite.db
restart: always
name: podcast-web
depends_on:
- server
@@ -25,6 +26,7 @@ services:
volumes:
- /opt/audio/output:/app/server/output
restart: always
name: podcast-server
volumes:
audio-data:

View File

@@ -23,10 +23,10 @@
"save20Percent": "Save 20%"
},
"configSelector": {
"loading": "Loading...",
"loading": "Loading..",
"selectTTSConfig": "Select TTS Config",
"noAvailableTTSConfig": "No available TTS config",
"pleaseConfigTTS": "Please configure TTS service in settings first"
"pleaseConfigTTS": "Loading.."
},
"contentSection": {
"viewAll": "View All",
@@ -151,7 +151,9 @@
"storytellingMode": "Storytelling Mode",
"apiAccess": "API Access"
},
"comingSoon": "(Coming Soon)"
"comingSoon": "(Coming Soon)",
"pricing_page_title": "Pricing Plans",
"pricing_page_description": "Flexible plans for all creators."
},
"settingsForm": {
"settings": "Settings",

View File

@@ -37,7 +37,8 @@
,
"failed_to_get_task_status": "Failed to get task status"
,
"insufficient_points_raw": "积分不足"
"insufficient_points_raw": "Not enough points",
"podcast_generation_task": "Podcast Generation Task"
,
"forbidden_user_id": "Forbidden: User ID not allowed to access this resource",
"invalid_request_parameters_add_points": "Invalid request parameters. `userId`, `pointsToAdd` (positive number), `reasonCode`, and `description` are required.",

View File

@@ -23,10 +23,10 @@
"save20Percent": "20%割引"
},
"configSelector": {
"loading": "読み込み中...",
"loading": "読み込み中",
"selectTTSConfig": "TTS設定を選択",
"noAvailableTTSConfig": "利用可能なTTS設定がありません",
"pleaseConfigTTS": "最初に設定でTTSサービスを設定してください"
"pleaseConfigTTS": "読み込み中"
},
"contentSection": {
"viewAll": "すべて表示",

View File

@@ -37,7 +37,8 @@
,
"failed_to_get_task_status": "タスクステータスの取得に失敗しました"
,
"insufficient_points_raw": "ポイント不足"
"insufficient_points_raw": "ポイント不足",
"podcast_generation_task": "ポッドキャスト生成タスク"
,
"forbidden_user_id": "禁止このリソースへのアクセスが許可されていないユーザーID",
"invalid_request_parameters_add_points": "無効なリクエストパラメータ。`userId`、`pointsToAdd`(正の数)、`reasonCode`、および`description`が必要です。",
@@ -45,5 +46,6 @@
,
"invalid_pagination_parameters": "無効なページネーションパラメータ"
,
"cannot_read_tts_provider_config": "TTSプロバイダー構成ファイルを読み取れません"
"cannot_read_tts_provider_config": "TTSプロバイダー構成ファイルを読み取れません",
"invalid_provider": "無効なTTSプロバイダー"
}

View File

@@ -23,10 +23,10 @@
"save20Percent": "节省 20%"
},
"configSelector": {
"loading": "加载中...",
"loading": "加载中..",
"selectTTSConfig": "选择TTS配置",
"noAvailableTTSConfig": "暂无可用的TTS配置",
"pleaseConfigTTS": "请先在设置中配置TTS服务"
"pleaseConfigTTS": "加载中.."
},
"contentSection": {
"viewAll": "查看全部",
@@ -151,7 +151,9 @@
"storytellingMode": "说书模式",
"apiAccess": "API访问"
},
"comingSoon": "(即将推出)"
"comingSoon": "(即将推出)",
"pricing_page_title": "价格方案",
"pricing_page_description": "适用于所有创作者的灵活方案。"
},
"settingsForm": {
"settings": "设置",

View File

@@ -37,7 +37,8 @@
,
"failed_to_get_task_status": "获取任务状态失败"
,
"insufficient_points_raw": "积分不足"
"insufficient_points_raw": "积分不足",
"podcast_generation_task": "播客生成任务"
,
"forbidden_user_id": "禁止该用户ID无权访问此资源",
"invalid_request_parameters_add_points": "无效的请求参数。`userId`、`pointsToAdd`(正数)、`reasonCode`和`description`是必需的。",

View File

@@ -4,14 +4,19 @@ import { createPointsAccount, recordPointsTransaction, checkUserPointsAccount }
export async function GET(request: NextRequest) {
const sessionData = await getSessionData();
console.log('获取到的 session:', sessionData);
let baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "/";
const pathname = request.nextUrl.searchParams.get('pathname');
if(!!pathname){
baseUrl += pathname.replace('/','');
}
// 如果没有获取到 session直接重定向到根目录
if (!sessionData?.user) {
const url = new URL('/', request.url);
const url = new URL(baseUrl, request.url);
return NextResponse.redirect(url);
}
const userId = sessionData.user.id; // 获取 userId
// 检查用户是否已存在积分账户
@@ -34,7 +39,7 @@ export async function GET(request: NextRequest) {
}
// 创建一个 URL 对象,指向要重定向到的根目录
const url = new URL('/', request.url);
const url = new URL(baseUrl, request.url);
// 返回重定向响应
return NextResponse.redirect(url);
}

View File

@@ -65,7 +65,7 @@ export async function PUT(request: NextRequest) {
// 5. 扣减积分
const pointsToDeduct = parseInt(process.env.POINTS_PER_PODCAST || '10', 10); // 从环境变量获取默认10
const reasonCode = "podcast_generation";
const description = `播客生成任务:${task_id}`; // Keep this as is if task_id is dynamic or not translatable
const description = `${t("podcast_generation_task")}: ${task_id}`; // 多语言实现
await deductUserPoints(userId, pointsToDeduct, reasonCode, description);

View File

@@ -11,6 +11,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
const { t, i18n } = useTranslation(lang, 'components'); // 初始化 useTranslation
const router = useRouter();
const currentPath = usePathname(); // 将 usePathname 移到组件顶层
// console.log('i18n.language', i18n.language, lang);
const switchLanguage = (locale: string) => {
// 获取当前路径,并替换语言部分
@@ -34,7 +35,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
<button
onClick={() => switchLanguage('zh-CN')}
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
i18n.language === 'zh-CN'
lang === 'zh-CN'
? 'text-gray-900 transition-colors duration-200'
: 'hover:text-gray-900 transition-colors duration-200'
}`}
@@ -44,7 +45,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
<button
onClick={() => switchLanguage('')}
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
i18n.language === 'en'
lang === 'en'
? 'text-gray-900 transition-colors duration-200'
: 'hover:text-gray-900 transition-colors duration-200'
}`}
@@ -54,7 +55,7 @@ const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ lang }) => {
<button
onClick={() => switchLanguage('ja')}
className={`px-3 py-1 rounded-md text-sm font-medium text-gray-500 ${
i18n.language === 'ja'
lang === 'ja'
? 'text-gray-900 transition-colors duration-200'
: 'hover:text-gray-900 transition-colors duration-200'
}`}

View File

@@ -7,6 +7,8 @@ import { createPortal } from "react-dom";
import { XMarkIcon } from "@heroicons/react/24/outline"; // 导入关闭图标
import { AiOutlineChrome, AiOutlineGithub } from "react-icons/ai"; // 从 react-icons/ai 导入 Google 和 GitHub 图标
import { useTranslation } from '../i18n/client'; // 导入 useTranslation
import { usePathname } from 'next/navigation'; // 导入 usePathname
import { getTruePathFromPathname } from '../lib/utils'; // 导入新函数
interface LoginModalProps {
isOpen: boolean;
@@ -30,6 +32,8 @@ const LoginModal: FC<LoginModalProps> = ({ isOpen, onClose, lang }) => {
if (!isOpen) return null;
const pathname = usePathname();
const truePath = getTruePathFromPathname(pathname, lang);
// 使用 React Portal 将模态框渲染到 body 下避免Z-index问题和父组件样式影响
return createPortal(
<div
@@ -58,7 +62,7 @@ const LoginModal: FC<LoginModalProps> = ({ isOpen, onClose, lang }) => {
<div className="space-y-4">
<button
onClick={() => signIn.social({ provider: "google" , newUserCallbackURL: "/api/newuser?provider=google"})}
onClick={() => signIn.social({ provider: "google" , newUserCallbackURL: "/api/newuser?provider=google&pathname=" + truePath})}
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-lg font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<AiOutlineChrome className="h-6 w-6" />
@@ -66,7 +70,7 @@ const LoginModal: FC<LoginModalProps> = ({ isOpen, onClose, lang }) => {
</button>
<button
onClick={() => signIn.social({ provider: "github" , newUserCallbackURL: "/api/newuser?provider=github" })}
onClick={() => signIn.social({ provider: "github" , newUserCallbackURL: "/api/newuser?provider=github&pathname=" + truePath })}
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-lg font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<AiOutlineGithub className="h-6 w-6" />