From a101357cbf07a7d1bd0bae1f9cb4883304c034c9 Mon Sep 17 00:00:00 2001 From: xueweihan <595666367@qq.com> Date: Fri, 12 Jul 2024 14:55:06 +0800 Subject: [PATCH] feat: home, side, repo page --- next-i18next.config.js | 7 +- public/locales/en/common.json | 50 +++- public/locales/en/home.json | 28 +++ public/locales/en/repository.json | 99 ++++++++ public/locales/zh/common.json | 48 +++- public/locales/zh/home.json | 28 +++ public/locales/zh/repository.json | 100 ++++++++ src/components/buttons/LanguageSwitcher.tsx | 111 +++++++++ src/components/buttons/RankButton.tsx | 24 +- src/components/dialog/GroupItem.tsx | 13 +- src/components/dialog/TagModal.tsx | 32 +-- src/components/home/Item.tsx | 15 +- src/components/home/Items.tsx | 22 +- src/components/layout/About.tsx | 17 -- src/components/layout/Footer.tsx | 32 ++- src/components/layout/Header.tsx | 50 ++-- src/components/layout/Layout.tsx | 2 +- src/components/navbar/IndexBar.tsx | 15 +- src/components/navbar/RepoNavbar.tsx | 17 +- .../respository/CommentContainer.tsx | 27 ++- src/components/respository/CommentItem.tsx | 49 ++-- src/components/respository/CommentSubmit.tsx | 70 +++--- src/components/respository/Info.tsx | 138 ++++++----- src/components/respository/MoreInfo.tsx | 29 ++- src/components/respository/Score.tsx | 8 +- src/components/respository/Tabs.tsx | 17 +- src/components/search/SearchInput.tsx | 19 +- src/components/side/Ad.tsx | 43 ++-- src/components/side/Recommend.tsx | 14 +- src/components/side/Side.tsx | 22 +- src/components/side/SideAd.tsx | 9 +- src/components/side/SideLoginButton.tsx | 8 +- src/components/side/Stats.tsx | 14 +- src/components/side/TagList.tsx | 16 +- src/components/side/UserStatus.tsx | 216 +++++++++--------- src/pages/_document.tsx | 11 +- src/pages/index.tsx | 14 +- src/pages/repository/[rid]/index.tsx | 17 +- src/types/home.tsx | 8 +- src/types/repository.tsx | 6 +- src/types/user.ts | 5 - src/utils/day.ts | 9 +- 42 files changed, 997 insertions(+), 482 deletions(-) create mode 100644 public/locales/en/home.json create mode 100644 public/locales/en/repository.json create mode 100644 public/locales/zh/home.json create mode 100644 public/locales/zh/repository.json create mode 100644 src/components/buttons/LanguageSwitcher.tsx delete mode 100644 src/components/layout/About.tsx diff --git a/next-i18next.config.js b/next-i18next.config.js index 9fe642c6..659b2333 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -6,12 +6,7 @@ module.exports = { defaultLocale: 'zh', locales: ['zh', 'en'], }, - ns: ['common'], - interpolation: { - prefix: '{', - suffix: '}', - }, - localeStructure: '{lng}/{ns}', + // eslint-disable-next-line @typescript-eslint/no-var-requires localePath: require('path').resolve('./public/locales'), // 指定翻译文件的路径 }; diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 16ccfe8e..307b7771 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,5 +1,49 @@ { - "a": "1111", - "b": "1111", - "c": "1111" + "header": { + "search": "Search Open Source Projects", + "home": "Home", + "periodical": "Monthly", + "rank": "Ranking", + "article": "Articles", + "submit": "Submit" + }, + "user_side": { + "contribute_label": "Contributions", + "profile": "Profile", + "admin": "Settings", + "logout": "Sign Out", + "login": "Sign In" + }, + "advert": { + "desc": "Server will expire in", + "desc2": "WeChat Sponsorship", + "next": "Short of this target by", + "next2": "Short of next target by", + "day": "D", + "year": "Y" + }, + "site_stats": { + "user": "Total Users", + "repo": "Projects", + "title": "About Us", + "desc": "HelloGitHub is a platform to discover and share interesting, beginner-friendly open source projects.\nWe aim to help everyone find joy in programming, easily solve technical challenges,\nexplore amazing open-source tools, and naturally embark on their open-source journey." + }, + "recommend": { + "title": "Recommended", + "desc": "Content is collected from spdx and GitHub, following the", + "desc2": "protocol.", + "change": "Refresh" + }, + "footer": { + "feedback": "Feedback", + "business": "Business", + "contact": "Contact Us", + "agreement": "Agreement", + "source": "Source Code", + "sitemap": "Sitemap", + "server_sponsor": "Server is sponsored by", + "server_sponsor2": "", + "cdn_sponsor": "Cloud service is sponsored by", + "cdn_sponsor2": "" + } } diff --git a/public/locales/en/home.json b/public/locales/en/home.json new file mode 100644 index 00000000..94c8447f --- /dev/null +++ b/public/locales/en/home.json @@ -0,0 +1,28 @@ +{ + "title": "Home", + "description": "Sharing interesting and beginner-friendly open-source projects on GitHub", + "bottom_text_login": "You have reached the bottom of the page", + "bottom_text_nologin": "End of the page! Sign in to read more", + "nav": { + "all": "All", + "featured": "Featured", + "tag": "Tags", + "submit": "Submit" + }, + "tag_side": { + "title": "Hot Tags", + "manage": "Preference", + "all_tags_label": "All" + }, + "tag_modal": { + "default_tag": "All", + "tips": "Tips: Click the tag on the left to 'Select', drag the selected tags on the right to 'Sort'", + "selected": "Selected: ", + "fetch_fail_msg": "Failed to fetch tags", + "add": "Add Tag", + "save": "Save", + "save_success_msg": "Save successful!", + "save_fail_msg": "Save failed!", + "max_tag_msg": "You can only select up to {{maxTotal}} tags!" + } +} diff --git a/public/locales/en/repository.json b/public/locales/en/repository.json new file mode 100644 index 00000000..7e731f95 --- /dev/null +++ b/public/locales/en/repository.json @@ -0,0 +1,99 @@ +{ + "nav": { + "title": "Project Details", + "desc": "Shared by", + "desc2": "" + }, + "info": { + "claimed": "Claimed", + "unclaim": "Unclaimed", + "score_desc": "HelloGitHub Rating", + "score_user_desc": "{{count}} ratings", + "vite": "Visit", + "voted": "Liked", + "vote": "Like", + "opensource": "Open Source", + "discuss": "Discuss", + "share": "Share", + "collect": "Collect", + "copy_desc": "More details at:", + "copy_success": "Project information copied successfully, share it now!", + "copy_fail": "Copy failed" + }, + "url": { + "home": "Website", + "document": "Documentation", + "download": "Download", + "online": "Demo", + "source": "Source Code" + }, + "favorite": { + "default": "Default Favorites", + "cancel": "Unfavorite", + "success": "Added to Favorites", + "fail": "Failed to add to Favorites", + "title": "Select Favorites", + "desc": "You can find favorited projects in 'My Homepage'", + "save": "Save" + }, + "history": { + "past_day_desc": "Past {{days}} days", + "total_desc": "Received {{total}} stars ✨", + "fail_desc": "No star history data available" + }, + "more": { + "yes": "Yes", + "no": "No", + "null": "None", + "star": "Stars", + "chinese": "Chinese", + "language": "Main Language", + "activity": "Active", + "contributors": "Contributors", + "org": "Organization", + "version": "Latest Version", + "license": "License", + "expand": "More", + "collapse": "Collapse" + }, + "content": { + "desc_tab": "Description", + "code_tab": "Code", + "volume_label": "Included in:", + "volume": "Issue {{volume}}", + "tag_label": "Tags:" + }, + "comment": { + "title": "Comments", + "sort_hot": "Hot", + "sort_new": "Newest", + "total": "{{total}} selected comments", + "more_reply": "View all {{total}} replies", + "load_more": "Load more...", + "no_comment": "No selected comments yet", + "used": "Used", + "unused": "Not Used", + "score": "Rating:", + "reply": "Reply", + "cancel": "Cancel", + "item": { + "featured": "Featured", + "unfeatured": "Not Featured", + "vote": "Like", + "expand": "Expand", + "collapse": "Collapse", + "login": "Please log in first" + }, + "submit": { + "reply_placeholder": "Replying to: {{nickname}}", + "placeholder": "Write your comment: share your experience, pros/cons, use cases, or any surprising aspects of this open-source project...", + "save": "Submit", + "success": "Posted successfully! Will be displayed after approval", + "fail": "Submission failed", + "err1": "Comment content cannot be empty", + "err2": "Comment content must be at least 5 characters", + "err3": "Comment content cannot exceed 500 characters", + "err4": "Please provide a rating" + } + } +} diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index b44c4a9d..ac2b0a33 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -1,3 +1,49 @@ { - "a": "某个中文翻译" + "header": { + "search": "搜索开源项目", + "home": "首页", + "periodical": "月刊", + "rank": "榜单", + "article": "文章", + "submit": "提交项目" + }, + "user_side": { + "contribute_label": "贡献值", + "profile": "我的主页", + "admin": "管理中心", + "logout": "退出登录", + "login": "立即登录" + }, + "advert": { + "desc": "服务器还剩", + "desc2": "微信扫码赞助本站", + "next": "距离目标还差", + "next2": "距离下个目标还差", + "day": "天", + "year": "年" + }, + "site_stats": { + "user": "用户总数", + "repo": "开源项目", + "title": "关于本站", + "desc": "HelloGitHub 是一个发现和分享有趣、入门级开源项目的平台。\n希望大家能够在这里找到编程的快乐、 轻松搞定问题的技术方案、\n大呼过瘾的开源神器, 顺其自然地开启开源之旅。" + }, + "recommend": { + "title": "推荐项目", + "desc": "内容整理自 spdx 和 GitHub 网站,并遵循", + "desc2": "协议。", + "change": "换一换" + }, + "footer": { + "feedback": "问题反馈", + "business":"商务合作", + "contact": "联系我们", + "agreement": "用户协议", + "source": "社区源码", + "sitemap": "站点地图", + "server_sponsor": "服务器由", + "server_sponsor2": "提供", + "cdn_sponsor": "专业的", + "cdn_sponsor2": "提供云存储服务" + } } diff --git a/public/locales/zh/home.json b/public/locales/zh/home.json new file mode 100644 index 00000000..50cc6534 --- /dev/null +++ b/public/locales/zh/home.json @@ -0,0 +1,28 @@ +{ + "title": "首页", + "description": "分享 GitHub 上有趣、入门级的开源项目", + "bottom_text_login": "你不经意间触碰到了底线", + "bottom_text_nologin": "到底啦!登录可查看更多内容", + "nav": { + "all": "全部", + "featured": "精选", + "tag": "标签", + "submit": "提交" + }, + "tag_side": { + "title": "热门标签", + "manage": "管理标签", + "all_tags_label": "综合" + }, + "tag_modal": { + "default_tag": "综合", + "tips": "操作提示:点击左侧标签为「选择」,拖拽右侧已选标签可「排序」", + "selected": "已选:", + "fetch_fail_msg": "获取标签失败", + "add": "添加标签", + "save": "保存", + "save_success_msg": "保存成功!", + "save_fail_msg": "保存失败!", + "max_tag_msg": "最多只能选择 {{maxTotal}} 个标签!" + } +} diff --git a/public/locales/zh/repository.json b/public/locales/zh/repository.json new file mode 100644 index 00000000..792e5dd1 --- /dev/null +++ b/public/locales/zh/repository.json @@ -0,0 +1,100 @@ +{ + "nav": { + "title": "项目详情", + "desc": "由", + "desc2": "分享" + }, + "info": { + "claimed": "已认领", + "unclaim": "未认领", + "socre_desc": "HelloGitHub 评分", + "socre_user_desc": "{{count}} 人评分", + "vite": "访问", + "voted": "已赞", + "vote": "点赞", + "opensource": "开源", + "discuss": "讨论", + "share": "分享", + "collect": "收藏", + "copy_desc": "更多详情尽在:", + "copy_success": "项目信息已复制,快去分享吧!", + "copy_fail": "复制失败" + }, + "url": { + "home": "官网", + "document": "文档", + "download": "下载", + "online": "演示", + "source": "源码" + }, + "favorite": { + "default": "默认收藏夹", + "cancel": "取消收藏", + "success": "收藏成功", + "fail": "收藏失败", + "title": "选择收藏夹", + "desc": "收藏的项目在「我的主页」可以找到", + "save": "确定" + }, + "history": { + "past_day_desc": "过去 {{days}} 天", + "total_desc": "共收获 {{total}} 颗 Star ✨", + "fail_desc": "暂无 Star 历史数据" + }, + "more": { + "yes": "是", + "no": "否", + "null": "无", + "star": "星数", + "chinese": "中文", + "language": "主语言", + "activity": "活跃", + "contributors": "贡献者", + "org": "组织", + "version": "最新版本", + "license": "协议", + "expand": "更多", + "collapse": "收起" + }, + "content": { + "desc_tab": "介绍", + "code_tab": "代码", + "volume_label": "收录于:", + "volume": "第 {{volume}} 期", + "tag_label": "标签:" + }, + "comment": { + "title": "评论", + "sort_hot": "热门", + "sort_new": "最新", + "total": "{{total}} 条精选评论", + "more_reply": "查看全部 {{total}} 条回复", + "load_more": "加载更多...", + "no_comment": "暂无精选评论", + "used": "用过", + "unused": "没用过", + "score": "评分:", + "reply": "回复", + "cancel": "取消", + "item": { + "featured": "精选", + "unfeatured": "未精选", + "vote": "点赞", + "expand": "展开", + "collapse": "收起", + "login": "请先登录" + }, + "submit": { + "reply_placeholder": "正在回复:{{nickname}}", + "placeholder": "写下你的评论:分享开源项目的使用体验、优点/吐槽、适用场景、惊艳之处...", + "save": "发表", + "success": "发布成功!通过审核后展示", + "fail": "提交失败", + "err1":"评论内容不能为空", + "err2":"评论内容不能少于 5 个字", + "err3":"评论内容不能超过 500 个字", + "err4":"请评分" + + } + } +} \ No newline at end of file diff --git a/src/components/buttons/LanguageSwitcher.tsx b/src/components/buttons/LanguageSwitcher.tsx new file mode 100644 index 00000000..f7e041e5 --- /dev/null +++ b/src/components/buttons/LanguageSwitcher.tsx @@ -0,0 +1,111 @@ +// components/LanguageSwitcher.tsx +import { useRouter } from 'next/router'; +import { useEffect, useRef, useState } from 'react'; + +const LanguageSwitcher = () => { + const router = useRouter(); + const { locale, asPath } = router; + const [selectedLocale, setSelectedLocale] = useState(locale); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const storedLocale = localStorage.getItem('locale'); + if (storedLocale && storedLocale !== locale) { + setSelectedLocale(storedLocale); + router.push(asPath, asPath, { locale: storedLocale }); + } + }, []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [dropdownRef]); + + const changeLanguage = (language: string) => { + localStorage.setItem('locale', language); + setSelectedLocale(language); + setIsOpen(false); // 关闭下拉菜单 + router.push(asPath, asPath, { locale: language }); + }; + + return ( +
+
+ +
+ + {isOpen && ( +
+
+ + + +
+
+ )} +
+ ); +}; + +export default LanguageSwitcher; diff --git a/src/components/buttons/RankButton.tsx b/src/components/buttons/RankButton.tsx index c491c268..79abcafd 100644 --- a/src/components/buttons/RankButton.tsx +++ b/src/components/buttons/RankButton.tsx @@ -5,24 +5,26 @@ import type { option } from '@/components/dropdown/Dropdown'; import Dropdown from '@/components/dropdown/Dropdown'; type RankButtonProps = { + t: (key: string) => string; type?: '' | 'dropdown'; }; -const btnList: option[] = [ - { key: '/', value: '首页' }, - { key: '/periodical', value: '月刊' }, - { key: '/report/tiobe', value: '榜单' }, - { key: '/article', value: '文章' }, - { key: '/onefile', value: 'OneFile' }, -]; - -const RankButton = (props: RankButtonProps) => { +const RankButton = ({ t, type = '' }: RankButtonProps) => { const router = useRouter(); + + const btnList: option[] = [ + { key: '/', value: t('header.home') }, + { key: '/periodical', value: t('header.periodical') }, + { key: '/report/tiobe', value: t('header.rank') }, + { key: '/article', value: t('header.article') }, + { key: '/onefile', value: 'OneFile' }, + ]; + const onChange = async (opt: option) => { router.push(opt.key as any); }; - if (props.type === 'dropdown') { + if (type === 'dropdown') { let key = '/'; if (router.isReady) { if (router.pathname.includes('periodical')) { @@ -47,7 +49,7 @@ const RankButton = (props: RankButtonProps) => { return ( 🏆 - 榜单 + {t('header.rank')} ); }; diff --git a/src/components/dialog/GroupItem.tsx b/src/components/dialog/GroupItem.tsx index 90e83a18..0568df33 100644 --- a/src/components/dialog/GroupItem.tsx +++ b/src/components/dialog/GroupItem.tsx @@ -11,11 +11,18 @@ interface IProps { groupName: string; portalTagGroupsRef: MutableRefObject; handleAddTag: (tid: string) => void; + t: (key: string, total?: any) => string; } const GroupItem = (props: IProps) => { - const { item, effectedTidList, groupName, portalTagGroupsRef, handleAddTag } = - props; + const { + t, + item, + effectedTidList, + groupName, + portalTagGroupsRef, + handleAddTag, + } = props; const sortItemRef = useRef(null); useEffect(() => { @@ -27,7 +34,7 @@ const GroupItem = (props: IProps) => { onChoose: function ({ item }) { if (effectedTidList.length >= maxTotal) { item.draggable = false; - Message.error(`最多只能选择 ${maxTotal} 个标签`); + Message.error(t('tag_modal.max_tag_msg', { maxTotal: maxTotal })); return false; } }, diff --git a/src/components/dialog/TagModal.tsx b/src/components/dialog/TagModal.tsx index dddddef8..643f4e91 100644 --- a/src/components/dialog/TagModal.tsx +++ b/src/components/dialog/TagModal.tsx @@ -17,17 +17,23 @@ import { getSelectTags, saveSelectTags } from '@/services/tag'; import BasicDialog from '../dialog/BasicDialog'; -import { maxTotal, PortalTag, PortalTagGroup } from '@/types/tag'; - -const defaultTag = { name: '综合', tid: '', icon_name: 'find' }; +import { maxTotal, PortalTag, PortalTagGroup, Tag } from '@/types/tag'; export function TagModal({ children, updateTags, + t, }: { children: JSX.Element; updateTags: any; + t: (key: string, total?: any) => string; }) { + const defaultTag: Tag = { + name: t('tag_modal.default_tag'), + tid: '', + icon_name: 'find', + }; + const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const { isLogin, login } = useLoginContext(); @@ -52,7 +58,7 @@ export function TagModal({ setEffectedTidList(res.effected); setIsOpen(true); } else { - Message.error('获取标签失败'); + Message.error(t('tag_modal.fetch_error_msg')); } } }; @@ -96,15 +102,15 @@ export function TagModal({ setLoading(true); const res = await saveSelectTags(effectedTidList); if (res.success) { - Message.success('保存成功!'); + Message.success(t('tag_modal.save_success_msg')); updateTags(selectTags); setIsOpen(false); } else { - Message.error(('保存失败:' + res?.message) as string); + Message.error(t('tag_modal.save_fail_msg')); } setLoading(false); } else { - Message.error(`最多只能选择 ${maxTotal} 个标签`); + Message.error(t('tag_modal.max_tag_msg', { maxTotal: maxTotal })); } }; @@ -142,9 +148,7 @@ export function TagModal({ >
- - 操作提示:点击左侧标签为「选择」,拖拽右侧已选标签可「排序」 - + {t('tag_modal.tips')}
@@ -168,6 +172,7 @@ export function TagModal({ groupName={group.group_name} portalTagGroupsRef={portalTagGroupsRef} handleAddTag={addTag} + t={t} /> ))}
, @@ -175,7 +180,8 @@ export function TagModal({
- 已选:{total} + {t('tag_modal.selected')} + {total} / {maxTotal}
@@ -230,7 +236,7 @@ export function TagModal({
- 添加标签 + {t('tag_modal.add')}
@@ -242,7 +248,7 @@ export function TagModal({ isLoading={loading} onClick={saveTags} > - 保存 + {t('tag_modal.save')} diff --git a/src/components/home/Item.tsx b/src/components/home/Item.tsx index 1aa310ce..57963d7d 100644 --- a/src/components/home/Item.tsx +++ b/src/components/home/Item.tsx @@ -1,5 +1,4 @@ import { NextPage } from 'next'; -import { useTranslation } from 'next-i18next'; import { AiFillFire, AiOutlineEye } from 'react-icons/ai'; import { GoVerified } from 'react-icons/go'; @@ -8,9 +7,14 @@ import CustomLink from '@/components/links/CustomLink'; import { fromNow } from '@/utils/day'; import { numFormat } from '@/utils/util'; -import { ItemProps } from '@/types/home'; +import { HomeItem } from '@/types/home'; -const Item: NextPage = ({ item }) => { +type Props = { + i18n_lang: string; + item: HomeItem; +}; + +const Item: NextPage = ({ item, i18n_lang }) => { const { item_id, author_avatar, @@ -27,7 +31,6 @@ const Item: NextPage = ({ item }) => { clicks_total, } = item; - const { t } = useTranslation('common'); if (!item_id || !name || !title) { console.warn('Missing essential item data:', item); return null; @@ -66,7 +69,7 @@ const Item: NextPage = ({ item }) => { )}

- {t('a')} + {name} — @@ -120,7 +123,7 @@ const Item: NextPage = ({ item }) => { ·

diff --git a/src/components/home/Items.tsx b/src/components/home/Items.tsx index 70a12a73..0debdea9 100644 --- a/src/components/home/Items.tsx +++ b/src/components/home/Items.tsx @@ -1,30 +1,20 @@ import { NextPage } from 'next'; -import { useRouter } from 'next/router'; -import { useTranslation } from 'next-i18next'; import React from 'react'; import Item from './Item'; import { HomeItem } from '@/types/home'; -import { RepositoryItems } from '@/types/repository'; -const Items: NextPage = ({ repositories }) => { - const { i18n } = useTranslation('common'); - const router = useRouter(); - - const changeLanguage = (lng: string) => { - i18n.changeLanguage(lng); - router.push(router.pathname, router.pathname, { locale: lng }); - }; +type Props = { + repositories: HomeItem[]; + i18n_lang: string; +}; +const Items: NextPage = ({ repositories, i18n_lang }) => { return (
-
- - -
{repositories.map((item: HomeItem) => ( - + ))}
); diff --git a/src/components/layout/About.tsx b/src/components/layout/About.tsx deleted file mode 100644 index e874605e..00000000 --- a/src/components/layout/About.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// 本组件用于展示关于本站的介绍信息 - -const About: React.FC = () => { - return ( -
-
关于本站
- -
- HelloGitHub - 是一个分享有趣、入门级开源项目的平台。希望大家能够在这里找到编程的快乐、轻松搞定问题的技术方案、大呼过瘾的开源神器, - 一个偶遇的开源项目,开启你的开源之旅。 -
-
- ); -}; - -export default About; diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 8fd04719..5826226e 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -6,38 +6,48 @@ import CustomLink from '@/components/links/CustomLink'; import FooterLink from './FooterLink'; -const Footer = () => { +import { SideProps } from '@/types/home'; + +const Footer = ({ t }: SideProps) => { return (
-
问题反馈
+
+ {t('footer.feedback')} +
· -
商务合作
+
+ {t('footer.business')} +
· - 联系我们 + + {t('footer.contact')} +

- 服务协议 + {t('footer.agreement')} · - 社区源码 + {t('footer.source')} · - 站点地图 + + {t('footer.sitemap')} +

- 服务器由 + {t('footer.server_sponsor')} { alt='ucloud_footer' /> - 提供 + {t('footer.server_sponsor2')}
- 专业的 + {t('footer.cdn_sponsor')} { alt='upyun_footer' /> - 提供云存储服务 + {t('footer.cdn_sponsor2')}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index d5ba34a9..71edd28a 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; import { useRouter } from 'next/router'; +import { useTranslation } from 'next-i18next'; import { useEffect, useState } from 'react'; -import { AiOutlineBell, AiOutlineGithub } from 'react-icons/ai'; +import { AiOutlineGithub } from 'react-icons/ai'; import { useLoginContext } from '@/hooks/useLoginContext'; @@ -10,13 +11,16 @@ import RankButton from '@/components/buttons/RankButton'; import { RepoModal } from '@/components/dialog/RepoModal'; import AvatarWithDropdown from '@/components/dropdown/AvatarWithDropdown'; +import LanguageSwitcher from '../buttons/LanguageSwitcher'; import { LoginButton } from '../buttons/LoginButton'; import SearchInput from '../search/SearchInput'; +import ThemeSwitch from '../ThemeSwitch'; const Header = () => { const router = useRouter(); - const { isLogin, userInfo } = useLoginContext(); + const { isLogin } = useLoginContext(); const [curPath, setCurPath] = useState(''); + const { t } = useTranslation('common'); useEffect(() => { setCurPath(router.pathname); @@ -49,22 +53,24 @@ const Header = () => { {/* 移动端显示的[排行榜]等按钮的下拉列表 */}
- +
- +
    {/* pc 端显示的顶部按钮 */}
  • - 首页 + {t('header.home')}
  • - 月刊 + + {t('header.periodical')} +
  • - +
  • - 文章 + {t('header.article')}
  • OneFile @@ -74,36 +80,16 @@ const Header = () => { {!isLogin ? : }
-
+
- {isLogin && userInfo?.success ? ( -
{ - router.push('/notification'); - }} - > - - - {userInfo?.unread.total > 0 ? ( - - ) : ( - - )} - -
- ) : ( - <> - )} + +
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 4951e9e4..6a1ceb92 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -42,7 +42,7 @@ const Layout: React.FC = ({ children }) => { {children}
- +
)} diff --git a/src/components/navbar/IndexBar.tsx b/src/components/navbar/IndexBar.tsx index afc0f42a..1951f9f1 100644 --- a/src/components/navbar/IndexBar.tsx +++ b/src/components/navbar/IndexBar.tsx @@ -7,12 +7,13 @@ import useTagHandling from '@/hooks/useTagHandling'; import { RepoModal } from '@/components/dialog/RepoModal'; import TagLink from '@/components/links/TagLink'; -interface Props { +type Props = { + t: (key: string) => string; tid: string; sort_by: string; -} +}; -const IndexBar: NextPage = ({ tid, sort_by }) => { +const IndexBar: NextPage = ({ t, tid, sort_by }) => { const { labelStatus, tagItems, featuredURL, allURL, handleTagButton } = useTagHandling(tid, sort_by); @@ -30,19 +31,19 @@ const IndexBar: NextPage = ({ tid, sort_by }) => {
- 精选 + {t('nav.featured')} - 全部 + {t('nav.all')} - 标签 + {t('nav.tag')}
diff --git a/src/components/navbar/RepoNavbar.tsx b/src/components/navbar/RepoNavbar.tsx index 348ef954..06a0cb13 100644 --- a/src/components/navbar/RepoNavbar.tsx +++ b/src/components/navbar/RepoNavbar.tsx @@ -1,11 +1,14 @@ -import { NextPage } from 'next'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { AiOutlineArrowLeft } from 'react-icons/ai'; -import { UserAvaterProps } from '@/types/user'; +interface Props { + avatar: string; + uid: string; + t: (key: string) => string; +} -const RepoDetailNavbar: NextPage = ({ avatar, uid }) => { +const RepoDetailNavbar = ({ avatar, uid, t }: Props) => { const router = useRouter(); const goBack = () => { @@ -25,10 +28,12 @@ const RepoDetailNavbar: NextPage = ({ avatar, uid }) => { size={18} />
-
项目详情
+
+ {t('nav.title')} +
- 由 + {t('nav.desc')} = ({ avatar, uid }) => { alt='navbar_avatar' /> - 分享 + {t('nav.desc2')}
diff --git a/src/components/respository/CommentContainer.tsx b/src/components/respository/CommentContainer.tsx index bea2b764..1b6109c4 100644 --- a/src/components/respository/CommentContainer.tsx +++ b/src/components/respository/CommentContainer.tsx @@ -13,11 +13,13 @@ import { CommentItemData } from '@/types/repository'; interface Props { belong: string; belongId: string; + t: (key: string, total?: any) => string; + i18n_lang: string; className?: string; } const CommentContainer = (props: Props) => { - const { belong, belongId, className } = props; + const { belong, belongId, className, t, i18n_lang } = props; const { list, total, @@ -90,10 +92,13 @@ const CommentContainer = (props: Props) => { key={`reply-item-${item.cid}`} onReply={(_, reply_id) => setCommentId(reply_id)} onChangeVote={(value) => handleChangeVote(index, value)} + t={t} + i18n_lang={i18n_lang} /> {item.reply_id === commentId && ( setCommentId(undefined)} @@ -110,6 +115,8 @@ const CommentContainer = (props: Props) => { return ( <> { {item.cid === commentId && ( { className='mb-6 flex cursor-pointer items-center justify-center rounded-md bg-gray-50 text-sm leading-10 hover:bg-gray-200 active:bg-gray-50 dark:bg-gray-700' onClick={() => loadMoreReply(item.cid)} > - 查看全部 {item.replies?.total} 条回复 + {t('comment.total', { total: item.replies.total })} )} @@ -166,15 +174,18 @@ const CommentContainer = (props: Props) => { return (
-

评论

+

{t('comment.title')}

{currentUserComment ? ( ) : ( { {total ? ( <>
- {total} 条精选评论 + {t('comment.total', { total: total })}
@@ -215,13 +226,13 @@ const CommentContainer = (props: Props) => { className='cursor-pointer rounded-md bg-gray-50 text-center text-sm leading-10 hover:bg-gray-200 active:bg-gray-50 dark:bg-gray-700' onClick={loadMore} > - 加载更多... + {t('comment.load_more')}
) : (
- 暂无精选评论 + {t('comment.no_comment')}
)} diff --git a/src/components/respository/CommentItem.tsx b/src/components/respository/CommentItem.tsx index 481ff434..3041b75d 100644 --- a/src/components/respository/CommentItem.tsx +++ b/src/components/respository/CommentItem.tsx @@ -16,18 +16,21 @@ import { MDRender } from '../mdRender/MDRender'; import { CommentItemData } from '@/types/repository'; -const CommentItem = ( - props: CommentItemData & { - className?: string; - /** 是否独自显示,以表示当前用户所发表过的评论 */ - alone?: boolean; - footerRight?: (data: CommentItemData) => React.ReactNode; - onChangeVote?: (value: boolean) => void; - onReply?: (cid: string, reply_id?: string) => void; - reply?: boolean; - } -) => { +type CommentItemProps = CommentItemData & { + t: (key: string, total?: any) => string; + i18n_lang: string; + className?: string; + alone?: boolean; // 是否独自显示,以表示当前用户所发表过的评论 + footerRight?: (data: CommentItemData) => React.ReactNode; + onChangeVote?: (value: boolean) => void; + onReply?: (cid: string, reply_id?: string) => void; + reply?: boolean; +}; + +const CommentItem = (props: CommentItemProps) => { const { + t, + i18n_lang, cid, user, score, @@ -71,7 +74,7 @@ const CommentItem = ( await like({ belong, belongId, cid }); onChangeVote(true); } else { - Message.error('请先登录!'); + Message.error(t('comment.item.login')); } }; @@ -80,7 +83,7 @@ const CommentItem = ( (() => alone ? ( - {isShow ? '已精选' : '未精选'} + {isShow ? t('comment.item.featured') : t('comment.item.unfeatured')} ) : (
@@ -92,7 +95,7 @@ const CommentItem = ( onClick={handleVote} > - {votes || '点赞'} + {votes || t('comment.item.vote')}
)} @@ -101,7 +104,9 @@ const CommentItem = ( onClick={() => props.onReply?.(cid, props.reply_id)} > - {props.reply ? '取消回复' : '回复'} + + {props.reply ? t('comment.cancel') : t('comment.reply')} + )); @@ -148,17 +153,19 @@ const CommentItem = ( {!props.reply_id ? ( <> - 评分: + {t('comment.score')} - {isUsed ? '用过' : '没用过'} + {isUsed ? t('comment.used') : t('comment.unused')} ) : ( props.reply_uid && ( <> - 回复 + + {t('comment.reply')} +
@@ -184,12 +191,14 @@ const CommentItem = ( className='text-blue-500' onClick={() => setExpand(!expand)} > - {expand ? '收起' : '展开'} + {expand ? t('comment.item.collapse') : t('comment.item.expand')} )}
- {fromNow(createdAt)} + + {fromNow(createdAt, i18n_lang)} + {footerRight(props)}
diff --git a/src/components/respository/CommentSubmit.tsx b/src/components/respository/CommentSubmit.tsx index efc0b222..53e3d11a 100644 --- a/src/components/respository/CommentSubmit.tsx +++ b/src/components/respository/CommentSubmit.tsx @@ -14,37 +14,20 @@ import { import { CommentItemData, CommentSuccessData } from '@/types/repository'; -function getErrMessage(commentData: { - comment: string; - isUsed: boolean; - score: number; -}) { - if (!commentData.comment) { - return '评论内容不能为空'; - } - if (commentData.comment.length < 5) { - return '评论内容不能少于 5 个字'; - } - if (commentData.comment.length > 500) { - return '评论内容不能超过 500 个字'; - } - if (!commentData.score) { - return '请评分'; - } - return ''; -} - -function CommentSubmit(props: { +interface CommentSubmitProps { + t: (key: string, text?: any) => string; belongId: string; className?: string; replyUser?: CommentItemData; onSuccess?: (data: CommentSuccessData) => void; onFail?: (error: any) => void; onCancelReply?: () => void; -}) { +} + +function CommentSubmit(props: CommentSubmitProps) { const { commentData, setCommentData } = useCommentData(); const { login, userInfo, isLogin } = useLoginContext(); - const { belongId, className, onSuccess, onFail } = props; + const { t, belongId, className, onSuccess, onFail } = props; const handleInput: FormEventHandler = (e) => { const { value } = e.currentTarget; @@ -70,6 +53,25 @@ function CommentSubmit(props: { const handleChangeRating = (rating: number) => { setCommentData({ ...commentData, score: rating }); }; + const getErrMessage = (commentData: { + comment: string; + isUsed: boolean; + score: number; + }) => { + if (!commentData.comment) { + return t('comment.submit.err1'); + } + if (commentData.comment.length < 5) { + return t('comment.submit.err2'); + } + if (commentData.comment.length > 500) { + return t('comment.submit.err3'); + } + if (!commentData.score) { + return t('comment.submit.err4'); + } + return ''; + }; const handleSubmit = () => { if (getErrMessage(commentData)) { @@ -95,19 +97,21 @@ function CommentSubmit(props: { if (data.success) { onSuccess && onSuccess(data); - Message.success('发布成功,通过审核后展示...'); + Message.success(t('comment.submit.success')); } else { onFail && onFail(data); } }) .catch((err) => { - Message.error(err.message || '提交失败'); + Message.error(err.message || t('comment.submit.fail')); }); }; const placeholder = props.replyUser - ? `正在回复:${props.replyUser.user.nickname}` - : '写评论:分享开源项目的使用体验、优点/吐槽、适用场景、惊艳之处...'; + ? t('comment.submit.reply_placeholder', { + nickname: props.replyUser.user.nickname, + }) + : t('comment.submit.placeholder'); return (
@@ -142,7 +146,7 @@ function CommentSubmit(props: { checked={!commentData.isUsed} onChange={() => handleRadioChange(false)} /> - 没用过 + {t('comment.unused')}
- 评分: + {t('comment.score')} - 取消回复 + {t('comment.cancel')}
) : ( @@ -186,7 +190,7 @@ function CommentSubmit(props: { className='ml-auto inline-flex h-8 min-h-[2rem] flex-shrink-0 cursor-pointer select-none flex-wrap items-center justify-center rounded-lg bg-gray-700 pl-3 pr-3 text-sm font-semibold text-white transition-transform focus:outline-none active:scale-90' onClick={handleSubmit} > - 发布 + {t('comment.submit.save')} )} diff --git a/src/components/respository/Info.tsx b/src/components/respository/Info.tsx index fa3a2168..337432ac 100644 --- a/src/components/respository/Info.tsx +++ b/src/components/respository/Info.tsx @@ -41,6 +41,11 @@ import Message from '../message'; import { Repository, RepositoryProps } from '@/types/repository'; +interface URLoptionProps { + repo: Repository; + t: (key: string, total?: any) => string; +} + type URLoption = { url: string; key: string; @@ -57,22 +62,30 @@ const iconMap: { [key: string]: JSX.Element } = { }; // Custom hook to handle URL options logic -const useURLOptions = (repo: Repository) => { +const useURLOptions = ({ repo, t }: URLoptionProps) => { const [urlOptions, setURLOptions] = useState([]); useEffect(() => { const options: URLoption[] = []; if (repo.homepage) - options.push({ key: 'home', name: '官网', url: repo.homepage }); + options.push({ key: 'home', name: t('url.home'), url: repo.homepage }); if (repo.document) - options.push({ key: 'document', name: '文档', url: repo.document }); + options.push({ + key: 'document', + name: t('url.document'), + url: repo.document, + }); if (repo.online) - options.push({ key: 'online', name: '演示', url: repo.online }); + options.push({ key: 'online', name: t('url.online'), url: repo.online }); if (repo.download) - options.push({ key: 'download', name: '下载', url: repo.download }); + options.push({ + key: 'download', + name: t('url.download'), + url: repo.download, + }); if (options.length > 0) { - options.unshift({ key: 'source', name: '源码', url: repo.url }); + options.unshift({ key: 'source', name: t('url.source'), url: repo.url }); setURLOptions(options); } else { setURLOptions([]); @@ -82,7 +95,7 @@ const useURLOptions = (repo: Repository) => { return urlOptions; }; -const Info: NextPage = ({ repo }) => { +const Info = ({ repo, t }: RepositoryProps) => { const { isLogin, login } = useLoginContext(); const [isVoted, setIsVoted] = useState(false); const [voteTotal, setVoteTotal] = useState(repo.votes); @@ -93,7 +106,7 @@ const Info: NextPage = ({ repo }) => { const [showDropdown, setShowDropdown] = useState(false); const dropdownRef = useRef(); - const urlOptions = useURLOptions(repo); + const urlOptions = useURLOptions({ repo, t }); useEffect(() => { const fetchUserRepoStatus = async () => { @@ -143,7 +156,7 @@ const Info: NextPage = ({ repo }) => { if (res.success) { setIsCollected(false); setCollectTotal((prev) => prev - 1); - Message.success('取消收藏'); + Message.success(t('favorite.cancel')); } } else { const res = await getFavoriteOptions(); @@ -152,22 +165,37 @@ const Info: NextPage = ({ repo }) => { res.data?.map((item: { fid: any; name: any }) => ({ key: item.fid, value: item.name, - })) || [{ key: '', value: '默认收藏夹' }] + })) || [{ key: '', value: t('favorite.default') }] ); setOpenModal(true); } } }; + const VoteButton = () => { + const voteClassName = classNames('', { + 'text-xl text-blue-500': isVoted, + 'text-lg': !isVoted, + }); + return ( +
+
+ +
+ {isVoted ? t('info.voted') : t('info.vote')} {voteTotal} +
+
+
+ ); + }; + const handleCopy = () => { - const text = `${ - repo.name - }:${repo.title.trim()}。\n\n更多详情尽在:https://hellogithub.com/repository/${ - repo.rid - }`; + const text = `${repo.name}:${repo.title.trim()}。\n\n${t( + 'info.copy_desc' + )}https://hellogithub.com/repository/${repo.rid}`; copy(text) - ? Message.success('项目信息已复制,快去分享吧!') - : Message.error('复制失败'); + ? Message.success(t('info.copy_success')) + : Message.error(t('info.copy_fail')); }; const handleSaveFavorite = async () => { @@ -177,9 +205,9 @@ const Info: NextPage = ({ repo }) => { setIsCollected(true); setCollectTotal(res.data.total); setOpenModal(false); - Message.success('收藏成功~'); + Message.success(t('favorite.success')); } else { - Message.error(res.message || '收藏失败'); + Message.error(res.message || t('favorite.fail')); } }; @@ -192,11 +220,6 @@ const Info: NextPage = ({ repo }) => { window.scrollTo({ top: offsetTop }); }; - const voteClassName = classNames('', { - 'text-xl text-blue-500': isVoted, - 'text-lg': !isVoted, - }); - const renderDropdownMenu = () => (
= ({ repo }) => {
- 已认领 + {t('info.claimed')}
@@ -319,7 +342,7 @@ const Info: NextPage = ({ repo }) => {
- +
@@ -349,16 +372,18 @@ const Info: NextPage = ({ repo }) => { style={{ height: 54, width: 320 }} opts={{ renderer: 'svg' }} /> -
{`过去 ${ - repo.star_history.x.length - } 天共收获 ${numFormat( - repo.star_history.increment, - 1 - )} 颗 Star ✨`}
+
+ {t('history.past_day_desc', { + days: repo.star_history.x.length, + })} + {t('history.total_desc', { + total: numFormat(repo.star_history.increment, 1), + })} +
) : (
-
暂无 Star 历史数据
+
{t('history.fail_desc')}
)} @@ -375,11 +400,15 @@ const Info: NextPage = ({ repo }) => { > {urlOptions.length > 0 ? (
-
访问
+
+ {t('info.vite')} +
) : ( -
访问
+
+ {t('info.vite')} +
)} @@ -393,7 +422,9 @@ const Info: NextPage = ({ repo }) => { > {urlOptions.length > 0 ? (
-
访问
+
+ {t('info.vite')} +
) : ( @@ -401,7 +432,9 @@ const Info: NextPage = ({ repo }) => { href={repo.url} onClick={() => handleClickLink('source', repo.rid)} > -
访问
+
+ {t('info.vite')} +
)} @@ -413,14 +446,7 @@ const Info: NextPage = ({ repo }) => { className='flex-1 origin-top scale-95 justify-center transition duration-200 ease-in-out hover:scale-100' onClick={handleVote} > -
-
- -
- {isVoted ? '已赞' : '点赞'} {voteTotal} -
-
-
+ @@ -432,7 +458,7 @@ const Info: NextPage = ({ repo }) => {
{repo.license_lid && (
- 开源 + {t('info.opensource')} @@ -448,7 +474,7 @@ const Info: NextPage = ({ repo }) => {
- 待认领 + {t('info.unclaim')}
)} @@ -458,15 +484,15 @@ const Info: NextPage = ({ repo }) => { className='hidden cursor-pointer items-center justify-center hover:text-blue-500 active:text-gray-400 md:flex' > - 讨论 + {t('info.discuss')}
-
+
- {isCollected ? numFormat(collectTotal, 1) : '收藏'} + {isCollected ? numFormat(collectTotal, 1) : t('info.collect')}
= ({ repo }) => { onClick={handleCopy} > - 分享 + {t('info.share')}
- + {/* 选择收藏夹的弹窗 */} = ({ repo }) => { maskClosable={false} title={ <> - 选择收藏夹 -

- 收藏的项目在「我的主页」可以找到 -

+ {t('favorite.title')} +

{t('favorite.desc')}

} onClose={() => setOpenModal(false)} @@ -512,7 +536,7 @@ const Info: NextPage = ({ repo }) => { variant='gradient' onClick={handleSaveFavorite} > - 确定 + {t('favorite.save')}
diff --git a/src/components/respository/MoreInfo.tsx b/src/components/respository/MoreInfo.tsx index ee61c3ae..7cc7ce57 100644 --- a/src/components/respository/MoreInfo.tsx +++ b/src/components/respository/MoreInfo.tsx @@ -1,27 +1,32 @@ -import { NextPage } from 'next'; import { useState } from 'react'; import { numFormat } from '@/utils/util'; import { RepositoryProps } from '@/types/repository'; -const MoreInfo: NextPage = ({ repo }) => { +const MoreInfo = ({ repo, t }: RepositoryProps) => { const [isShowMore, setIsShowMore] = useState(false); const infoList = [ - { title: '星数', value: repo.stars_str }, - { title: '中文', value: repo.has_chinese ? '是' : '否' }, - { title: '主语言', value: repo.primary_lang }, - { title: '活跃', value: repo.is_active ? '是' : '否' }, + { title: t('more.star'), value: repo.stars_str }, { - title: '贡献者', - value: repo.contributors ? numFormat(repo.contributors) : '无', + title: t('more.chinese'), + value: repo.has_chinese ? t('more.yes') : t('more.no'), + }, + { title: t('more.language'), value: repo.primary_lang }, + { + title: t('more.activity'), + value: repo.is_active ? t('more.yes') : t('more.no'), + }, + { + title: t('more.contributors'), + value: repo.contributors ? numFormat(repo.contributors) : t('more.null'), }, { title: 'Issues', value: numFormat(repo.open_issues) }, - { title: '组织', value: repo.is_org ? '是' : '否' }, - { title: '最新版本', value: repo.release_tag || '无' }, + { title: t('more.org'), value: repo.is_org ? t('more.yes') : t('more.no') }, + { title: t('more.version'), value: repo.release_tag || t('more.null') }, { title: 'Forks', value: numFormat(repo.forks) }, - { title: '协议', value: repo.license || '无' }, + { title: t('more.license'), value: repo.license || t('more.null') }, ]; return ( @@ -46,7 +51,7 @@ const MoreInfo: NextPage = ({ repo }) => { className='absolute right-3 bottom-0 translate-y-full cursor-pointer rounded-b-lg bg-gray-100 px-4 py-1 text-xs text-gray-400 hover:bg-gray-200 active:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 dark:active:bg-gray-700 lg:right-9' onClick={() => setIsShowMore(!isShowMore)} > - {isShowMore ? '收起' : '更多'} + {isShowMore ? t('more.collapse') : t('more.expand')} ); diff --git a/src/components/respository/Score.tsx b/src/components/respository/Score.tsx index 48f0cb26..f3223412 100644 --- a/src/components/respository/Score.tsx +++ b/src/components/respository/Score.tsx @@ -1,10 +1,8 @@ -import { NextPage } from 'next'; - import Rating from './Rating'; import { RepositoryProps } from '@/types/repository'; -const Score: NextPage = ({ repo }) => { +const Score = ({ t, repo }: RepositoryProps) => { const jumpComment = () => { const { offsetTop } = document.querySelector('#comment') as HTMLElement; // 根据 offsetTop 滚动到指定位置 @@ -17,7 +15,7 @@ const Score: NextPage = ({ repo }) => {
- HelloGitHub 评分 + {t('info.socre_desc')}
{repo.score ? ( @@ -33,7 +31,7 @@ const Score: NextPage = ({ repo }) => { className='h-1/2 cursor-pointer py-1 text-xs text-blue-500' onClick={jumpComment} > - {repo.comment_total} 人评分 + {t('info.socre_user_desc', { count: repo.comment_total })}
diff --git a/src/components/respository/Tabs.tsx b/src/components/respository/Tabs.tsx index cb0386d6..b77ce08a 100644 --- a/src/components/respository/Tabs.tsx +++ b/src/components/respository/Tabs.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames'; -import { NextPage } from 'next'; import Link from 'next/link'; import { useState } from 'react'; @@ -8,7 +7,7 @@ import { MDRender } from '../mdRender/MDRender'; import { RepositoryProps } from '@/types/repository'; -const Tabs: NextPage = ({ repo }) => { +const Tabs = ({ repo, t }: RepositoryProps) => { const [selectTab, setSelectTab] = useState('summary'); const tabClassName = (tabName: string) => @@ -37,11 +36,13 @@ const Tabs: NextPage = ({ repo }) => {
{repo.volume_name && ( <> -
收录于:
+
+ {t('content.volume_label')} +
- 第 {repo.volume_name} 期 + {t('content.volume', { volume: repo.volume_name })}
@@ -49,7 +50,9 @@ const Tabs: NextPage = ({ repo }) => { )} {repo.tags.length > 0 && ( <> -
标签:
+
+ {t('content.tag_label')} +
{repo.tags.map((item) => ( @@ -80,14 +83,14 @@ const Tabs: NextPage = ({ repo }) => { className={tabClassName('summary')} onClick={() => setSelectTab('summary')} > - 介绍 + {t('content.desc_tab')} {repo.code && ( setSelectTab('code')} > - 代码 + {t('content.code_tab')} )} diff --git a/src/components/search/SearchInput.tsx b/src/components/search/SearchInput.tsx index ad5e3786..4854d0fd 100644 --- a/src/components/search/SearchInput.tsx +++ b/src/components/search/SearchInput.tsx @@ -13,11 +13,14 @@ type DropdownList = { name: string; }; +type SearchInputProps = { + t: (key: string) => string; +}; + /** * 顶部搜索输入框组件 - * @returns {JSX.Element} */ -export default function SearchInput(): JSX.Element { +const SearchInput = ({ t }: SearchInputProps) => { const router = useRouter(); const initialQuery = router.query?.q as string; const [query, setQuery] = useState(initialQuery || ''); @@ -50,11 +53,7 @@ export default function SearchInput(): JSX.Element { const jumpToResultPage = (query: string) => { setShowDropdown(false); - if (query) { - router.push(`/search/result?q=${encodeURIComponent(query)}`); - } else { - router.push(`/`); - } + router.push(query ? `/search/result?q=${encodeURIComponent(query)}` : `/`); }; const handleKeyDown = (e: React.KeyboardEvent) => { @@ -105,7 +104,7 @@ export default function SearchInput(): JSX.Element {
); -} +}; + +export default SearchInput; diff --git a/src/components/side/Ad.tsx b/src/components/side/Ad.tsx index 4e49edd5..4577e5fe 100644 --- a/src/components/side/Ad.tsx +++ b/src/components/side/Ad.tsx @@ -9,9 +9,11 @@ import { AdvertItem } from '@/types/home'; interface Props { data: AdvertItem; className?: string; + t: (key: string) => string; } -interface RewardAdContentProps { +interface AdContentProps { + t: (key: string) => string; data: AdvertItem; } @@ -32,7 +34,7 @@ const ImageAdContent = ({ data, handleClose }: ImageAdContentProps) => ( ); -export default function Ad({ data, className }: Props) { +export default function Ad({ data, className, t }: Props) { const [visible, setVisible] = useState(true); const handleClose: MouseEventHandler = (e) => { @@ -55,50 +57,47 @@ export default function Ad({ data, className }: Props) { ); - const AdTargetInfo = ({ data }: { data: AdvertItem }) => ( + const AdTargetInfo = ({ data, t }: AdContentProps) => (
- {data.year ? '距离下个目标还差' : '距离目标还差'} + {data.year ? t('advert.next2') : t('advert.next')} {100 - data.percent}%
); - const AdServerInfo = ({ data }: { data: AdvertItem }) => ( + const AdServerInfo = ({ data, t }: AdContentProps) => (
- 服务器还剩{data.day}天 + {t('advert.desc')} + {data.day} + {t('advert.day')}
- +{data.year}年 + +{data.year} + {t('advert.year')}
); - const RewardAdDetail = ({ data }: { data: AdvertItem }) => ( - <> -
- -
-
- -
- - ); - - const RewardAdContent = ({ data }: RewardAdContentProps) => ( + const RewardAdContent = ({ data, t }: AdContentProps) => (
ad
- 微信扫码赞助本站 + {t('advert.desc2')}
- +
+ +
+
+ +
@@ -115,7 +114,7 @@ export default function Ad({ data, className }: Props) { rel='noreferrer' > {data.is_reward ? ( - + ) : ( )} diff --git a/src/components/side/Recommend.tsx b/src/components/side/Recommend.tsx index 7f9eaf20..7a0d2ed0 100644 --- a/src/components/side/Recommend.tsx +++ b/src/components/side/Recommend.tsx @@ -8,7 +8,7 @@ import { getRecommend } from '@/services/home'; import { RecommendSkeleton } from '../loading/skeleton'; -import { RecommendItem } from '@/types/home'; +import { RecommendItem, SideProps } from '@/types/home'; const RecommendList = ({ repositories }: { repositories: RecommendItem[] }) => (
@@ -36,7 +36,7 @@ const RecommendList = ({ repositories }: { repositories: RecommendItem[] }) => ( + /> {item.primary_lang} @@ -52,7 +52,7 @@ const RecommendList = ({ repositories }: { repositories: RecommendItem[] }) => ( const isLicenseDetailPath = (pathname: string) => pathname === '/license/[lid]'; -export default function Recommend() { +export default function Recommend({ t }: SideProps) { const router = useRouter(); const { pathname, query } = router; @@ -78,14 +78,14 @@ export default function Recommend() {
- 推荐项目 + {t('recommend.title')}
- 换一换 + {t('recommend.change')}
{repositories.length === 0 ? ( @@ -96,7 +96,7 @@ export default function Recommend() {
{ShowIsLicenseDetail && (
)} diff --git a/src/components/side/Side.tsx b/src/components/side/Side.tsx index f136638e..e209a0f9 100644 --- a/src/components/side/Side.tsx +++ b/src/components/side/Side.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'next-i18next'; import { useEffect, useRef, useState } from 'react'; import useSWRImmutable from 'swr/immutable'; @@ -13,11 +14,8 @@ import UserStatus from './UserStatus'; import { AdvertItems } from '@/types/home'; -interface Props { - index: boolean; -} - -export const Side = ({ index }: Props) => { +export const Side = ({ isHome }: { isHome: boolean }) => { + const { t } = useTranslation('common'); const [displayAdOnly, setDisplayAdOnly] = useState(false); const containerRef = useRef(null); const { data, isValidating } = useSWRImmutable( @@ -46,18 +44,16 @@ export const Side = ({ index }: Props) => {
- +
- {!isValidating ? : <>} - {index ? : } + {!isValidating && } + {isHome ? : }
- {index ?
: <>} + {isHome &&
}
- {adverts ? ( - - ) : ( - <> + {adverts && ( + )} ); diff --git a/src/components/side/SideAd.tsx b/src/components/side/SideAd.tsx index 64b7eb08..f91c97c7 100644 --- a/src/components/side/SideAd.tsx +++ b/src/components/side/SideAd.tsx @@ -7,26 +7,27 @@ import { AdvertItem } from '@/types/home'; interface Props { data: AdvertItem[]; displayAdOnly?: boolean; + t: (key: string) => string; } -export const SideAd: NextPage = ({ data }) => { +export const SideAd: NextPage = ({ data, t }) => { return (
{data.map((item: AdvertItem) => ( - + ))}
); }; -export const SideFixAd: NextPage = ({ data, displayAdOnly }) => { +export const SideFixAd: NextPage = ({ data, displayAdOnly, t }) => { return ( ); diff --git a/src/components/side/SideLoginButton.tsx b/src/components/side/SideLoginButton.tsx index f44706f0..093c6443 100644 --- a/src/components/side/SideLoginButton.tsx +++ b/src/components/side/SideLoginButton.tsx @@ -1,6 +1,10 @@ import { LoginModal } from '../user/Login'; -const SideLoginButton = () => { +interface Props { + text: string; +} + +const SideLoginButton = ({ text }: Props) => { return (
@@ -8,7 +12,7 @@ const SideLoginButton = () => { type='button' className='button box-border rounded-md border-2 border-gray-400 px-3 py-2 text-gray-500 hover:border-blue-500 hover:text-blue-500 dark:text-gray-400' > - 立即登录 + {text}
diff --git a/src/components/side/Stats.tsx b/src/components/side/Stats.tsx index 24140501..92cfec38 100644 --- a/src/components/side/Stats.tsx +++ b/src/components/side/Stats.tsx @@ -6,9 +6,9 @@ import { numFormat } from '@/utils/util'; import { StatsSkeleton } from '../loading/skeleton'; -import { Stats } from '@/types/home'; +import { SideProps, Stats } from '@/types/home'; -export default function SiteStats() { +export default function SiteStats({ t }: SideProps) { const { data: stats } = useSWRImmutable(makeUrl('/stats/'), fetcher); return ( @@ -17,7 +17,7 @@ export default function SiteStats() {
- 用户总数 + {t('site_stats.user')}
{numFormat(stats?.user_total, 1, 10000)} @@ -25,7 +25,7 @@ export default function SiteStats() {
- 开源项目 + {t('site_stats.repo')}
{numFormat(stats?.repo_total, 1, 10000)} @@ -36,11 +36,9 @@ export default function SiteStats() { )} -
关于本站
+
{t('site_stats.title')}
- HelloGitHub 是一个发现和分享有趣、入门级开源项目的平台。 - 希望大家能够在这里找到编程的快乐、 轻松搞定问题的技术方案、 - 大呼过瘾的开源神器, 顺其自然地开启开源之旅。 + {t('site_stats.desc')}
); diff --git a/src/components/side/TagList.tsx b/src/components/side/TagList.tsx index 74b17b6d..43ab21b9 100644 --- a/src/components/side/TagList.tsx +++ b/src/components/side/TagList.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useTranslation } from 'next-i18next'; import { useCallback, useEffect, useState } from 'react'; import { AiOutlineAppstore, AiOutlineSetting } from 'react-icons/ai'; @@ -13,9 +14,14 @@ import { TagListSkeleton } from '../loading/skeleton'; import { Tag } from '@/types/tag'; -const defaultTag: Tag = { name: '综合', tid: '', icon_name: 'find' }; - export default function TagList() { + const { t } = useTranslation('home'); + const defaultTag: Tag = { + name: t('tag_side.all_tags_label'), + tid: '', + icon_name: 'find', + }; + const router = useRouter(); const { tid = '', sort_by = 'featured' } = router.query; const [tags, setTags] = useState([]); @@ -53,7 +59,7 @@ export default function TagList() {
-
热门标签
+
{t('tag_side.title')}
@@ -71,10 +77,10 @@ export default function TagList() { ))}
- +
-
管理标签
+
{t('tag_side.manage')}
diff --git a/src/components/side/UserStatus.tsx b/src/components/side/UserStatus.tsx index dc6188b0..71219540 100644 --- a/src/components/side/UserStatus.tsx +++ b/src/components/side/UserStatus.tsx @@ -1,131 +1,129 @@ import Link from 'next/link'; +import { useRouter } from 'next/router'; import { useMemo } from 'react'; -import { AiOutlineQuestionCircle } from 'react-icons/ai'; +import { AiOutlineBell, AiOutlineQuestionCircle } from 'react-icons/ai'; import { useLoginContext } from '@/hooks/useLoginContext'; import { DEFAULT_AVATAR } from '@/utils/constants'; import SideLoginButton from './SideLoginButton'; -import Loading from '../loading/Loading'; -import ThemeSwitch from '../ThemeSwitch'; -export default function UserStatus() { - const { userInfo, isValidating, isLogin, logout } = useLoginContext(); +import { SideProps } from '@/types/home'; + +export default function UserStatus({ t }: SideProps) { + const router = useRouter(); + const { userInfo, isLogin, logout } = useLoginContext(); const levelPercent = useMemo(() => { if ( typeof userInfo?.contribute === 'number' && typeof userInfo?.next_level_score === 'number' ) { - return (userInfo?.contribute / userInfo?.next_level_score) * 100; + return (userInfo.contribute / userInfo?.next_level_score) * 100; } // next_level_score 为 null 时则达到了最大等级 - if (!userInfo?.next_level_score) { - return 100; - } - return 0; + return !userInfo?.next_level_score ? 100 : 0; }, [userInfo]); + if (!isLogin || !userInfo?.success) { + return ; + } + return ( - <> - {!isValidating || isLogin ? ( - <> - {isLogin && userInfo?.success ? ( - <> -
-
- - -
- side_avatar -
-
- -
-
-
- {userInfo?.nickname} -
-
-
- -
-
-
- Lv.{userInfo?.level} -
-
-
- {/* 等级展示 */} -
-
- - - {userInfo.contribute} - / - {userInfo.next_level_score || 'Max'} - -
-
-
-
-
-
-
- - -
- 我的主页 -
-
- - {userInfo.permission?.code == 'super' ? ( - -
- 管理后台 -
-
- ) : ( -
- 退出登录 -
- )} +
+
+ + +
+ side_avatar +
+
+ +
+
+
+ {userInfo.nickname} +
+
+
+
{ + router.push('/notification'); + }} + > + + + {userInfo?.unread.total > 0 ? ( + + ) : ( + + )} +
- - ) : ( - <> - - - )} - - ) : ( - - )} - +
+
+
+ Lv.{userInfo.level} +
+
+
+ {/* 等级展示 */} +
+
+ + + {userInfo.contribute} + / + {userInfo.next_level_score || 'Max'} + +
+
+
+
+
+
+ + + {t('user_side.profile')} + + + {userInfo.permission?.code == 'super' ? ( + +
+ {t('user_side.admin')} +
+
+ ) : ( +
+ {t('user_side.logout')} +
+ )} +
+
); } diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 6fc7920b..5785c03c 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -6,6 +6,9 @@ import Document, { NextScript, } from 'next/document'; import Script from 'next/script'; + +import i18nextConfig from '../../next-i18next.config'; + class MyDocument extends Document { static async getInitialProps(ctx: DocumentContext) { const initialProps = await Document.getInitialProps(ctx); @@ -13,9 +16,13 @@ class MyDocument extends Document { } render() { + const currentLocale = + this.props.__NEXT_DATA__.locale ?? i18nextConfig.i18n.defaultLocale; return ( - - + + + +
diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 473e4927..e12c28b6 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,6 @@ import { NextPage } from 'next'; import { useRouter } from 'next/router'; +import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useLoginContext } from '@/hooks/useLoginContext'; @@ -21,13 +22,12 @@ const Index: NextPage = () => { const { repositories, isValidating, hasMore, size, sentryRef } = useRepositories(sort_by as string, tid as string); + const { t, i18n } = useTranslation('home'); const handleItemBottom = () => { if (!isValidating && !hasMore) { return ( ); } @@ -36,10 +36,10 @@ const Index: NextPage = () => { return ( <> - - + +
- +
{ export async function getStaticProps({ locale }: { locale: string }) { return { props: { - ...(await serverSideTranslations(locale, ['common'])), + ...(await serverSideTranslations(locale, ['common', 'home'])), }, }; } diff --git a/src/pages/repository/[rid]/index.tsx b/src/pages/repository/[rid]/index.tsx index f16d873d..8dc40e68 100644 --- a/src/pages/repository/[rid]/index.tsx +++ b/src/pages/repository/[rid]/index.tsx @@ -5,35 +5,41 @@ import CommentContainer from '@/components/respository/CommentContainer'; import Info from '@/components/respository/Info'; import Tabs from '@/components/respository/Tabs'; import Seo from '@/components/Seo'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { getDetail } from '@/services/repository'; import { Repository } from '@/types/repository'; +import { useTranslation } from 'next-i18next'; interface Props { repo: Repository; } const RepositoryPage: NextPage = ({ repo }) => { + const { t, i18n } = useTranslation('repository'); + return ( <> -
- - + +
@@ -43,6 +49,7 @@ const RepositoryPage: NextPage = ({ repo }) => { export const getServerSideProps: GetServerSideProps = async ({ req, query, + locale, }) => { let ip; if (req.headers['x-forwarded-for']) { @@ -59,6 +66,10 @@ export const getServerSideProps: GetServerSideProps = async ({ if (data.success) { return { props: { + ...(await serverSideTranslations(locale as string, [ + 'common', + 'repository', + ])), repo: data.data, }, }; diff --git a/src/types/home.tsx b/src/types/home.tsx index 73102cd9..a5e87bf6 100644 --- a/src/types/home.tsx +++ b/src/types/home.tsx @@ -1,5 +1,9 @@ import { TagType } from './tag'; +export interface SideProps { + t: (key: string) => string; +} + export interface HomeItems { success: boolean; page: number; @@ -8,10 +12,6 @@ export interface HomeItems { has_more: boolean; } -export interface ItemProps { - item: HomeItem; -} - export interface HomeItem { item_id: string; author: string; diff --git a/src/types/repository.tsx b/src/types/repository.tsx index 38d00145..7b5d093f 100644 --- a/src/types/repository.tsx +++ b/src/types/repository.tsx @@ -1,13 +1,9 @@ -import { HomeItem } from './home'; import { TagType } from './tag'; import { UserType } from './user'; -export interface RepositoryItems { - repositories: HomeItem[]; -} - export interface RepositoryProps { repo: Repository; + t: (key: string, total?: any) => string; } export interface StarHistory { diff --git a/src/types/user.ts b/src/types/user.ts index 604f7ccd..fad98105 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,10 +1,5 @@ import { RepoType } from './repository'; -export interface UserAvaterProps { - avatar: string; - uid: string; -} - export interface User { uid: string; token: string; diff --git a/src/utils/day.ts b/src/utils/day.ts index f8df8b49..0232a416 100644 --- a/src/utils/day.ts +++ b/src/utils/day.ts @@ -6,13 +6,18 @@ declare module 'dayjs' { } } +const locales: { [key: string]: string } = { + en: 'en', + zh: 'zh-cn', +}; + /* eslint-disable */ const relativeTime = require('dayjs/plugin/relativeTime'); /* eslint-enable */ dayjs.extend(relativeTime); -dayjs.locale('zh-cn'); -export const fromNow = (datetime: string): string => { +export const fromNow = (datetime: string, locale = 'zh'): string => { + dayjs.locale(locales[locale] || 'en'); return dayjs(datetime).fromNow(); };