补全翻译
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-11-16 12:44:52 +08:00
parent 30fc4ed64d
commit 7c5fc40209
31 changed files with 279 additions and 54 deletions

View File

@@ -5,24 +5,26 @@ import { folder } from "../../../../generated/prisma/client";
import { Folder } from "lucide-react";
import { useRouter } from "next/navigation";
import { Center } from "@/components/Center";
import { useTranslations } from "next-intl";
interface FolderSelectorProps {
folders: (folder & { total_pairs: number })[];
}
const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
const t = useTranslations("memorize/folder-selector");
const router = useRouter();
return (
<Center>
<Container className="p-6 gap-4 flex flex-col">
{(folders.length === 0 && (
<h1 className="text-2xl text-gray-900 font-light">
No folders found.
{t("noFolders")}
</h1>
)) || (
<>
<h1 className="text-2xl text-gray-900 font-light">
Select a folder:
{t("selectFolder")}
</h1>
<div className="text-gray-900 border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
{folders.map((folder) => (
@@ -36,9 +38,12 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
<Folder />
<div className="flex-1 flex gap-2">
<span className="group-hover:text-blue-500">
{folder.id}. {folder.name}
{t("folderInfo", {
id: folder.id,
name: folder.name,
count: folder.total_pairs,
})}
</span>
<span>({folder.total_pairs})</span>
</div>
</div>
))}

View File

@@ -8,12 +8,14 @@ import LightButton from "@/components/buttons/LightButton";
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { getTTSAudioUrl } from "@/lib/tts";
import { VOICES } from "@/config/locales";
import { useTranslations } from "next-intl";
interface MemorizeProps {
textPairs: text_pair[];
}
const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
const t = useTranslations("memorize.memorize");
const [reverse, setReverse] = useState(false);
const [dictation, setDictation] = useState(false);
const [index, setIndex] = useState(0);
@@ -27,7 +29,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
<>
<div className="h-36 flex flex-col gap-2 justify-start items-center font-serif text-3xl">
<div className="text-sm text-gray-500">
{index + 1}/{textPairs.length}
{t("progress", { current: index + 1, total: textPairs.length })}
</div>
{dictation ? (
show === "question" ? (
@@ -86,7 +88,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
setShow(show === "question" ? "answer" : "question");
}}
>
{show === "question" ? "Show Answer" : "Next"}
{show === "question" ? t("showAnswer") : t("next")}
</LightButton>
<LightButton
onClick={() => {
@@ -94,7 +96,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
}}
selected={reverse}
>
Reverse
{t("reverse")}
</LightButton>
<LightButton
onClick={() => {
@@ -102,11 +104,11 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
}}
selected={dictation}
>
Dictation
{t("dictation")}
</LightButton>
</div>
</>
)) || <p>No text pairs available</p>}
)) || <p>{t("noTextPairs")}</p>}
</Container>
</Center>
);

View File

@@ -2,6 +2,7 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import {
getFoldersWithTotalPairsByOwner,
getOwnerByFolderId,
@@ -18,9 +19,14 @@ export default async function MemorizePage({
}) {
const session = await getServerSession();
const username = session?.user?.name;
const t = await getTranslations("memorize.page");
const t = (await searchParams).folder_id;
const folder_id = t ? (isNonNegativeInteger(t) ? parseInt(t) : null) : null;
const tParam = (await searchParams).folder_id;
const folder_id = tParam
? isNonNegativeInteger(tParam)
? parseInt(tParam)
: null
: null;
if (!username)
redirect(
@@ -37,7 +43,7 @@ export default async function MemorizePage({
const owner = await getOwnerByFolderId(folder_id);
if (owner !== username) {
return <p>访</p>;
return <p>{t("unauthorized")}</p>;
}
return <Memorize textPairs={await getTextPairsByFolderId(folder_id)} />;

View File

@@ -9,6 +9,7 @@ import { getFoldersByOwner } from "@/lib/services/folderService";
import { Folder } from "lucide-react";
import { createTextPair } from "@/lib/services/textPairService";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
interface AddToFolderProps {
item: z.infer<typeof TranslationHistorySchema>;
@@ -18,6 +19,7 @@ interface AddToFolderProps {
const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
const session = useSession();
const [folders, setFolders] = useState<folder[]>([]);
const t = useTranslations("translator.add-to-folder");
useEffect(() => {
const username = session.data!.user!.name as string;
@@ -28,7 +30,7 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
return (
<div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center">
<Container className="p-6">
<div>You are not authenticated</div>;
<div>{t("notAuthenticated")}</div>
</Container>
</div>
);
@@ -36,7 +38,7 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
return (
<div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center">
<Container className="p-6">
<h1>Choose a Folder to Add to</h1>
<h1>{t("chooseFolder")}</h1>
<div className="border border-gray-200 rounded-2xl">
{(folders.length > 0 &&
folders.map((folder) => (
@@ -56,20 +58,20 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
},
})
.then(() => {
toast.success("Text pair added to folder");
toast.success(t("success"));
setShow(false);
})
.catch(() => {
toast.error("Failed to add text pair to folder");
toast.error(t("error"));
});
}}
>
<Folder />
{folder.id}. {folder.name}
{t("folderInfo", { id: folder.id, name: folder.name })}
</button>
))) || <div>No folders found</div>}
))) || <div>{t("noFolders")}</div>}
</div>
<LightButton onClick={() => setShow(false)}>Close</LightButton>
<LightButton onClick={() => setShow(false)}>{t("close")}</LightButton>
</Container>
</div>
);

View File

@@ -238,7 +238,7 @@ export default function TranslatorPage() {
<LightButton
selected={!["chinese", "english", "italian"].includes(lang)}
onClick={() => {
const newLang = prompt("Enter language");
const newLang = prompt(t("enterLanguage"));
if (newLang) {
setLang(newLang);
}
@@ -261,7 +261,7 @@ export default function TranslatorPage() {
</div>
{history.length > 0 && (
<div className="m-6 flex flex-col items-center">
<h1 className="text-2xl font-light">History</h1>
<h1 className="text-2xl font-light">{t("history")}</h1>
<div className="border border-gray-200 rounded-2xl m-4">
{history.map((item, index) => (
<div key={index}>

View File

@@ -10,6 +10,7 @@ import {
deleteFolderById,
getFoldersWithTotalPairsByOwner,
} from "@/lib/services/folderService";
import { useTranslations } from "next-intl";
interface FolderProps {
folder: folder & { total_pairs: number };
@@ -18,6 +19,7 @@ interface FolderProps {
}
const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
const t = useTranslations("folders");
return (
<div
className="flex justify-between items-center group p-4 border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors"
@@ -30,7 +32,11 @@ const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
<div className="flex-1">
<h3 className="font-medium text-gray-900">
{folder.id}. {folder.name} ({folder.total_pairs})
{t("folderInfo", {
id: folder.id,
name: folder.name,
totalPairs: folder.total_pairs,
})}
</h3>
{/*<p className="text-sm text-gray-500">{} items</p>*/}
</div>
@@ -55,6 +61,7 @@ const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
};
export default function FoldersClient({ username }: { username: string }) {
const t = useTranslations("folders");
const [folders, setFolders] = useState<(folder & { total_pairs: number })[]>(
[],
);
@@ -80,13 +87,13 @@ export default function FoldersClient({ username }: { username: string }) {
<Center>
<div className="w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl p-6">
<div className="mb-6">
<h1 className="text-2xl font-light text-gray-900">Folders</h1>
<p className="text-sm text-gray-500 mt-1">Manage your collections</p>
<h1 className="text-2xl font-light text-gray-900">{t("title")}</h1>
<p className="text-sm text-gray-500 mt-1">{t("subtitle")}</p>
</div>
<button
onClick={async () => {
const folderName = prompt("Enter folder name:");
const folderName = prompt(t("enterFolderName"));
if (!folderName) return;
setLoading(true);
try {
@@ -103,7 +110,7 @@ export default function FoldersClient({ username }: { username: string }) {
className="w-full p-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-500 hover:border-gray-400 hover:text-gray-600 transition-colors flex items-center justify-center gap-2"
>
<FolderPlus size={18} />
<span>{loading ? "Creating..." : "New Folder"}</span>
<span>{loading ? t("creating") : t("newFolder")}</span>
</button>
<div className="mt-4 max-h-96 overflow-y-auto">
@@ -112,7 +119,7 @@ export default function FoldersClient({ username }: { username: string }) {
<div className="w-16 h-16 mx-auto mb-3 rounded-lg bg-gray-100 flex items-center justify-center">
<FolderPlus size={24} className="text-gray-400" />
</div>
<p className="text-sm">No folders yet</p>
<p className="text-sm">{t("noFoldersYet")}</p>
</div>
) : (
<div className="rounded-xl border border-gray-200 overflow-hidden">
@@ -121,7 +128,9 @@ export default function FoldersClient({ username }: { username: string }) {
key={folder.id}
folder={folder}
deleteCallback={() => {
const confirm = prompt(`Type "${folder.name}" to delete:`);
const confirm = prompt(
t("confirmDelete", { name: folder.name }),
);
if (confirm === folder.name) {
deleteFolderById(folder.id).then(updateFolders);
}

View File

@@ -2,6 +2,7 @@ import LightButton from "@/components/buttons/LightButton";
import Input from "@/components/Input";
import { X } from "lucide-react";
import { useRef } from "react";
import { useTranslations } from "next-intl";
interface AddTextPairModalProps {
isOpen: boolean;
@@ -19,6 +20,7 @@ export default function AddTextPairModal({
onClose,
onAdd,
}: AddTextPairModalProps) {
const t = useTranslations("folders.folder_id");
const input1Ref = useRef<HTMLInputElement>(null);
const input2Ref = useRef<HTMLInputElement>(null);
const input3Ref = useRef<HTMLInputElement>(null);
@@ -66,25 +68,29 @@ export default function AddTextPairModal({
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
<div className="flex">
<h2 className="flex-1 text-xl font-light mb-4 text-center">
Add New Text Pair
{t("addNewTextPair")}
</h2>
<X onClick={onClose} className="hover:cursor-pointer"></X>
</div>
<div>
<div>
text1<Input ref={input1Ref} className="w-full"></Input>
{t("text1")}
<Input ref={input1Ref} className="w-full"></Input>
</div>
<div>
text2<Input ref={input2Ref} className="w-full"></Input>
{t("text2")}
<Input ref={input2Ref} className="w-full"></Input>
</div>
<div>
locale1<Input ref={input3Ref} className="w-full"></Input>
{t("locale1")}
<Input ref={input3Ref} className="w-full"></Input>
</div>
<div>
locale2<Input ref={input4Ref} className="w-full"></Input>
{t("locale2")}
<Input ref={input4Ref} className="w-full"></Input>
</div>
</div>
<LightButton onClick={handleAdd}>Add</LightButton>
<LightButton onClick={handleAdd}>{t("add")}</LightButton>
</div>
</div>
);

View File

@@ -13,6 +13,8 @@ import {
import AddTextPairModal from "./AddTextPairModal";
import TextPairCard from "./TextPairCard";
import LightButton from "@/components/buttons/LightButton";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
export interface TextPair {
id: number;
@@ -27,6 +29,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
const [loading, setLoading] = useState(true);
const [openAddModal, setAddModal] = useState(false);
const router = useRouter();
const t = useTranslations("folders.folder_id");
useEffect(() => {
const fetchTextPairs = async () => {
@@ -64,14 +67,16 @@ export default function InFolder({ folderId }: { folderId: number }) {
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors mb-4"
>
<ArrowLeft size={16} />
<span className="text-sm">Back to folders</span>
<span className="text-sm">{t("back")}</span>
</button>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-light text-gray-900">Text Pairs</h1>
<h1 className="text-2xl font-light text-gray-900">
{t("textPairs")}
</h1>
<p className="text-sm text-gray-500 mt-1">
{textPairs.length} items
{t("itemsCount", { count: textPairs.length })}
</p>
</div>
@@ -81,7 +86,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
redirect(`/memorize?folder_id=${folderId}`);
}}
>
Memorize
{t("memorize")}
</LightButton>
<button
className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
@@ -102,11 +107,11 @@ export default function InFolder({ folderId }: { folderId: number }) {
{loading ? (
<div className="p-8 text-center">
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
<p className="text-sm text-gray-500">Loading text pairs...</p>
<p className="text-sm text-gray-500">{t("loadingTextPairs")}</p>
</div>
) : textPairs.length === 0 ? (
<div className="p-12 text-center">
<p className="text-sm text-gray-500 mb-2">No text pair items</p>
<p className="text-sm text-gray-500 mb-2">{t("noTextPairs")}</p>
</div>
) : (
<div className="divide-y divide-gray-100">

View File

@@ -4,6 +4,7 @@ import { updateTextPairById } from "@/lib/services/textPairService";
import { useState } from "react";
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
import UpdateTextPairModal from "./UpdateTextPairModal";
import { useTranslations } from "next-intl";
interface TextPairCardProps {
textPair: TextPair;
@@ -16,6 +17,7 @@ export default function TextPairCard({
onDel,
refreshTextPairs,
}: TextPairCardProps) {
const t = useTranslations("folders.folder_id");
const [openUpdateModal, setOpenUpdateModal] = useState(false);
return (
<div className="group border-b border-gray-100 hover:bg-gray-50 transition-colors">

View File

@@ -4,6 +4,7 @@ import { X } from "lucide-react";
import { useRef } from "react";
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
import { TextPair } from "./InFolder";
import { useTranslations } from "next-intl";
interface UpdateTextPairModalProps {
isOpen: boolean;
@@ -18,6 +19,7 @@ export default function UpdateTextPairModal({
onUpdate,
textPair,
}: UpdateTextPairModalProps) {
const t = useTranslations("folders.folder_id");
const input1Ref = useRef<HTMLInputElement>(null);
const input2Ref = useRef<HTMLInputElement>(null);
const input3Ref = useRef<HTMLInputElement>(null);
@@ -65,25 +67,45 @@ export default function UpdateTextPairModal({
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
<div className="flex">
<h2 className="flex-1 text-xl font-light mb-4 text-center">
Update Text Pair
{t("updateTextPair")}
</h2>
<X onClick={onClose} className="hover:cursor-pointer"></X>
</div>
<div>
<div>
text1<Input defaultValue={textPair.text1} ref={input1Ref} className="w-full"></Input>
{t("text1")}
<Input
defaultValue={textPair.text1}
ref={input1Ref}
className="w-full"
></Input>
</div>
<div>
text2<Input defaultValue={textPair.text2} ref={input2Ref} className="w-full"></Input>
{t("text2")}
<Input
defaultValue={textPair.text2}
ref={input2Ref}
className="w-full"
></Input>
</div>
<div>
locale1<Input defaultValue={textPair.locale1} ref={input3Ref} className="w-full"></Input>
{t("locale1")}
<Input
defaultValue={textPair.locale1}
ref={input3Ref}
className="w-full"
></Input>
</div>
<div>
locale2<Input defaultValue={textPair.locale2} ref={input4Ref} className="w-full"></Input>
{t("locale2")}
<Input
defaultValue={textPair.locale2}
ref={input4Ref}
className="w-full"
></Input>
</div>
</div>
<LightButton onClick={handleUpdate}>Update</LightButton>
<LightButton onClick={handleUpdate}>{t("update")}</LightButton>
</div>
</div>
);

View File

@@ -1,5 +1,6 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import InFolder from "./InFolder";
import { getOwnerByFolderId } from "@/lib/services/folderService";
export default async function FoldersPage({
@@ -10,12 +11,14 @@ export default async function FoldersPage({
const session = await getServerSession();
const { folder_id } = await params;
const id = Number(folder_id);
const t = await getTranslations("folders.folder_id");
if (!id) {
redirect("/folders");
}
if (!session?.user?.name) redirect(`/login?redirect=/folders/${id}`);
if ((await getOwnerByFolderId(id)) !== session.user.name) {
return "you are not the owner of this folder";
return <p>{t("unauthorized")}</p>;
}
return <InFolder folderId={id} />;
}

View File

@@ -7,11 +7,13 @@ import { signIn, useSession } from "next-auth/react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useTranslations } from "next-intl";
export default function LoginPage() {
const session = useSession();
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations("login");
useEffect(() => {
if (session.status === "authenticated") {
@@ -22,7 +24,7 @@ export default function LoginPage() {
return (
<Center>
{session.status === "loading" ? (
<div>Loading...</div>
<div>{t("loading")}</div>
) : (
<LightButton
className="flex flex-row p-2 gap-2"
@@ -34,7 +36,7 @@ export default function LoginPage() {
width={32}
height={32}
/>
<span>GitHub Login</span>
<span>{t("githubLogin")}</span>
</LightButton>
)}
</Center>

View File

@@ -7,11 +7,13 @@ import { useEffect } from "react";
import { Center } from "@/components/Center";
import Container from "@/components/cards/Container";
import LightButton from "@/components/buttons/LightButton";
import { useTranslations } from "next-intl";
export default function MePage() {
const session = useSession();
const router = useRouter();
const pathname = usePathname();
const t = useTranslations("profile");
useEffect(() => {
if (session.status !== "authenticated") {
@@ -22,7 +24,7 @@ export default function MePage() {
return (
<Center>
<Container className="p-6">
<h1>My Profile</h1>
<h1>{t("myProfile")}</h1>
{(session.data?.user?.image as string) && (
<Image
width={64}
@@ -33,8 +35,8 @@ export default function MePage() {
></Image>
)}
<p>{session.data?.user?.name}</p>
<p>Email: {session.data?.user?.email}</p>
<LightButton onClick={signOut}>Logout</LightButton>
<p>{t("email", { email: session.data?.user?.email })}</p>
<LightButton onClick={signOut}>{t("logout")}</LightButton>
</Container>
</Center>
);