mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 14:22:13 +08:00
feat: implement i18n
This commit is contained in:
@@ -23,6 +23,7 @@ import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { Tooltip } from "@/components/workspace/tooltip";
|
||||
import { Welcome } from "@/components/workspace/welcome";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { type AgentThread } from "@/core/threads";
|
||||
import { useSubmitThread, useThreadStream } from "@/core/threads/hooks";
|
||||
@@ -31,6 +32,7 @@ import { uuid } from "@/core/utils/uuid";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function ChatPage() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const [settings, setSettings] = useLocalSettings();
|
||||
const { setOpen: setSidebarOpen } = useSidebar();
|
||||
@@ -41,7 +43,6 @@ export default function ChatPage() {
|
||||
setArtifacts,
|
||||
selectedArtifact,
|
||||
} = useArtifacts();
|
||||
|
||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
||||
const isNewThread = useMemo(
|
||||
() => threadIdFromPath === "new",
|
||||
@@ -117,7 +118,7 @@ export default function ChatPage() {
|
||||
}}
|
||||
>
|
||||
<FilesIcon />
|
||||
Artifacts
|
||||
{t.common.artifacts}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -10,11 +10,13 @@ import {
|
||||
WorkspaceContainer,
|
||||
WorkspaceHeader,
|
||||
} from "@/components/workspace/workspace-container";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useThreads } from "@/core/threads/hooks";
|
||||
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||
import { formatTimeAgo } from "@/core/utils/datetime";
|
||||
|
||||
export default function ChatsPage() {
|
||||
const { t } = useI18n();
|
||||
const { data: threads } = useThreads();
|
||||
const [search, setSearch] = useState("");
|
||||
const filteredThreads = useMemo(() => {
|
||||
@@ -31,7 +33,7 @@ export default function ChatsPage() {
|
||||
<Input
|
||||
type="search"
|
||||
className="h-12 w-full max-w-(--container-width-md) text-xl"
|
||||
placeholder="Search chats"
|
||||
placeholder={t.chats.searchChats}
|
||||
autoFocus
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -41,6 +42,7 @@ export function ArtifactFileDetail({
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
const isWriteFile = useMemo(() => {
|
||||
return filepathFromProps.startsWith("write-file:");
|
||||
@@ -124,26 +126,26 @@ export function ArtifactFileDetail({
|
||||
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
||||
<ArtifactAction
|
||||
icon={SquareArrowOutUpRightIcon}
|
||||
label="Open in new window"
|
||||
tooltip="Open in new window"
|
||||
label={t.common.openInNewWindow}
|
||||
tooltip={t.common.openInNewWindow}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
icon={CopyIcon}
|
||||
label="Copy"
|
||||
label={t.clipboard.copyToClipboard}
|
||||
disabled={!content}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content ?? "");
|
||||
toast.success("Copied to clipboard");
|
||||
toast.success(t.clipboard.copiedToClipboard);
|
||||
} catch (error) {
|
||||
toast.error("Failed to copy to clipboard");
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
tooltip="Copy content to clipboard"
|
||||
tooltip={t.clipboard.copyToClipboard}
|
||||
/>
|
||||
)}
|
||||
{!isWriteFile && (
|
||||
@@ -153,17 +155,16 @@ export function ArtifactFileDetail({
|
||||
>
|
||||
<ArtifactAction
|
||||
icon={DownloadIcon}
|
||||
label="Download"
|
||||
onClick={() => console.log("Download")}
|
||||
tooltip="Download file"
|
||||
label={t.common.download}
|
||||
tooltip={t.common.download}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<ArtifactAction
|
||||
icon={XIcon}
|
||||
label="Close"
|
||||
label={t.common.close}
|
||||
onClick={() => setOpen(false)}
|
||||
tooltip="Close"
|
||||
tooltip={t.common.close}
|
||||
/>
|
||||
</ArtifactActions>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { getFileExtensionDisplayName, getFileName } from "@/core/utils/files";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -24,6 +25,7 @@ export function ArtifactFileList({
|
||||
files: string[];
|
||||
threadId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { select: selectArtifact, setOpen } = useArtifacts();
|
||||
const handleClick = useCallback(
|
||||
(filepath: string) => {
|
||||
@@ -57,7 +59,7 @@ export function ArtifactFileList({
|
||||
>
|
||||
<Button variant="ghost">
|
||||
<DownloadIcon className="size-4" />
|
||||
Download
|
||||
{t.common.download}
|
||||
</Button>
|
||||
</a>
|
||||
</CardAction>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useCallback, useState, type ComponentProps } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
@@ -11,6 +12,7 @@ export function CopyButton({
|
||||
}: ComponentProps<typeof Button> & {
|
||||
clipboardData: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleCopy = useCallback(() => {
|
||||
void navigator.clipboard.writeText(clipboardData);
|
||||
@@ -18,7 +20,7 @@ export function CopyButton({
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, [clipboardData]);
|
||||
return (
|
||||
<Tooltip content="Copy to clipboard">
|
||||
<Tooltip content={t.clipboard.copyToClipboard}>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ChatStatus } from "ai";
|
||||
import { CheckIcon, LightbulbIcon, LightbulbOffIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useState, type ComponentProps } from "react";
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
PromptInputTextarea,
|
||||
type PromptInputMessage,
|
||||
} from "@/components/ai-elements/prompt-input";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -40,11 +43,11 @@ export function InputBox({
|
||||
assistantId?: string | null;
|
||||
status?: ChatStatus;
|
||||
context: Omit<AgentThreadContext, "thread_id">;
|
||||
showWelcome?: boolean;
|
||||
onContextChange?: (context: Omit<AgentThreadContext, "thread_id">) => void;
|
||||
onSubmit?: (message: PromptInputMessage) => void;
|
||||
onStop?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [modelDialogOpen, setModelDialogOpen] = useState(false);
|
||||
const { models } = useModels();
|
||||
const selectedModel = useMemo(
|
||||
@@ -97,7 +100,7 @@ export function InputBox({
|
||||
<PromptInputBody>
|
||||
<PromptInputTextarea
|
||||
className={cn("size-full")}
|
||||
placeholder="How can I assist you today?"
|
||||
placeholder={t.inputBox.placeholder}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</PromptInputBody>
|
||||
@@ -107,13 +110,17 @@ export function InputBox({
|
||||
content={
|
||||
context.thinking_enabled ? (
|
||||
<div className="tex-sm flex flex-col gap-1">
|
||||
<div>Thinking is enabled</div>
|
||||
<div className="opacity-50">Click to disable thinking</div>
|
||||
<div>{t.inputBox.thinkingEnabled}</div>
|
||||
<div className="opacity-50">
|
||||
{t.inputBox.clickToDisableThinking}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tex-sm flex flex-col gap-1">
|
||||
<div>Thinking is disabled</div>
|
||||
<div className="opacity-50">Click to enable thinking</div>
|
||||
<div>{t.inputBox.thinkingDisabled}</div>
|
||||
<div className="opacity-50">
|
||||
{t.inputBox.clickToEnableThinking}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -142,7 +149,7 @@ export function InputBox({
|
||||
</PromptInputButton>
|
||||
</ModelSelectorTrigger>
|
||||
<ModelSelectorContent>
|
||||
<ModelSelectorInput placeholder="Search models..." />
|
||||
<ModelSelectorInput placeholder={t.inputBox.searchModels} />
|
||||
<ModelSelectorList>
|
||||
{models.map((m) => (
|
||||
<ModelSelectorItem
|
||||
|
||||
@@ -21,8 +21,10 @@ import {
|
||||
ChainOfThoughtSearchResults,
|
||||
ChainOfThoughtStep,
|
||||
} from "@/components/ai-elements/chain-of-thought";
|
||||
import { CodeBlock } from "@/components/ai-elements/code-block";
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
extractReasoningContentFromMessage,
|
||||
findToolCallResult,
|
||||
@@ -33,7 +35,6 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
import { useArtifacts } from "../artifacts";
|
||||
import { FlipDisplay } from "../flip-display";
|
||||
import { CodeBlock } from "@/components/ai-elements/code-block";
|
||||
|
||||
export function MessageGroup({
|
||||
className,
|
||||
@@ -44,6 +45,7 @@ export function MessageGroup({
|
||||
messages: Message[];
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [showAbove, setShowAbove] = useState(false);
|
||||
const [showLastThinking, setShowLastThinking] = useState(false);
|
||||
const steps = useMemo(() => convertToSteps(messages), [messages]);
|
||||
@@ -84,8 +86,8 @@ export function MessageGroup({
|
||||
label={
|
||||
<span className="opacity-60">
|
||||
{showAbove
|
||||
? "Less steps"
|
||||
: `${aboveLastToolCallSteps.length} more step${aboveLastToolCallSteps.length === 1 ? "" : "s"}`}
|
||||
? t.toolCalls.lessSteps
|
||||
: t.toolCalls.moreSteps(aboveLastToolCallSteps.length)}
|
||||
</span>
|
||||
}
|
||||
icon={
|
||||
@@ -134,7 +136,7 @@ export function MessageGroup({
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<ChainOfThoughtStep
|
||||
className="font-normal"
|
||||
label="Thinking"
|
||||
label={t.common.thinking}
|
||||
icon={LightbulbIcon}
|
||||
></ChainOfThoughtStep>
|
||||
<div>
|
||||
@@ -178,16 +180,12 @@ function ToolCall({
|
||||
args: Record<string, unknown>;
|
||||
result?: string | Record<string, unknown>;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { select, setOpen } = useArtifacts();
|
||||
if (name === "web_search") {
|
||||
let label: React.ReactNode = "Search for related information";
|
||||
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
||||
if (typeof args.query === "string") {
|
||||
label = (
|
||||
<div>
|
||||
Search on the web for{" "}
|
||||
<span className="font-bold">"{args.query}"</span>
|
||||
</div>
|
||||
);
|
||||
label = t.toolCalls.searchOnWebFor(args.query);
|
||||
}
|
||||
return (
|
||||
<ChainOfThoughtStep key={id} label={label} icon={SearchIcon}>
|
||||
@@ -217,7 +215,7 @@ function ToolCall({
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
className="cursor-pointer"
|
||||
label="View web page"
|
||||
label={t.toolCalls.viewWebPage}
|
||||
icon={GlobeIcon}
|
||||
onClick={() => {
|
||||
window.open(url, "_blank");
|
||||
@@ -236,7 +234,7 @@ function ToolCall({
|
||||
let description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
description = "List folder";
|
||||
description = t.toolCalls.listFolder;
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
return (
|
||||
@@ -250,7 +248,7 @@ function ToolCall({
|
||||
let description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
description = "Read file";
|
||||
description = t.toolCalls.readFile;
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
return (
|
||||
@@ -264,7 +262,7 @@ function ToolCall({
|
||||
let description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
description = "Write file";
|
||||
description = t.toolCalls.writeFile;
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
return (
|
||||
@@ -291,7 +289,7 @@ function ToolCall({
|
||||
const description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
if (!description) {
|
||||
return "Execute command";
|
||||
return t.toolCalls.executeCommand;
|
||||
}
|
||||
const command: string | undefined = (args as { command: string })?.command;
|
||||
return (
|
||||
@@ -312,7 +310,11 @@ function ToolCall({
|
||||
);
|
||||
} else if (name === "present_files") {
|
||||
return (
|
||||
<ChainOfThoughtStep key={id} label="Present files" icon={FileTextIcon}>
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={t.toolCalls.presentFiles}
|
||||
icon={FileTextIcon}
|
||||
>
|
||||
<ChainOfThoughtSearchResult>
|
||||
{Array.isArray((args as { filepaths: string[] }).filepaths) &&
|
||||
(args as { filepaths: string[] }).filepaths.map(
|
||||
@@ -330,7 +332,7 @@ function ToolCall({
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label="Need your help"
|
||||
label={t.toolCalls.needYourHelp}
|
||||
icon={MessageCircleQuestionMarkIcon}
|
||||
></ChainOfThoughtStep>
|
||||
);
|
||||
@@ -340,13 +342,7 @@ function ToolCall({
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={
|
||||
description ?? (
|
||||
<div>
|
||||
Use "<span className="font-bold">{name}</span>" tool
|
||||
</div>
|
||||
)
|
||||
}
|
||||
label={description ?? t.toolCalls.useTool(name)}
|
||||
icon={WrenchIcon}
|
||||
></ChainOfThoughtStep>
|
||||
);
|
||||
|
||||
@@ -20,10 +20,12 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useDeleteThread, useThreads } from "@/core/threads/hooks";
|
||||
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||
|
||||
export function RecentChatList() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
||||
@@ -52,7 +54,7 @@ export function RecentChatList() {
|
||||
}
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Recent chats</SidebarGroupLabel>
|
||||
<SidebarGroupLabel>{t.sidebar.recentChats}</SidebarGroupLabel>
|
||||
<SidebarGroupContent className="group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0">
|
||||
<SidebarMenu>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
@@ -78,7 +80,7 @@ export function RecentChatList() {
|
||||
className="bg-background/50 hover:bg-background"
|
||||
>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
<span className="sr-only">{t.common.more}</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -90,7 +92,7 @@ export function RecentChatList() {
|
||||
onSelect={() => handleDelete(thread.thread_id)}
|
||||
>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete</span>
|
||||
<span>{t.common.delete}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
0
frontend/src/components/workspace/settings/index.ts
Normal file
0
frontend/src/components/workspace/settings/index.ts
Normal file
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Welcome({ className }: { className?: string }) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -8,17 +12,13 @@ export function Welcome({ className }: { className?: string }) {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="text-2xl font-bold">👋 Hello, again!</div>
|
||||
<div className="text-2xl font-bold">{t.welcome.greeting}</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<p>
|
||||
Welcome to 🦌 DeerFlow, an open source super agent. With built-in and
|
||||
custom
|
||||
</p>
|
||||
<p>
|
||||
skills, DeerFlow helps you search on the web, analyze data, and
|
||||
generate
|
||||
</p>{" "}
|
||||
<p>artifacts like slides, web pages and do almost anything.</p>
|
||||
{t.welcome.description.includes("\n") ? (
|
||||
<pre className="whitespace-pre">{t.welcome.description}</pre>
|
||||
) : (
|
||||
<p>{t.welcome.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { GithubIcon } from "./github-icon";
|
||||
@@ -34,6 +35,7 @@ export function WorkspaceHeader({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"header">) {
|
||||
const { t } = useI18n();
|
||||
const pathname = usePathname();
|
||||
const segments = useMemo(() => {
|
||||
const parts = pathname?.split("/") || [];
|
||||
@@ -56,7 +58,7 @@ export function WorkspaceHeader({
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={`/${segments[0]}`}>
|
||||
{nameOfSegment(segments[0])}
|
||||
{nameOfSegment(segments[0], t)}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
@@ -68,12 +70,12 @@ export function WorkspaceHeader({
|
||||
{segments.length >= 2 ? (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={`/${segments[0]}/${segments[1]}`}>
|
||||
{nameOfSegment(segments[1])}
|
||||
{nameOfSegment(segments[1], t)}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
) : (
|
||||
<BreadcrumbPage>
|
||||
{nameOfSegment(segments[1])}
|
||||
{nameOfSegment(segments[1], t)}
|
||||
</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
@@ -89,7 +91,7 @@ export function WorkspaceHeader({
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="pr-4">
|
||||
<Tooltip content="DeerFlow on Github">
|
||||
<Tooltip content={t.workspace.githubTooltip}>
|
||||
<a
|
||||
href="https://github.com/bytedance/deer-flow"
|
||||
target="_blank"
|
||||
@@ -122,7 +124,12 @@ export function WorkspaceBody({
|
||||
);
|
||||
}
|
||||
|
||||
function nameOfSegment(segment: string | undefined) {
|
||||
if (!segment) return "Home";
|
||||
function nameOfSegment(
|
||||
segment: string | undefined,
|
||||
t: ReturnType<typeof useI18n>["t"],
|
||||
) {
|
||||
if (!segment) return t.common.home;
|
||||
if (segment === "workspace") return t.breadcrumb.workspace;
|
||||
if (segment === "chats") return t.breadcrumb.chats;
|
||||
return segment[0]?.toUpperCase() + segment.slice(1);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function WorkspaceHeader({ className }: { className?: string }) {
|
||||
const { t } = useI18n();
|
||||
const { state } = useSidebar();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
@@ -48,7 +50,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
|
||||
>
|
||||
<Link className="text-muted-foreground" href="/workspace/chats/new">
|
||||
<MessageSquarePlus size={16} />
|
||||
<span>New chat</span>
|
||||
<span>{t.sidebar.newChat}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
|
||||
export function WorkspaceNavMenu() {
|
||||
const { t } = useI18n();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<SidebarGroup className="pt-1">
|
||||
@@ -20,7 +22,7 @@ export function WorkspaceNavMenu() {
|
||||
<SidebarMenuButton isActive={pathname === "/workspace/chats"} asChild>
|
||||
<Link className="text-muted-foreground" href="/workspace/chats">
|
||||
<MessagesSquare />
|
||||
<span>Chats</span>
|
||||
<span>{t.sidebar.chats}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarRail,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
|
||||
import { RecentChatList } from "./recent-chat-list";
|
||||
import { WorkspaceHeader } from "./workspace-header";
|
||||
@@ -13,6 +21,7 @@ import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||
export function WorkspaceSidebar({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar>) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Sidebar variant="sidebar" collapsible="icon" {...props}>
|
||||
<SidebarHeader className="py-0">
|
||||
@@ -22,7 +31,22 @@ export function WorkspaceSidebar({
|
||||
<WorkspaceNavMenu />
|
||||
<RecentChatList />
|
||||
</SidebarContent>
|
||||
<SidebarFooter></SidebarFooter>
|
||||
<SidebarFooter>
|
||||
<SidebarGroup className="px-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<div className="text-muted-foreground cursor-pointer">
|
||||
<SettingsIcon size={16} />
|
||||
<span>{t.common.settings}</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
57
frontend/src/core/i18n/hooks.ts
Normal file
57
frontend/src/core/i18n/hooks.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { enUS } from "./locales/en-US";
|
||||
import { zhCN } from "./locales/zh-CN";
|
||||
|
||||
import { detectLocale, type Locale, type Translations } from "./index";
|
||||
|
||||
const translations: Record<Locale, Translations> = {
|
||||
"en-US": enUS,
|
||||
"zh-CN": zhCN,
|
||||
};
|
||||
|
||||
export function useI18n() {
|
||||
const [locale, setLocale] = useState<Locale>(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return "en-US";
|
||||
}
|
||||
|
||||
// Try to get from localStorage first
|
||||
const saved = localStorage.getItem("locale") as Locale | null;
|
||||
if (saved && (saved === "en-US" || saved === "zh-CN")) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
// Otherwise detect from browser
|
||||
return detectLocale();
|
||||
});
|
||||
|
||||
const t = translations[locale];
|
||||
|
||||
const changeLocale = (newLocale: Locale) => {
|
||||
setLocale(newLocale);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("locale", newLocale);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize locale on mount
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem("locale") as Locale | null;
|
||||
if (!saved) {
|
||||
const detected = detectLocale();
|
||||
setLocale(detected);
|
||||
localStorage.setItem("locale", detected);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
locale,
|
||||
t,
|
||||
changeLocale,
|
||||
};
|
||||
}
|
||||
23
frontend/src/core/i18n/index.ts
Normal file
23
frontend/src/core/i18n/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export { enUS } from "./locales/en-US";
|
||||
export { zhCN } from "./locales/zh-CN";
|
||||
export type { Translations } from "./locales/types";
|
||||
|
||||
export type Locale = "en-US" | "zh-CN";
|
||||
|
||||
// Helper function to detect browser locale
|
||||
export function detectLocale(): Locale {
|
||||
// if (typeof window === "undefined") {
|
||||
// return "en-US";
|
||||
// }
|
||||
|
||||
// const browserLang =
|
||||
// navigator.language ||
|
||||
// (navigator as unknown as { userLanguage: string }).userLanguage;
|
||||
|
||||
// // Check if browser language is Chinese (zh, zh-CN, zh-TW, etc.)
|
||||
// if (browserLang.toLowerCase().startsWith("zh")) {
|
||||
// return "zh-CN";
|
||||
// }
|
||||
|
||||
return "en-US";
|
||||
}
|
||||
86
frontend/src/core/i18n/locales/en-US.ts
Normal file
86
frontend/src/core/i18n/locales/en-US.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { Translations } from "./types";
|
||||
|
||||
export const enUS: Translations = {
|
||||
// Common
|
||||
common: {
|
||||
home: "Home",
|
||||
settings: "Settings",
|
||||
delete: "Delete",
|
||||
openInNewWindow: "Open in new window",
|
||||
close: "Close",
|
||||
more: "More",
|
||||
search: "Search",
|
||||
download: "Download",
|
||||
thinking: "Thinking",
|
||||
artifacts: "Artifacts",
|
||||
},
|
||||
|
||||
// Welcome
|
||||
welcome: {
|
||||
greeting: "👋 Hello, again!",
|
||||
description:
|
||||
"Welcome to 🦌 DeerFlow, an open source super agent. With built-in and custom skills, DeerFlow helps you search on the web, analyze data, and generate artifacts like slides, web pages and do almost anything.",
|
||||
},
|
||||
|
||||
// Clipboard
|
||||
clipboard: {
|
||||
copyToClipboard: "Copy to clipboard",
|
||||
copiedToClipboard: "Copied to clipboard",
|
||||
failedToCopyToClipboard: "Failed to copy to clipboard",
|
||||
},
|
||||
|
||||
// Input Box
|
||||
inputBox: {
|
||||
placeholder: "How can I assist you today?",
|
||||
thinkingEnabled: "Thinking is enabled",
|
||||
thinkingDisabled: "Thinking is disabled",
|
||||
clickToDisableThinking: "Click to disable thinking",
|
||||
clickToEnableThinking: "Click to enable thinking",
|
||||
searchModels: "Search models...",
|
||||
},
|
||||
|
||||
// Sidebar
|
||||
sidebar: {
|
||||
newChat: "New chat",
|
||||
chats: "Chats",
|
||||
recentChats: "Recent chats",
|
||||
},
|
||||
|
||||
// Breadcrumb
|
||||
breadcrumb: {
|
||||
workspace: "Workspace",
|
||||
chats: "Chats",
|
||||
},
|
||||
|
||||
// Workspace
|
||||
workspace: {
|
||||
githubTooltip: "DeerFlow on Github",
|
||||
},
|
||||
|
||||
// Conversation
|
||||
conversation: {
|
||||
noMessages: "No messages yet",
|
||||
startConversation: "Start a conversation to see messages here",
|
||||
},
|
||||
|
||||
// Chats
|
||||
chats: {
|
||||
searchChats: "Search chats",
|
||||
},
|
||||
|
||||
// Tool calls
|
||||
toolCalls: {
|
||||
moreSteps: (count: number) => `${count} more step${count === 1 ? "" : "s"}`,
|
||||
lessSteps: "Less steps",
|
||||
executeCommand: "Execute command",
|
||||
presentFiles: "Present files",
|
||||
needYourHelp: "Need your help",
|
||||
useTool: (toolName: string) => `Use "${toolName}" tool`,
|
||||
searchForRelatedInfo: "Search for related information",
|
||||
searchOnWebFor: (query: string) => `Search on the web for "${query}"`,
|
||||
viewWebPage: "View web page",
|
||||
listFolder: "List folder",
|
||||
readFile: "Read file",
|
||||
writeFile: "Write file",
|
||||
},
|
||||
};
|
||||
3
frontend/src/core/i18n/locales/index.ts
Normal file
3
frontend/src/core/i18n/locales/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { enUS } from "./en-US";
|
||||
export { zhCN } from "./zh-CN";
|
||||
export type { Translations } from "./types";
|
||||
83
frontend/src/core/i18n/locales/types.ts
Normal file
83
frontend/src/core/i18n/locales/types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
export interface Translations {
|
||||
// Common
|
||||
common: {
|
||||
home: string;
|
||||
settings: string;
|
||||
delete: string;
|
||||
openInNewWindow: string;
|
||||
close: string;
|
||||
more: string;
|
||||
search: string;
|
||||
download: string;
|
||||
thinking: string;
|
||||
artifacts: string;
|
||||
};
|
||||
|
||||
// Welcome
|
||||
welcome: {
|
||||
greeting: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
// Clipboard
|
||||
clipboard: {
|
||||
copyToClipboard: string;
|
||||
copiedToClipboard: string;
|
||||
failedToCopyToClipboard: string;
|
||||
};
|
||||
|
||||
// Input Box
|
||||
inputBox: {
|
||||
placeholder: string;
|
||||
thinkingEnabled: string;
|
||||
thinkingDisabled: string;
|
||||
clickToDisableThinking: string;
|
||||
clickToEnableThinking: string;
|
||||
searchModels: string;
|
||||
};
|
||||
|
||||
// Sidebar
|
||||
sidebar: {
|
||||
recentChats: string;
|
||||
newChat: string;
|
||||
chats: string;
|
||||
};
|
||||
|
||||
// Breadcrumb
|
||||
breadcrumb: {
|
||||
workspace: string;
|
||||
chats: string;
|
||||
};
|
||||
|
||||
// Workspace
|
||||
workspace: {
|
||||
githubTooltip: string;
|
||||
};
|
||||
|
||||
// Conversation
|
||||
conversation: {
|
||||
noMessages: string;
|
||||
startConversation: string;
|
||||
};
|
||||
|
||||
// Chats
|
||||
chats: {
|
||||
searchChats: string;
|
||||
};
|
||||
|
||||
// Tool calls
|
||||
toolCalls: {
|
||||
moreSteps: (count: number) => string;
|
||||
lessSteps: string;
|
||||
executeCommand: string;
|
||||
presentFiles: string;
|
||||
needYourHelp: string;
|
||||
useTool: (toolName: string) => string;
|
||||
searchForRelatedInfo: string;
|
||||
searchOnWebFor: (query: string) => string;
|
||||
viewWebPage: string;
|
||||
listFolder: string;
|
||||
readFile: string;
|
||||
writeFile: string;
|
||||
};
|
||||
}
|
||||
86
frontend/src/core/i18n/locales/zh-CN.ts
Normal file
86
frontend/src/core/i18n/locales/zh-CN.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { Translations } from "./types";
|
||||
|
||||
export const zhCN: Translations = {
|
||||
// Common
|
||||
common: {
|
||||
home: "首页",
|
||||
settings: "设置",
|
||||
delete: "删除",
|
||||
openInNewWindow: "在新窗口打开",
|
||||
close: "关闭",
|
||||
more: "更多",
|
||||
search: "搜索",
|
||||
download: "下载",
|
||||
thinking: "思考",
|
||||
artifacts: "文件",
|
||||
},
|
||||
|
||||
// Welcome
|
||||
welcome: {
|
||||
greeting: "👋 你好,欢迎回来!",
|
||||
description:
|
||||
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和\n自定义的 Skills,DeerFlow 可以帮你搜索网络、分析数据,\n还能为你生成幻灯片、网页等作品,几乎可以做任何事情。",
|
||||
},
|
||||
|
||||
// Clipboard
|
||||
clipboard: {
|
||||
copyToClipboard: "复制到剪贴板",
|
||||
copiedToClipboard: "已复制到剪贴板",
|
||||
failedToCopyToClipboard: "复制到剪贴板失败",
|
||||
},
|
||||
|
||||
// Input Box
|
||||
inputBox: {
|
||||
placeholder: "今天我能为你做些什么?",
|
||||
thinkingEnabled: "思考功能已启用",
|
||||
thinkingDisabled: "思考功能已禁用",
|
||||
clickToDisableThinking: "点击禁用思考功能",
|
||||
clickToEnableThinking: "点击启用思考功能",
|
||||
searchModels: "搜索模型...",
|
||||
},
|
||||
|
||||
// Sidebar
|
||||
sidebar: {
|
||||
newChat: "新对话",
|
||||
chats: "对话",
|
||||
recentChats: "最近的聊天",
|
||||
},
|
||||
|
||||
// Breadcrumb
|
||||
breadcrumb: {
|
||||
workspace: "工作区",
|
||||
chats: "对话",
|
||||
},
|
||||
|
||||
// Workspace
|
||||
workspace: {
|
||||
githubTooltip: "DeerFlow 在 Github",
|
||||
},
|
||||
|
||||
// Conversation
|
||||
conversation: {
|
||||
noMessages: "还没有消息",
|
||||
startConversation: "开始新的对话以查看消息",
|
||||
},
|
||||
|
||||
// Chats
|
||||
chats: {
|
||||
searchChats: "搜索对话",
|
||||
},
|
||||
|
||||
// Tool calls
|
||||
toolCalls: {
|
||||
moreSteps: (count: number) => `查看其他 ${count} 个步骤`,
|
||||
lessSteps: "隐藏步骤",
|
||||
executeCommand: "执行命令",
|
||||
presentFiles: "展示文件",
|
||||
needYourHelp: "需要你的协助",
|
||||
useTool: (toolName: string) => `使用 “${toolName}” 工具`,
|
||||
searchForRelatedInfo: "搜索相关信息",
|
||||
searchOnWebFor: (query: string) => `在网络上搜索 “${query}”`,
|
||||
viewWebPage: "查看网页",
|
||||
listFolder: "列出文件夹",
|
||||
readFile: "读取文件",
|
||||
writeFile: "写入文件",
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user