mirror of
https://github.com/wahyd4/SAI2.git
synced 2026-08-08 21:01:21 +10:00
Add language support and localization files
This commit is contained in:
@@ -10,6 +10,7 @@ import { BookOpenText } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import Image from "next/image";
|
||||
import Locale from "../locales";
|
||||
|
||||
export const Answer: FC<{ markdown: string; sources: Source[] }> = ({
|
||||
markdown,
|
||||
@@ -19,7 +20,7 @@ export const Answer: FC<{ markdown: string; sources: Source[] }> = ({
|
||||
<Wrapper
|
||||
title={
|
||||
<>
|
||||
<BookOpenText></BookOpenText> Answer
|
||||
<BookOpenText></BookOpenText> {Locale.Answer.answer}
|
||||
</>
|
||||
}
|
||||
content={
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Mails } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
|
||||
import Locale from "../locales";
|
||||
export const Footer: FC = () => {
|
||||
return (
|
||||
<div className="text-center flex flex-col items-center text-xs text-zinc-700 gap-1">
|
||||
<div className="text-zinc-400">
|
||||
Answer generated by large language models, plz double check for
|
||||
correctness.
|
||||
</div>
|
||||
<div className="text-zinc-400">{Locale.Footer.statement}</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<div>
|
||||
<a
|
||||
|
||||
@@ -4,13 +4,14 @@ import { Wrapper } from "@/app/components/wrapper";
|
||||
import { Relate } from "@/app/interfaces/relate";
|
||||
import { MessageSquareQuote } from "lucide-react";
|
||||
import React, { FC } from "react";
|
||||
import Locale from "../locales";
|
||||
|
||||
export const Relates: FC<{ relates: Relate[] | null }> = ({ relates }) => {
|
||||
return (
|
||||
<Wrapper
|
||||
title={
|
||||
<>
|
||||
<MessageSquareQuote></MessageSquareQuote> Related
|
||||
<MessageSquareQuote></MessageSquareQuote> {Locale.Relates.related}
|
||||
</>
|
||||
}
|
||||
content={
|
||||
@@ -21,7 +22,9 @@ export const Relates: FC<{ relates: Relate[] | null }> = ({ relates }) => {
|
||||
<PresetQuery key={question} query={question}></PresetQuery>
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm">No related questions.</div>
|
||||
<div className="text-sm">
|
||||
{Locale.Relates.no_related_questions}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Source } from "@/app/interfaces/source";
|
||||
import { parseStreaming } from "@/app/utils/parse-streaming";
|
||||
import { Annoyed } from "lucide-react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import Locale, { getLang } from "../locales";
|
||||
|
||||
export const Result: FC<{ query: string; rid: string }> = ({ query, rid }) => {
|
||||
const [sources, setSources] = useState<Source[]>([]);
|
||||
@@ -19,6 +20,7 @@ export const Result: FC<{ query: string; rid: string }> = ({ query, rid }) => {
|
||||
controller,
|
||||
query,
|
||||
rid,
|
||||
getLang(),
|
||||
setSources,
|
||||
setMarkdown,
|
||||
setRelates,
|
||||
@@ -37,9 +39,7 @@ export const Result: FC<{ query: string; rid: string }> = ({ query, rid }) => {
|
||||
<div className="absolute inset-4 flex items-center justify-center bg-white/40 backdrop-blur-sm">
|
||||
<div className="p-4 bg-white shadow-2xl rounded text-blue-500 font-medium flex gap-4">
|
||||
<Annoyed></Annoyed>
|
||||
{error === 429
|
||||
? "Sorry, you have made too many requests recently, try again later."
|
||||
: "Sorry, we might be overloaded, try again later."}
|
||||
{error === 429 ? Locale.Err[429] : Locale.Err[500]}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { FC, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Locale from "../locales";
|
||||
|
||||
interface SearchProps {
|
||||
useContinueButton?: boolean; // true: 使用“继续对话”按钮; false: 使用“新的搜索”按钮
|
||||
@@ -41,7 +42,7 @@ export const Search: FC<SearchProps> = ({ useContinueButton = false }) => {
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
autoFocus
|
||||
placeholder="Ask me anything ..."
|
||||
placeholder={Locale.Search.placeholder}
|
||||
className="px-2 pr-6 w-full rounded-md flex-1 outline-none bg-white"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Wrapper } from "@/app/components/wrapper";
|
||||
import { Source } from "@/app/interfaces/source";
|
||||
import { BookText } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import Locale from "../locales";
|
||||
|
||||
const SourceItem: FC<{ source: Source; index: number }> = ({
|
||||
source,
|
||||
@@ -42,7 +43,7 @@ export const Sources: FC<{ sources: Source[] }> = ({ sources }) => {
|
||||
<Wrapper
|
||||
title={
|
||||
<>
|
||||
<BookText></BookText> Sources
|
||||
<BookText></BookText> {Locale.Sources.sources}
|
||||
</>
|
||||
}
|
||||
content={
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getSearchUrl } from "@/app/utils/get-search-url";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Locale from "../locales";
|
||||
|
||||
export const Title = ({ query }: { query: string }) => {
|
||||
const router = useRouter();
|
||||
@@ -22,7 +23,8 @@ export const Title = ({ query }: { query: string }) => {
|
||||
type="button"
|
||||
className="rounded flex gap-2 items-center bg-transparent px-2 py-1 text-xs font-semibold text-blue-500 hover:bg-zinc-100"
|
||||
>
|
||||
<RefreshCcw size={12}></RefreshCcw>Rewrite
|
||||
<RefreshCcw size={12}></RefreshCcw>
|
||||
{Locale.Title.rewrite}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const cn = {
|
||||
Err: {
|
||||
429: "请求过于频繁,请稍后再试",
|
||||
500: "抱歉,我们可能负载过重,请稍后再试。",
|
||||
},
|
||||
Title: {
|
||||
rewrite: "重写",
|
||||
},
|
||||
Search: {
|
||||
placeholder: "有问必答...",
|
||||
},
|
||||
Footer: {
|
||||
statement: "这是由大语言模型提供的答案, 请务必核实正确性。",
|
||||
},
|
||||
Relates: {
|
||||
no_related_questions: "没有相关问题。",
|
||||
related: "相关",
|
||||
},
|
||||
Answer: {
|
||||
answer: "答案",
|
||||
},
|
||||
Sources: {
|
||||
sources: "来源",
|
||||
},
|
||||
};
|
||||
|
||||
type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
export type LocaleType = typeof cn;
|
||||
export type PartialLocaleType = DeepPartial<typeof cn>;
|
||||
|
||||
export default cn;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { LocaleType } from "./index";
|
||||
|
||||
const en: LocaleType = {
|
||||
Err: {
|
||||
429: "Sorry, you have made too many requests recently, try again later.",
|
||||
500: "Sorry, we might be overloaded, try again later.",
|
||||
},
|
||||
Title: {
|
||||
rewrite: "Rewrite",
|
||||
},
|
||||
Search: {
|
||||
placeholder: "Ask me anything ...",
|
||||
},
|
||||
Footer: {
|
||||
statement:
|
||||
"Answer generated by large language models, plz double check for correctness.",
|
||||
},
|
||||
Relates: {
|
||||
no_related_questions: "No related questions.",
|
||||
related: "Related",
|
||||
},
|
||||
Answer: {
|
||||
answer: "Answer",
|
||||
},
|
||||
Sources: {
|
||||
sources: "Sources",
|
||||
},
|
||||
};
|
||||
|
||||
export default en;
|
||||
@@ -0,0 +1,75 @@
|
||||
// reference source: https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/blob/main/app/locales/index.ts
|
||||
import cn from "./cn";
|
||||
import en from "./en";
|
||||
import jp from "./jp";
|
||||
|
||||
import type { LocaleType } from "./cn";
|
||||
export type { LocaleType, PartialLocaleType } from "./cn";
|
||||
|
||||
const ALL_LANGS = {
|
||||
en,
|
||||
cn,
|
||||
jp,
|
||||
};
|
||||
|
||||
export type Lang = keyof typeof ALL_LANGS;
|
||||
|
||||
export const AllLangs = Object.keys(ALL_LANGS) as Lang[];
|
||||
|
||||
export const ALL_LANG_OPTIONS: Record<Lang, string> = {
|
||||
en: "English",
|
||||
cn: "简体中文",
|
||||
jp: "日本語",
|
||||
};
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
const DEFAULT_LANG = "en";
|
||||
|
||||
const targetLang = ALL_LANGS[getLang()] as LocaleType;
|
||||
|
||||
export default targetLang as LocaleType;
|
||||
|
||||
function getItem(key: string) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setItem(key: string, value: string) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getLanguage() {
|
||||
try {
|
||||
return navigator.language.toLowerCase();
|
||||
} catch {
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLang(): Lang {
|
||||
const savedLang = getItem(LANG_KEY);
|
||||
|
||||
if (AllLangs.includes((savedLang ?? "") as Lang)) {
|
||||
return savedLang as Lang;
|
||||
}
|
||||
|
||||
const lang = getLanguage();
|
||||
|
||||
for (const option of AllLangs) {
|
||||
if (lang.includes(option)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
|
||||
export function changeLang(lang: Lang) {
|
||||
setItem(LANG_KEY, lang);
|
||||
location.reload();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { LocaleType } from "./index";
|
||||
|
||||
const jp: LocaleType = {
|
||||
Err: {
|
||||
429: "申し訳ありませんが、最近のリクエストが多すぎます。後ほど再試行してください。",
|
||||
500: "申し訳ありませんが、サーバーが過負荷の可能性があります。後ほど再試行してください。",
|
||||
},
|
||||
Title: {
|
||||
rewrite: "書き直し",
|
||||
},
|
||||
Search: {
|
||||
placeholder: "何でも聞いてください...",
|
||||
},
|
||||
Footer: {
|
||||
statement:
|
||||
"回答は大規模な言語モデルによって生成されます。正確性を再確認してください。",
|
||||
},
|
||||
Relates: {
|
||||
no_related_questions: "関連する質問はありません。",
|
||||
related: "関連性",
|
||||
},
|
||||
Answer: {
|
||||
answer: "回答",
|
||||
},
|
||||
Sources: {
|
||||
sources: "情報源",
|
||||
},
|
||||
};
|
||||
|
||||
export default jp;
|
||||
@@ -4,10 +4,28 @@ import { Logo } from "@/app/components/logo";
|
||||
import { PresetQuery } from "@/app/components/preset-query";
|
||||
import { Search } from "@/app/components/search";
|
||||
import React from "react";
|
||||
import { AllLangs, ALL_LANG_OPTIONS, changeLang, getLang } from "./locales";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="absolute inset-0 min-h-[500px] flex items-center justify-center">
|
||||
{/* 语言下拉菜单 */}
|
||||
<div className="absolute top-0 right-0 m-4">
|
||||
<select
|
||||
className="rounded border border-gray-300 text-zinc-400 bg-white p-1 shadow-sm text-sm"
|
||||
value={getLang()}
|
||||
onChange={(e) => {
|
||||
changeLang(e.target.value as any);
|
||||
}}
|
||||
>
|
||||
{AllLangs.map((lang) => (
|
||||
<option value={lang} key={lang}>
|
||||
{" "}
|
||||
{ALL_LANG_OPTIONS[lang]}{" "}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="relative flex flex-col gap-8 px-4 -mt-24">
|
||||
<Logo></Logo>
|
||||
<Search></Search>
|
||||
|
||||
@@ -9,6 +9,7 @@ export const parseStreaming = async (
|
||||
controller: AbortController,
|
||||
query: string,
|
||||
search_uuid: string,
|
||||
lang: string,
|
||||
onSources: (value: Source[]) => void,
|
||||
onMarkdown: (value: string) => void,
|
||||
onRelates: (value: Relate[]) => void,
|
||||
@@ -28,6 +29,7 @@ export const parseStreaming = async (
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
search_uuid,
|
||||
lang,
|
||||
}),
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
|
||||
Reference in New Issue
Block a user