feat: implement i18n

This commit is contained in:
Henry Li
2026-01-20 14:06:47 +08:00
parent 33e6197f65
commit ac9ef30780
21 changed files with 455 additions and 69 deletions

View File

@@ -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>
)}

View File

@@ -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)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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">&quot;{args.query}&quot;</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 &quot;<span className="font-bold">{name}</span>&quot; tool
</div>
)
}
label={description ?? t.toolCalls.useTool(name)}
icon={WrenchIcon}
></ChainOfThoughtStep>
);

View File

@@ -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>

View 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>
);

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);

View 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,
};
}

View 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";
}

View 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",
},
};

View File

@@ -0,0 +1,3 @@
export { enUS } from "./en-US";
export { zhCN } from "./zh-CN";
export type { Translations } from "./types";

View 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;
};
}

View 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自定义的 SkillsDeerFlow 可以帮你搜索网络、分析数据,\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: "写入文件",
},
};