+
+
-
- Click Allow when the browser asks you{" "}
- Share clipboard?
+ {t.rich("shareClipboard", {
+ strong: (chunks) => {chunks},
+ })}
-
- We recommend using Google Chrome{" "}
+ {t("useChrome")}{" "}
- -
- GCopy protects your privacy data by temporarily storing your latest
- clipboard data in memory only. GCopy does not persist your clipboard
- data.
-
+ - {t("protectPrivacy")}
>
diff --git a/frontend/app/page.tsx b/frontend/components/sync-clipboard.tsx
similarity index 66%
rename from frontend/app/page.tsx
rename to frontend/components/sync-clipboard.tsx
index 5c557504..6a423771 100644
--- a/frontend/app/page.tsx
+++ b/frontend/components/sync-clipboard.tsx
@@ -1,9 +1,5 @@
"use client";
-import Navbar from "@/components/navbar";
-import Notice from "@/components/notice";
-import Title from "@/components/title";
-import Footer from "@/components/footer";
import LogBox from "@/components/log-box";
import FileLink from "@/components/file-link";
import useSession from "@/lib/use-session";
@@ -12,12 +8,16 @@ import { CursorArrowRippleIcon } from "@heroicons/react/24/solid";
import { DragEvent, useRef, useState } from "react";
import clsx from "clsx";
import { useRouter } from "next/navigation";
+import Title from "@/components/title";
+import { useLocale, useTranslations } from "next-intl";
-export default function Home() {
+export default function SyncClipboard() {
+ const locale = useLocale();
+ const t = useTranslations("SyncClipboard");
let syncLogs: Log[] = [
{
level: "text-warning",
- message: "click the right button to sync clipboard 👉",
+ message: t("log.clickButton") + " 👉",
},
];
const [logs, setLogs] = useState(syncLogs);
@@ -41,7 +41,7 @@ export default function Home() {
}
const ensureLoggedIn = () => {
if (!session.isLoggedIn) {
- router.push("/user/email-code");
+ router.push(`/${locale}/user/email-code`);
return false;
}
return true;
@@ -70,7 +70,7 @@ export default function Home() {
};
const fetchClipboard = async () => {
- addInfoLog("fetching...");
+ addInfoLog(t("log.fetching") + "...");
fetch("/api/v1/clipboard", {
headers: {
"X-Index": clipboard.index,
@@ -91,16 +91,14 @@ export default function Home() {
xtype == "" ||
xtype == null
) {
- addInfoLog("already up to date.");
+ addInfoLog(t("log.up2Date"));
readClipboard();
return;
}
- addInfoLog("received " + xtype + "(" + xindex + ")");
+ addInfoLog(`${t("log.received")} ${t(xtype)}(${xindex})`);
if (xtype == "file") {
- addSuccessLog(
- "file download should start shortly. if not, please click the file link below.",
- );
+ addSuccessLog(t("log.downloadTip"));
}
let blob = await response.blob();
@@ -124,7 +122,7 @@ export default function Home() {
blobId: nextBlobId,
index: xindex,
});
- addSuccessLog("wrote data to the clipboard successfully");
+ addSuccessLog(t("log.writeClipboardSuccessfully"));
},
(err) => {
addErrorLog(err);
@@ -156,7 +154,7 @@ export default function Home() {
name: permissionClipboardRead,
});
if (permission.state === "denied") {
- addErrorLog("not allowed to read clipboard!");
+ addErrorLog(t("log.denyReadClipboard"));
return;
}
const clipboardItems = await navigator.clipboard.read();
@@ -178,7 +176,8 @@ export default function Home() {
if (nextBlobId == clipboard.blobId) {
return;
}
- addInfoLog("read data from the clipboard successfully.");
+ addInfoLog(t("log.readClipboardSuccessfully"));
+ addInfoLog(`${t("log.uploading")} ${t(xtype)}`);
fetch("/api/v1/clipboard", {
method: "POST",
@@ -204,7 +203,7 @@ export default function Home() {
blobId: nextBlobId,
index: xindex,
});
- addSuccessLog(xtype + "(" + xindex + ") uploaded.");
+ addSuccessLog(`${t("log.uploaded")} ${t(xtype)}(${xindex}).`);
});
}
}
@@ -213,14 +212,14 @@ export default function Home() {
const uploadFileHandler = async (file: File) => {
resetLog();
if (file.size > 10 * 1024 * 1024) {
- addErrorLog("sorry, the file cannot exceed 10mb!");
+ addErrorLog(t("log.fileTooLarge"));
return;
}
const nextBlobId: string = file.type + file.size + encodeURI(file.name);
if (nextBlobId == clipboard.blobId) {
return;
}
- addInfoLog("uploading file " + file.name);
+ addInfoLog(`${t("log.uploading")} ${file.name}`);
await fetch("/api/v1/clipboard", {
method: "POST",
@@ -250,7 +249,7 @@ export default function Home() {
fileName: file.name,
fileURL: "",
});
- addSuccessLog("file(" + xindex + ") uploaded.");
+ addSuccessLog(`${t("log.uploaded")} ${t("file")}(${xindex}).`);
});
};
@@ -288,85 +287,72 @@ export default function Home() {
const onDragOver = (ev: DragEvent
) => {
ev.preventDefault();
};
-
return (
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
-
-
+
+
-
- {!dragging && (
- <>
-
-
- drag and drop a file here
-
-
- >
- )}
-
{
- if (inputRef.current?.files) {
- const selectedFile = inputRef.current.files[0];
- await uploadFileHandler(selectedFile);
- setDragging(false);
- }
- }}
- />
-
+
+ {!dragging && (
+ <>
+
+
+ {t("syncFile.dragDropTip")}
+
+
+ >
+ )}
+
{
+ if (inputRef.current?.files) {
+ const selectedFile = inputRef.current.files[0];
+ await uploadFileHandler(selectedFile);
+ setDragging(false);
+ }
+ }}
+ />
-
-
-
-
-
+
+ >
);
}
diff --git a/frontend/i18n.ts b/frontend/i18n.ts
new file mode 100644
index 00000000..3426ee2e
--- /dev/null
+++ b/frontend/i18n.ts
@@ -0,0 +1,14 @@
+import { notFound } from "next/navigation";
+import { getRequestConfig } from "next-intl/server";
+
+// Can be imported from a shared config
+const locales = ["en", "zh"];
+
+export default getRequestConfig(async ({ locale }) => {
+ // Validate that the incoming `locale` parameter is valid
+ if (!locales.includes(locale as any)) notFound();
+
+ return {
+ messages: (await import(`@/messages/${locale}.json`)).default,
+ };
+});
diff --git a/frontend/lib/i18n.ts b/frontend/lib/i18n.ts
new file mode 100644
index 00000000..81a00feb
--- /dev/null
+++ b/frontend/lib/i18n.ts
@@ -0,0 +1,23 @@
+import { match } from "@formatjs/intl-localematcher";
+import Negotiator from "negotiator";
+
+export const locales = ["en", "zh"];
+export const defaultLocale = "en";
+export function getAcceptLanguageLocale(requestHeaders: Headers) {
+ let locale;
+ const languages = new Negotiator({
+ headers: {
+ "accept-language": requestHeaders.get("accept-language") || undefined,
+ },
+ }).languages();
+ try {
+ locale = match(
+ languages,
+ locales as unknown as Array
,
+ defaultLocale,
+ );
+ } catch (e) {
+ // Invalid language
+ }
+ return locale || defaultLocale;
+}
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
new file mode 100644
index 00000000..139718f7
--- /dev/null
+++ b/frontend/messages/en.json
@@ -0,0 +1,72 @@
+{
+ "Metadata": {
+ "title": "GCopy - Sync text screenshot & file",
+ "description": "A clipboard synchronization web service for different devices that can synchronize text, screenshots, and files."
+ },
+ "Navbar": {
+ "title": "Document",
+ "signIn": "Sign in"
+ },
+ "Notice": {
+ "title": "Notice",
+ "shareClipboard": "Click Allow when the browser asks you if it can See text and images copied to the clipboard.",
+ "useChrome": "We recommend using Google Chrome",
+ "protectPrivacy": "GCopy protects your privacy data by temporarily storing your latest clipboard data in memory only. GCopy does not persist your clipboard data."
+ },
+ "Avator": {
+ "logout": "Logout"
+ },
+ "EmailCode": {
+ "title": "Sign in",
+ "smallTitle": "to continue to GCopy",
+ "subTitle": "Support for text, screenshots & file synchronization.",
+ "placeholder": "Enter email",
+ "tip": "Your privacy is important to GCopy!",
+ "buttonText": "Next",
+ "invalidEmail": "Invalid email.",
+ "sendEmail": {
+ "subject": "{code} is your verification code",
+ "text": "Enter the verification code when prompted: {code}. Code will expire in 5 minutes. To protect your account, do not share this code.",
+ "failed": "Failed to send email. Please check and retry.",
+ "success": "Success"
+ }
+ },
+ "Login": {
+ "title": "Enter code",
+ "subTitle": "We emailed a code to {email}. Please enter the code to sign in.",
+ "placeholder": "Enter code",
+ "tip": "Your privacy is important to GCopy!",
+ "buttonText": "Sign in",
+ "invalidEmail": "Invalid email.",
+ "incorrectCode": "Incorrect code. Please go back and retry.",
+ "expiredCode": "The code was expired. Please go back and retry.",
+ "success": "Success"
+ },
+ "SyncClipboard": {
+ "title": "Sync the clipboard",
+ "subTitle": "Click-click and the clipboard synced between devices.",
+ "text": "TEXT",
+ "screenshot": "SCREENSHOT",
+ "file": "FILE",
+ "syncButtonText": "Click to sync clipboard",
+ "log": {
+ "clickButton": "Click the button to sync clipboard",
+ "up2Date": "Already up to date.",
+ "received": "Received",
+ "uploaded": "Uploaded",
+ "uploading": "Uploading",
+ "fetching": "Fetching",
+ "readClipboardSuccessfully": "Read data from the clipboard successfully.",
+ "writeClipboardSuccessfully": "Written data to the clipboard successfully.",
+ "downloadTip": "File download should start shortly. if not, please click the file link below.",
+ "denyReadClipboard": "Not allowed to read clipboard!",
+ "fileTooLarge": "Sorry, the file cannot exceed 10mb!"
+ },
+ "syncFile": {
+ "title": "Sync the files",
+ "subTitle": "Drag and drop a file here to sync it to different devices.",
+ "dragDropTip": "Drag and drop a file here",
+ "fileInputText": "Choose a file"
+ }
+ }
+}
diff --git a/frontend/messages/zh.json b/frontend/messages/zh.json
new file mode 100644
index 00000000..ec9f037d
--- /dev/null
+++ b/frontend/messages/zh.json
@@ -0,0 +1,72 @@
+{
+ "Metadata": {
+ "title": "GCopy - 不同设备间的剪切板同步服务",
+ "description": "GCopy是一个不同设备间的剪切板同步服务, 它支持文字, 截图和文件. 只需要点击两下就实现数据同步."
+ },
+ "Navbar": {
+ "title": "文档",
+ "signIn": "登录"
+ },
+ "Notice": {
+ "title": "注意",
+ "shareClipboard": "当浏览器询问是否允许查看复制到剪切板的文字和图片时, 点击 允许.",
+ "useChrome": "我们推荐使用谷歌Chrome浏览器",
+ "protectPrivacy": "GCopy保护您的隐私, 它仅在内存中临时存储最新剪切板数据. GCopy并不持久化存储您的数据."
+ },
+ "Avator": {
+ "logout": "登出"
+ },
+ "EmailCode": {
+ "title": "登录",
+ "smallTitle": "以使用GCopy",
+ "subTitle": "支持文字, 截图和文件的同步.",
+ "placeholder": "输入邮箱地址",
+ "tip": "你的隐私安全对GCopy很重要!",
+ "buttonText": "下一步",
+ "invalidEmail": "无效的邮箱地址.",
+ "sendEmail": {
+ "subject": "{code}是您的验证码",
+ "text": "请输入您的验证码: {code}. 该验证码有效期5分钟. 为保护您的账户, 请不要分享这个验证码.",
+ "failed": "发送邮件失败, 请检查并重试.",
+ "success": "成功"
+ }
+ },
+ "Login": {
+ "title": "输入验证码",
+ "subTitle": "我们向{email}发送了一个验证码. 请输入验证码登录.",
+ "placeholder": "输入验证码",
+ "tip": "你的隐私安全对GCopy很重要!",
+ "buttonText": "登录",
+ "invalidEmail": "无效的邮箱地址.",
+ "incorrectCode": "验证码不正确, 请退回重试.",
+ "expiredCode": "验证码过期, 请退回重试.",
+ "success": "成功"
+ },
+ "SyncClipboard": {
+ "title": "同步剪切板",
+ "subTitle": "Click-click剪切板就能在不同设备间同步.",
+ "text": "文字",
+ "screenshot": "截图",
+ "file": "文件",
+ "log": {
+ "clickButton": "点击按钮同步剪切板",
+ "up2Date": "已经最新.",
+ "received": "已接收",
+ "uploaded": "已上传",
+ "uploading": "正在上传",
+ "fetching": "正在获取",
+ "readClipboardSuccessfully": "成功从剪切板读到数据.",
+ "writeClipboardSuccessfully": "数据成功写入剪切板.",
+ "downloadTip": "很快文件会自动下载. 如果没有, 请点击下面的文件链接.",
+ "denyReadClipboard": "不允许读取剪切板!",
+ "fileTooLarge": "对不起, 文件不能超过10mb!"
+ },
+ "syncButtonText": "点击同步剪切板",
+ "syncFile": {
+ "title": "同步文件",
+ "subTitle": "把文件拖拽到这里就能在不同设备间同步它.",
+ "dragDropTip": "把文件拖拽到这里",
+ "fileInputText": "选择一个文件"
+ }
+ }
+}
diff --git a/frontend/middleware.ts b/frontend/middleware.ts
new file mode 100644
index 00000000..923aceed
--- /dev/null
+++ b/frontend/middleware.ts
@@ -0,0 +1,11 @@
+import createMiddleware from "next-intl/middleware";
+import { locales, defaultLocale } from "@/lib/i18n";
+
+export default createMiddleware({
+ locales: locales,
+ defaultLocale: defaultLocale,
+});
+
+export const config = {
+ matcher: ["/", "/(en|zh)/:path*"],
+};
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 150a6f7d..9d36d3f7 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -1,3 +1,5 @@
+const withNextIntl = require("next-intl/plugin")("./i18n.ts");
+
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
@@ -11,4 +13,4 @@ const nextConfig = {
},
};
-module.exports = nextConfig;
+module.exports = withNextIntl(nextConfig);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f32f158d..47dc8cb8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,18 +1,21 @@
{
"name": "gcopy",
- "version": "1.0.0",
+ "version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gcopy",
- "version": "1.0.0",
+ "version": "1.1.0",
"dependencies": {
+ "@formatjs/intl-localematcher": "^0.5.4",
"@heroicons/react": "^2.1.1",
"@types/nodemailer": "^6.4.14",
"clsx": "^2.1.0",
"iron-session": "^8.0.1",
+ "negotiator": "^0.6.3",
"next": "14.0.4",
+ "next-intl": "^3.4.5",
"nodemailer": "^6.9.8",
"react": "^18",
"react-dom": "^18",
@@ -21,6 +24,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
+ "@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
@@ -117,6 +121,84 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
+ "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.5.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
+ "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
+ "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/icu-skeleton-parser": "1.3.6",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
+ "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
+ "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@heroicons/react": {
"version": "2.1.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@heroicons/react/-/react-2.1.1.tgz",
@@ -435,6 +517,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==",
+ "dev": true
+ },
"node_modules/@types/node": {
"version": "20.10.5",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.10.5.tgz",
@@ -2196,6 +2284,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/intl-messageformat": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
+ "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/fast-memoize": "1.2.1",
+ "@formatjs/icu-messageformat-parser": "2.1.0",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/iron-session": {
"version": "8.0.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/iron-session/-/iron-session-8.0.1.tgz",
@@ -2761,6 +2877,14 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/next": {
"version": "14.0.4",
"resolved": "https://registry.npmmirror.com/next/-/next-14.0.4.tgz",
@@ -2807,6 +2931,34 @@
}
}
},
+ "node_modules/next-intl": {
+ "version": "3.4.5",
+ "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.4.5.tgz",
+ "integrity": "sha512-k3oCyFavZNar/JowdKfJVBIDISmcvkO32DEwRUxa46Yv6LQ3GTnOlINoaPvGr4M81gp/7WVBuYlQBZyu8aJ6Ug==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/amannn"
+ }
+ ],
+ "dependencies": {
+ "@formatjs/intl-localematcher": "^0.2.32",
+ "negotiator": "^0.6.3",
+ "use-intl": "^3.4.5"
+ },
+ "peerDependencies": {
+ "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/next-intl/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.32",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
+ "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz",
@@ -3980,6 +4132,18 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-intl": {
+ "version": "3.4.5",
+ "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.4.5.tgz",
+ "integrity": "sha512-QXDYgJMAYX+aAyNWUg1paZbdAu5DbBpwpBcflM85PxnAJ7dvRX9KCPiEXKE6/I9e88ZzMmr7gtb+t4iM/7BsCA==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "^1.11.4",
+ "intl-messageformat": "^9.3.18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 4b28e27b..0286f8e8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "gcopy",
- "version": "1.0.0",
+ "version": "1.1.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -11,11 +11,14 @@
"prettier:check": "prettier --check --ignore-unknown ."
},
"dependencies": {
+ "@formatjs/intl-localematcher": "^0.5.4",
"@heroicons/react": "^2.1.1",
"@types/nodemailer": "^6.4.14",
"clsx": "^2.1.0",
"iron-session": "^8.0.1",
+ "negotiator": "^0.6.3",
"next": "14.0.4",
+ "next-intl": "^3.4.5",
"nodemailer": "^6.9.8",
"react": "^18",
"react-dom": "^18",
@@ -24,6 +27,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
+ "@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
diff --git a/internal/server/auth.go b/internal/server/auth.go
index 10eb1356..3a671a3b 100644
--- a/internal/server/auth.go
+++ b/internal/server/auth.go
@@ -33,7 +33,7 @@ func (s *Server) verifyAuth(c *gin.Context) {
}
if response.StatusCode != http.StatusOK {
- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
@@ -51,7 +51,7 @@ func (s *Server) verifyAuth(c *gin.Context) {
}
if !userInfo.IsLoggedIn || userInfo.Email == "" {
- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
diff --git a/internal/server/server.go b/internal/server/server.go
index e3bd942e..0822e5b9 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -71,12 +71,12 @@ func (s *Server) Run() {
func (s *Server) getClipboardHandler(c *gin.Context) {
subject, ok := c.Get("subject")
if !ok {
- c.JSON(http.StatusNotFound, gin.H{"message": "subject not found"})
+ c.JSON(http.StatusNotFound, gin.H{"message": "Subject not found"})
return
}
sub, ok := subject.(string)
if !ok {
- c.JSON(http.StatusInternalServerError, gin.H{"message": "subject type assert failed"})
+ c.JSON(http.StatusInternalServerError, gin.H{"message": "Subject type assert failed"})
return
}
cb := s.cbs.Get(sub)
@@ -97,7 +97,7 @@ func (s *Server) getClipboardHandler(c *gin.Context) {
c.Header("X-Type", cb.Type)
c.Header("X-FileName", cb.FileName)
if _, err := c.Writer.Write(cb.Data); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"message": "write data failed"})
+ c.JSON(http.StatusInternalServerError, gin.H{"message": "Write data failed"})
s.log.Error(err)
}
}
@@ -105,12 +105,12 @@ func (s *Server) getClipboardHandler(c *gin.Context) {
func (s *Server) updateClipboardHandler(c *gin.Context) {
subject, ok := c.Get("subject")
if !ok {
- c.JSON(http.StatusNotFound, gin.H{"message": "subject not found"})
+ c.JSON(http.StatusNotFound, gin.H{"message": "Subject not found"})
return
}
sub, ok := subject.(string)
if !ok {
- c.JSON(http.StatusInternalServerError, gin.H{"message": "subject type assert failed"})
+ c.JSON(http.StatusInternalServerError, gin.H{"message": "Subject type assert failed"})
return
}
@@ -120,18 +120,18 @@ func (s *Server) updateClipboardHandler(c *gin.Context) {
}
defer c.Request.Body.Close()
if data == nil {
- c.JSON(http.StatusBadRequest, gin.H{"message": "request body is nil"})
+ c.JSON(http.StatusBadRequest, gin.H{"message": "Request body is nil"})
return
}
xType := c.Request.Header.Get("X-Type")
xFileName := c.Request.Header.Get("X-FileName")
if xType == "" || (xType == gcopy.TypeFile && xFileName == "") {
- c.JSON(http.StatusBadRequest, gin.H{"message": "request header invalid"})
+ c.JSON(http.StatusBadRequest, gin.H{"message": "Request header invalid"})
return
}
if xType == gcopy.TypeFile && len(data) > 10*1024*1024 {
- c.JSON(http.StatusRequestEntityTooLarge, gin.H{"message": "the file cannot exceed 10mb"})
+ c.JSON(http.StatusRequestEntityTooLarge, gin.H{"message": "The file cannot exceed 10mb"})
return
}
@@ -150,5 +150,5 @@ func (s *Server) updateClipboardHandler(c *gin.Context) {
s.cbs.Set(sub, cb)
c.Header("X-Index", strconv.Itoa(cb.Index))
- c.JSON(http.StatusOK, gin.H{"message": "success"})
+ c.JSON(http.StatusOK, gin.H{"message": "Success"})
}