diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b0db98e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "i18n-ally.localesPaths": [ + "messages", + "src/i18n" + ] +} \ No newline at end of file diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..483f0db --- /dev/null +++ b/messages/en.json @@ -0,0 +1,130 @@ +{ + "AppearanceSettings": { + "title": "Choose a theme", + "items": { + "System": "System", + "Light": "Light", + "Dark": "Dark" + } + }, + "AvatarButton": { + "Settings": "Settings", + "LogIn": "LogIn", + "LogOut": "LogOut" + }, + "Banner": { + "Text": "Star this project if you like it." + }, + "BackButton": "Back", + "Bot": { + "title": "Ask Bot", + "description": "Powered by Vercel Ai SDK", + "placeholder": "Bot will automatically get your current code" + }, + "BotVisibilityToggle": { + "open": "Open Bot", + "close": "Close Bot" + }, + "DetailsPage": { + "BackButton": "All Submissions", + "Time": "Submitted on", + "Input": "Input", + "ExpectedOutput": "Expected Output", + "ActualOutput": "Acutal Output", + "Code": "Code" + }, + "Difficulty": { + "EASY": "EASY", + "MEDIUM": "MEDIUM", + "HARD": "HARD" + }, + "LanguageSettings": { + "en": { + "flag": "🇺🇸", + "name": "English" + }, + "zh": { + "flag": "🇨🇳", + "name": "Chinese" + } + }, + "PlaygroundHeader": { + "RunCodeButton": { + "TooltipTrigger": { + "loading": "Running...", + "ready": "Run" + }, + "TooltipContent": "Run Code" + } + }, + "ProblemPage": { + "Description": "Description", + "Solutions": "Solutions", + "Submissions": "Submissions", + "Details": "Details", + "Code": "Code", + "Testcase": "Testcase", + "Bot": "Bot" + }, + "ProblemsetPage": { + "Status": "Status", + "Title": "Title", + "Difficulty": "Difficulty" + }, + "SettingsDialog": { + "title": "Settings", + "description": "Customize your settings here.", + "breadcrumb": "Settings", + "nav": { + "Appearance": "Appearance", + "Language": "Language", + "CodeEditor": "CodeEditor", + "Advanced": "Advanced" + } + }, + "StatusMessage": { + "PD": "Pending", + "QD": "Queued", + "CP": "Compiling", + "CE": "Compilation Error", + "CS": "Compilation Success", + "RU": "Running", + "TLE": "Time Limit Exceeded", + "MLE": "Memory Limit Exceeded", + "RE": "Runtime Error", + "AC": "Accepted", + "WA": "Wrong Answer", + "SE": "System Error" + }, + "SubmissionsTable": { + "Index": "Index", + "Status": "Status", + "Language": "Language", + "Time": "Time", + "Memory": "Memory" + }, + "WorkspaceEditorHeader": { + "LspStatusButton": { + "TooltipContent": "Language Server" + }, + "ResetButton": { + "TooltipContent": "Reset Code" + }, + "UndoButton": { + "TooltipContent": "Undo" + }, + "RedoButton": { + "TooltipContent": "Redo" + }, + "FormatButton": { + "TooltipContent": "Format" + }, + "CopyButton": { + "TooltipContent": "Copy" + } + }, + "WorkspaceEditorFooter": { + "Row": "Row", + "Column": "Column" + } +} \ No newline at end of file diff --git a/messages/zh.json b/messages/zh.json new file mode 100644 index 0000000..f15991d --- /dev/null +++ b/messages/zh.json @@ -0,0 +1,130 @@ +{ + "AppearanceSettings": { + "title": "选择一个主题", + "items": { + "System": "系统", + "Light": "浅色", + "Dark": "深色" + } + }, + "AvatarButton": { + "Settings": "设置", + "LogIn": "登录", + "LogOut": "登出" + }, + "Banner": { + "Text": "如果喜欢该项目不妨收藏一下" + }, + "BackButton": "返回", + "Bot": { + "title": "询问AI助手", + "description": "由Vercel Ai SDK驱动", + "placeholder": "AI助手将自动获取您当前的代码" + }, + "BotVisibilityToggle": { + "open": "打开AI助手", + "close": "关闭AI助手" + }, + "DetailsPage": { + "BackButton": "所有提交记录", + "Time": "提交于", + "Input": "输入", + "ExpectedOutput": "期望输出", + "ActualOutput": "实际输出", + "Code": "代码" + }, + "Difficulty": { + "EASY": "简单", + "MEDIUM": "中等", + "HARD": "困难" + }, + "LanguageSettings": { + "en": { + "flag": "🇺🇸", + "name": "英语" + }, + "zh": { + "flag": "🇨🇳", + "name": "中文" + } + }, + "PlaygroundHeader": { + "RunCodeButton": { + "TooltipTrigger": { + "loading": "运行中...", + "ready": "运行" + }, + "TooltipContent": "运行代码" + } + }, + "ProblemPage": { + "Description": "题目描述", + "Solutions": "题解", + "Submissions": "提交记录", + "Details": "详情", + "Code": "代码", + "Testcase": "测试用例", + "Bot": "AI助手" + }, + "ProblemsetPage": { + "Status": "状态", + "Title": "题目", + "Difficulty": "难度" + }, + "SettingsDialog": { + "title": "设置", + "description": "在此处自定义设置。", + "breadcrumb": "设置", + "nav": { + "Appearance": "外观", + "Language": "语言", + "CodeEditor": "代码编辑器", + "Advanced": "高级设置" + } + }, + "StatusMessage": { + "PD": "待处理", + "QD": "排队中", + "CP": "编译中", + "CE": "编译错误", + "CS": "编译成功", + "RU": "运行中", + "TLE": "超出时间限制", + "MLE": "超出内存限制", + "RE": "运行时错误", + "AC": "通过", + "WA": "解答错误", + "SE": "系统错误" + }, + "SubmissionsTable": { + "Index": "序号", + "Status": "状态", + "Language": "语言", + "Time": "执行用时", + "Memory": "消耗内存" + }, + "WorkspaceEditorHeader": { + "LspStatusButton": { + "TooltipContent": "语言服务" + }, + "ResetButton": { + "TooltipContent": "重置代码" + }, + "UndoButton": { + "TooltipContent": "撤销" + }, + "RedoButton": { + "TooltipContent": "恢复" + }, + "FormatButton": { + "TooltipContent": "格式化" + }, + "CopyButton": { + "TooltipContent": "复制" + } + }, + "WorkspaceEditorFooter": { + "Row": "行", + "Column": "列" + } +} \ No newline at end of file diff --git a/src/app/(app)/problems/[id]/@Bot/page.tsx b/src/app/(app)/problems/[id]/@Bot/page.tsx index 09bc148..d0b9020 100644 --- a/src/app/(app)/problems/[id]/@Bot/page.tsx +++ b/src/app/(app)/problems/[id]/@Bot/page.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/tooltip"; import { useCallback } from "react"; import { useChat } from "@ai-sdk/react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; import MdxPreview from "@/components/mdx-preview"; @@ -19,6 +20,7 @@ import { ChatMessageList } from "@/components/ui/chat/chat-message-list"; import { ChatBubble, ChatBubbleMessage } from "@/components/ui/chat/chat-bubble"; export default function Bot() { + const t = useTranslations("Bot"); const { problemId, problem, currentLang, currentValue } = useProblem(); const { messages, input, handleInputChange, setMessages, handleSubmit } = useChat({ @@ -58,12 +60,12 @@ export default function Bot() { {!messages.some( (message) => message.role === "user" || message.role === "assistant" ) && ( -
- - Ask Bot - Powered by Vercel Ai SDK -
- )} +
+ + {t("title")} + {t("description")} +
+ )}
@@ -100,7 +102,7 @@ export default function Bot() { } }} className="h-full bg-muted border-transparent shadow-none rounded-lg" - placeholder="Bot will automatically get your current code" + placeholder={t("placeholder")} /> diff --git a/src/app/(app)/problems/[id]/@Details/layout.tsx b/src/app/(app)/problems/[id]/@Details/layout.tsx new file mode 100644 index 0000000..5cf90c9 --- /dev/null +++ b/src/app/(app)/problems/[id]/@Details/layout.tsx @@ -0,0 +1,8 @@ +import { getUserLocale } from "@/i18n/locale"; +import DetailsPage from "@/app/(app)/problems/[id]/@Details/page"; + +export default async function DetailsLayout() { + const locale = await getUserLocale(); + + return ; +} diff --git a/src/app/(app)/problems/[id]/@Details/page.tsx b/src/app/(app)/problems/[id]/@Details/page.tsx index 37f9348..07127f7 100644 --- a/src/app/(app)/problems/[id]/@Details/page.tsx +++ b/src/app/(app)/problems/[id]/@Details/page.tsx @@ -7,7 +7,10 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; +import { Locale } from "@/config/i18n"; +import { getLocale } from "@/lib/i18n"; import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; import { ArrowLeftIcon } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -20,7 +23,14 @@ import type { TestcaseResultWithTestcase } from "@/types/prisma"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { formatDistanceToNow, isBefore, subDays, format } from "date-fns"; -export default function DetailsPage() { +interface DetailsPageProps { + locale: Locale; +} + +export default function DetailsPage({ locale }: DetailsPageProps) { + const localeInstance = getLocale(locale); + const t = useTranslations("DetailsPage"); + const s = useTranslations("StatusMessage"); const { api, submission } = useDockviewStore(); const { editorLanguageConfigs, problemId } = useProblem(); const [lastFailedTestcase, setLastFailedTestcase] = @@ -53,7 +63,7 @@ export default function DetailsPage() { const createdAt = new Date(submission.createdAt); const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1)) ? format(createdAt, "yyyy-MM-dd") - : formatDistanceToNow(createdAt, { addSuffix: true }); + : formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance }); const source = `\`\`\`${submission?.language}\n${submission?.code}\n\`\`\``; @@ -76,7 +86,7 @@ export default function DetailsPage() { className="h-8 w-auto p-2 hover:bg-transparent text-muted-foreground hover:text-foreground" >
@@ -92,10 +102,10 @@ export default function DetailsPage() { getStatusColorClass(submission.status) )} > - {statusMap.get(submission.status)?.message} + {s(`${statusMap.get(submission.status)?.message}`)}
- Submitted on + {t("Time")} {submittedDisplay} @@ -118,7 +128,7 @@ export default function DetailsPage() { className="bg-background has-focus-visible:border-ring has-focus-visible:ring-ring/50 relative border px-4 py-1 outline-none first:rounded-t-md last:rounded-b-md last:border-b has-focus-visible:z-10 has-focus-visible:ring-[3px]" > -

Input

+

{t("Input")}

@@ -142,7 +152,7 @@ export default function DetailsPage() {
-

Expected Output

+

{t("ExpectedOutput")}

-

Your Output

+

{t("ActualOutput")}

- )} + + )}
- Code + {t("Code")} - +
diff --git a/src/app/(app)/problems/[id]/layout.tsx b/src/app/(app)/problems/[id]/layout.tsx index 558c043..304f5b9 100644 --- a/src/app/(app)/problems/[id]/layout.tsx +++ b/src/app/(app)/problems/[id]/layout.tsx @@ -1,5 +1,6 @@ import prisma from "@/lib/prisma"; import { notFound } from "next/navigation"; +import { getUserLocale } from "@/i18n/locale"; import ProblemPage from "@/app/(app)/problems/[id]/page"; import { ProblemStoreProvider } from "@/providers/problem-store-provider"; import { PlaygroundHeader } from "@/components/features/playground/header"; @@ -59,6 +60,8 @@ export default async function ProblemLayout({ return notFound(); } + const locale = await getUserLocale(); + return (
{ + setKey((prevKey) => prevKey + 1); + }, [locale]); + return ( s.status === "AC").map(s => s.problemId)); const attemptedProblems = new Set(submissions.filter(s => s.status !== "AC").map(s => s.problemId)); + const t = await getTranslations(); + return ( - Status - Title - Difficulty + {t("ProblemsetPage.Status")} + {t("ProblemsetPage.Title")} + {t("ProblemsetPage.Difficulty")} @@ -60,7 +63,7 @@ export default async function ProblemsetPage() { - {problem.difficulty} + {t(`Difficulty.${problem.difficulty}`)} ))} diff --git a/src/components/appearance-settings.tsx b/src/components/appearance-settings.tsx index ad5c0a7..fc1429b 100644 --- a/src/components/appearance-settings.tsx +++ b/src/components/appearance-settings.tsx @@ -2,22 +2,25 @@ import Image from "next/image"; import { useTheme } from "next-themes"; +import { useTranslations } from "next-intl"; import { CheckIcon, MinusIcon } from "lucide-react"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -const items = [ - { value: "system", label: "System", image: "/ui-system.png" }, - { value: "light", label: "Light", image: "/ui-light.png" }, - { value: "dark", label: "Dark", image: "/ui-dark.png" }, -]; - export default function AppearanceSettings() { + const t = useTranslations("AppearanceSettings"); + + const items = [ + { value: "system", label: t("items.System"), image: "/ui-system.png" }, + { value: "light", label: t("items.Light"), image: "/ui-light.png" }, + { value: "dark", label: t("items.Dark"), image: "/ui-dark.png" }, + ]; + const { theme, setTheme } = useTheme(); return (
- Choose a theme + {t("title")} {items.map((item) => ( diff --git a/src/components/avatar-button.tsx b/src/components/avatar-button.tsx index 51bd2c7..62853e3 100644 --- a/src/components/avatar-button.tsx +++ b/src/components/avatar-button.tsx @@ -9,6 +9,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { auth, signOut } from "@/lib/auth"; +import { getTranslations } from "next-intl/server"; import { Skeleton } from "@/components/ui/skeleton"; import LogInButton from "@/components/log-in-button"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; @@ -28,6 +29,7 @@ async function handleLogOut() { export async function AvatarButton() { const session = await auth(); + const t = await getTranslations("AvatarButton"); const isLoggedIn = !!session?.user; const image = session?.user?.image ?? "https://github.com/shadcn.png"; const name = session?.user?.name ?? "unknown"; @@ -64,7 +66,7 @@ export async function AvatarButton() { - Log out + {t("LogOut")} diff --git a/src/components/back-button.tsx b/src/components/back-button.tsx index 8410f35..d894fd0 100644 --- a/src/components/back-button.tsx +++ b/src/components/back-button.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { cn } from "@/lib/utils"; +import { useTranslations } from "next-intl"; import { ArrowLeftIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; @@ -14,6 +15,8 @@ export default function BackButton({ className, ...props }: BackButtonProps) { + const t = useTranslations(); + return ( @@ -29,7 +32,9 @@ export default function BackButton({ - Back + + {t("BackButton")} + ); diff --git a/src/components/banner.tsx b/src/components/banner.tsx index 9ad68ca..d1da4f9 100644 --- a/src/components/banner.tsx +++ b/src/components/banner.tsx @@ -1,5 +1,6 @@ import { cn } from "@/lib/utils"; import { siteConfig } from "@/config/site"; +import { useTranslations } from 'next-intl'; import { ArrowRightIcon } from "lucide-react"; interface BannerProps { @@ -11,9 +12,11 @@ interface BannerProps { export function Banner({ className, link = siteConfig.url.repo.github, - text = "Star this project if you like it.", + text, ...props }: BannerProps) { + const t = useTranslations(); + return (
- {text} + {text || t("Banner.Text")} (true); const [isBotVisible, setBotVisible] = useState(false); @@ -37,7 +39,7 @@ export default function BotVisibilityToggle() { id: "Bot", component: "Bot", tabComponent: "Bot", - title: "Bot", + title: t("ProblemPage.Bot"), position: { direction: "right", }, @@ -70,7 +72,7 @@ export default function BotVisibilityToggle() { -

{isBotVisible ? "Close Bot" : "Open Bot"}

+

{isBotVisible ? t("BotVisibilityToggle.close") : t("BotVisibilityToggle.open")}

diff --git a/src/components/features/playground/header.tsx b/src/components/features/playground/header.tsx index 1bbbe06..bd730a1 100644 --- a/src/components/features/playground/header.tsx +++ b/src/components/features/playground/header.tsx @@ -1,6 +1,7 @@ import { cn } from "@/lib/utils"; -import { RunCode } from "@/components/run-code"; +import { auth } from "@/lib/auth"; import BackButton from "@/components/back-button"; +import { RunCodeButton } from "@/components/run-code"; import { AvatarButton } from "@/components/avatar-button"; import BotVisibilityToggle from "@/components/bot-visibility-toggle"; @@ -8,10 +9,12 @@ interface PlaygroundHeaderProps { className?: string; } -export function PlaygroundHeader({ +export async function PlaygroundHeader({ className, ...props }: PlaygroundHeaderProps) { + const session = await auth(); + return (
- +
diff --git a/src/components/features/playground/workspace/editor/components/copy-button.tsx b/src/components/features/playground/workspace/editor/components/copy-button.tsx index 000d5e6..b229c86 100644 --- a/src/components/features/playground/workspace/editor/components/copy-button.tsx +++ b/src/components/features/playground/workspace/editor/components/copy-button.tsx @@ -9,12 +9,14 @@ import { import { cn } from "@/lib/utils"; import { useState } from "react"; import { Check, Copy } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; export function CopyButton() { const { editor } = useProblem(); const [copied, setCopied] = useState(false); + const t = useTranslations("WorkspaceEditorHeader.CopyButton"); const handleCopy = async () => { try { @@ -65,7 +67,7 @@ export function CopyButton() { - Click to Copy + {t("TooltipContent")} diff --git a/src/components/features/playground/workspace/editor/components/footer.tsx b/src/components/features/playground/workspace/editor/components/footer.tsx index bdc34d8..132ee1c 100644 --- a/src/components/features/playground/workspace/editor/components/footer.tsx +++ b/src/components/features/playground/workspace/editor/components/footer.tsx @@ -2,6 +2,7 @@ import { cn } from "@/lib/utils"; import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; import { useProblem } from "@/hooks/use-problem"; import { CircleXIcon, TriangleAlertIcon } from "lucide-react"; @@ -20,6 +21,7 @@ export function WorkspaceEditorFooter({ className, ...props }: WorkspaceEditorFooterProps) { + const t = useTranslations("WorkspaceEditorFooter"); const { editor, markers } = useProblem(); const [position, setPosition] = useState<{ lineNumber: number; column: number } | null>(null); @@ -62,8 +64,8 @@ export function WorkspaceEditorFooter({ {position - ? `Row ${position.lineNumber}, Column ${position.column}` - : "Row -, Column -"} + ? `${t("Row")} ${position.lineNumber}, ${t("Column")} ${position.column}` + : `${t("Row")} -, ${t("Column")} -`} diff --git a/src/components/features/playground/workspace/editor/components/format-button.tsx b/src/components/features/playground/workspace/editor/components/format-button.tsx index ddcc687..ab934b6 100644 --- a/src/components/features/playground/workspace/editor/components/format-button.tsx +++ b/src/components/features/playground/workspace/editor/components/format-button.tsx @@ -7,11 +7,13 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Paintbrush } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; export function FormatButton() { const { editor } = useProblem(); + const t = useTranslations("WorkspaceEditorHeader.FormatButton"); return ( @@ -20,7 +22,7 @@ export function FormatButton() { - Format Code + {t("TooltipContent")} diff --git a/src/components/features/playground/workspace/editor/components/lsp-status-button.tsx b/src/components/features/playground/workspace/editor/components/lsp-status-button.tsx index 0891d18..bddb4c2 100644 --- a/src/components/features/playground/workspace/editor/components/lsp-status-button.tsx +++ b/src/components/features/playground/workspace/editor/components/lsp-status-button.tsx @@ -6,6 +6,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; @@ -28,6 +29,7 @@ const getLspStatusColor = (webSocket: WebSocket | null) => { export function LspStatusButton() { const { webSocket } = useProblem(); + const t = useTranslations("WorkspaceEditorHeader.LspStatusButton"); return ( @@ -48,7 +50,7 @@ export function LspStatusButton() { - Language Server + {t("TooltipContent")} diff --git a/src/components/features/playground/workspace/editor/components/redo-button.tsx b/src/components/features/playground/workspace/editor/components/redo-button.tsx index c914f5e..92bcd63 100644 --- a/src/components/features/playground/workspace/editor/components/redo-button.tsx +++ b/src/components/features/playground/workspace/editor/components/redo-button.tsx @@ -7,11 +7,13 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Redo2 } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; export function RedoButton() { const { editor } = useProblem(); + const t = useTranslations("WorkspaceEditorHeader.RedoButton"); return ( @@ -20,7 +22,7 @@ export function RedoButton() { - Redo Code + {t("TooltipContent")} diff --git a/src/components/features/playground/workspace/editor/components/reset-button.tsx b/src/components/features/playground/workspace/editor/components/reset-button.tsx index 9c56776..4f4b5d8 100644 --- a/src/components/features/playground/workspace/editor/components/reset-button.tsx +++ b/src/components/features/playground/workspace/editor/components/reset-button.tsx @@ -7,11 +7,13 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { RotateCcw } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; export function ResetButton() { const { editor, currentTemplate } = useProblem(); + const t = useTranslations("WorkspaceEditorHeader.ResetButton"); const handleReset = () => { if (editor) { @@ -38,7 +40,7 @@ export function ResetButton() { - Undo Code + {t("TooltipContent")} diff --git a/src/components/language-settings.tsx b/src/components/language-settings.tsx new file mode 100644 index 0000000..184c332 --- /dev/null +++ b/src/components/language-settings.tsx @@ -0,0 +1,65 @@ +"use client"; + +import Flag from "react-world-flags"; +import { Globe } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { Locale, locales } from "@/config/i18n"; +import { useState, useMemo, useEffect } from "react"; +import { getUserLocale, setUserLocale } from "@/i18n/locale"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +export function LanguageSettings() { + const t = useTranslations(); + const [selectedOption, setSelectedOption] = useState(); + + useEffect(() => { + const fetchLocale = async () => { + const userLocale = await getUserLocale(); + if (!userLocale) return; + setSelectedOption(userLocale); + }; + fetchLocale(); + }, []); + + const localeOptions = useMemo(() => { + const options = locales.map((locale) => ({ + value: locale, + label: `${t(`LanguageSettings.${locale}.name`)}`, + })); + return options.sort((a, b) => a.value.localeCompare(b.value)); + }, [t]); + + const handleValueChange = (value: Locale) => { + setSelectedOption(value); + setUserLocale(value); + }; + + const getIconForLocale = (locale: Locale) => { + switch (locale) { + case "en": + return ; + case "zh": + return ; + default: + return ; + } + }; + + return ( + + ); +} diff --git a/src/components/log-in-button.tsx b/src/components/log-in-button.tsx index 0f9a6f0..e7438a1 100644 --- a/src/components/log-in-button.tsx +++ b/src/components/log-in-button.tsx @@ -1,6 +1,7 @@ "use client"; import { LogIn } from "lucide-react"; +import { useTranslations } from "next-intl"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; @@ -8,6 +9,7 @@ export default function LogInButton() { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const t = useTranslations("AvatarButton"); const handleLogIn = () => { const params = new URLSearchParams(searchParams.toString()); @@ -17,8 +19,8 @@ export default function LogInButton() { return ( - - Log In + + {t("LogIn")} ); } diff --git a/src/components/run-code.tsx b/src/components/run-code.tsx index 3bfc9e6..b8da82a 100644 --- a/src/components/run-code.tsx +++ b/src/components/run-code.tsx @@ -8,24 +8,32 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useState } from "react"; +import { Session } from "next-auth"; import { judge } from "@/actions/judge"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; import { useDockviewStore } from "@/stores/dockview"; import { LoaderCircleIcon, PlayIcon } from "lucide-react"; import { showStatusToast } from "@/hooks/show-status-toast"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; -interface RunCodeProps { +interface RunCodeButtonProps { className?: string; + session: Session | null; } -export function RunCode({ +export function RunCodeButton({ className, - ...props -}: RunCodeProps) { + session, +}: RunCodeButtonProps) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const { api } = useDockviewStore(); const { currentLang, editor, problemId } = useProblem(); const [isLoading, setIsLoading] = useState(false); + const t = useTranslations("PlaygroundHeader.RunCodeButton"); const handleJudge = async () => { if (!editor) return; @@ -33,18 +41,24 @@ export function RunCode({ const code = editor.getValue() || ""; setIsLoading(true); - try { - const result = await judge(currentLang, code, problemId); - showStatusToast({ status: result.status }); - const panel = api?.getPanel("Submissions"); - if (panel && !panel.api.isActive) { - panel.api.setActive(); + if (!session) { + const params = new URLSearchParams(searchParams.toString()); + params.set("redirectTo", pathname); + router.push(`/sign-in?${params.toString()}`); + } else { + try { + const result = await judge(currentLang, code, problemId); + showStatusToast({ status: result.status }); + const panel = api?.getPanel("Submissions"); + if (panel && !panel.api.isActive) { + panel.api.setActive(); + } + } catch (error) { + console.error("Error occurred while judging the code:"); + console.error(error); + } finally { + setIsLoading(false); } - } catch (error) { - console.error("Error occurred while judging the code:"); - console.error(error); - } finally { - setIsLoading(false); } }; @@ -53,7 +67,6 @@ export function RunCode({ - Run Code + + {t("TooltipContent")} + ); diff --git a/src/components/settings-button.tsx b/src/components/settings-button.tsx index 2d33383..b0b2511 100644 --- a/src/components/settings-button.tsx +++ b/src/components/settings-button.tsx @@ -1,16 +1,18 @@ "use client"; import { Settings } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useSettingsStore } from "@/stores/useSettingsStore"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; export function SettingsButton() { + const t = useTranslations("AvatarButton"); const { setDialogOpen } = useSettingsStore(); return ( setDialogOpen(true)}> - Settings + {t("Settings")} ); } diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 425a3df..3ce0475 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -25,29 +25,31 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import AppearanceSettings from "./appearance-settings"; +import { useTranslations } from "next-intl"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useSettingsStore } from "@/stores/useSettingsStore"; +import AppearanceSettings from "@/components/appearance-settings"; +import { LanguageSettings } from "@/components/language-settings"; import { CodeXml, Globe, Paintbrush, Settings } from "lucide-react"; -const data = { - nav: [ - { name: "Appearance", icon: Paintbrush }, - { name: "Language & region", icon: Globe }, - { name: "Code Editor", icon: CodeXml }, - { name: "Advanced", icon: Settings }, - ], -}; - export function SettingsDialog() { + const t = useTranslations("SettingsDialog"); + const data = { + nav: [ + { id: "Appearance", name: t("nav.Appearance"), icon: Paintbrush }, + { id: "Language", name: t("nav.Language"), icon: Globe }, + { id: "CodeEditor", name: t("nav.CodeEditor"), icon: CodeXml }, + { id: "Advanced", name: t("nav.Advanced"), icon: Settings }, + ], + }; const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } = useSettingsStore(); return ( - Settings + {t("title")} - Customize your settings here. + {t("description")} @@ -59,10 +61,10 @@ export function SettingsDialog() { setActiveSetting(item.name)} + isActive={item.id === activeSetting} + onClick={() => setActiveSetting(item.id)} > -
+ {item.name} @@ -80,11 +82,11 @@ export function SettingsDialog() { - Settings + {t("breadcrumb")} - {activeSetting} + {t(`nav.${activeSetting}`)} @@ -92,16 +94,8 @@ export function SettingsDialog() {
- {activeSetting === "Appearance" ? ( - - ) : ( - Array.from({ length: 10 }).map((_, i) => ( -
- )) - )} + {activeSetting === "Appearance" && } + {activeSetting === "Language" && }
diff --git a/src/components/submissions-table.tsx b/src/components/submissions-table.tsx index 5b2ae0a..3f74c02 100644 --- a/src/components/submissions-table.tsx +++ b/src/components/submissions-table.tsx @@ -9,6 +9,9 @@ import { TableRow, } from "@/components/ui/table"; import { cn } from "@/lib/utils"; +import { Locale } from "@/config/i18n"; +import { useTranslations } from "next-intl"; +import { enUS, zhCN } from "date-fns/locale"; import { useProblem } from "@/hooks/use-problem"; import { Clock4Icon, CpuIcon } from "lucide-react"; import { useDockviewStore } from "@/stores/dockview"; @@ -18,10 +21,23 @@ import { EditorLanguageIcons } from "@/config/editor-language-icons"; import { formatDistanceToNow, isBefore, subDays, format } from "date-fns"; interface SubmissionsTableProps { + locale: Locale; submissions: SubmissionWithTestcaseResult[]; } -export default function SubmissionsTable({ submissions }: SubmissionsTableProps) { +const getLocale = (locale: Locale) => { + switch (locale) { + case "zh": + return zhCN; + case "en": + default: + return enUS; + } +} + +export default function SubmissionsTable({ locale, submissions }: SubmissionsTableProps) { + const s = useTranslations("StatusMessage"); + const t = useTranslations("SubmissionsTable"); const { editorLanguageConfigs } = useProblem(); const { api, setSubmission } = useDockviewStore(); @@ -41,7 +57,7 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps) id: "Details", component: "Details", tabComponent: "Details", - title: submission.status, + title: s(`${submission.status}`), position: { referencePanel: "Submissions", direction: "within", @@ -54,11 +70,11 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
- Index - Status - Language - Time - Memory + {t("Index")} + {t("Status")} + {t("Language")} + {t("Time")} + {t("Memory")} @@ -68,11 +84,13 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps) {sortedSubmissions.map((submission, index) => { const Icon = EditorLanguageIcons[submission.language]; const createdAt = new Date(submission.createdAt); + const localeInstance = getLocale(locale); const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1)) ? format(createdAt, "yyyy-MM-dd") - : formatDistanceToNow(createdAt, { addSuffix: true }); + : formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance }); const isEven = (submissions.length - index) % 2 === 0; + const message = statusMap.get(submission.status)?.message; return (
- {statusMap.get(submission.status)?.message} + {s(`${message}`)} {submittedDisplay}
diff --git a/src/hooks/show-status-toast.tsx b/src/hooks/show-status-toast.tsx index ccbba66..a8f9cd8 100644 --- a/src/hooks/show-status-toast.tsx +++ b/src/hooks/show-status-toast.tsx @@ -3,6 +3,7 @@ import { XIcon, } from "lucide-react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import type { Status } from "@/generated/client"; import { getStatusColorClass, statusMap } from "@/lib/status"; @@ -17,34 +18,38 @@ const StatusToast = ({ Icon: LucideIcon; message: string; colorClass: string; -}) => ( -
-
-
-