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写作就是正常的对话交流,点击寻找文献会根据输入的主题词去寻找对应论文"
+ )}
/>
{/* 论文网站 */}