diff --git a/app/1global-error.tsx b/app/[lng]/1global-error.tsx similarity index 100% rename from app/1global-error.tsx rename to app/[lng]/1global-error.tsx diff --git a/app/api/[...abc]/route.ts b/app/[lng]/api/[...abc]/route.ts similarity index 100% rename from app/api/[...abc]/route.ts rename to app/[lng]/api/[...abc]/route.ts diff --git a/app/api/lemon/callback/route.ts b/app/[lng]/api/lemon/callback/route.ts similarity index 100% rename from app/api/lemon/callback/route.ts rename to app/[lng]/api/lemon/callback/route.ts diff --git a/app/api/supa/data/route.ts b/app/[lng]/api/supa/data/route.ts similarity index 100% rename from app/api/supa/data/route.ts rename to app/[lng]/api/supa/data/route.ts diff --git a/app/api/supa/paper-numbers/route.ts b/app/[lng]/api/supa/paper-numbers/route.ts similarity index 100% rename from app/api/supa/paper-numbers/route.ts rename to app/[lng]/api/supa/paper-numbers/route.ts diff --git a/app/api/supa/user-papers/route.ts b/app/[lng]/api/supa/user-papers/route.ts similarity index 100% rename from app/api/supa/user-papers/route.ts rename to app/[lng]/api/supa/user-papers/route.ts diff --git a/app/api/supa/vip/route.ts b/app/[lng]/api/supa/vip/route.ts similarity index 100% rename from app/api/supa/vip/route.ts rename to app/[lng]/api/supa/vip/route.ts diff --git a/app/auth/callback/route.ts b/app/[lng]/auth/callback/route.ts similarity index 100% rename from app/auth/callback/route.ts rename to app/[lng]/auth/callback/route.ts diff --git a/app/favicon.ico b/app/[lng]/favicon.ico similarity index 100% rename from app/favicon.ico rename to app/[lng]/favicon.ico diff --git a/app/global-error.jsx b/app/[lng]/global-error.jsx similarity index 100% rename from app/global-error.jsx rename to app/[lng]/global-error.jsx diff --git a/app/globals.css b/app/[lng]/globals.css similarity index 100% rename from app/globals.css rename to app/[lng]/globals.css diff --git a/app/layout.tsx b/app/[lng]/layout.tsx similarity index 100% rename from app/layout.tsx rename to app/[lng]/layout.tsx diff --git a/app/login/page.tsx b/app/[lng]/login/page.tsx similarity index 100% rename from app/login/page.tsx rename to app/[lng]/login/page.tsx diff --git a/app/nodes/page.tsx b/app/[lng]/nodes/page.tsx similarity index 100% rename from app/nodes/page.tsx rename to app/[lng]/nodes/page.tsx diff --git a/app/opengraph-image.png b/app/[lng]/opengraph-image.png similarity index 100% rename from app/opengraph-image.png rename to app/[lng]/opengraph-image.png diff --git a/app/page.tsx b/app/[lng]/page.tsx similarity index 87% rename from app/page.tsx rename to app/[lng]/page.tsx index 5fc264e..585a36f 100644 --- a/app/page.tsx +++ b/app/[lng]/page.tsx @@ -10,9 +10,14 @@ import QuillWrapper from "@/components/QuillWrapper"; // import SEditor from "../components/SlateEditor"; import SettingsLink from "@/components/SettingsLink"; import PaperManagementWrapper from "@/components/PaperManagementWrapper"; +//i18n +import { useTranslation } from "@/app/i18n"; +import { IndexProps } from "@/utils/global"; // import Error from "@/app/global-error"; -export default function Index() { +export default async function Index({ params: { lng } }: IndexProps) { + const { t } = await useTranslation(lng); + const cookieStore = cookies(); const canInitSupabaseClient = () => { @@ -39,8 +44,8 @@ export default function Index() { - - + + diff --git a/app/[lng]/settings/page.tsx b/app/[lng]/settings/page.tsx new file mode 100644 index 0000000..af2a879 --- /dev/null +++ b/app/[lng]/settings/page.tsx @@ -0,0 +1,12 @@ +//这里是settings页面 +import SettingsWrapper from "@/components/SettingsWrapper"; +//i18n +import { IndexProps } from "@/utils/global"; + +export default function settings({ params: { lng } }: IndexProps) { + return ( +
+ +
+ ); +} diff --git a/app/twitter-image.png b/app/[lng]/twitter-image.png similarity index 100% rename from app/twitter-image.png rename to app/[lng]/twitter-image.png diff --git a/app/i18n/client.js b/app/i18n/client.js new file mode 100644 index 0000000..29a0b98 --- /dev/null +++ b/app/i18n/client.js @@ -0,0 +1,60 @@ +"use client"; + +import { useEffect, useState } from "react"; +import i18next from "i18next"; +import { + initReactI18next, + useTranslation as useTranslationOrg, +} from "react-i18next"; +import { useCookies } from "react-cookie"; +import resourcesToBackend from "i18next-resources-to-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { getOptions, languages, cookieName } from "./settings"; + +const runsOnServerSide = typeof window === "undefined"; + +// +i18next + .use(initReactI18next) + .use(LanguageDetector) + .use( + resourcesToBackend((language, namespace) => + import(`./locales/${language}/${namespace}.json`) + ) + ) + .init({ + ...getOptions(), + lng: undefined, // let detect the language on client side + detection: { + order: ["path", "htmlTag", "cookie", "navigator"], + }, + preload: runsOnServerSide ? languages : [], + }); + +export function useTranslation(lng, ns, options) { + const [cookies, setCookie] = useCookies([cookieName]); + const ret = useTranslationOrg(ns, options); + const { i18n } = ret; + if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) { + i18n.changeLanguage(lng); + } else { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage); + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (activeLng === i18n.resolvedLanguage) return; + setActiveLng(i18n.resolvedLanguage); + }, [activeLng, i18n.resolvedLanguage]); + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (!lng || i18n.resolvedLanguage === lng) return; + i18n.changeLanguage(lng); + }, [lng, i18n]); + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (cookies.i18next === lng) return; + setCookie(cookieName, lng, { path: "/" }); + }, [lng, cookies.i18next]); + } + return ret; +} diff --git a/app/i18n/en.json b/app/i18n/en.json deleted file mode 100644 index e69de29..0000000 diff --git a/app/i18n/index.js b/app/i18n/index.js new file mode 100644 index 0000000..30a4033 --- /dev/null +++ b/app/i18n/index.js @@ -0,0 +1,29 @@ +import { createInstance } from "i18next"; +import resourcesToBackend from "i18next-resources-to-backend"; +import { initReactI18next } from "react-i18next/initReactI18next"; +import { getOptions } from "./settings"; + +const initI18next = async (lng, ns) => { + const i18nInstance = createInstance(); + await i18nInstance + .use(initReactI18next) + .use( + resourcesToBackend((language, namespace) => + import(`./locales/${language}/${namespace}.json`) + ) + ) + .init(getOptions(lng, ns)); + return i18nInstance; +}; + +export async function useTranslation(lng, ns, options = {}) { + const i18nextInstance = await initI18next(lng, ns); + return { + t: i18nextInstance.getFixedT( + lng, + Array.isArray(ns) ? ns[0] : ns, + options.keyPrefix + ), + i18n: i18nextInstance, + }; +} diff --git a/app/i18n/locales/en/translation.json b/app/i18n/locales/en/translation.json new file mode 100644 index 0000000..c0b437c --- /dev/null +++ b/app/i18n/locales/en/translation.json @@ -0,0 +1,29 @@ +{ + "give me a star in GitHub": " give me a star in GitHub", + "更新索引": "update paper reference index", + "AI写作": "AI writing", + "Paper2AI": "Paper2AI", + "点击AI写作就是正常的对话交流,点击寻找文献会根据输入的主题词去寻找对应论文": "Click AI Write for normal conversation, click Paper2AI to find corresponding papers based on the input topic", + "+ Add Paper": "+ Add Paper", + "Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously": "Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously", + "Paper Management": "Paper Management", + "Your Cloud Papers": "Your Cloud Papers", + "复制": "Copy", + "添加自定义引用": "Add Custom Reference", + "复制所有引用": "Copy All References", + "删除所有引用": "Delete All References", + "Title": "Title", + "Author": "Author", + "Year": "Year", + "Publisher": "Publisher", + "Url": "Url", + "配置选择器": "Configure Selector", + "Upstream URL:": "Upstream URL:", + "System Prompt(Paper2AI):": "System Prompt(Paper2AI):", + "configurations": { + "cocopilot-gpt4": "cocopilot-gpt4 (apiKey prefix with ghu, as GitHub does not allow uploading complete keys)", + "deepseek-chat": "deepseek-chat (Model needs to be manually changed to this one)", + "caifree": "caifree (Recommended)", + "custom": "Custom" + } +} diff --git a/app/i18n/locales/zh-CN/translation.json b/app/i18n/locales/zh-CN/translation.json new file mode 100644 index 0000000..6b1241e --- /dev/null +++ b/app/i18n/locales/zh-CN/translation.json @@ -0,0 +1,30 @@ +{ + "give me a star in GitHub": "在GitHub上给我一颗star", + "更新索引": "更新索引", + "AI写作": "AI写作", + "Paper2AI": "寻找文献", + "点击AI写作就是正常的对话交流,点击寻找文献会根据输入的主题词去寻找对应论文": "点击AI写作就是正常的对话交流,点击寻找文献会根据输入的主题词去寻找对应论文", + "+ Add Paper": "+ 添加文献", + "Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously": "购买VIP解锁云同步和同时编辑多篇论文", + "Paper Management": "文献管理", + "Your Cloud Papers": "您的云端论文", + "复制": "复制", + "添加自定义引用": "添加自定义引用", + "复制所有引用": "复制所有引用", + "删除所有引用": "删除所有引用", + "Title": "标题", + "Author": "作者", + "Year": "年份", + "Publisher": "出版商", + "Url": "论文网址", + "配置选择器": "配置选择器", + "Upstream URL:": "请求模型的URL:", + "System Prompt(Paper2AI):": "系统提示(Paper2AI):", + + "configurations": { + "cocopilot-gpt4": "cocopilot-gpt4(apiKey前面手动加上ghu,因为GitHub不允许上传完整的密钥)", + "deepseek-chat": "deepseek-chat(需要手动修改模型为这个)", + "caifree": "caifree(推荐)", + "custom": "自定义" + } +} diff --git a/app/i18n/settings.js b/app/i18n/settings.js new file mode 100644 index 0000000..9e57093 --- /dev/null +++ b/app/i18n/settings.js @@ -0,0 +1,16 @@ +export const fallbackLng = "en"; +export const languages = [fallbackLng, "zh-CN"]; +export const defaultNS = "translation"; +export const cookieName = "i18next"; + +export function getOptions(lng = fallbackLng, ns = defaultNS) { + return { + // debug: true, + supportedLngs: languages, + fallbackLng, + lng, + fallbackNS: defaultNS, + defaultNS, + ns, + }; +} diff --git a/app/i18n/zh-CN.json b/app/i18n/zh-CN.json deleted file mode 100644 index e69de29..0000000 diff --git a/app/settings/page.tsx b/app/settings/page.tsx deleted file mode 100644 index 00b4bd0..0000000 --- a/app/settings/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -//这里是settings页面 -import Settings from "@/components/SettingsWrapper"; - -export default function settings() { - return ( -
- -
- ); -} diff --git a/app/store/index.ts b/app/store/index.ts index 2d481a0..ac7a7d6 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -27,6 +27,7 @@ const statePersistConfig = { "paperNumberRedux", "contentUpdatedFromNetwork", "isVip", + "language", ], }; diff --git a/app/store/slices/stateSlice.ts b/app/store/slices/stateSlice.ts index 826a3a5..3d8dd4b 100644 --- a/app/store/slices/stateSlice.ts +++ b/app/store/slices/stateSlice.ts @@ -4,6 +4,7 @@ export interface APIState { paperNumberRedux: string; contentUpdatedFromNetwork: boolean; isVip: boolean; + language: string; } const initialState: APIState = { @@ -11,6 +12,7 @@ const initialState: APIState = { paperNumberRedux: "1", //默认得给个值 contentUpdatedFromNetwork: false, isVip: false, + language: "en", }; export const stateSlice = createSlice({ @@ -35,6 +37,9 @@ export const stateSlice = createSlice({ setIsVip: (state, action: PayloadAction) => { state.isVip = action.payload; }, + setLanguage: (state, action: PayloadAction) => { + state.language = action.payload; + }, }, }); @@ -44,6 +49,7 @@ export const { setPaperNumberRedux, setContentUpdatedFromNetwork, setIsVip, + setLanguage, } = stateSlice.actions; export const stateReducer = stateSlice.reducer; diff --git a/components/BuyVipButton.tsx b/components/BuyVipButton.tsx index 42394b3..31e86de 100644 --- a/components/BuyVipButton.tsx +++ b/components/BuyVipButton.tsx @@ -1,8 +1,11 @@ import React from "react"; import { sendGAEvent } from "@next/third-parties/google"; - +//i18n +import { useTranslation } from "@/app/i18n/client"; // BuyVipButton 组件 -function BuyVipButton() { +function BuyVipButton({ lng }: { lng: string }) { + //i18n + const { t } = useTranslation(lng); // 这是购买VIP的目标URL const targetUrl = "https://store.paperai.life"; return ( @@ -13,7 +16,9 @@ function BuyVipButton() { sendGAEvent({ event: "buyVipButtonClicked", value: "buy vip" }) } > - Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously + {t( + "Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously" + )} ); diff --git a/components/PaperManagement.tsx b/components/PaperManagement.tsx index dae63c4..3f2a6c0 100644 --- a/components/PaperManagement.tsx +++ b/components/PaperManagement.tsx @@ -24,14 +24,18 @@ import { } from "@/utils/supabase/supabaseutils"; //动画 import { CSSTransition } from "react-transition-group"; -import { animated, useSpring } from "@react-spring/web"; +// import { animated, useSpring } from "@react-spring/web"; //删除远程论文按钮 import ParagraphDeleteButton from "@/components/ParagraphDeleteInterface"; //vip充值按钮 import BuyVipButton from "@/components/BuyVipButton"; // 假设这是购买VIP的按钮组件 +//i18n +import { useTranslation } from "@/app/i18n/client"; -const PaperManagement = () => { +const PaperManagement = ({ lng }) => { + //i18n + const { t } = useTranslation(lng); //supabase const supabase = createClient(); //redux @@ -133,7 +137,10 @@ const PaperManagement = () => { <>
-

Paper Management

+

+ {" "} + {t("Paper Management")} +

{isVip ? (
@@ -141,10 +148,13 @@ const PaperManagement = () => { onClick={handleAddPaperClick} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" > - + Add Paper + {t("+ Add Paper")}
-

Your Papers

+

+ {" "} + {t("Your Cloud Papers")} +

{paperNumbers.length > 0 ? (
    {[...paperNumbers] @@ -188,7 +198,7 @@ const PaperManagement = () => {
) : ( - + )}
diff --git a/components/PaperManagementWrapper.tsx b/components/PaperManagementWrapper.tsx index e7fdad4..14fbb57 100644 --- a/components/PaperManagementWrapper.tsx +++ b/components/PaperManagementWrapper.tsx @@ -3,10 +3,10 @@ import ReduxProvider from "@/app/store/ReduxProvider"; import PaperManagement from "@/components/PaperManagement"; -export default function PaperManagementWrapper() { +export default function PaperManagementWrapper({ lng }) { return ( - + ); } diff --git a/components/QuillEditor.tsx b/components/QuillEditor.tsx index d67fd13..5d6e44d 100644 --- a/components/QuillEditor.tsx +++ b/components/QuillEditor.tsx @@ -39,6 +39,8 @@ import { } from "@/utils/supabase/supabaseutils"; //debounce import { debounce } from "lodash"; +//i18n +import { useTranslation } from "@/app/i18n/client"; const toolbarOptions = [ ["bold", "italic", "underline", "strike"], // 加粗、斜体、下划线和删除线 @@ -60,7 +62,10 @@ const toolbarOptions = [ ["clean"], // 清除格式按钮 ]; -const QEditor = () => { +const QEditor = ({ lng }) => { + //i18n + const { t } = useTranslation(lng); + //读取redux中的API key const apiKey = useAppSelector((state: any) => state.auth.apiKey); const upsreamUrl = useAppSelector((state: any) => state.auth.upsreamUrl); @@ -367,19 +372,21 @@ const QEditor = () => { value={userInput} onChange={handleInputChange} className="textarea-focus-expand flex-grow shadow appearance-none border rounded py-2 px-3 mr-2 text-grey-darker" - placeholder="点击AI Write就是正常的对话交流,点击Paper2AI会根据输入的主题词去寻找对应论文" + placeholder={t( + "点击AI写作就是正常的对话交流,点击寻找文献会根据输入的主题词去寻找对应论文" + )} /> {/* 论文网站 */} setNewAuthor(e.target.value)} - placeholder="Author" + placeholder={t("Author")} /> setNewYear(e.target.value)} - placeholder="Year" + placeholder={t("Year")} /> setNewPublisher(e.target.value)} - placeholder="Publisher" + placeholder={t("Publisher")} /> setNewUrl(e.target.value)} - placeholder="URL" + placeholder={t("Url")} />
@@ -218,7 +222,7 @@ function ReferenceList({ editor }: ReferenceListProps) { type="submit" form="referenceForm" > - 添加自定义引用 + {t("添加自定义引用")}
diff --git a/components/Settings.tsx b/components/Settings.tsx index 766bb59..84d614c 100644 --- a/components/Settings.tsx +++ b/components/Settings.tsx @@ -11,32 +11,33 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; import Link from "next/link"; import { useLocalStorage } from "react-use"; +import { useTranslation } from "@/app/i18n/client"; -// 在 Settings.tsx 或一个单独的配置文件中 -const CONFIG_OPTIONS = [ - { - name: "cocopilot-gpt4(apiKey在前面手动加上ghu,因为GitHub不允许上传完整的密钥)", - apiKey: "_pXVxLPBzcvCjSvG0Mv4K7G9ffw3xsM2ZKolZ", - upstreamUrl: "https://proxy.cocopilot.org", - }, - { - name: "deepseek-chat(需要手动修改模型为这个)", - apiKey: "sk-ffe19ebe9fa44d00884330ff1c18cf82", - upstreamUrl: "https://api.deepseek.com", - }, - { - name: "caifree(推荐)", - apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1", - upstreamUrl: "https://one.caifree.com", - }, - { - name: "自定义", - apiKey: "", - upstreamUrl: "", - }, -]; - -const Settings = () => { +const Settings = ({ lng }: { lng: string }) => { + //i18n + const { t } = useTranslation(lng); + const CONFIG_OPTIONS = [ + { + name: t("configurations.cocopilot-gpt4"), + apiKey: "_pXVxLPBzcvCjSvG0Mv4K7G9ffw3xsM2ZKolZ", + upstreamUrl: "https://proxy.cocopilot.org", + }, + { + name: t("configurations.deepseek-chat"), + apiKey: "sk-ffe19ebe9fa44d00884330ff1c18cf82", + upstreamUrl: "https://api.deepseek.com", + }, + { + name: t("configurations.caifree"), + apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1", + upstreamUrl: "https://one.caifree.com", + }, + { + name: t("configurations.custom"), + apiKey: "", + upstreamUrl: "", + }, + ]; //redux const dispatch = useAppDispatch(); const apiKey = useAppSelector((state) => state.auth.apiKey); @@ -65,7 +66,7 @@ const Settings = () => { className="block text-gray-700 text-sm font-bold mb-2" htmlFor="config-selector" > - 配置选择器 + {t("配置选择器")} { className="block text-gray-700 text-sm font-bold mb-2" htmlFor="system-prompt" > - System Prompt(Paper2AI): + {t("System Prompt(Paper2AI):")}