feat(i18n): 添加播客生成任务的多语言支持
refactor(login): 改进登录后重定向逻辑 style(components): 统一加载中的文本显示 chore(docker): 为服务添加名称配置
This commit is contained in:
@@ -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:
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
"save20Percent": "20%割引"
|
||||
},
|
||||
"configSelector": {
|
||||
"loading": "読み込み中...",
|
||||
"loading": "読み込み中",
|
||||
"selectTTSConfig": "TTS設定を選択",
|
||||
"noAvailableTTSConfig": "利用可能なTTS設定がありません",
|
||||
"pleaseConfigTTS": "最初に設定でTTSサービスを設定してください"
|
||||
"pleaseConfigTTS": "読み込み中"
|
||||
},
|
||||
"contentSection": {
|
||||
"viewAll": "すべて表示",
|
||||
|
||||
@@ -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プロバイダー"
|
||||
}
|
||||
@@ -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": "设置",
|
||||
|
||||
@@ -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`是必需的。",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user