diff --git a/packages/web/README.md b/README.md
similarity index 91%
rename from packages/web/README.md
rename to README.md
index d41a1b660..c5d2f83e8 100644
--- a/packages/web/README.md
+++ b/README.md
@@ -77,7 +77,7 @@ Gloddy는 국적에 상관없이 자유롭게 모임을 형성하고 원하는
-
+
|
@@ -88,8 +88,8 @@ Gloddy는 국적에 상관없이 자유롭게 모임을 형성하고 원하는
-
- 강주혁
+
+ 김희수
|
diff --git a/packages/web/src/apis/openApi/apis.ts b/packages/web/src/apis/openApi/apis.ts
new file mode 100644
index 000000000..13192dc86
--- /dev/null
+++ b/packages/web/src/apis/openApi/apis.ts
@@ -0,0 +1,21 @@
+import axios from 'axios';
+
+import { Message } from '.';
+
+export const postOpenAIAPI = async (messages: Message[]) => {
+ const response = await axios.post(
+ 'https://api.openai.com/v1/chat/completions',
+ {
+ messages,
+ model: 'gpt-3.5-turbo',
+ },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPEN_API}`,
+ },
+ }
+ );
+
+ return JSON.parse(response.data.choices[0].message.content);
+};
diff --git a/packages/web/src/apis/openApi/index.ts b/packages/web/src/apis/openApi/index.ts
new file mode 100644
index 000000000..519345f9e
--- /dev/null
+++ b/packages/web/src/apis/openApi/index.ts
@@ -0,0 +1,3 @@
+export * from './apis';
+export * from './mutations';
+export * from './type';
diff --git a/packages/web/src/apis/openApi/mutations.tsx b/packages/web/src/apis/openApi/mutations.tsx
new file mode 100644
index 000000000..a37a8d93e
--- /dev/null
+++ b/packages/web/src/apis/openApi/mutations.tsx
@@ -0,0 +1,42 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { Message } from '.';
+import { postOpenAIAPI } from './apis';
+
+export const usePostTranslateGPT = () => {
+ const { mutateAsync, isPending } = useMutation({
+ mutationFn: postOpenAIAPI,
+ });
+
+ const postTranslate = async ({
+ title,
+ content,
+ targetLang,
+ }: {
+ title?: string;
+ content: string;
+ targetLang: string;
+ }) => {
+ const userContent = title
+ ? `Translate the following text to ${targetLang} and format the output as requested: { title: "${title}", content: "${content}" }`
+ : `Translate the following text to ${targetLang} and format the output as requested: { content: "${content}" }`;
+
+ const messages: Message[] = [
+ {
+ role: 'system',
+ content:
+ 'You are a translator assistant that provides JSON output. Please ensure the translation output is in the format of {title: "translated text", content: "translated text"} if title is provided, otherwise just {content: "translated text"}.',
+ },
+ {
+ role: 'user',
+ content: userContent,
+ },
+ ];
+ return mutateAsync(messages);
+ };
+
+ return {
+ postTranslate,
+ isPending,
+ };
+};
diff --git a/packages/web/src/apis/openApi/type.ts b/packages/web/src/apis/openApi/type.ts
new file mode 100644
index 000000000..01b8c459b
--- /dev/null
+++ b/packages/web/src/apis/openApi/type.ts
@@ -0,0 +1,29 @@
+export type RoleType = 'system' | 'user' | 'assistant' | 'tool' | 'function';
+
+export interface OpenAIResponse {
+ id: string;
+ object: string;
+ created: number;
+ model: string;
+ choices: Choice[];
+ usage: Usage;
+ system_fingerprint: string;
+}
+
+export interface Choice {
+ index: number;
+ message: Message;
+ logprobs: null | any;
+ finish_reason: string;
+}
+
+export interface Message {
+ role: RoleType;
+ content: string;
+}
+
+export interface Usage {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+}
diff --git a/packages/web/src/app/[lng]/(main)/community/[articleId]/components/ArticleItem.tsx b/packages/web/src/app/[lng]/(main)/community/[articleId]/components/ArticleItem.tsx
index f2a4d5244..7b63947c1 100644
--- a/packages/web/src/app/[lng]/(main)/community/[articleId]/components/ArticleItem.tsx
+++ b/packages/web/src/app/[lng]/(main)/community/[articleId]/components/ArticleItem.tsx
@@ -1,15 +1,20 @@
import { format, parseISO } from 'date-fns';
import Image from 'next/image';
+import { useState } from 'react';
import { CommunityArticle, usePostCommunityArticleLike } from '@/apis/community';
+import { usePostTranslateGPT } from '@/apis/openApi';
import { useTranslation } from '@/app/i18n/client';
+import { cookieName } from '@/app/i18n/settings';
import { CardHeader } from '@/components/Card';
import { Icon } from '@/components/Icon';
import { Flex } from '@/components/Layout';
+import { Loading } from '@/components/Loading';
import { ImageModal } from '@/components/Modal';
import { Spacing } from '@/components/Spacing';
import { useModal } from '@/hooks/useModal';
import cn from '@/utils/cn';
+import { getLocalCookie } from '@/utils/cookieController';
interface ArticleItemProps {
article: CommunityArticle;
@@ -45,28 +50,60 @@ export default function ArticleItem({ article }: ArticleItemProps) {
} = article.writer;
const { mutate: mutateLike } = usePostCommunityArticleLike(articleId);
+ const [articleState, setArticleState] = useState({ title, content });
+
+ const { postTranslate, isPending } = usePostTranslateGPT();
+ const cookieLanguage = getLocalCookie(cookieName);
const handleLikeClick = () => {
mutateLike();
};
+ const handleTranslateClick = async () => {
+ if (!cookieLanguage) return;
+
+ const translatedText = await postTranslate({
+ title: articleState.title,
+ content: articleState.content,
+ targetLang: cookieLanguage,
+ });
+ setArticleState({ title: translatedText.title, content: translatedText.content });
+ };
+
return (
-
+
+
+
+ {t('detail.translate')}
+
+
-
{title}
-
-
{content}
+ {isPending ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+
{articleState.title}
+
+
+ {articleState.content}
+
+ >
+ )}
{images.length > 0 && (
{images.map((imageUrl, index) => (
diff --git a/packages/web/src/app/[lng]/(main)/community/[articleId]/components/CommentItem.tsx b/packages/web/src/app/[lng]/(main)/community/[articleId]/components/CommentItem.tsx
index 2d38c2eb3..5f5fb08a1 100644
--- a/packages/web/src/app/[lng]/(main)/community/[articleId]/components/CommentItem.tsx
+++ b/packages/web/src/app/[lng]/(main)/community/[articleId]/components/CommentItem.tsx
@@ -1,4 +1,5 @@
import { format, parseISO } from 'date-fns';
+import { useState } from 'react';
import { useCommentContext } from './CommentProvider';
import CommunityModal from './CommunityModal';
@@ -11,17 +12,21 @@ import {
useGetCommunityReply,
usePostCommunityCommentLike,
} from '@/apis/community';
+import { usePostTranslateGPT } from '@/apis/openApi';
import { useTranslation } from '@/app/i18n/client';
+import { cookieName } from '@/app/i18n/settings';
import { IconButton } from '@/components/Button';
import { CardHeader } from '@/components/Card';
import { DropDown } from '@/components/DropDown';
import { DropDownOptionType } from '@/components/DropDown/DropDown';
import { Icon } from '@/components/Icon';
import { Flex } from '@/components/Layout';
+import { Loading } from '@/components/Loading';
import { Spacing } from '@/components/Spacing';
import { useModal } from '@/hooks/useModal';
import { useBlockStore } from '@/store/useBlockStore';
import cn from '@/utils/cn';
+import { getLocalCookie } from '@/utils/cookieController';
interface CommentItemProps {
comment: Comment;
@@ -63,6 +68,10 @@ export default function CommentItem({
const { data: replyDataList } = useGetCommunityReply(articleId, commentId);
const { setCommentType, setCommentId } = useCommentContext();
+ const [commentState, setCommentState] = useState(content);
+ const { postTranslate, isPending } = usePostTranslateGPT();
+ const cookieLanguage = getLocalCookie(cookieName);
+
const handleBlockComment = () => {
openModal(() => (
{
+ if (!cookieLanguage) return;
+
+ const translatedText = await postTranslate({
+ content: commentState,
+ targetLang: cookieLanguage,
+ });
+ setCommentState(translatedText.content);
+ };
+
return (
<>
@@ -114,6 +133,9 @@ export default function CommentItem({
isWriterCaptain={articleWriterId === userId}
countryImage={countryImage}
>
+
+ {t('detail.translate')}
+
@@ -121,7 +143,17 @@ export default function CommentItem({
- {content}
+ {isPending ? (
+ <>
+
+
+
+ >
+ ) : (
+
+ {commentState}
+
+ )}
{
openModal(() => (
{
+ if (!cookieLanguage) return;
+
+ const translatedText = await postTranslate({
+ content: commentState,
+ targetLang: cookieLanguage,
+ });
+ setCommentState(translatedText.content);
+ };
+
return (
@@ -82,6 +101,9 @@ export default function ReplyItem({ reply, articleWriterId }: ReplyItemProps) {
isWriterCaptain={articleWriterId === userId}
countryImage={countryImage}
>
+
+ {t('detail.translate')}
+
@@ -89,7 +111,17 @@ export default function ReplyItem({ reply, articleWriterId }: ReplyItemProps) {
- {content}
+ {isPending ? (
+ <>
+
+
+
+ >
+ ) : (
+
+ {commentState}
+
+ )}
{
const cookieLanguage = getLocalCookie(cookieName);
- const deviceLanguage = navigator.language === 'ko-KR' ? 'ko' : 'en';
+ let deviceLanguage;
+ switch (navigator.language) {
+ case 'ko-KR':
+ deviceLanguage = 'ko';
+ break;
+ case 'zh-CN':
+ deviceLanguage = 'zh-CN';
+ break;
+ case 'zh-TW':
+ deviceLanguage = 'zh-TW';
+ break;
+ default:
+ deviceLanguage = 'en';
+ }
+
const browserLanguage = cookieLanguage || deviceLanguage;
setLocalCookie(cookieName, browserLanguage, { expires: afterDay60 });
diff --git a/packages/web/src/app/i18n/locales/en/community.json b/packages/web/src/app/i18n/locales/en/community.json
index e44c93b2e..eb9240a76 100644
--- a/packages/web/src/app/i18n/locales/en/community.json
+++ b/packages/web/src/app/i18n/locales/en/community.json
@@ -38,7 +38,8 @@
"report": "Report post",
"report_content": "Do you want to report this post?",
"delete": "Delete post",
- "delete_content": "Do you want to delete this post?"
+ "delete_content": "Do you want to delete this post?",
+ "translate": "Translate"
},
"comment": {
diff --git a/packages/web/src/app/i18n/locales/ko/community.json b/packages/web/src/app/i18n/locales/ko/community.json
index 767ae21c7..831163c95 100644
--- a/packages/web/src/app/i18n/locales/ko/community.json
+++ b/packages/web/src/app/i18n/locales/ko/community.json
@@ -38,7 +38,8 @@
"report": "게시물 신고",
"report_content": "게시글을 신고하시겠습니까?",
"delete": "게시물 삭제",
- "delete_content": "게시글을 삭제하시겠습니까?"
+ "delete_content": "게시글을 삭제하시겠습니까?",
+ "translate": "번역하기"
},
"comment": {
diff --git a/packages/web/src/app/i18n/locales/zh-CN/common.json b/packages/web/src/app/i18n/locales/zh-CN/common.json
new file mode 100644
index 000000000..1fc66a944
--- /dev/null
+++ b/packages/web/src/app/i18n/locales/zh-CN/common.json
@@ -0,0 +1,20 @@
+{
+ "time": "次",
+ "nickname": "昵称",
+ "male": "男性",
+ "female": "女性",
+ "grouping": "匹配",
+ "meeting": "我的会议",
+ "profile": "个人资料",
+ "community": "社区",
+ "notification": "通知",
+ "yes": "是",
+ "no": "否",
+ "명": "人",
+ "next": "下一步",
+ "confirm": "确认",
+ "complete": "完成",
+ "reportMessage1": "举报已收到。",
+ "reportMessage2": "我们将尽快处理。",
+ "blockMessage": "屏蔽已完成。"
+}
diff --git a/packages/web/src/app/i18n/locales/zh-CN/community.json b/packages/web/src/app/i18n/locales/zh-CN/community.json
new file mode 100644
index 000000000..a51cacc5f
--- /dev/null
+++ b/packages/web/src/app/i18n/locales/zh-CN/community.json
@@ -0,0 +1,60 @@
+{
+ "category": {
+ "All": "全部",
+ "K-POP": "K-POP",
+ "Q&A": "我想知道",
+ "Language": "语言交换"
+ },
+ "create": {
+ "headerTitle": "创建帖子",
+ "category": {
+ "name": "类别",
+ "K-POP": "K-POP",
+ "Q&A": "我想知道",
+ "Language": "语言交换"
+ },
+ "title": {
+ "placeholder": "帖子标题"
+ },
+ "content": {
+ "placeholder": "请至少写20个字符的帖子。"
+ },
+ "submit": {
+ "label": "发布",
+ "content": "您确定要发布这篇帖子吗?"
+ },
+ "cancel": {
+ "content": "您确定要取消写帖子吗?"
+ }
+ },
+ "detail": {
+ "likeCount": "个",
+ "commentCount": "评论 {{commentCount}}个",
+ "block": "屏蔽帖子",
+ "block_content": "您确定要屏蔽这篇帖子吗?",
+ "report": "举报帖子",
+ "report_content": "您确定要举报这篇帖子吗?",
+ "delete": "删除帖子",
+ "delete_content": "您确定要删除这篇帖子吗?",
+ "translate": "翻译"
+ },
+ "comment": {
+ "placeholder_comment": "写评论",
+ "placeholder_reply": "写回复",
+ "commentLengthError": "* 请写不超过150个字符。",
+ "firstComment": "来成为第一个评论的人吧!",
+ "blockComment": "这是一个被屏蔽的评论。",
+ "delete": {
+ "label": "删除",
+ "content": "您确定要删除这条评论吗?"
+ },
+ "report": {
+ "label": "举报",
+ "content": "您确定要举报这条评论吗?"
+ },
+ "block": {
+ "label": "屏蔽",
+ "content": "您确定要屏蔽这条评论吗?"
+ }
+ }
+}
diff --git a/packages/web/src/app/i18n/locales/zh-CN/groupDetail.json b/packages/web/src/app/i18n/locales/zh-CN/groupDetail.json
new file mode 100644
index 000000000..5645d386f
--- /dev/null
+++ b/packages/web/src/app/i18n/locales/zh-CN/groupDetail.json
@@ -0,0 +1,128 @@
+{
+ "more": "更多",
+ "fold": "折叠",
+ "group": {
+ "exit": {
+ "label": "退出群组",
+ "content": "您确定要退出该群组吗?",
+ "description1": "一旦退出群组,",
+ "description2": "信任分数",
+ "description3": "将被扣除。"
+ },
+ "report": {
+ "label": "举报",
+ "content": "您确定要举报该群组吗?"
+ },
+ "block": {
+ "label": "屏蔽",
+ "content": "您确定要屏蔽该群组吗?"
+ }
+ },
+ "comment": {
+ "placeholder": "写评论",
+ "commentLengthError": "* 请限制在150字以内。",
+ "firstComment": "快来发表第一条评论吧!",
+ "blockComment": "这条评论已被屏蔽。",
+ "delete": {
+ "label": "删除",
+ "content": "您确定要删除这条评论吗?"
+ },
+ "report": {
+ "label": "举报",
+ "content": "您确定要举报这条评论吗?"
+ },
+ "block": {
+ "label": "屏蔽",
+ "content": "您确定要屏蔽这条评论吗?"
+ }
+ },
+ "article": {
+ "headerTitle": "文章",
+ "delete": {
+ "label": "删除",
+ "content": "您确定要删除这篇文章吗?"
+ },
+ "report": {
+ "label": "举报",
+ "content": "您确定要举报这篇文章吗?"
+ },
+ "block": {
+ "label": "屏蔽",
+ "content": "您确定要屏蔽这篇文章吗?"
+ }
+ },
+ "notice": {
+ "headerTitle": "公告",
+ "delete": {
+ "label": "删除",
+ "content": "您确定要删除这条公告吗?"
+ },
+ "report": {
+ "label": "举报",
+ "content": "您确定要举报这条公告吗?"
+ },
+ "block": {
+ "label": "屏蔽",
+ "content": "您确定要屏蔽这条公告吗?"
+ }
+ },
+ "details": {
+ "tab": "详情",
+ "members": "群组成员 ({{memberCount}}/{{maxMemberCount}})",
+ "viewAll": "查看全部",
+ "meetDate": "聚会时间",
+ "place": "聚会地点",
+ "join": "加入聚会",
+ "wait": "等待批准"
+ },
+ "board": {
+ "tab": "留言板",
+ "notice": "公告",
+ "emptyNotice": "没有注册的公告。",
+ "commentCount": "评论 {{commentCount}}条"
+ },
+ "members": {
+ "headerTitle": "群组成员"
+ },
+ "manage": {
+ "headerTitle": "管理申请",
+ "empty": "还没有申请。",
+ "description": "请查看想要加入群组的成员申请",
+ "refuse": {
+ "label": "拒绝",
+ "description": "过于匆忙的拒绝可能会错过合适的申请者。",
+ "content": "您确定要拒绝这个申请吗?"
+ },
+ "approve": {
+ "label": "批准",
+ "description": "以相互尊重创建健康的群组文化!",
+ "content": "您确定要批准这个申请吗?"
+ }
+ },
+ "apply": {
+ "headerTitle": "填写申请",
+ "description": "请填写申请以加入群组。",
+ "introduce": "我是这样的人!",
+ "reason": "我想参加聚会的原因",
+ "placeholder": "请输入内容。",
+ "submit": {
+ "label": "申请",
+ "content": "您确定要提交申请吗?",
+ "description": "提交申请后无法再进行修改。"
+ }
+ },
+ "writeArticle": {
+ "headerTitle": "写文章",
+ "content": {
+ "placeholder": "请写至少20个字符的文章。"
+ },
+ "notice": "将此文章设置为公告。",
+ "submit": {
+ "label": "发布",
+ "content": "您确定要发布这篇文章吗?"
+ },
+ "cancel": {
+ "content": "您确定要取消写文章吗?"
+ }
+ }
+}
diff --git a/packages/web/src/app/i18n/locales/zh-CN/grouping.json b/packages/web/src/app/i18n/locales/zh-CN/grouping.json
new file mode 100644
index 000000000..d4a0895a5
--- /dev/null
+++ b/packages/web/src/app/i18n/locales/zh-CN/grouping.json
@@ -0,0 +1,46 @@
+{
+ "headerTitle": "匹配",
+ "create": {
+ "headerTitle": "创建小组",
+ "title": {
+ "label": "小组标题",
+ "placeholder": "请输入标题。"
+ },
+ "content": {
+ "label": "小组信息",
+ "placeholder": "请编写活动简介。"
+ },
+ "meetDate": {
+ "label": "日期和时间",
+ "placeholder": "设置会议的日期和时间。",
+ "year": "年",
+ "month": "月"
+ },
+ "time": {
+ "label": "开始时间",
+ "am": "上午",
+ "pm": "下午",
+ "hour": "小时",
+ "minute": "分钟"
+ },
+ "place": {
+ "label": "地点",
+ "placeholder": "请设置会议地点。",
+ "noResult": "未找到结果。"
+ },
+ "maxUser": {
+ "label": "参与人数"
+ },
+ "error": {
+ "time": "请设置一个比当前时间晚的时间。"
+ },
+ "continue": "继续",
+ "submit": {
+ "label": "提交",
+ "message": "创建小组后无法进行更改。\n您希望继续吗?",
+ "ok": "是",
+ "cancel": "否"
+ }
+ },
+ "noGroup": "未找到小组。"
+}
diff --git a/packages/web/src/app/i18n/locales/zh-CN/join.json b/packages/web/src/app/i18n/locales/zh-CN/join.json
new file mode 100644
index 000000000..de4673501
--- /dev/null
+++ b/packages/web/src/app/i18n/locales/zh-CN/join.json
@@ -0,0 +1,82 @@
+{
+ "signUp": "注册",
+ "phoneNumber": "手机号码",
+ "enterPhoneNumber": "请输入您的手机号码",
+ "phoneSafe": "您的手机号码将被安全存储。",
+ "phoneNotShared": "您的手机号码不会被公开。",
+ "inputVerificationCode": "请输入验证码",
+ "sendVerificationCode": "发送验证码",
+ "inputSix": "请输入6位验证码。",
+ "resend": "重新发送",
+ "complete": "完成",
+ "verifyCodeAgain": "请重新确认验证码。",
+ "verifyCode": "验证码",
+ "verificationTimeExceeded": "*验证码超时:请重新请求新的验证码!",
+ "agreeToTerms": "同意条款",
+ "agreeAll": "全选同意",
+ "agreeServiceTerms": "(必须) 同意服务条款",
+ "agreePrivacyPolicy": "(必须) 同意隐私政策",
+ "mandatoryInfoDeclined": "必要信息收集与访问权限被拒绝。",
+ "chooseSchool": "请选择您就读的学校",
+ "enterSchoolName": "输入学校名称",
+ "setChosenSchool": "设置为选择的学校吗?",
+ "enterSchoolEmail": "为了验证学生身份,请输入您的学校邮箱",
+ "schoolEmail": "学校邮箱",
+ "verifyToEarnBadge": "进行学生验证后,您将获得验证标记。",
+ "verifyForCredibility": "为了确保会议的可信度,请一定要进行学生验证。",
+ "studentEmailGuide": "学生邮箱发放指南",
+ "checkEmailAgain": "* 请再次确认您的学校邮箱。",
+ "skipVerification": "您要跳过学生验证吗?",
+ "verifyLater": "您可以在注册后,在个人资料中进行学生验证。",
+ "enterVerificationCode": "输入验证码",
+ "enter6DigitCode": "输入6位验证码",
+ "invalidCode": "请重新确认验证码。",
+ "enterNickname": "请输入昵称",
+ "checkDuplicate": "检查重复",
+ "nicknameGuideline": "请使用至少3个字符,最多15个字符",
+ "dob": "出生日期",
+ "next": "下一步",
+ "enterDOB": "请输入您的出生日期",
+ "dobFormat": "*请输入正确的8位出生日期。",
+ "gender": "性别",
+ "male": "男性",
+ "female": "女性",
+ "noProfanity": "昵称中不能使用亵渎语言或低俗词汇。",
+ "invalidNicknameFormat": "* 格式不正确(最少3个字符,最多15个字符,禁止使用特殊字符)",
+ "verifyUsername": "用户名重复检查",
+ "nicknameAvailable": "昵称可用。",
+ "pickPersonality": "请选择您的性格类型!",
+ "allAgree": "全部同意",
+ "essential": "必需的",
+ "extroverted": "外向的",
+ "introverted": "内向的",
+ "discreet": "谨慎的",
+ "affable": "友好的",
+ "optimistic": "乐观的",
+ "sociable": "社交的",
+ "outspoken": "直言不讳的",
+ "responsible": "有责任感的",
+ "calm": "冷静的",
+ "outgoing": "外向的",
+ "playful": "顽皮的",
+ "sensible": "明智的",
+ "leaderType": "有领导力的",
+ "humorous": "幽默的",
+ "country": "国家",
+ "personality_later_title": "要跳过选择性格类型吗?",
+ "personality_later": "您可以在注册后,在个人资料中选择性格。",
+ "재학생 인증을 건너뛰시겠습니까?": "要跳过学生验证吗?",
+ "재학생 인증을 진행하면": "进行学生验证后,",
+ "인증마크": "您将获得验证标记",
+ "를 받을 수 있어요": "。",
+ "신뢰있는 모임을 위해 재학생 인증을 꼭 진행해주세요": "为了确保会议的可信度,请一定要进行学生验证。",
+ "재학생 이메일 발급": "学生邮箱发放",
+ "* 휴대폰 번호를 다시 확인해주세요.": "* 请再次确认您的手机号码。",
+ "생년월일 8자리를 입력해주세요.": "请输入您的出生日期8位数。",
+ "* 최소 3글자, 최대 15글자 이하": "* 最少3个字符,最多15个字符。",
+ "* 학교 이메일을 다시 확인해주세요.": "* 请再次确认您的学校邮箱。",
+ "건너뛰기": "跳过",
+ "인증번호": "验证码",
+ "인증 번호를 다시 확인해주세요.": "请重新确认验证码。",
+ "인증 번호 재전송은 1분에 한 번만 가능합니다.": "验证码重发每分钟只能一次。"
+}
diff --git a/packages/web/src/app/i18n/locales/zh-CN/meeting.json b/packages/web/src/app/i18n/locales/zh-CN/meeting.json
new file mode 100644
index 000000000..42cd3d126
--- /dev/null
+++ b/packages/web/src/app/i18n/locales/zh-CN/meeting.json
@@ -0,0 +1,55 @@
+{
+ "home": {
+ "joinedGroup": "参与的聚会",
+ "favoritedGroup": "收藏的聚会",
+ "participating": "参与中",
+ "waiting": "等待中",
+ "evaluation": "评价",
+ "memberGroup": "作为成员参与的聚会",
+ "hostingGroup": "我主持的聚会",
+ "newApplications": "新的申请",
+ "awaitingApproval": "等待批准的聚会",
+ "underReview": "审核中",
+ "rejected": "已拒绝",
+ "rejectedGroups": "被拒绝的聚会",
+ "mutualEvaluationRequired": "需要相互评价的聚会",
+ "evaluateGroup": "评价聚会",
+ "enjoyedGroup": "聚会愉快吗?",
+ "complimentMembers": "请赞美一下共同参与的成员!",
+ "move": "移动",
+ "cancel": "取消",
+ "rejectedApplication": "很遗憾,您的申请被拒绝了",
+ "excitingActivities1": "其他有趣的活动",
+ "excitingActivities2": "正在等待您!",
+ "applyOtherGroups": "去申请其他聚会",
+ "noParticipatingGroups": "没有参与中的聚会。",
+ "noHostingGroups": "没有正在进行的聚会。",
+ "noPendingGroups": "没有等待中的聚会。",
+ "noRejectedGroups": "没有被拒绝的聚会",
+ "noMutualEvaluationGroups": "没有需要相互评价的聚会",
+ "noFavoritedGroups": "还没有收藏的聚会。",
+ "noReview": "没有写过的评论。"
+ },
+ "evaluation": {
+ "evaluateGroup": "评价聚会",
+ "howWasGroup": "聚会怎么样?",
+ "traits": {
+ "calm": "平静",
+ "kind": "友好",
+ "active": "积极",
+ "witty": "幽默"
+ },
+ "absence": "Gloddy没参加聚会吗?",
+ "bestPartner": "最佳搭档",
+ "whoBestPartner": "最佳搭档是谁?",
+ "reasonBestPartner1": "选择为最佳搭档的理由",
+ "reasonBestPartner2": "是什么?",
+ "leaveReview": "请给最佳搭档留下评论。",
+ "evaluationComplete": "聚会评价完成。",
+ "excitingGroups1": "其他有趣的聚会",
+ "excitingGroups2": "正在等待您!",
+ "evaluationHelp1": "成员评价对未来的匹配",
+ "evaluationHelp2": "非常有帮助!",
+ "reallyNotEvaluate": "真的不评价吗?"
+ }
+}
diff --git a/packages/web/src/app/i18n/locales/zh-CN/profile.json b/packages/web/src/app/i18n/locales/zh-CN/profile.json
new file mode 100644
index 000000000..3b7eebc78
--- /dev/null
+++ b/packages/web/src/app/i18n/locales/zh-CN/profile.json
@@ -0,0 +1,86 @@
+{
+ "birth": "生日",
+ "gender": "性别",
+ "introduce": "自我介绍",
+ "personality": "性格",
+ "세": "岁",
+ "selectPeronsonality": "选择性格",
+ "praise": {
+ "calm": "很冷静。",
+ "kind": "很友好。",
+ "active": "很积极。",
+ "humor": "很幽默。"
+ },
+ "keyword": {
+ "extroverted": "外向的",
+ "introverted": "内向的",
+ "discreet": "谨慎的",
+ "affable": "和蔼的",
+ "optimistic": "乐观的",
+ "sociable": "社交的",
+ "outspoken": "直言不讳的",
+ "responsible": "有责任心的",
+ "calm": "冷静的",
+ "outgoing": "外向的",
+ "playful": "顽皮的",
+ "sensible": "有感觉的",
+ "leaderType": "具有领导力的",
+ "humorous": "幽默的"
+ },
+ "재학생 인증 필요": "需要在校学生认证",
+ "거절됨": "已拒绝",
+ "신규 지원": "新申请",
+ "심사중": "审核中",
+ "모집중": "招募中",
+ "home": {
+ "gender": {
+ "male": "男",
+ "female": "女"
+ },
+ "trustScore": "信任指数",
+ "joined": "加入",
+ "selfIntro": "自我介绍",
+ "noSelfIntro": "自我介绍尚未注册!",
+ "participatedGroupCount": "累计参与的聚会",
+ "praiseCount": "收到的赞美",
+ "reviewCount": "聚会评论",
+ "deleteReview": "是否删除Gloddy的评论?"
+ },
+ "settings": {
+ "settings": "设置",
+ "version": "版本",
+ "termsOfService": "服务条款",
+ "customerService": "隐私政策",
+ "changeLanguage": "语言设置",
+ "한글": "韩文",
+ "영어": "英文",
+ "deleteAccount": "删除账户",
+ "editProfile": "编辑个人资料",
+ "accountDeletion": "会员注销",
+ "confirmDeletion": "您真的要注销吗?",
+ "reasonForWithdrawal": "注销的理由是什么?",
+ "deleteAllPraises": "到目前为止参加的所有聚会中收到的\n所有赞美记录将被删除",
+ "deleteBestPartnerReviews": "到目前为止参加的聚会中收到的其他会员的\n‘最佳伙伴’评论内容将被删除",
+ "deleteTrustScore": "注销会员时,个人资料中的信任度指标\n数据将被删除,无法恢复。",
+ "deleteGroupData": "注销会员时,进行的\n所有聚会活动数据将被删除,无法恢复。",
+ "agreeTerms": "我已阅读以上内容,并同意。",
+ "privacyPolicy": "根据隐私政策,您的个人信息将在注销后安全销毁。",
+ "proceedDeletion": "注销",
+ "feedback": "使用服务中有不便之处吗?我们将尽力改进。\n我们将尽力改进。",
+ "noDesiredGroups": "没有想参加的聚会",
+ "difficultToFindMembers": "难以找到想要的聚会成员",
+ "appInconvenience": "应用使用不便",
+ "rudeMembers": "遇到了不礼貌的会员",
+ "dataLeakConcern": "担心个人信息泄露",
+ "poorMatching": "匹配进行得不好",
+ "otherReasons": "其他",
+ "ifYouDelete": "删除账户后\n所有活动信息将被删除\n且无法恢复。",
+ "confirmWithdrawal": "您真的要注销吗?",
+ "withdrawalComplete": "会员注销完成。",
+ "thankYou": "感谢您使用Gloddy\n并给予爱护。",
+ "promise": "我们将成为一个更加进步和努力的\nGloddy。",
+ "pickThree": "请选择三个。",
+ "사용자님의 성격을": "您的性格",
+ "선택해주세요!": "请选择!"
+ }
+}
diff --git a/packages/web/src/app/i18n/settings.ts b/packages/web/src/app/i18n/settings.ts
index 5586df2f5..1291205f3 100644
--- a/packages/web/src/app/i18n/settings.ts
+++ b/packages/web/src/app/i18n/settings.ts
@@ -1,5 +1,5 @@
export const fallbackLng = 'en';
-export const languages = ['ko', 'en'];
+export const languages = ['ko', 'en', 'zh-CN', 'zh-TW'];
export const defaultNS = 'translation';
export const cookieName = 'i18next';
diff --git a/packages/web/src/middleware.ts b/packages/web/src/middleware.ts
index dfa850de5..e66a353c7 100644
--- a/packages/web/src/middleware.ts
+++ b/packages/web/src/middleware.ts
@@ -5,12 +5,12 @@ import { cookieName, languages } from './app/i18n/settings';
import { AUTH_KEYS } from './constants/token';
import { afterDay1, afterDay60 } from './utils/date';
-const privatePages = /\/(?:en|ko)\/(grouping|meeting|profile|community)/;
+const privatePages = /\/(?:en|ko|zh-CN|zh-TW)\/(grouping|meeting|profile|community)/;
const excludePages = [
- /\/(?:en|ko)\/profile\/setting\/information/,
- /\/(?:en|ko)\/profile\/setting\/service/,
- /\/(?:en|ko)\/notification/,
+ /\/(?:en|ko|zh-CN|zh-TW)\/profile\/setting\/information/,
+ /\/(?:en|ko|zh-CN|zh-TW)\/profile\/setting\/service/,
+ /\/(?:en|ko|zh-CN|zh-TW)\/notification/,
];
const isPrivatePage = (path: string) =>