feat: i18n中文英文

This commit is contained in:
liuweiqing 2024-02-12 20:55:14 +08:00
parent 38ede8e285
commit d64295e27a
43 changed files with 482 additions and 101 deletions

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

View File

@ -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() {
<SettingsLink />
</div>
</nav>
<PaperManagementWrapper />
<QuillWrapper />
<PaperManagementWrapper lng={lng} />
<QuillWrapper lng={lng} />
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
<p>
<a
@ -49,7 +54,7 @@ export default function Index() {
className="font-bold hover:underline"
rel="noreferrer"
>
give me a star in GitHub
{t("give me a star in GitHub")}
</a>
</p>
</footer>

View File

@ -0,0 +1,12 @@
//这里是settings页面
import SettingsWrapper from "@/components/SettingsWrapper";
//i18n
import { IndexProps } from "@/utils/global";
export default function settings({ params: { lng } }: IndexProps) {
return (
<div className="h-screen w-full ">
<SettingsWrapper lng={lng} />
</div>
);
}

View File

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

60
app/i18n/client.js Normal file
View File

@ -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;
}

View File

29
app/i18n/index.js Normal file
View File

@ -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,
};
}

View File

@ -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"
}
}

View File

@ -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-gpt4apiKey前面手动加上ghu因为GitHub不允许上传完整的密钥",
"deepseek-chat": "deepseek-chat需要手动修改模型为这个",
"caifree": "caifree推荐",
"custom": "自定义"
}
}

16
app/i18n/settings.js Normal file
View File

@ -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,
};
}

View File

View File

@ -1,10 +0,0 @@
//这里是settings页面
import Settings from "@/components/SettingsWrapper";
export default function settings() {
return (
<div className="h-screen w-full ">
<Settings />
</div>
);
}

View File

@ -27,6 +27,7 @@ const statePersistConfig = {
"paperNumberRedux",
"contentUpdatedFromNetwork",
"isVip",
"language",
],
};

View File

@ -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<boolean>) => {
state.isVip = action.payload;
},
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload;
},
},
});
@ -44,6 +49,7 @@ export const {
setPaperNumberRedux,
setContentUpdatedFromNetwork,
setIsVip,
setLanguage,
} = stateSlice.actions;
export const stateReducer = stateSlice.reducer;

View File

@ -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"
)}
</button>
</a>
);

View File

@ -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 = () => {
<>
<div className="paper-management-container flex flex-col items-center space-y-4">
<div className="max-w-md w-full bg-blue-gray-100 rounded overflow-hidden shadow-lg mx-auto p-5">
<h1 className="font-bold text-3xl text-center">Paper Management</h1>
<h1 className="font-bold text-3xl text-center">
{" "}
{t("Paper Management")}
</h1>
</div>
{isVip ? (
<div>
@ -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")}
</button>
<div className="flex flex-col items-center space-y-2">
<h2 className="text-xl font-semibold">Your Papers</h2>
<h2 className="text-xl font-semibold">
{" "}
{t("Your Cloud Papers")}
</h2>
{paperNumbers.length > 0 ? (
<ul className="list-disc">
{[...paperNumbers]
@ -188,7 +198,7 @@ const PaperManagement = () => {
</div>
</div>
) : (
<BuyVipButton />
<BuyVipButton lng={lng} />
)}
</div>
</>

View File

@ -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 (
<ReduxProvider>
<PaperManagement />
<PaperManagement lng={lng} />
</ReduxProvider>
);
}

View File

@ -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写作就是正常的对话交流点击寻找文献会根据输入的主题词去寻找对应论文"
)}
/>
<button
onClick={handleAIWrite}
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 mr-2 rounded"
>
AI Write
{t("AI写作")}
</button>
<button
onClick={() => paper2AI(userInput)}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 mr-2 rounded"
>
Paper2AI
{t("Paper2AI")}
</button>
{/* 论文网站 */}
<select
@ -405,13 +412,13 @@ const QEditor = () => {
onClick={() => formatTextInEditor(quill)} // 假设 updateIndex 是处理更新操作的函数
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded"
>
{t("更新索引")}
</button>
</div>
<div>
<div id="editor"></div>
<ReferenceList editor={quill} />
<ExportDocx editor={quill} />
<ReferenceList editor={quill} lng={lng} />
<ExportDocx editor={quill} lng={lng} />
</div>
</div>
);

View File

@ -3,10 +3,10 @@
import ReduxProvider from "@/app/store/ReduxProvider";
import QEditor from "@/components/QuillEditor";
export default function QuillWrapper() {
export default function QuillWrapper({ lng }) {
return (
<ReduxProvider>
<QEditor />
<QEditor lng={lng} />
</ReduxProvider>
);
}

View File

@ -21,12 +21,16 @@ import {
//supabase
import { submitPaper } from "@/utils/supabase/supabaseutils";
import { createClient } from "@/utils/supabase/client";
//i18n
import { useTranslation } from "@/app/i18n/client";
type ReferenceListProps = {
editor: any;
lng: string;
};
function ReferenceList({ editor }: ReferenceListProps) {
// console.log("editor in ReferenceList", editor);
function ReferenceList({ editor, lng }: ReferenceListProps) {
//i18n
const { t } = useTranslation(lng);
const [newTitle, setNewTitle] = useState("");
const [newAuthor, setNewAuthor] = useState("");
const [newYear, setNewYear] = useState("");
@ -141,7 +145,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
copyToClipboard(formatReferenceForCopy(reference))
}
>
{t("复制")}
</button>
<ParagraphDeleteButton
index={index}
@ -180,35 +184,35 @@ function ReferenceList({ editor }: ReferenceListProps) {
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Title"
placeholder={t("Title")}
/>
<input
className="border p-2 rounded"
type="text"
value={newAuthor}
onChange={(e) => setNewAuthor(e.target.value)}
placeholder="Author"
placeholder={t("Author")}
/>
<input
className="border p-2 rounded"
type="text"
value={newYear}
onChange={(e) => setNewYear(e.target.value)}
placeholder="Year"
placeholder={t("Year")}
/>
<input
className="border p-2 rounded"
type="text"
value={newPublisher}
onChange={(e) => setNewPublisher(e.target.value)}
placeholder="Publisher"
placeholder={t("Publisher")}
/>
<input
className="border p-2 rounded"
type="text"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
placeholder="URL"
placeholder={t("Url")}
/>
</div>
<div className="container mx-auto p-4">
@ -218,7 +222,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
type="submit"
form="referenceForm"
>
{t("添加自定义引用")}
</button>
<button
@ -228,7 +232,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
copyToClipboard(formatAllReferencesForCopy(references))
}
>
{t("复制所有引用")}
</button>
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded "
@ -236,7 +240,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
// onClick={() => setReferences([])} // 设置引用列表为空数组
onClick={() => handleClearReferences()}
>
{t("删除所有引用")}
</button>
</div>
</div>

View File

@ -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-gpt4apiKey在前面手动加上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("配置选择器")}
</label>
<select
id="config-selector"
@ -106,7 +107,7 @@ const Settings = () => {
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="upstream-url"
>
Upstream URL:
{t("Upstream URL:")}
</label>
<input
id="upstream-url"
@ -122,7 +123,7 @@ const Settings = () => {
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="system-prompt"
>
System Prompt(Paper2AI):
{t("System Prompt(Paper2AI):")}
</label>
<textarea
id="system-prompt"

View File

@ -3,10 +3,10 @@
import ReduxProvider from "@/app/store/ReduxProvider";
import Settings from "@/components/Settings";
export default function SettingsWrapper() {
export default function SettingsWrapper({ lng }) {
return (
<ReduxProvider>
<Settings />
<Settings lng={lng} />
</ReduxProvider>
);
}

8
dictionaries.js Normal file
View File

@ -0,0 +1,8 @@
import "server-only";
const dictionaries = {
en: () => import("./dictionaries/en.json").then((module) => module.default),
nl: () => import("./dictionaries/nl.json").then((module) => module.default),
};
export const getDictionary = async (locale) => dictionaries[locale]();

5
dictionaries/en.json Normal file
View File

@ -0,0 +1,5 @@
{
"products": {
"cart": "Toevoegen aan Winkelwagen"
}
}

5
dictionaries/zh-CN.json Normal file
View File

@ -0,0 +1,5 @@
{
"products": {
"cart": "Add to Cart"
}
}

View File

@ -1,17 +1,53 @@
import { NextResponse, type NextRequest } from 'next/server'
import { createClient } from '@/utils/supabase/middleware'
import { NextResponse, type NextRequest } from "next/server";
import { createClient } from "@/utils/supabase/middleware";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
let locales = ["en", "zh-CN"];
function getLocale(request: NextRequest) {
// 从请求中获取`Accept-Language`头
const headers = {
"accept-language": request.headers.get("accept-language") || undefined,
};
// 使用`Negotiator`根据`Accept-Language`头获取优先语言列表
const languages = new Negotiator({ headers }).languages();
// 定义默认语言
let defaultLocale = "en";
// 使用`match`函数匹配最合适的语言
return match(languages, locales, defaultLocale);
}
export async function middleware(request: NextRequest) {
// Check if there is any supported locale in the pathname
const { pathname } = request.nextUrl;
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
try {
// This `try/catch` block is only here for the interactive tutorial.
// Feel free to remove once you have Supabase connected.
const { supabase, response } = createClient(request)
const { supabase, response } = createClient(request);
// 如果URL中已经包含地区代码则刷新会话
// Refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware
await supabase.auth.getSession()
return response
if (pathnameHasLocale) {
await supabase.auth.getSession();
return response;
}
// 如果没有地区代码则重定向到包含首选地区的URL
if (!pathnameHasLocale) {
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(request.nextUrl);
}
} catch (e) {
// If you are here, a Supabase client could not be created!
// This is likely because you have not set up environment variables.
@ -20,7 +56,7 @@ export async function middleware(request: NextRequest) {
request: {
headers: request.headers,
},
})
});
}
}
@ -33,6 +69,6 @@ export const config = {
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.png).*)',
"/((?!_next/static|_next/image|favicon.png).*)",
],
}
};

133
package-lock.json generated
View File

@ -24,6 +24,8 @@
"geist": "^1.0.0",
"i": "^0.3.7",
"i18next": "^23.8.2",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-resources-to-backend": "^1.2.0",
"lodash": "^4.17.21",
"negotiator": "^0.6.3",
"next": "latest",
@ -34,6 +36,7 @@
"quill-to-word": "^1.3.0",
"raw-body": "^2.5.2",
"react": "^18.2.0",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.5",
"react-quill": "^2.0.0",
@ -814,6 +817,11 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -825,6 +833,15 @@
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
"dev": true
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/is-hotkey": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz",
@ -877,8 +894,7 @@
"node_modules/@types/prop-types": {
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"devOptional": true
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"node_modules/@types/quill": {
"version": "1.3.10",
@ -892,7 +908,6 @@
"version": "18.2.48",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
"integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -938,8 +953,7 @@
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"devOptional": true
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
@ -2047,6 +2061,22 @@
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz",
"integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-resources-to-backend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz",
"integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -3033,6 +3063,19 @@
"node": ">=0.10.0"
}
},
"node_modules/react-cookie": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.0.2.tgz",
"integrity": "sha512-UnW1rZw1VibRdTvV8Ksr0BKKZoajeUxYLE89sIygDeyQgtz6ik89RHOM+3kib36G9M7HxheORggPoLk5DxAK7Q==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.5",
"hoist-non-react-statics": "^3.3.2",
"universal-cookie": "^7.0.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -3911,6 +3954,23 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/universal-cookie": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.0.2.tgz",
"integrity": "sha512-EC9PA+1nojhJtVnKW2Z7WYah01jgYJApqhX+Y8XU97TnFd7KaoxWTHiTZFtfpfV50jEF1L8V5p64ZxIx3Q67dg==",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0"
}
},
"node_modules/universal-cookie/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -4685,6 +4745,11 @@
"tslib": "^2.4.0"
}
},
"@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
},
"@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -4696,6 +4761,15 @@
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
"dev": true
},
"@types/hoist-non-react-statics": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/is-hotkey": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz",
@ -4747,8 +4821,7 @@
"@types/prop-types": {
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"devOptional": true
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"@types/quill": {
"version": "1.3.10",
@ -4762,7 +4835,6 @@
"version": "18.2.48",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
"integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
"devOptional": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -4810,8 +4882,7 @@
"@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"devOptional": true
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
},
"@types/use-sync-external-store": {
"version": "0.0.3",
@ -5601,6 +5672,22 @@
"@babel/runtime": "^7.23.2"
}
},
"i18next-browser-languagedetector": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz",
"integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==",
"requires": {
"@babel/runtime": "^7.23.2"
}
},
"i18next-resources-to-backend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz",
"integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==",
"requires": {
"@babel/runtime": "^7.23.2"
}
},
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -6280,6 +6367,16 @@
"loose-envify": "^1.1.0"
}
},
"react-cookie": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.0.2.tgz",
"integrity": "sha512-UnW1rZw1VibRdTvV8Ksr0BKKZoajeUxYLE89sIygDeyQgtz6ik89RHOM+3kib36G9M7HxheORggPoLk5DxAK7Q==",
"requires": {
"@types/hoist-non-react-statics": "^3.3.5",
"hoist-non-react-statics": "^3.3.2",
"universal-cookie": "^7.0.0"
}
},
"react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -6927,6 +7024,22 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"universal-cookie": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.0.2.tgz",
"integrity": "sha512-EC9PA+1nojhJtVnKW2Z7WYah01jgYJApqhX+Y8XU97TnFd7KaoxWTHiTZFtfpfV50jEF1L8V5p64ZxIx3Q67dg==",
"requires": {
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0"
},
"dependencies": {
"cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
}
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@ -23,6 +23,8 @@
"geist": "^1.0.0",
"i": "^0.3.7",
"i18next": "^23.8.2",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-resources-to-backend": "^1.2.0",
"lodash": "^4.17.21",
"negotiator": "^0.6.3",
"next": "latest",
@ -33,6 +35,7 @@
"quill-to-word": "^1.3.0",
"raw-body": "^2.5.2",
"react": "^18.2.0",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.5",
"react-quill": "^2.0.0",

20
utils/global.d.ts vendored
View File

@ -7,10 +7,16 @@ export type JournalInfo = {
};
export type Reference = {
title: string;
author: string;
year: number|string;
url: string;
venue?: string;
journal?: JournalInfo;
};
title: string;
author: string;
year: number | string;
url: string;
venue?: string;
journal?: JournalInfo;
};
export interface IndexProps {
params: {
lng: string;
};
}