...
This commit is contained in:
@@ -24,11 +24,7 @@
|
|||||||
"noFoldersYet": "No folders yet",
|
"noFoldersYet": "No folders yet",
|
||||||
"folderInfo": "{id}. {name} ({totalPairs})",
|
"folderInfo": "{id}. {name} ({totalPairs})",
|
||||||
"enterFolderName": "Enter folder name:",
|
"enterFolderName": "Enter folder name:",
|
||||||
"confirmDelete": "Type \"{name}\" to delete:",
|
"confirmDelete": "Type \"{name}\" to delete:"
|
||||||
"createFolderSuccess": "Folder created successfully",
|
|
||||||
"deleteFolderSuccess": "Folder deleted successfully",
|
|
||||||
"createFolderError": "Failed to create folder",
|
|
||||||
"deleteFolderError": "Failed to delete folder"
|
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "You are not the owner of this folder",
|
"unauthorized": "You are not the owner of this folder",
|
||||||
@@ -82,10 +78,6 @@
|
|||||||
"description": "Under development, stay tuned"
|
"description": "Under development, stay tuned"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"login": {
|
|
||||||
"loading": "Loading...",
|
|
||||||
"githubLogin": "GitHub Login"
|
|
||||||
},
|
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "Authentication",
|
"title": "Authentication",
|
||||||
"signIn": "Sign In",
|
"signIn": "Sign In",
|
||||||
@@ -93,18 +85,11 @@
|
|||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"confirmPassword": "Confirm Password",
|
"confirmPassword": "Confirm Password",
|
||||||
"name": "Name",
|
|
||||||
"signInButton": "Sign In",
|
|
||||||
"signUpButton": "Sign Up",
|
|
||||||
"noAccount": "Don't have an account?",
|
"noAccount": "Don't have an account?",
|
||||||
"hasAccount": "Already have an account?",
|
"hasAccount": "Already have an account?",
|
||||||
"signInWithGitHub": "Sign In with GitHub",
|
|
||||||
"signUpWithGitHub": "Sign Up with GitHub",
|
|
||||||
"invalidEmail": "Please enter a valid email address",
|
"invalidEmail": "Please enter a valid email address",
|
||||||
"passwordTooShort": "Password must be at least 8 characters",
|
"passwordTooShort": "Password must be at least 8 characters",
|
||||||
"passwordsNotMatch": "Passwords do not match",
|
"passwordsNotMatch": "Passwords do not match",
|
||||||
"signInFailed": "Sign in failed, please check your email and password",
|
|
||||||
"signUpFailed": "Sign up failed, please try again later",
|
|
||||||
"nameRequired": "Please enter your name",
|
"nameRequired": "Please enter your name",
|
||||||
"emailRequired": "Please enter your email",
|
"emailRequired": "Please enter your email",
|
||||||
"passwordRequired": "Please enter your password",
|
"passwordRequired": "Please enter your password",
|
||||||
@@ -151,42 +136,10 @@
|
|||||||
"next": "Next",
|
"next": "Next",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"autoPause": "Auto Pause ({enabled})",
|
"autoPause": "Auto Pause ({enabled})",
|
||||||
"playbackSpeed": "Playback Speed",
|
|
||||||
"subtitleSettings": "Subtitle Settings",
|
|
||||||
"fontSize": "Font Size",
|
|
||||||
"backgroundColor": "Background Color",
|
|
||||||
"textColor": "Text Color",
|
|
||||||
"fontFamily": "Font Family",
|
|
||||||
"opacity": "Opacity",
|
|
||||||
"position": "Position",
|
|
||||||
"top": "Top",
|
|
||||||
"center": "Center",
|
|
||||||
"bottom": "Bottom",
|
|
||||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
|
||||||
"uploadVideoAndSubtitle": "Please upload video and subtitle files",
|
|
||||||
"uploadVideoFile": "Please upload video file",
|
|
||||||
"uploadSubtitleFile": "Please upload subtitle file",
|
|
||||||
"processingSubtitle": "Processing subtitle file...",
|
|
||||||
"needBothFiles": "Both video and subtitle files are required to start learning",
|
|
||||||
"videoFile": "Video File",
|
|
||||||
"subtitleFile": "Subtitle File",
|
|
||||||
"uploaded": "Uploaded",
|
|
||||||
"notUploaded": "Not Uploaded",
|
|
||||||
"upload": "Upload",
|
|
||||||
"autoPauseStatus": "Auto Pause: {enabled}",
|
|
||||||
"on": "On",
|
"on": "On",
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"videoUploadFailed": "Video upload failed",
|
"videoUploadFailed": "Video upload failed",
|
||||||
"subtitleUploadFailed": "Subtitle upload failed",
|
"subtitleUploadFailed": "Subtitle upload failed"
|
||||||
"subtitleLoadSuccess": "Subtitle file loaded successfully",
|
|
||||||
"subtitleLoadFailed": "Subtitle file loading failed",
|
|
||||||
"shortcuts": {
|
|
||||||
"playPause": "Play/Pause",
|
|
||||||
"next": "Next",
|
|
||||||
"previous": "Previous",
|
|
||||||
"restart": "Restart",
|
|
||||||
"autoPause": "Toggle Auto Pause"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "Generate IPA",
|
"generateIPA": "Generate IPA",
|
||||||
|
|||||||
@@ -24,11 +24,7 @@
|
|||||||
"noFoldersYet": "还没有文件夹",
|
"noFoldersYet": "还没有文件夹",
|
||||||
"folderInfo": "{id}. {name} ({totalPairs})",
|
"folderInfo": "{id}. {name} ({totalPairs})",
|
||||||
"enterFolderName": "输入文件夹名称:",
|
"enterFolderName": "输入文件夹名称:",
|
||||||
"confirmDelete": "输入 \"{name}\" 以删除:",
|
"confirmDelete": "输入 \"{name}\" 以删除:"
|
||||||
"createFolderSuccess": "文件夹创建成功",
|
|
||||||
"deleteFolderSuccess": "文件夹删除成功",
|
|
||||||
"createFolderError": "创建文件夹失败",
|
|
||||||
"deleteFolderError": "删除文件夹失败"
|
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "您不是此文件夹的所有者",
|
"unauthorized": "您不是此文件夹的所有者",
|
||||||
@@ -82,10 +78,6 @@
|
|||||||
"description": "开发中,敬请期待"
|
"description": "开发中,敬请期待"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"login": {
|
|
||||||
"loading": "加载中...",
|
|
||||||
"githubLogin": "GitHub登录"
|
|
||||||
},
|
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "登录",
|
"title": "登录",
|
||||||
"signIn": "登录",
|
"signIn": "登录",
|
||||||
@@ -93,28 +85,18 @@
|
|||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"confirmPassword": "确认密码",
|
"confirmPassword": "确认密码",
|
||||||
"name": "用户名",
|
|
||||||
"signInButton": "登录",
|
|
||||||
"signUpButton": "注册",
|
|
||||||
"noAccount": "还没有账户?",
|
"noAccount": "还没有账户?",
|
||||||
"hasAccount": "已有账户?",
|
"hasAccount": "已有账户?",
|
||||||
"signInWithGitHub": "使用GitHub登录",
|
|
||||||
"signUpWithGitHub": "使用GitHub注册",
|
|
||||||
"invalidEmail": "请输入有效的邮箱地址",
|
"invalidEmail": "请输入有效的邮箱地址",
|
||||||
"passwordTooShort": "密码至少需要8个字符",
|
"passwordTooShort": "密码至少需要8个字符",
|
||||||
"passwordsNotMatch": "两次输入的密码不匹配",
|
"passwordsNotMatch": "两次输入的密码不匹配",
|
||||||
"signInFailed": "登录失败,请检查您的邮箱和密码",
|
|
||||||
"signUpFailed": "注册失败,请稍后再试",
|
|
||||||
"nameRequired": "请输入用户名",
|
"nameRequired": "请输入用户名",
|
||||||
"emailRequired": "请输入邮箱",
|
"emailRequired": "请输入邮箱",
|
||||||
"passwordRequired": "请输入密码",
|
"passwordRequired": "请输入密码",
|
||||||
"confirmPasswordRequired": "请确认密码"
|
"confirmPasswordRequired": "请确认密码",
|
||||||
|
"loading": "加载中..."
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"choose": {
|
|
||||||
"back": "返回",
|
|
||||||
"choose": "选择"
|
|
||||||
},
|
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
"selectFolder": "选择文件夹",
|
"selectFolder": "选择文件夹",
|
||||||
"noFolders": "未找到文件夹",
|
"noFolders": "未找到文件夹",
|
||||||
@@ -155,41 +137,10 @@
|
|||||||
"next": "下句",
|
"next": "下句",
|
||||||
"restart": "句首",
|
"restart": "句首",
|
||||||
"autoPause": "自动暂停({enabled})",
|
"autoPause": "自动暂停({enabled})",
|
||||||
"playbackSpeed": "播放速度",
|
|
||||||
"subtitleSettings": "字幕设置",
|
|
||||||
"fontSize": "字体大小",
|
|
||||||
"backgroundColor": "背景颜色",
|
|
||||||
"textColor": "文字颜色",
|
|
||||||
"fontFamily": "字体",
|
|
||||||
"opacity": "透明度",
|
|
||||||
"position": "位置",
|
|
||||||
"top": "顶部",
|
|
||||||
"center": "居中",
|
|
||||||
"bottom": "底部",
|
|
||||||
"keyboardShortcuts": "键盘快捷键",
|
|
||||||
"uploadVideoAndSubtitle": "请上传视频和字幕文件",
|
|
||||||
"uploadVideoFile": "请上传视频文件",
|
|
||||||
"uploadSubtitleFile": "请上传字幕文件",
|
|
||||||
"processingSubtitle": "字幕文件正在处理中...",
|
|
||||||
"needBothFiles": "需要同时上传视频和字幕文件才能开始学习",
|
|
||||||
"videoFile": "视频文件",
|
|
||||||
"subtitleFile": "字幕文件",
|
|
||||||
"uploaded": "已上传",
|
|
||||||
"notUploaded": "未上传",
|
|
||||||
"autoPauseStatus": "自动暂停: {enabled}",
|
|
||||||
"on": "开",
|
"on": "开",
|
||||||
"off": "关",
|
"off": "关",
|
||||||
"videoUploadFailed": "视频上传失败",
|
"videoUploadFailed": "视频上传失败",
|
||||||
"subtitleUploadFailed": "字幕上传失败",
|
"subtitleUploadFailed": "字幕上传失败"
|
||||||
"subtitleLoadSuccess": "字幕文件加载成功",
|
|
||||||
"subtitleLoadFailed": "字幕文件加载失败",
|
|
||||||
"shortcuts": {
|
|
||||||
"playPause": "播放/暂停",
|
|
||||||
"next": "下一句",
|
|
||||||
"previous": "上一句",
|
|
||||||
"restart": "句首",
|
|
||||||
"autoPause": "切换自动暂停"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "生成IPA",
|
"generateIPA": "生成IPA",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { SubtitleEntry } from "../types/subtitle";
|
import { SubtitleEntry } from "../types/subtitle";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
export function parseSrt(data: string): SubtitleEntry[] {
|
export function parseSrt(data: string): SubtitleEntry[] {
|
||||||
const lines = data.split(/\r?\n/);
|
const lines = data.split(/\r?\n/);
|
||||||
@@ -93,7 +94,7 @@ export async function loadSubtitle(url: string): Promise<SubtitleEntry[]> {
|
|||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
return parseSrt(data);
|
return parseSrt(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load subtitle:', error);
|
logger.error('加载字幕失败', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||||
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
||||||
import { genIPA, genLocale } from "@/lib/server/translatorActions";
|
import { genIPA, genLocale } from "@/lib/server/translatorActions";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import PageLayout from "@/components/ui/PageLayout";
|
import PageLayout from "@/components/ui/PageLayout";
|
||||||
|
|
||||||
export default function TextSpeakerPage() {
|
export default function TextSpeakerPage() {
|
||||||
@@ -75,7 +76,7 @@ export default function TextSpeakerPage() {
|
|||||||
setIPA(data.ipa);
|
setIPA(data.ipa);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
logger.error("生成 IPA 失败", e);
|
||||||
setIPA("");
|
setIPA("");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,6 @@ export default function TextSpeakerPage() {
|
|||||||
try {
|
try {
|
||||||
let theLocale = locale;
|
let theLocale = locale;
|
||||||
if (!theLocale) {
|
if (!theLocale) {
|
||||||
console.log("downloading text info");
|
|
||||||
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
|
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
|
||||||
setLocale(tmp_locale);
|
setLocale(tmp_locale);
|
||||||
theLocale = tmp_locale;
|
theLocale = tmp_locale;
|
||||||
@@ -123,8 +123,7 @@ export default function TextSpeakerPage() {
|
|||||||
load(objurlRef.current);
|
load(objurlRef.current);
|
||||||
play();
|
play();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
logger.error("播放音频失败", e);
|
||||||
|
|
||||||
setPause(true);
|
setPause(true);
|
||||||
setLocale(null);
|
setLocale(null);
|
||||||
|
|
||||||
@@ -181,7 +180,6 @@ export default function TextSpeakerPage() {
|
|||||||
try {
|
try {
|
||||||
let theLocale = locale;
|
let theLocale = locale;
|
||||||
if (!theLocale) {
|
if (!theLocale) {
|
||||||
console.log("downloading text info");
|
|
||||||
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
|
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
|
||||||
setLocale(tmp_locale);
|
setLocale(tmp_locale);
|
||||||
theLocale = tmp_locale;
|
theLocale = tmp_locale;
|
||||||
@@ -218,7 +216,7 @@ export default function TextSpeakerPage() {
|
|||||||
}
|
}
|
||||||
setIntoLocalStorage(save);
|
setIntoLocalStorage(save);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
logger.error("保存到本地存储失败", e);
|
||||||
setLocale(null);
|
setLocale(null);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
|||||||
import { TranslationHistorySchema } from "@/lib/interfaces";
|
import { TranslationHistorySchema } from "@/lib/interfaces";
|
||||||
import { tlsoPush, tlso } from "@/lib/browser/localStorageOperators";
|
import { tlsoPush, tlso } from "@/lib/browser/localStorageOperators";
|
||||||
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { Plus, Trash } from "lucide-react";
|
import { Plus, Trash } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
@@ -67,7 +68,7 @@ export default function TranslatorPage() {
|
|||||||
lastTTS.current.url = url;
|
lastTTS.current.url = url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to generate audio");
|
toast.error("Failed to generate audio");
|
||||||
console.error(error);
|
logger.error("生成音频失败", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await play();
|
await play();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Folder } from "../../../generated/prisma/browser";
|
import { Folder } from "../../../generated/prisma/browser";
|
||||||
import {
|
import {
|
||||||
@@ -101,7 +102,7 @@ export default function FoldersClient({ userId }: { userId: string }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
logger.error("加载文件夹失败", error);
|
||||||
toast.error("加载出错,请重试。");
|
toast.error("加载出错,请重试。");
|
||||||
});
|
});
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
@@ -111,7 +112,7 @@ export default function FoldersClient({ userId }: { userId: string }) {
|
|||||||
const updatedFolders = await getFoldersWithTotalPairsByUserId(userId);
|
const updatedFolders = await getFoldersWithTotalPairsByUserId(userId);
|
||||||
setFolders(updatedFolders);
|
setFolders(updatedFolders);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
logger.error("更新文件夹失败", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import Input from "@/components/ui/Input";
|
import Input from "@/components/ui/Input";
|
||||||
|
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { LOCALES } from "@/config/locales";
|
|
||||||
|
|
||||||
interface AddTextPairModalProps {
|
interface AddTextPairModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -16,53 +16,6 @@ interface AddTextPairModalProps {
|
|||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMON_LOCALES = [
|
|
||||||
{ label: "中文", value: "zh-CN" },
|
|
||||||
{ label: "英文", value: "en-US" },
|
|
||||||
{ label: "意大利语", value: "it-IT" },
|
|
||||||
{ label: "日语", value: "ja-JP" },
|
|
||||||
{ label: "其他", value: "other" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface LocaleSelectorProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (val: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
|
||||||
const isCommonLocale = COMMON_LOCALES.some((l) => l.value === value && l.value !== "other");
|
|
||||||
const showFullList = value === "other" || !isCommonLocale;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<select
|
|
||||||
value={isCommonLocale ? value : "other"}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
|
||||||
>
|
|
||||||
{COMMON_LOCALES.map((locale) => (
|
|
||||||
<option key={locale.value} value={locale.value}>
|
|
||||||
{locale.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{showFullList && (
|
|
||||||
<select
|
|
||||||
value={value === "other" ? LOCALES[0] : value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] mt-2"
|
|
||||||
>
|
|
||||||
{LOCALES.map((locale) => (
|
|
||||||
<option key={locale} value={locale}>
|
|
||||||
{locale}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddTextPairModal({
|
export default function AddTextPairModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import TextPairCard from "./TextPairCard";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import PageLayout from "@/components/ui/PageLayout";
|
import PageLayout from "@/components/ui/PageLayout";
|
||||||
import { GreenButton } from "@/components/ui/buttons";
|
import { GreenButton } from "@/components/ui/buttons";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { IconButton } from "@/components/ui/buttons";
|
import { IconButton } from "@/components/ui/buttons";
|
||||||
import CardList from "@/components/ui/CardList";
|
import CardList from "@/components/ui/CardList";
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
|||||||
const data = await getPairsByFolderId(folderId);
|
const data = await getPairsByFolderId(folderId);
|
||||||
setTextPairs(data as TextPair[]);
|
setTextPairs(data as TextPair[]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch text pairs:", error);
|
logger.error("获取文本对失败", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
|||||||
const data = await getPairsByFolderId(folderId);
|
const data = await getPairsByFolderId(folderId);
|
||||||
setTextPairs(data as TextPair[]);
|
setTextPairs(data as TextPair[]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch text pairs:", error);
|
logger.error("获取文本对失败", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import Input from "@/components/ui/Input";
|
import Input from "@/components/ui/Input";
|
||||||
|
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { PairUpdateInput } from "../../../../generated/prisma/models";
|
import { PairUpdateInput } from "../../../../generated/prisma/models";
|
||||||
import { TextPair } from "./InFolder";
|
import { TextPair } from "./InFolder";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -22,23 +23,23 @@ export default function UpdateTextPairModal({
|
|||||||
const t = useTranslations("folder_id");
|
const t = useTranslations("folder_id");
|
||||||
const input1Ref = useRef<HTMLInputElement>(null);
|
const input1Ref = useRef<HTMLInputElement>(null);
|
||||||
const input2Ref = useRef<HTMLInputElement>(null);
|
const input2Ref = useRef<HTMLInputElement>(null);
|
||||||
const input3Ref = useRef<HTMLInputElement>(null);
|
const [locale1, setLocale1] = useState(textPair.locale1);
|
||||||
const input4Ref = useRef<HTMLInputElement>(null);
|
const [locale2, setLocale2] = useState(textPair.locale2);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
if (
|
if (
|
||||||
!input1Ref.current?.value ||
|
!input1Ref.current?.value ||
|
||||||
!input2Ref.current?.value ||
|
!input2Ref.current?.value ||
|
||||||
!input3Ref.current?.value ||
|
!locale1 ||
|
||||||
!input4Ref.current?.value
|
!locale2
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const text1 = input1Ref.current.value;
|
const text1 = input1Ref.current.value;
|
||||||
const text2 = input2Ref.current.value;
|
const text2 = input2Ref.current.value;
|
||||||
const locale1 = input3Ref.current.value;
|
|
||||||
const locale2 = input4Ref.current.value;
|
|
||||||
if (
|
if (
|
||||||
typeof text1 === "string" &&
|
typeof text1 === "string" &&
|
||||||
typeof text2 === "string" &&
|
typeof text2 === "string" &&
|
||||||
@@ -50,8 +51,6 @@ export default function UpdateTextPairModal({
|
|||||||
locale2.trim() !== ""
|
locale2.trim() !== ""
|
||||||
) {
|
) {
|
||||||
onUpdate(textPair.id, { text1, text2, locale1, locale2 });
|
onUpdate(textPair.id, { text1, text2, locale1, locale2 });
|
||||||
input1Ref.current.value = "";
|
|
||||||
input2Ref.current.value = "";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -90,19 +89,11 @@ export default function UpdateTextPairModal({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t("locale1")}
|
{t("locale1")}
|
||||||
<Input
|
<LocaleSelector value={locale1} onChange={setLocale1} />
|
||||||
defaultValue={textPair.locale1}
|
|
||||||
ref={input3Ref}
|
|
||||||
className="w-full"
|
|
||||||
></Input>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t("locale2")}
|
{t("locale2")}
|
||||||
<Input
|
<LocaleSelector value={locale2} onChange={setLocale2} />
|
||||||
defaultValue={textPair.locale2}
|
|
||||||
ref={input4Ref}
|
|
||||||
className="w-full"
|
|
||||||
></Input>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LightButton onClick={handleUpdate}>{t("update")}</LightButton>
|
<LightButton onClick={handleUpdate}>{t("update")}</LightButton>
|
||||||
|
|||||||
48
src/components/ui/LocaleSelector.tsx
Normal file
48
src/components/ui/LocaleSelector.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { LOCALES } from "@/config/locales";
|
||||||
|
|
||||||
|
const COMMON_LOCALES = [
|
||||||
|
{ label: "中文", value: "zh-CN" },
|
||||||
|
{ label: "英文", value: "en-US" },
|
||||||
|
{ label: "意大利语", value: "it-IT" },
|
||||||
|
{ label: "日语", value: "ja-JP" },
|
||||||
|
{ label: "其他", value: "other" },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface LocaleSelectorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
||||||
|
const isCommonLocale = COMMON_LOCALES.some((l) => l.value === value && l.value !== "other");
|
||||||
|
const showFullList = value === "other" || !isCommonLocale;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={isCommonLocale ? value : "other"}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
||||||
|
>
|
||||||
|
{COMMON_LOCALES.map((locale) => (
|
||||||
|
<option key={locale.value} value={locale.value}>
|
||||||
|
{locale.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{showFullList && (
|
||||||
|
<select
|
||||||
|
value={value === "other" ? LOCALES[0] : value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] mt-2"
|
||||||
|
>
|
||||||
|
{LOCALES.map((locale) => (
|
||||||
|
<option key={locale} value={locale}>
|
||||||
|
{locale}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@/lib/interfaces";
|
} from "@/lib/interfaces";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { shallowEqual } from "../utils";
|
import { shallowEqual } from "../utils";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
||||||
key: string,
|
key: string,
|
||||||
@@ -24,14 +25,14 @@ export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
return result.data;
|
return result.data;
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
logger.error(
|
||||||
"Invalid data structure in localStorage:",
|
"Invalid data structure in localStorage:",
|
||||||
result.error,
|
result.error,
|
||||||
);
|
);
|
||||||
return [] as z.infer<T>;
|
return [] as z.infer<T>;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to parse ${key} data:`, e);
|
logger.error(`Failed to parse ${key} data:`, e);
|
||||||
return [] as z.infer<T>;
|
return [] as z.infer<T>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
29
src/lib/logger.ts
Normal file
29
src/lib/logger.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 统一的日志工具
|
||||||
|
* 在生产环境中可以通过环境变量控制日志级别
|
||||||
|
*/
|
||||||
|
|
||||||
|
type LogLevel = 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
error: (message: string, error?: unknown) => {
|
||||||
|
if (isDevelopment) {
|
||||||
|
console.error(message, error);
|
||||||
|
}
|
||||||
|
// 在生产环境中,这里可以发送到错误追踪服务(如 Sentry)
|
||||||
|
},
|
||||||
|
|
||||||
|
warn: (message: string, data?: unknown) => {
|
||||||
|
if (isDevelopment) {
|
||||||
|
console.warn(message, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
info: (message: string, data?: unknown) => {
|
||||||
|
if (isDevelopment) {
|
||||||
|
console.info(message, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user