feat: add i18n support and add Chinese (#372)

* feat: add i18n support and add Chinese

* fix: resolve conflicts

* Update en.json with cancle settings

* Update zh.json with settngs cancle

---------

Co-authored-by: johnny0120 <15564476+johnny0120@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <143703838+willem-bd@users.noreply.github.com>
This commit is contained in:
johnny0120
2025-07-12 15:18:28 +08:00
committed by GitHub
parent 136f7eaa4e
commit e1187d7d02
31 changed files with 917 additions and 266 deletions
@@ -2,17 +2,12 @@
// SPDX-License-Identifier: MIT
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { cn } from "~/lib/utils";
import { Welcome } from "./welcome";
const questions = [
"How many times taller is the Eiffel Tower than the tallest building in the world?",
"How many years does an average Tesla battery last compared to a gasoline engine?",
"How many liters of water are required to produce 1 kg of beef?",
"How many times faster is the speed of light compared to the speed of sound?",
];
export function ConversationStarter({
className,
onSend,
@@ -20,6 +15,9 @@ export function ConversationStarter({
className?: string;
onSend?: (message: string) => void;
}) {
const t = useTranslations("chat");
const questions = t.raw("conversationStarters") as string[];
return (
<div className={cn("flex flex-col items-center", className)}>
<div className="pointer-events-none fixed inset-0 flex items-center justify-center">
@@ -41,7 +39,7 @@ export function ConversationStarter({
}}
>
<div
className="bg-card text-muted-foreground cursor-pointer rounded-2xl border px-4 py-4 opacity-75 transition-all duration-300 hover:opacity-100 hover:shadow-md"
className="bg-card text-muted-foreground h-full w-full cursor-pointer rounded-2xl border px-4 py-4 opacity-75 transition-all duration-300 hover:opacity-100 hover:shadow-md"
onClick={() => {
onSend?.(question);
}}
+17 -14
View File
@@ -4,6 +4,7 @@
import { MagicWandIcon } from "@radix-ui/react-icons";
import { AnimatePresence, motion } from "framer-motion";
import { ArrowUp, Lightbulb, X } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useMemo, useRef, useState } from "react";
import { Detective } from "~/components/deer-flow/icons/detective";
@@ -46,6 +47,8 @@ export function InputBox({
onCancel?: () => void;
onRemoveFeedback?: () => void;
}) {
const t = useTranslations("chat.inputBox");
const tCommon = useTranslations("common");
const enableDeepThinking = useSettingsStore(
(state) => state.general.enableDeepThinking,
);
@@ -217,12 +220,14 @@ export function InputBox({
title={
<div>
<h3 className="mb-2 font-bold">
Deep Thinking Mode: {enableDeepThinking ? "On" : "Off"}
{t("deepThinkingTooltip.title", {
status: enableDeepThinking ? t("on") : t("off"),
})}
</h3>
<p>
When enabled, DeerFlow will use reasoning model (
{config.models.reasoning?.[0]}) to generate more thoughtful
plans.
{t("deepThinkingTooltip.description", {
model: config.models.reasoning?.[0] ?? "",
})}
</p>
</div>
}
@@ -237,7 +242,7 @@ export function InputBox({
setEnableDeepThinking(!enableDeepThinking);
}}
>
<Lightbulb /> Deep Thinking
<Lightbulb /> {t("deepThinking")}
</Button>
</Tooltip>
)}
@@ -247,13 +252,11 @@ export function InputBox({
title={
<div>
<h3 className="mb-2 font-bold">
Investigation Mode: {backgroundInvestigation ? "On" : "Off"}
{t("investigationTooltip.title", {
status: backgroundInvestigation ? t("on") : t("off"),
})}
</h3>
<p>
When enabled, DeerFlow will perform a quick search before
planning. This is useful for researches related to ongoing
events and news.
</p>
<p>{t("investigationTooltip.description")}</p>
</div>
}
>
@@ -267,13 +270,13 @@ export function InputBox({
setEnableBackgroundInvestigation(!backgroundInvestigation)
}
>
<Detective /> Investigation
<Detective /> {t("investigation")}
</Button>
</Tooltip>
<ReportStyleDialog />
</div>
<div className="flex shrink-0 items-center gap-2">
<Tooltip title="Enhance prompt with AI">
<Tooltip title={t("enhancePrompt")}>
<Button
variant="ghost"
size="icon"
@@ -293,7 +296,7 @@ export function InputBox({
)}
</Button>
</Tooltip>
<Tooltip title={responding ? "Stop" : "Send"}>
<Tooltip title={responding ? tCommon("stop") : tCommon("send")}>
<Button
variant="outline"
size="icon"
@@ -10,6 +10,7 @@ import {
ChevronRight,
Lightbulb,
} from "lucide-react";
import { useTranslations } from "next-intl";
import React, { useCallback, useMemo, useRef, useState } from "react";
import { LoadingAnimation } from "~/components/deer-flow/loading-animation";
@@ -252,6 +253,7 @@ function ResearchCard({
researchId: string;
onToggleResearch?: () => void;
}) {
const t = useTranslations("chat.research");
const reportId = useStore((state) => state.researchReportIds.get(researchId));
const hasReport = reportId !== undefined;
const reportGenerating = useStore(
@@ -260,10 +262,10 @@ function ResearchCard({
const openResearchId = useStore((state) => state.openResearchId);
const state = useMemo(() => {
if (hasReport) {
return reportGenerating ? "Generating report..." : "Report generated";
return reportGenerating ? t("generatingReport") : t("reportGenerated");
}
return "Researching...";
}, [hasReport, reportGenerating]);
return t("researching");
}, [hasReport, reportGenerating, t]);
const msg = useResearchMessage(researchId);
const title = useMemo(() => {
if (msg) {
@@ -283,8 +285,8 @@ function ResearchCard({
<Card className={cn("w-full", className)}>
<CardHeader>
<CardTitle>
<RainbowText animated={state !== "Report generated"}>
{title !== undefined && title !== "" ? title : "Deep Research"}
<RainbowText animated={state !== t("reportGenerated")}>
{title !== undefined && title !== "" ? title : t("deepResearch")}
</RainbowText>
</CardTitle>
</CardHeader>
@@ -297,7 +299,7 @@ function ResearchCard({
variant={!openResearchId ? "default" : "outline"}
onClick={handleOpen}
>
{researchId !== openResearchId ? "Open" : "Close"}
{researchId !== openResearchId ? t("open") : t("close")}
</Button>
</div>
</CardFooter>
@@ -316,6 +318,7 @@ function ThoughtBlock({
isStreaming?: boolean;
hasMainContent?: boolean;
}) {
const t = useTranslations("chat.research");
const [isOpen, setIsOpen] = useState(true);
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
@@ -359,7 +362,7 @@ function ThoughtBlock({
isStreaming ? "text-primary" : "text-foreground",
)}
>
Deep Thinking
{t("deepThinking")}
</span>
{isStreaming && <LoadingAnimation className="ml-2 scale-75" />}
<div className="flex-grow" />
@@ -432,6 +435,7 @@ function PlanCard({
) => void;
waitForFeedback?: boolean;
}) {
const t = useTranslations("chat.research");
const plan = useMemo<{
title?: string;
thought?: string;
@@ -482,7 +486,7 @@ function PlanCard({
{`### ${
plan.title !== undefined && plan.title !== ""
? plan.title
: "Deep Research"
: t("deepResearch")
}`}
</Markdown>
</CardTitle>
+11 -10
View File
@@ -3,6 +3,7 @@
import { motion } from "framer-motion";
import { FastForward, Play } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useRef, useState } from "react";
import { RainbowText } from "~/components/deer-flow/rainbow-text";
@@ -27,6 +28,7 @@ import { MessageListView } from "./message-list-view";
import { Welcome } from "./welcome";
export function MessagesBlock({ className }: { className?: string }) {
const t = useTranslations("chat.messages");
const messageIds = useMessageIds();
const messageCount = messageIds.length;
const responding = useStore((state) => state.responding);
@@ -152,16 +154,16 @@ export function MessagesBlock({ className }: { className?: string }) {
<CardHeader className={cn("flex-grow", responding && "pl-3")}>
<CardTitle>
<RainbowText animated={responding}>
{responding ? "Replaying" : `${replayTitle}`}
{responding ? t("replaying") : `${replayTitle}`}
</RainbowText>
</CardTitle>
<CardDescription>
<RainbowText animated={responding}>
{responding
? "DeerFlow is now replaying the conversation..."
? t("replayDescription")
: replayStarted
? "The replay has been stopped."
: `You're now in DeerFlow's replay mode. Click the "Play" button on the right to start.`}
? t("replayHasStopped")
: t("replayModeDescription")}
</RainbowText>
</CardDescription>
</CardHeader>
@@ -175,13 +177,13 @@ export function MessagesBlock({ className }: { className?: string }) {
onClick={handleFastForwardReplay}
>
<FastForward size={16} />
Fast Forward
{t("fastForward")}
</Button>
)}
{!replayStarted && (
<Button className="w-24" onClick={handleStartReplay}>
<Play size={16} />
Play
{t("play")}
</Button>
)}
</div>
@@ -190,17 +192,16 @@ export function MessagesBlock({ className }: { className?: string }) {
</Card>
{!replayStarted && env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
<div className="text-muted-foreground w-full text-center text-xs">
* This site is for demo purposes only. If you want to try your
own question, please{" "}
{t("demoNotice")}{" "}
<a
className="underline"
href="https://github.com/bytedance/deer-flow"
target="_blank"
rel="noopener noreferrer"
>
click here
{t("clickHere")}
</a>{" "}
to clone it locally and run it.
{t("cloneLocally")}
</div>
)}
</motion.div>
@@ -5,6 +5,7 @@ import { PythonOutlined } from "@ant-design/icons";
import { motion } from "framer-motion";
import { LRUCache } from "lru-cache";
import { BookOpenText, FileText, PencilRuler, Search } from "lucide-react";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
import { useMemo } from "react";
import SyntaxHighlighter from "react-syntax-highlighter";
@@ -122,6 +123,7 @@ type SearchResult =
};
function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
const t = useTranslations("chat.research");
const searching = useMemo(() => {
return toolCall.result === undefined;
}, [toolCall.result]);
@@ -159,7 +161,7 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
animated={searchResults === undefined}
>
<Search size={16} className={"mr-2"} />
<span>Searching for&nbsp;</span>
<span>{t("searchingFor")}&nbsp;</span>
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
{(toolCall.args as { query: string }).query}
</span>
@@ -238,6 +240,7 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
}
function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
const t = useTranslations("chat.research");
const url = useMemo(
() => (toolCall.args as { url: string }).url,
[toolCall.args],
@@ -251,7 +254,7 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
animated={toolCall.result === undefined}
>
<BookOpenText size={16} className={"mr-2"} />
<span>Reading</span>
<span>{t("reading")}</span>
</RainbowText>
</div>
<ul className="mt-2 flex flex-wrap gap-4">
@@ -279,6 +282,7 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
}
function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
const t = useTranslations("chat.research");
const searching = useMemo(() => {
return toolCall.result === undefined;
}, [toolCall.result]);
@@ -292,7 +296,7 @@ function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
<div className="font-medium italic">
<RainbowText className="flex items-center" animated={searching}>
<Search size={16} className={"mr-2"} />
<span>Retrieving documents from RAG&nbsp;</span>
<span>{t("retrievingDocuments")}&nbsp;</span>
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
{(toolCall.args as { keywords: string }).keywords}
</span>
@@ -337,6 +341,7 @@ function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
}
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
const t = useTranslations("chat.research");
const code = useMemo<string | undefined>(() => {
return (toolCall.args as { code?: string }).code;
}, [toolCall.args]);
@@ -349,7 +354,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
className="text-base font-medium italic"
animated={toolCall.result === undefined}
>
Running Python code
{t("runningPythonCode")}
</RainbowText>
</div>
<div>
@@ -373,6 +378,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
}
function PythonToolCallResult({ result }: { result: string }) {
const t = useTranslations("chat.research");
const { resolvedTheme } = useTheme();
const hasError = useMemo(
() => result.includes("Error executing code:\n"),
@@ -399,7 +405,7 @@ function PythonToolCallResult({ result }: { result: string }) {
return (
<>
<div className="mt-4 font-medium italic">
{hasError ? "Error when executing the above code" : "Execution output"}
{hasError ? t("errorExecutingCode") : t("executionOutput")}
</div>
<div className="bg-accent mt-2 max-h-[400px] max-w-[calc(100%-120px)] overflow-y-auto rounded-md p-2 text-sm">
<SyntaxHighlighter
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
import { Check, Copy, Headphones, Pencil, Undo2, X, Download } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { ScrollContainer } from "~/components/deer-flow/scroll-container";
@@ -23,6 +24,7 @@ export function ResearchBlock({
className?: string;
researchId: string | null;
}) {
const t = useTranslations("chat.research");
const reportId = useStore((state) =>
researchId ? state.researchReportIds.get(researchId) : undefined,
);
@@ -108,7 +110,7 @@ export function ResearchBlock({
<div className="absolute right-4 flex h-9 items-center justify-center">
{hasReport && !reportStreaming && (
<>
<Tooltip title="Generate podcast">
<Tooltip title={t("generatePodcast")}>
<Button
className="text-gray-400"
size="icon"
@@ -119,7 +121,7 @@ export function ResearchBlock({
<Headphones />
</Button>
</Tooltip>
<Tooltip title="Edit">
<Tooltip title={t("edit")}>
<Button
className="text-gray-400"
size="icon"
@@ -130,7 +132,7 @@ export function ResearchBlock({
{editing ? <Undo2 /> : <Pencil />}
</Button>
</Tooltip>
<Tooltip title="Copy">
<Tooltip title={t("copy")}>
<Button
className="text-gray-400"
size="icon"
@@ -140,7 +142,7 @@ export function ResearchBlock({
{copied ? <Check /> : <Copy />}
</Button>
</Tooltip>
<Tooltip title="Download report as markdown">
<Tooltip title={t("downloadReport")}>
<Button
className="text-gray-400"
size="icon"
@@ -152,7 +154,7 @@ export function ResearchBlock({
</Tooltip>
</>
)}
<Tooltip title="Close">
<Tooltip title={t("close")}>
<Button
className="text-gray-400"
size="sm"
@@ -177,10 +179,10 @@ export function ResearchBlock({
value="report"
disabled={!hasReport}
>
Report
{t("report")}
</TabsTrigger>
<TabsTrigger className="px-8" value="activities">
Activities
{t("activities")}
</TabsTrigger>
</TabsList>
</div>
+8 -3
View File
@@ -3,12 +3,16 @@
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import { useTranslations } from 'next-intl';
import { LanguageSwitcher } from "~/components/deer-flow/language-switcher";
import { NumberTicker } from "~/components/magicui/number-ticker";
import { Button } from "~/components/ui/button";
import { env } from "~/env";
export async function SiteHeader() {
export function SiteHeader() {
const t = useTranslations('common');
return (
<header className="supports-backdrop-blur:bg-background/80 bg-background/40 sticky top-0 left-0 z-40 flex h-15 w-full flex-col items-center backdrop-blur-lg">
<div className="container flex h-15 items-center justify-between px-3">
@@ -16,7 +20,8 @@ export async function SiteHeader() {
<span className="mr-1 text-2xl">🦌</span>
<span>DeerFlow</span>
</div>
<div className="relative flex items-center">
<div className="relative flex items-center gap-2">
<LanguageSwitcher />
<div
className="pointer-events-none absolute inset-0 z-0 h-full w-full rounded-full opacity-60 blur-2xl"
style={{
@@ -32,7 +37,7 @@ export async function SiteHeader() {
>
<Link href="https://github.com/bytedance/deer-flow" target="_blank">
<GitHubLogoIcon className="size-4" />
Star on GitHub
{t('starOnGitHub')}
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY &&
env.GITHUB_OAUTH_TOKEN && <StarCounter />}
</Link>
+5 -14
View File
@@ -2,10 +2,13 @@
// SPDX-License-Identifier: MIT
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { cn } from "~/lib/utils";
export function Welcome({ className }: { className?: string }) {
const t = useTranslations("chat.welcome");
return (
<motion.div
className={cn("flex flex-col", className)}
@@ -13,21 +16,9 @@ export function Welcome({ className }: { className?: string }) {
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
>
<h3 className="mb-2 text-center text-3xl font-medium">
👋 Hello, there!
</h3>
<h3 className="mb-2 text-center text-3xl font-medium">{t("greeting")}</h3>
<div className="text-muted-foreground px-4 text-center text-lg">
Welcome to{" "}
<a
href="https://github.com/bytedance/deer-flow"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
🦌 DeerFlow
</a>
, a deep research assistant built on cutting-edge language models, helps
you search on web, browse information, and handle complex tasks.
{t("description")}
</div>
</motion.div>
);