From aa4f4258aa29a938e588237286a3fa204982f166 Mon Sep 17 00:00:00 2001 From: codytseng Date: Tue, 19 Nov 2024 18:25:56 +0800 Subject: [PATCH] chore: i18n --- package-lock.json | 86 +++++++++++++++++++ package.json | 3 + .../AccountButton/ProfileButton.tsx | 6 +- .../src/components/FollowButton/index.tsx | 10 ++- .../src/components/LoginDialog/index.tsx | 6 +- .../components/NoteCard/RepostNoteCard.tsx | 23 +++-- .../src/components/NoteList/index.tsx | 6 +- .../src/components/NoteStats/LikeButton.tsx | 4 +- .../NoteStats/NoteOptions/index.tsx | 6 +- .../src/components/NoteStats/ReplyButton.tsx | 3 + .../src/components/NoteStats/RepostButton.tsx | 8 +- .../components/ParentNotePreview/index.tsx | 4 +- .../src/components/PostButton/index.tsx | 6 +- .../src/components/PostDialog/Metions.tsx | 4 +- .../src/components/PostDialog/index.tsx | 18 ++-- .../src/components/RefreshButton/index.tsx | 6 +- .../components/RelaySettingsPopover/index.tsx | 6 +- .../src/components/ReplyNote/index.tsx | 4 +- .../src/components/ReplyNoteList/index.tsx | 4 +- src/renderer/src/components/Sidebar/index.tsx | 4 +- .../src/components/ThemeToggle/index.tsx | 8 +- src/renderer/src/i18n/en.ts | 49 +++++++++++ src/renderer/src/i18n/index.ts | 32 +++++++ src/renderer/src/i18n/zh.ts | 49 +++++++++++ .../src/lib/{timestamp.ts => timestamp.tsx} | 12 +-- src/renderer/src/main.tsx | 1 + .../secondary/FollowingListPage/index.tsx | 8 +- .../src/pages/secondary/HomePage/index.tsx | 4 +- .../src/pages/secondary/NotePage/index.tsx | 4 +- .../src/pages/secondary/ProfilePage/index.tsx | 11 +-- 30 files changed, 336 insertions(+), 59 deletions(-) create mode 100644 src/renderer/src/i18n/en.ts create mode 100644 src/renderer/src/i18n/index.ts create mode 100644 src/renderer/src/i18n/zh.ts rename src/renderer/src/lib/{timestamp.ts => timestamp.tsx} (59%) diff --git a/package-lock.json b/package-lock.json index 06a8f47..d40d62f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,11 +32,14 @@ "dataloader": "^2.2.2", "dayjs": "^1.11.13", "framer-motion": "^11.11.17", + "i18next": "^23.16.5", + "i18next-browser-languagedetector": "^8.0.0", "lru-cache": "^11.0.1", "lucide-react": "^0.453.0", "nostr-tools": "^2.9.1", "path-to-regexp": "^8.2.0", "qrcode.react": "^4.1.0", + "react-i18next": "^15.1.1", "react-resizable-panels": "^2.1.5", "react-string-replace": "^1.1.1", "tailwind-merge": "^2.5.4", @@ -340,6 +343,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -6732,6 +6746,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -6776,6 +6798,36 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "23.16.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.5.tgz", + "integrity": "sha512-KTlhE3EP9x6pPTAW7dy0WKIhoCpfOGhRQlO+jttQLgzVaoOjWwBWramu7Pp0i+8wDNduuzXfe3kkVbzrKyrbTA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -8587,6 +8639,27 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.1.tgz", + "integrity": "sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8781,6 +8854,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -10082,6 +10160,14 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 3ea7325..3af7547 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,14 @@ "dataloader": "^2.2.2", "dayjs": "^1.11.13", "framer-motion": "^11.11.17", + "i18next": "^23.16.5", + "i18next-browser-languagedetector": "^8.0.0", "lru-cache": "^11.0.1", "lucide-react": "^0.453.0", "nostr-tools": "^2.9.1", "path-to-regexp": "^8.2.0", "qrcode.react": "^4.1.0", + "react-i18next": "^15.1.1", "react-resizable-panels": "^2.1.5", "react-string-replace": "^1.1.1", "tailwind-merge": "^2.5.4", diff --git a/src/renderer/src/components/AccountButton/ProfileButton.tsx b/src/renderer/src/components/AccountButton/ProfileButton.tsx index ba43c97..c2b0bb8 100644 --- a/src/renderer/src/components/AccountButton/ProfileButton.tsx +++ b/src/renderer/src/components/AccountButton/ProfileButton.tsx @@ -11,6 +11,7 @@ import { toProfile } from '@renderer/lib/link' import { generateImageByPubkey } from '@renderer/lib/pubkey' import { useSecondaryPage } from '@renderer/PageManager' import { useNostr } from '@renderer/providers/NostrProvider' +import { useTranslation } from 'react-i18next' export default function ProfileButton({ pubkey, @@ -19,6 +20,7 @@ export default function ProfileButton({ pubkey: string variant?: 'titlebar' | 'sidebar' }) { + const { t } = useTranslation() const { logout } = useNostr() const { profile } = useFetchProfile(pubkey) const { push } = useSecondaryPage() @@ -61,9 +63,9 @@ export default function ProfileButton({ {triggerComponent} - push(toProfile(pubkey))}>Profile + push(toProfile(pubkey))}>{t('Profile')} - Logout + {t('Logout')} diff --git a/src/renderer/src/components/FollowButton/index.tsx b/src/renderer/src/components/FollowButton/index.tsx index a367f67..aaaad5f 100644 --- a/src/renderer/src/components/FollowButton/index.tsx +++ b/src/renderer/src/components/FollowButton/index.tsx @@ -4,8 +4,10 @@ import { useFollowList } from '@renderer/providers/FollowListProvider' import { useNostr } from '@renderer/providers/NostrProvider' import { Loader } from 'lucide-react' import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' export default function FollowButton({ pubkey }: { pubkey: string }) { + const { t } = useTranslation() const { toast } = useToast() const { pubkey: accountPubkey, checkLogin } = useNostr() const { followListEvent, followings, isReady, follow, unfollow } = useFollowList() @@ -24,7 +26,7 @@ export default function FollowButton({ pubkey }: { pubkey: string }) { await follow(pubkey) } catch (error) { toast({ - title: 'Follow failed', + title: t('Follow failed'), description: (error as Error).message, variant: 'destructive' }) @@ -44,7 +46,7 @@ export default function FollowButton({ pubkey }: { pubkey: string }) { await unfollow(pubkey) } catch (error) { toast({ - title: 'Unfollow failed', + title: t('Unfollow failed'), description: (error as Error).message, variant: 'destructive' }) @@ -61,11 +63,11 @@ export default function FollowButton({ pubkey }: { pubkey: string }) { onClick={handleUnfollow} disabled={updating} > - {updating ? : 'Unfollow'} + {updating ? : t('Unfollow')} ) : ( ) } diff --git a/src/renderer/src/components/LoginDialog/index.tsx b/src/renderer/src/components/LoginDialog/index.tsx index 09b7cff..918dd5b 100644 --- a/src/renderer/src/components/LoginDialog/index.tsx +++ b/src/renderer/src/components/LoginDialog/index.tsx @@ -9,6 +9,7 @@ import { import { Input } from '@renderer/components/ui/input' import { useNostr } from '@renderer/providers/NostrProvider' import { Dispatch, useState } from 'react' +import { useTranslation } from 'react-i18next' export default function LoginDialog({ open, @@ -17,6 +18,7 @@ export default function LoginDialog({ open: boolean setOpen: Dispatch }) { + const { t } = useTranslation() const { login, canLogin } = useNostr() const [nsec, setNsec] = useState('') const [errMsg, setErrMsg] = useState(null) @@ -40,7 +42,7 @@ export default function LoginDialog({ - Sign in + {!canLogin && 'Encryption is not available in your device.'} @@ -57,7 +59,7 @@ export default function LoginDialog({ {errMsg &&
{errMsg}
}
diff --git a/src/renderer/src/components/NoteCard/RepostNoteCard.tsx b/src/renderer/src/components/NoteCard/RepostNoteCard.tsx index f85a839..d78ffa4 100644 --- a/src/renderer/src/components/NoteCard/RepostNoteCard.tsx +++ b/src/renderer/src/components/NoteCard/RepostNoteCard.tsx @@ -3,18 +3,25 @@ import { Repeat2 } from 'lucide-react' import { Event, kinds, verifyEvent } from 'nostr-tools' import Username from '../Username' import ShortTextNoteCard from './ShortTextNoteCard' +import { useTranslation } from 'react-i18next' +import { useMemo } from 'react' export default function RepostNoteCard({ event, className }: { event: Event; className?: string }) { - const targetEvent = event.content ? (JSON.parse(event.content) as Event) : null - try { - if (!targetEvent || !verifyEvent(targetEvent) || targetEvent.kind !== kinds.ShortTextNote) { + const { t } = useTranslation() + const targetEvent = useMemo(() => { + const targetEvent = event.content ? (JSON.parse(event.content) as Event) : null + try { + if (!targetEvent || !verifyEvent(targetEvent) || targetEvent.kind !== kinds.ShortTextNote) { + return null + } + client.addEventToCache(targetEvent) + } catch { return null } - } catch { - return null - } - client.addEventToCache(targetEvent) + return targetEvent + }, [event]) + if (!targetEvent) return null return (
@@ -25,7 +32,7 @@ export default function RepostNoteCard({ event, className }: { event: Event; cla className="font-semibold truncate" skeletonClassName="h-3" /> -
reposted
+
{t('reposted')}
diff --git a/src/renderer/src/components/NoteList/index.tsx b/src/renderer/src/components/NoteList/index.tsx index 994bdc5..5160522 100644 --- a/src/renderer/src/components/NoteList/index.tsx +++ b/src/renderer/src/components/NoteList/index.tsx @@ -7,6 +7,7 @@ import dayjs from 'dayjs' import { Event, Filter, kinds } from 'nostr-tools' import { useEffect, useMemo, useRef, useState } from 'react' import NoteCard from '../NoteCard' +import { useTranslation } from 'react-i18next' export default function NoteList({ relayUrls, @@ -17,6 +18,7 @@ export default function NoteList({ filter?: Filter className?: string }) { + const { t } = useTranslation() const { isReady, singEvent } = useNostr() const [events, setEvents] = useState([]) const [newEvents, setNewEvents] = useState([]) @@ -127,7 +129,7 @@ export default function NoteList({ {newEvents.length > 0 && (
)} @@ -137,7 +139,7 @@ export default function NoteList({ ))}
- {hasMore ?
loading...
: 'no more notes'} + {hasMore ?
{t('loading...')}
: t('no more notes')}
) diff --git a/src/renderer/src/components/NoteStats/LikeButton.tsx b/src/renderer/src/components/NoteStats/LikeButton.tsx index 14cb434..401b073 100644 --- a/src/renderer/src/components/NoteStats/LikeButton.tsx +++ b/src/renderer/src/components/NoteStats/LikeButton.tsx @@ -7,6 +7,7 @@ import { Heart, Loader } from 'lucide-react' import { Event } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { formatCount } from './utils' +import { useTranslation } from 'react-i18next' export default function LikeButton({ event, @@ -17,6 +18,7 @@ export default function LikeButton({ variant?: 'normal' | 'reply' canFetch?: boolean }) { + const { t } = useTranslation() const { publish, checkLogin } = useNostr() const { noteStatsMap, fetchNoteLikedStatus, fetchNoteLikeCount, markNoteAsLiked } = useNoteStats() const [liking, setLiking] = useState(false) @@ -74,7 +76,7 @@ export default function LikeButton({ )} onClick={like} disabled={!canLike} - title="like" + title={t('Like')} > {liking ? ( diff --git a/src/renderer/src/components/NoteStats/NoteOptions/index.tsx b/src/renderer/src/components/NoteStats/NoteOptions/index.tsx index 6fe84dd..7b40080 100644 --- a/src/renderer/src/components/NoteStats/NoteOptions/index.tsx +++ b/src/renderer/src/components/NoteStats/NoteOptions/index.tsx @@ -9,8 +9,10 @@ import { Code, Copy, Ellipsis } from 'lucide-react' import { Event } from 'nostr-tools' import { useState } from 'react' import RawEventDialog from './RawEventDialog' +import { useTranslation } from 'react-i18next' export default function NoteOptions({ event }: { event: Event }) { + const { t } = useTranslation() const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false) return ( @@ -30,7 +32,7 @@ export default function NoteOptions({ event }: { event: Event }) { }} > - copy embedded code + {t('copy embedded code')} { @@ -39,7 +41,7 @@ export default function NoteOptions({ event }: { event: Event }) { }} > - raw event + {t('raw event')} diff --git a/src/renderer/src/components/NoteStats/ReplyButton.tsx b/src/renderer/src/components/NoteStats/ReplyButton.tsx index 90f591e..9bfb457 100644 --- a/src/renderer/src/components/NoteStats/ReplyButton.tsx +++ b/src/renderer/src/components/NoteStats/ReplyButton.tsx @@ -5,8 +5,10 @@ import { Event } from 'nostr-tools' import { useMemo, useState } from 'react' import PostDialog from '../PostDialog' import { formatCount } from './utils' +import { useTranslation } from 'react-i18next' export default function ReplyButton({ event }: { event: Event }) { + const { t } = useTranslation() const { noteStatsMap } = useNoteStats() const { pubkey } = useNostr() const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id]) @@ -21,6 +23,7 @@ export default function ReplyButton({ event }: { event: Event }) { e.stopPropagation() setOpen(true) }} + title={t('Reply')} >
{formatCount(replyCount)}
diff --git a/src/renderer/src/components/NoteStats/RepostButton.tsx b/src/renderer/src/components/NoteStats/RepostButton.tsx index 7a0d686..c81a01e 100644 --- a/src/renderer/src/components/NoteStats/RepostButton.tsx +++ b/src/renderer/src/components/NoteStats/RepostButton.tsx @@ -15,6 +15,7 @@ import { Event } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import PostDialog from '../PostDialog' import { formatCount } from './utils' +import { useTranslation } from 'react-i18next' export default function RepostButton({ event, @@ -23,6 +24,7 @@ export default function RepostButton({ event: Event canFetch?: boolean }) { + const { t } = useTranslation() const { publish, checkLogin } = useNostr() const { noteStatsMap, fetchNoteRepostCount, fetchNoteRepostedStatus, markNoteAsReposted } = useNoteStats() @@ -84,7 +86,7 @@ export default function RepostButton({ )} onClick={(e) => e.stopPropagation()} disabled={!canRepost} - title="repost" + title={t('Repost')} > {reposting ? : }
{formatCount(repostCount)}
@@ -97,7 +99,7 @@ export default function RepostButton({ }} > - Repost + {t('Repost')} { @@ -105,7 +107,7 @@ export default function RepostButton({ setIsPostDialogOpen(true) }} > - Quote + {t('Quote')} diff --git a/src/renderer/src/components/ParentNotePreview/index.tsx b/src/renderer/src/components/ParentNotePreview/index.tsx index f0307f0..c593f22 100644 --- a/src/renderer/src/components/ParentNotePreview/index.tsx +++ b/src/renderer/src/components/ParentNotePreview/index.tsx @@ -1,6 +1,7 @@ import { Event } from 'nostr-tools' import UserAvatar from '../UserAvatar' import { cn } from '@renderer/lib/utils' +import { useTranslation } from 'react-i18next' export default function ParentNotePreview({ event, @@ -11,6 +12,7 @@ export default function ParentNotePreview({ className?: string onClick?: React.MouseEventHandler | undefined }) { + const { t } = useTranslation() return (
-
reply to
+
{t('reply to')}
{event.content}
diff --git a/src/renderer/src/components/PostButton/index.tsx b/src/renderer/src/components/PostButton/index.tsx index b3d31bb..cd79d4d 100644 --- a/src/renderer/src/components/PostButton/index.tsx +++ b/src/renderer/src/components/PostButton/index.tsx @@ -2,8 +2,10 @@ import PostDialog from '@renderer/components/PostDialog' import { Button } from '@renderer/components/ui/button' import { PencilLine } from 'lucide-react' import { useState } from 'react' +import { useTranslation } from 'react-i18next' export default function PostButton({ variant = 'titlebar' }: { variant?: 'titlebar' | 'sidebar' }) { + const { t } = useTranslation() const [open, setOpen] = useState(false) return ( @@ -11,14 +13,14 @@ export default function PostButton({ variant = 'titlebar' }: { variant?: 'titleb diff --git a/src/renderer/src/components/PostDialog/Metions.tsx b/src/renderer/src/components/PostDialog/Metions.tsx index 5299307..9af683c 100644 --- a/src/renderer/src/components/PostDialog/Metions.tsx +++ b/src/renderer/src/components/PostDialog/Metions.tsx @@ -6,6 +6,7 @@ import { Event } from 'nostr-tools' import { useEffect, useState } from 'react' import UserAvatar from '../UserAvatar' import Username from '../Username' +import { useTranslation } from 'react-i18next' export default function Mentions({ content, @@ -14,6 +15,7 @@ export default function Mentions({ content: string parentEvent?: Event }) { + const { t } = useTranslation() const { pubkey } = useNostr() const [pubkeys, setPubkeys] = useState([]) @@ -32,7 +34,7 @@ export default function Mentions({ disabled={pubkeys.length === 0} onClick={(e) => e.stopPropagation()} > - Mentions {pubkeys.length > 0 && `(${pubkeys.length})`} + {t('Mentions')} {pubkeys.length > 0 && `(${pubkeys.length})`} diff --git a/src/renderer/src/components/PostDialog/index.tsx b/src/renderer/src/components/PostDialog/index.tsx index 92b2700..726fee2 100644 --- a/src/renderer/src/components/PostDialog/index.tsx +++ b/src/renderer/src/components/PostDialog/index.tsx @@ -19,6 +19,7 @@ import UserAvatar from '../UserAvatar' import Mentions from './Metions' import Preview from './Preview' import Uploader from './Uploader' +import { useTranslation } from 'react-i18next' export default function PostDialog({ defaultContent = '', @@ -31,6 +32,7 @@ export default function PostDialog({ open: boolean setOpen: Dispatch }) { + const { t } = useTranslation() const { toast } = useToast() const { publish, checkLogin } = useNostr() const [content, setContent] = useState(defaultContent) @@ -65,14 +67,14 @@ export default function PostDialog({ error.errors.forEach((e) => toast({ variant: 'destructive', - title: 'Failed to post', + title: t('Failed to post'), description: e.message }) ) } else if (error instanceof Error) { toast({ variant: 'destructive', - title: 'Failed to post', + title: t('Failed to post'), description: error.message }) } @@ -82,8 +84,8 @@ export default function PostDialog({ setPosting(false) } toast({ - title: 'Post successful', - description: 'Your post has been published' + title: t('Post successful'), + description: t('Your post has been published') }) }) } @@ -102,7 +104,7 @@ export default function PostDialog({
{parentEvent.content}
) : ( - 'New post' + t('New post') )} @@ -111,7 +113,7 @@ export default function PostDialog({ className="h-32" onChange={handleTextareaChange} value={content} - placeholder="Write something..." + placeholder={t('Write something...')} /> {content && }
@@ -125,11 +127,11 @@ export default function PostDialog({ setOpen(false) }} > - Cancel + {t('Cancel')}
diff --git a/src/renderer/src/components/RefreshButton/index.tsx b/src/renderer/src/components/RefreshButton/index.tsx index e30f980..11e9928 100644 --- a/src/renderer/src/components/RefreshButton/index.tsx +++ b/src/renderer/src/components/RefreshButton/index.tsx @@ -1,17 +1,19 @@ import { Button } from '@renderer/components/ui/button' import { usePrimaryPage } from '@renderer/PageManager' import { RefreshCcw } from 'lucide-react' +import { useTranslation } from 'react-i18next' export default function RefreshButton({ variant = 'titlebar' }: { variant?: 'titlebar' | 'sidebar' }) { + const { t } = useTranslation() const { refresh } = usePrimaryPage() return ( - ) } diff --git a/src/renderer/src/components/RelaySettingsPopover/index.tsx b/src/renderer/src/components/RelaySettingsPopover/index.tsx index 019f227..add4a4d 100644 --- a/src/renderer/src/components/RelaySettingsPopover/index.tsx +++ b/src/renderer/src/components/RelaySettingsPopover/index.tsx @@ -3,18 +3,20 @@ import { Button } from '@renderer/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover' import { ScrollArea } from '@renderer/components/ui/scroll-area' import { Server } from 'lucide-react' +import { useTranslation } from 'react-i18next' export default function RelaySettingsPopover({ variant = 'titlebar' }: { variant?: 'titlebar' | 'sidebar' }) { + const { t } = useTranslation() return ( - void highlight?: boolean }) { + const { t } = useTranslation() const [isPostDialogOpen, setIsPostDialogOpen] = useState(false) return ( @@ -42,7 +44,7 @@ export default function ReplyNote({ className="text-muted-foreground hover:text-primary cursor-pointer" onClick={() => setIsPostDialogOpen(true)} > - reply + {t('reply')} diff --git a/src/renderer/src/components/ReplyNoteList/index.tsx b/src/renderer/src/components/ReplyNoteList/index.tsx index e87b5af..f71ff68 100644 --- a/src/renderer/src/components/ReplyNoteList/index.tsx +++ b/src/renderer/src/components/ReplyNoteList/index.tsx @@ -8,8 +8,10 @@ import dayjs from 'dayjs' import { Event } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import ReplyNote from '../ReplyNote' +import { useTranslation } from 'react-i18next' export default function ReplyNoteList({ event, className }: { event: Event; className?: string }) { + const { t } = useTranslation() const [replies, setReplies] = useState([]) const [replyMap, setReplyMap] = useState< Record @@ -98,7 +100,7 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas className={`text-sm text-center text-muted-foreground ${!loading ? 'hover:text-foreground cursor-pointer' : ''}`} onClick={loadMore} > - {loading ? 'loading...' : hasMore ? 'load more older replies' : null} + {loading ? t('loading...') : hasMore ? t('load more older replies') : null} {replies.length > 0 && (loading || hasMore) && }
diff --git a/src/renderer/src/components/Sidebar/index.tsx b/src/renderer/src/components/Sidebar/index.tsx index af0590a..0620e49 100644 --- a/src/renderer/src/components/Sidebar/index.tsx +++ b/src/renderer/src/components/Sidebar/index.tsx @@ -8,8 +8,10 @@ import AccountButton from '../AccountButton' import PostButton from '../PostButton' import RefreshButton from '../RefreshButton' import RelaySettingsPopover from '../RelaySettingsPopover' +import { useTranslation } from 'react-i18next' export default function PrimaryPageSidebar() { + const { t } = useTranslation() return (
@@ -23,7 +25,7 @@ export default function PrimaryPageSidebar() { )} diff --git a/src/renderer/src/components/ThemeToggle/index.tsx b/src/renderer/src/components/ThemeToggle/index.tsx index 9cca61c..36aa3e7 100644 --- a/src/renderer/src/components/ThemeToggle/index.tsx +++ b/src/renderer/src/components/ThemeToggle/index.tsx @@ -1,8 +1,10 @@ import { Button } from '@renderer/components/ui/button' import { useTheme } from '@renderer/providers/ThemeProvider' import { Moon, Sun, SunMoon } from 'lucide-react' +import { useTranslation } from 'react-i18next' export default function ThemeToggle() { + const { t } = useTranslation() const { themeSetting, setThemeSetting } = useTheme() return ( @@ -12,7 +14,7 @@ export default function ThemeToggle() { variant="titlebar" size="titlebar" onClick={() => setThemeSetting('light')} - title="switch to light theme" + title={t('switch to light theme')} > @@ -21,7 +23,7 @@ export default function ThemeToggle() { variant="titlebar" size="titlebar" onClick={() => setThemeSetting('dark')} - title="switch to dark theme" + title={t('switch to dark theme')} > @@ -30,7 +32,7 @@ export default function ThemeToggle() { variant="titlebar" size="titlebar" onClick={() => setThemeSetting('system')} - title="switch to system theme" + title={t('switch to system theme')} > diff --git a/src/renderer/src/i18n/en.ts b/src/renderer/src/i18n/en.ts new file mode 100644 index 0000000..e9d5162 --- /dev/null +++ b/src/renderer/src/i18n/en.ts @@ -0,0 +1,49 @@ +export default { + translation: { + 'Welcome! 🥳': 'Welcome! 🥳', + About: 'About', + 'New post': 'New post', + Post: 'Post', + 'Relay settings': 'Relay settings', + SidebarRelays: 'Relays', + Refresh: 'Refresh', + Profile: 'Profile', + Logout: 'Logout', + Following: 'Following', + reposted: 'reposted', + 'just now': 'just now', + 'n minutes ago': '{{n}} minutes ago', + 'n hours ago': '{{n}} hours ago', + 'n days ago': '{{n}} days ago', + date: '{{timestamp, date}}', + Follow: 'Follow', + Unfollow: 'Unfollow', + 'Follow failed': 'Follow failed', + 'Unfollow failed': 'Unfollow failed', + 'show new notes': 'show new notes', + 'loading...': 'loading...', + 'no more notes': 'no more notes', + 'reply to': 'reply to', + reply: 'reply', + Reply: 'Reply', + 'load more older replies': 'load more older replies', + 'Write something...': 'Write something...', + Cancel: 'Cancel', + Mentions: 'Mentions', + 'Failed to post': 'Failed to post', + 'Post successful': 'Post successful', + 'Your post has been published': 'Your post has been published', + Repost: 'Repost', + Quote: 'Quote', + 'copy embedded code': 'copy embedded code', + 'raw event': 'raw event', + Like: 'Like', + 'switch to light theme': 'switch to light theme', + 'switch to dark theme': 'switch to dark theme', + 'switch to system theme': 'switch to system theme', + note: 'note', + "username's following": "{{username}}'s following", + following: 'following', + Login: 'Login' + } +} diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts new file mode 100644 index 0000000..c0737f1 --- /dev/null +++ b/src/renderer/src/i18n/index.ts @@ -0,0 +1,32 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' +import en from './en' +import zh from './zh' +import dayjs from 'dayjs' + +const resources = { + en, + zh +} + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + resources, + interpolation: { + escapeValue: false // react already safes from xss + } + }) + +i18n.services.formatter?.add('date', (value, lng) => { + console.log('lng', lng) + if (lng?.startsWith('zh')) { + return dayjs(value).format('YYYY-MM-DD') + } + return dayjs(value).format('MMM D, YYYY') +}) + +export default i18n diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts new file mode 100644 index 0000000..e3e8b9f --- /dev/null +++ b/src/renderer/src/i18n/zh.ts @@ -0,0 +1,49 @@ +export default { + translation: { + 'Welcome! 🥳': '欢迎!🥳', + About: '关于', + 'New post': '发布新笔记', + Post: '发布笔记', + 'Relay settings': '中继设置', + SidebarRelays: '中继设置', + Refresh: '刷新列表', + Profile: '个人资料', + Logout: '退出登录', + Following: '关注', + reposted: '转发', + 'just now': '刚刚', + 'n minutes ago': '{{n}} 分钟前', + 'n hours ago': '{{n}} 小时前', + 'n days ago': '{{n}} 天前', + date: '{{timestamp, date}}', + Follow: '关注', + Unfollow: '取消关注', + 'Follow failed': '关注失败', + 'Unfollow failed': '取消关注失败', + 'show new notes': '显示新笔记', + 'loading...': '加载中...', + 'no more notes': '到底了', + 'reply to': '回复', + reply: '回复', + Reply: '回复', + 'load more older replies': '加载更多早期回复', + 'Write something...': '写点什么...', + Cancel: '取消', + Mentions: '提及', + 'Failed to post': '发布失败', + 'Post successful': '发布成功', + 'Your post has been published': '您的笔记已发布', + Repost: '转发', + Quote: '引用', + 'copy embedded code': '复制嵌入代码', + 'raw event': '原始事件', + Like: '点赞', + 'switch to light theme': '切换到浅色主题', + 'switch to dark theme': '切换到深色主题', + 'switch to system theme': '切换到系统主题', + note: '笔记', + "username's following": '{{username}} 的关注', + following: '关注', + Login: '登录' + } +} diff --git a/src/renderer/src/lib/timestamp.ts b/src/renderer/src/lib/timestamp.tsx similarity index 59% rename from src/renderer/src/lib/timestamp.ts rename to src/renderer/src/lib/timestamp.tsx index 799a129..d3a9e8b 100644 --- a/src/renderer/src/lib/timestamp.ts +++ b/src/renderer/src/lib/timestamp.tsx @@ -1,28 +1,30 @@ import dayjs from 'dayjs' +import { useTranslation } from 'react-i18next' export function formatTimestamp(timestamp: number) { + const { t } = useTranslation() const time = dayjs(timestamp * 1000) const now = dayjs() const diffMonth = now.diff(time, 'month') if (diffMonth >= 1) { - return time.format('MMM D, YYYY') + return t('date', { timestamp: time.valueOf() }) } const diffDay = now.diff(time, 'day') if (diffDay >= 1) { - return `${diffDay} days ago` + return t('n days ago', { n: diffDay }) } const diffHour = now.diff(time, 'hour') if (diffHour >= 1) { - return `${diffHour} hours ago` + return t('n hours ago', { n: diffHour }) } const diffMinute = now.diff(time, 'minute') if (diffMinute >= 1) { - return `${diffMinute} minutes ago` + return t('n minutes ago', { n: diffMinute }) } - return 'just now' + return t('just now') } diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index f4d40c7..e319268 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,4 +1,5 @@ import './assets/main.css' +import './i18n' import React from 'react' import ReactDOM from 'react-dom/client' diff --git a/src/renderer/src/pages/secondary/FollowingListPage/index.tsx b/src/renderer/src/pages/secondary/FollowingListPage/index.tsx index 27f5896..08ab0c5 100644 --- a/src/renderer/src/pages/secondary/FollowingListPage/index.tsx +++ b/src/renderer/src/pages/secondary/FollowingListPage/index.tsx @@ -5,8 +5,10 @@ import Username from '@renderer/components/Username' import { useFetchFollowings, useFetchProfile } from '@renderer/hooks' import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' export default function FollowingListPage({ id }: { id?: string }) { + const { t } = useTranslation() const { profile } = useFetchProfile(id) const { followings } = useFetchFollowings(profile?.pubkey) const [visibleFollowings, setVisibleFollowings] = useState([]) @@ -46,7 +48,11 @@ export default function FollowingListPage({ id }: { id?: string }) { return (
{visibleFollowings.map((pubkey, index) => ( diff --git a/src/renderer/src/pages/secondary/HomePage/index.tsx b/src/renderer/src/pages/secondary/HomePage/index.tsx index 7c196f5..3fa28b8 100644 --- a/src/renderer/src/pages/secondary/HomePage/index.tsx +++ b/src/renderer/src/pages/secondary/HomePage/index.tsx @@ -1,10 +1,12 @@ import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' +import { useTranslation } from 'react-i18next' export default function HomePage() { + const { t } = useTranslation() return (
- Welcome! 🥳 + {t('Welcome! 🥳')}
) diff --git a/src/renderer/src/pages/secondary/NotePage/index.tsx b/src/renderer/src/pages/secondary/NotePage/index.tsx index 81c7d70..c985b5e 100644 --- a/src/renderer/src/pages/secondary/NotePage/index.tsx +++ b/src/renderer/src/pages/secondary/NotePage/index.tsx @@ -12,8 +12,10 @@ import { getParentEventId, getRootEventId } from '@renderer/lib/event' import { toNote } from '@renderer/lib/link' import { useMemo } from 'react' import NotFoundPage from '../NotFoundPage' +import { useTranslation } from 'react-i18next' export default function NotePage({ id }: { id?: string }) { + const { t } = useTranslation() const { event, isFetching } = useFetchEventById(id) const parentEventId = useMemo(() => getParentEventId(event), [event]) const rootEventId = useMemo(() => getRootEventId(event), [event]) @@ -28,7 +30,7 @@ export default function NotePage({ id }: { id?: string }) { if (!event) return return ( - + diff --git a/src/renderer/src/pages/secondary/ProfilePage/index.tsx b/src/renderer/src/pages/secondary/ProfilePage/index.tsx index bc22915..f3792c1 100644 --- a/src/renderer/src/pages/secondary/ProfilePage/index.tsx +++ b/src/renderer/src/pages/secondary/ProfilePage/index.tsx @@ -5,6 +5,7 @@ import ProfileAbout from '@renderer/components/ProfileAbout' import ProfileBanner from '@renderer/components/ProfileBanner' import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' import { Separator } from '@renderer/components/ui/separator' +import { Skeleton } from '@renderer/components/ui/skeleton' import { useFetchFollowings, useFetchProfile } from '@renderer/hooks' import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList' import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' @@ -14,13 +15,13 @@ import { SecondaryPageLink } from '@renderer/PageManager' import { useFollowList } from '@renderer/providers/FollowListProvider' import { useNostr } from '@renderer/providers/NostrProvider' import { useMemo } from 'react' +import NotFoundPage from '../NotFoundPage' import PubkeyCopy from './PubkeyCopy' import QrCodePopover from './QrCodePopover' -import LoadingPage from '../LoadingPage' -import NotFoundPage from '../NotFoundPage' -import { Skeleton } from '@renderer/components/ui/skeleton' +import { useTranslation } from 'react-i18next' export default function ProfilePage({ id }: { id?: string }) { + const { t } = useTranslation() const { profile, isFetching } = useFetchProfile(id) const relayList = useFetchRelayList(profile?.pubkey) const { pubkey: accountPubkey } = useNostr() @@ -85,10 +86,10 @@ export default function ProfilePage({ id }: { id?: string }) { {isSelf ? selfFollowings.length : followings.length} -
Following
+
{t('Following')}