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

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
"use client";
import { useLocale } from "next-intl";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
type LanguageOption = {
code: string;
name: string;
flag: string;
};
const languages: Array<LanguageOption> = [
{ code: "en", name: "English", flag: "🇺🇸" },
{ code: "zh", name: "中文", flag: "🇨🇳" },
];
export function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const currentLanguage =
languages.find((lang) => lang.code === locale) ??
(languages[0] as LanguageOption);
const handleLanguageChange = (newLocale: string) => {
startTransition(() => {
console.log(`updateing locale to ${newLocale}`)
// Set locale in cookie
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000; SameSite=lax`;
// Reload the page to apply the new locale
window.location.reload();
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" disabled={isPending}>
<span className="mr-2">{currentLanguage.flag}</span>
{currentLanguage.name}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{languages.map((language) => (
<DropdownMenuItem
key={language.code}
onClick={() => handleLanguageChange(language.code)}
className={locale === language.code ? "bg-accent" : ""}
>
<span className="mr-2">{language.flag}</span>
{language.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -2,6 +2,7 @@ import { useMemo } from "react";
import { useStore, useToolCalls } from "~/core/store";
import { Tooltip } from "./tooltip";
import { WarningFilled } from "@ant-design/icons";
import { useTranslations } from "next-intl";
export const Link = ({
href,
@@ -38,16 +39,14 @@ export const Link = ({
: true;
}, [credibleLinks, href, responding, checkLinkCredibility]);
const t = useTranslations("common");
return (
<span className="inline-flex items-center gap-1.5">
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
{!isCredible && (
<Tooltip
title="This link might be a hallucination from AI model and may not be reliable."
delayDuration={300}
>
<Tooltip title={t("linkNotReliable")} delayDuration={300}>
<WarningFilled className="text-sx transition-colors hover:!text-yellow-500" />
</Tooltip>
)}

View File

@@ -15,6 +15,7 @@ import {
} from "novel";
import { Markdown } from "tiptap-markdown";
import { useDebouncedCallback } from "use-debounce";
import { useTranslations } from "next-intl";
import "~/styles/prosemirror.css";
import { resourceSuggestion } from "./resource-suggestion";
@@ -82,6 +83,7 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
{ className, loading, config, onChange, onEnter }: MessageInputProps,
ref,
) => {
const t = useTranslations("messageInput");
const editorRef = useRef<Editor>(null);
const handleEnterRef = useRef<
((message: string, resources: Array<Resource>) => void) | undefined
@@ -136,9 +138,7 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
}),
Placeholder.configure({
showOnlyCurrent: false,
placeholder: config?.rag.provider
? "What can I do for you? \nYou may refer to RAG resources by using @."
: "What can I do for you?",
placeholder: config?.rag.provider ? t("placeholderWithRag") : t("placeholder"),
emptyEditorClass: "placeholder",
}),
Extension.create({

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { useTranslations } from "next-intl";
import { useState } from "react";
import { Check, FileText, Newspaper, Users, GraduationCap } from "lucide-react";
@@ -21,31 +22,32 @@ import { Tooltip } from "./tooltip";
const REPORT_STYLES = [
{
value: "academic" as const,
label: "Academic",
description: "Formal, objective, and analytical with precise terminology",
labelKey: "academic",
descriptionKey: "academicDesc",
icon: GraduationCap,
},
{
value: "popular_science" as const,
label: "Popular Science",
description: "Engaging and accessible for general audience",
labelKey: "popularScience",
descriptionKey: "popularScienceDesc",
icon: FileText,
},
{
value: "news" as const,
label: "News",
description: "Factual, concise, and impartial journalistic style",
labelKey: "news",
descriptionKey: "newsDesc",
icon: Newspaper,
},
{
value: "social_media" as const,
label: "Social Media",
description: "Concise, attention-grabbing, and shareable",
labelKey: "socialMedia",
descriptionKey: "socialMediaDesc",
icon: Users,
},
];
export function ReportStyleDialog() {
const t = useTranslations("settings.reportStyle");
const [open, setOpen] = useState(false);
const currentStyle = useSettingsStore((state) => state.general.reportStyle);
@@ -68,12 +70,9 @@ export function ReportStyleDialog() {
title={
<div>
<h3 className="mb-2 font-bold">
Writing Style: {currentStyleConfig.label}
{t("writingStyle")}: {t(currentStyleConfig.labelKey)}
</h3>
<p>
Choose the writing style for your research reports. Different
styles are optimized for different audiences and purposes.
</p>
<p>{t("chooseDesc")}</p>
</div>
}
>
@@ -82,17 +81,14 @@ export function ReportStyleDialog() {
className="!border-brand !text-brand rounded-2xl"
variant="outline"
>
<CurrentIcon className="h-4 w-4" /> {currentStyleConfig.label}
<CurrentIcon className="h-4 w-4" /> {t(currentStyleConfig.labelKey)}
</Button>
</DialogTrigger>
</Tooltip>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Choose Writing Style</DialogTitle>
<DialogDescription>
Select the writing style for your research reports. Each style is
optimized for different audiences and purposes.
</DialogDescription>
<DialogTitle>{t("chooseTitle")}</DialogTitle>
<DialogDescription>{t("chooseDesc")}</DialogDescription>
</DialogHeader>
<div className="grid gap-3 py-4">
{REPORT_STYLES.map((style) => {
@@ -111,11 +107,11 @@ export function ReportStyleDialog() {
<Icon className="mt-0.5 h-5 w-5 shrink-0" />
<div className="flex-1 space-y-1">
<div className="flex items-center gap-2">
<h4 className="font-medium">{style.label}</h4>
<h4 className="font-medium">{t(style.labelKey)}</h4>
{isSelected && <Check className="text-primary h-4 w-4" />}
</div>
<p className="text-muted-foreground text-sm">
{style.description}
{t(style.descriptionKey)}
</p>
</div>
</button>

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { useTranslations } from "next-intl";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import type { Resource } from "~/core/messages";
import { cn } from "~/lib/utils";
@@ -14,6 +15,7 @@ export const ResourceMentions = forwardRef<
{ onKeyDown: (args: { event: KeyboardEvent }) => boolean },
ResourceMentionsProps
>((props, ref) => {
const t = useTranslations("common")
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index: number) => {
@@ -79,7 +81,7 @@ export const ResourceMentions = forwardRef<
))
) : (
<div className="items-center justify-center text-gray-500">
No result
{t("noResult")}
</div>
)}
</div>