From cfda9ba8997b2b3cbfa873b2a6ba243cf3c78045 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 29 Jan 2024 13:38:22 +0700 Subject: [PATCH] feat: migrate ui components to i18n --- packages/ark/src/components/column/header.tsx | 15 ++-- .../src/components/column/interestModal.tsx | 12 +-- .../ark/src/components/user/followButton.tsx | 8 +- packages/ui/src/account/active.tsx | 6 +- packages/ui/src/account/logout.tsx | 14 ++-- packages/ui/src/avatarUploadButton.tsx | 5 +- packages/ui/src/editor/form.tsx | 12 +-- packages/ui/src/editor/replyForm.tsx | 7 +- packages/ui/src/emptyFeed.tsx | 9 ++- packages/ui/src/mentions.tsx | 6 +- packages/ui/src/nip05.tsx | 74 ------------------- packages/ui/src/replyList.tsx | 5 +- packages/ui/src/routes/suggest.tsx | 8 +- packages/ui/src/routes/user.tsx | 6 +- packages/ui/src/search/dialog.tsx | 11 ++- src-tauri/locales/en.json | 48 +++++++++++- 16 files changed, 122 insertions(+), 124 deletions(-) delete mode 100644 packages/ui/src/nip05.tsx diff --git a/packages/ark/src/components/column/header.tsx b/packages/ark/src/components/column/header.tsx index dbfd7173..341e604d 100644 --- a/packages/ark/src/components/column/header.tsx +++ b/packages/ark/src/components/column/header.tsx @@ -3,12 +3,11 @@ import { MoveLeftIcon, MoveRightIcon, RefreshIcon, - ThreadIcon, TrashIcon, } from "@lume/icons"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { useQueryClient } from "@tanstack/react-query"; -import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import { InterestModal } from "./interestModal"; import { useColumnContext } from "./provider"; @@ -16,14 +15,14 @@ export function ColumnHeader({ id, title, queryKey, - icon, }: { id: number; title: string; queryKey?: string[]; - icon?: ReactNode; }) { const queryClient = useQueryClient(); + + const { t } = useTranslation(); const { moveColumn, removeColumn } = useColumnContext(); const refresh = async () => { @@ -63,7 +62,7 @@ export function ColumnHeader({ className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" > - Refresh + {t("global.refresh")} {queryKey?.[0] === "foryou-9998" ? ( @@ -81,7 +80,7 @@ export function ColumnHeader({ className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" > - Move left + {t("global.moveLeft")} @@ -91,7 +90,7 @@ export function ColumnHeader({ className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" > - Move right + {t("global.moveRight")} @@ -102,7 +101,7 @@ export function ColumnHeader({ className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none" > - Delete + {t("global.Delete")} diff --git a/packages/ark/src/components/column/interestModal.tsx b/packages/ark/src/components/column/interestModal.tsx index 66e9a583..b52647a5 100644 --- a/packages/ark/src/components/column/interestModal.tsx +++ b/packages/ark/src/components/column/interestModal.tsx @@ -4,6 +4,7 @@ import { TOPICS, cn } from "@lume/utils"; import * as Dialog from "@radix-ui/react-dialog"; import { useQueryClient } from "@tanstack/react-query"; import { ReactNode, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; export function InterestModal({ @@ -14,6 +15,7 @@ export function InterestModal({ const storage = useStorage(); const queryClient = useQueryClient(); + const [t] = useTranslation(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [hashtags, setHashtags] = useState(storage.interests?.hashtags || []); @@ -65,7 +67,7 @@ export function InterestModal({ ) : ( <> - Edit interest + {t("interests.edit")} )} @@ -80,7 +82,7 @@ export function InterestModal({
-

Edit Interest

+

{t("interests.edit")}

@@ -104,7 +106,7 @@ export function InterestModal({ onClick={() => toggleAll(topic.content)} className="text-sm font-medium text-blue-500" > - Follow All + {t("interests.followAll")}
@@ -131,7 +133,7 @@ export function InterestModal({
- Cancel + {t("global.cancel")}
diff --git a/packages/ark/src/components/user/followButton.tsx b/packages/ark/src/components/user/followButton.tsx index 20bf6ada..29d4af7c 100644 --- a/packages/ark/src/components/user/followButton.tsx +++ b/packages/ark/src/components/user/followButton.tsx @@ -1,6 +1,7 @@ import { LoaderIcon } from "@lume/icons"; import { cn } from "@lume/utils"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useArk } from "../../hooks/useArk"; export function UserFollowButton({ @@ -9,6 +10,7 @@ export function UserFollowButton({ }: { target: string; className?: string }) { const ark = useArk(); + const [t] = useTranslation(); const [loading, setLoading] = useState(false); const [followed, setFollowed] = useState(false); @@ -43,14 +45,14 @@ export function UserFollowButton({ type="button" disabled={loading} onClick={toggleFollow} - className={cn("", className)} + className={cn("w-max", className)} > {loading ? ( ) : followed ? ( - "Unfollow" + t("user.unfollow") ) : ( - "Follow" + t("user.follow") )} ); diff --git a/packages/ui/src/account/active.tsx b/packages/ui/src/account/active.tsx index 30a4fb8b..d6ecf5f8 100644 --- a/packages/ui/src/account/active.tsx +++ b/packages/ui/src/account/active.tsx @@ -5,6 +5,7 @@ import * as Avatar from "@radix-ui/react-avatar"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { minidenticon } from "minidenticons"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { Logout } from "./logout"; @@ -19,6 +20,7 @@ export function ActiveAccount() { [], ); + const { t } = useTranslation(); const { user } = useProfile(ark.account.pubkey); return ( @@ -62,7 +64,7 @@ export function ActiveAccount() { className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" > - Edit profile + {t("user.editProfile")} @@ -71,7 +73,7 @@ export function ActiveAccount() { className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" > - Settings + {t("user.settings")} diff --git a/packages/ui/src/account/logout.tsx b/packages/ui/src/account/logout.tsx index 0b06132b..a375f9d1 100644 --- a/packages/ui/src/account/logout.tsx +++ b/packages/ui/src/account/logout.tsx @@ -3,6 +3,7 @@ import { LogoutIcon } from "@lume/icons"; import { useStorage } from "@lume/storage"; import * as AlertDialog from "@radix-ui/react-alert-dialog"; import { useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -12,6 +13,8 @@ export function Logout() { const queryClient = useQueryClient(); const navigate = useNavigate(); + const { t } = useTranslation(); + const logout = async () => { try { // logout @@ -38,7 +41,7 @@ export function Logout() { className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" > - Logout + {t("user.logout")} @@ -47,11 +50,10 @@ export function Logout() {
- Are you sure! + {t("user.logoutConfirmTitle")} - You can always log back in at any time. If you just want to - switch accounts, you can do that by adding an existing account. + {t("user.logoutConfirmSubtitle")}
@@ -60,7 +62,7 @@ export function Logout() { type="button" className="inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium text-neutral-900 outline-none hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800" > - Cancel + {t("global.cancel")} @@ -69,7 +71,7 @@ export function Logout() { onClick={() => logout()} className="inline-flex h-9 items-center justify-center rounded-lg bg-red-500 px-4 text-sm font-medium text-white outline-none hover:bg-red-600" > - Logout + {t("user.logout")}
diff --git a/packages/ui/src/avatarUploadButton.tsx b/packages/ui/src/avatarUploadButton.tsx index bd9f1235..8abd1d57 100644 --- a/packages/ui/src/avatarUploadButton.tsx +++ b/packages/ui/src/avatarUploadButton.tsx @@ -1,6 +1,7 @@ import { useArk } from "@lume/ark"; import { LoaderIcon } from "@lume/icons"; import { Dispatch, SetStateAction, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; export function AvatarUploadButton({ @@ -9,6 +10,8 @@ export function AvatarUploadButton({ setPicture: Dispatch>; }) { const ark = useArk(); + + const [t] = useTranslation(); const [loading, setLoading] = useState(false); const uploadAvatar = async () => { @@ -36,7 +39,7 @@ export function AvatarUploadButton({ {loading ? ( ) : ( - "Change avatar" + t("user.avatarButton") )} ); diff --git a/packages/ui/src/editor/form.tsx b/packages/ui/src/editor/form.tsx index 85e29d5a..488cb5c8 100644 --- a/packages/ui/src/editor/form.tsx +++ b/packages/ui/src/editor/form.tsx @@ -6,6 +6,7 @@ import { COL_TYPES, cn, editorValueAtom } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useAtom } from "jotai"; import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Descendant, Editor, @@ -200,6 +201,7 @@ export function EditorForm() { withMentions(withNostrEvent(withImages(withReact(createEditor())))), ); + const { t } = useTranslation(); const { addColumn } = useColumnContext(); const filters = contacts @@ -247,9 +249,7 @@ export function EditorForm() { const publish = await event.publish(); if (publish) { - toast.success( - `Event has been published successfully to ${publish.size} relays.`, - ); + toast.success(t("editor.successMessage")); // add current post as column thread addColumn({ @@ -321,7 +321,7 @@ export function EditorForm() { >
-

New Post

+

{t("editor.title")}

@@ -336,7 +336,7 @@ export function EditorForm() { {loading ? ( ) : ( - "Post" + t("global.post") )}
@@ -349,7 +349,7 @@ export function EditorForm() { autoCorrect="none" spellCheck={false} renderElement={(props) => } - placeholder="What are you up to?" + placeholder={t("editor.placeholder")} className="focus:outline-none" /> {target && filters.length > 0 && ( diff --git a/packages/ui/src/editor/replyForm.tsx b/packages/ui/src/editor/replyForm.tsx index 8747538e..274ae395 100644 --- a/packages/ui/src/editor/replyForm.tsx +++ b/packages/ui/src/editor/replyForm.tsx @@ -6,6 +6,7 @@ import { cn } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { Portal } from "@radix-ui/react-dropdown-menu"; import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Descendant, Editor, @@ -207,6 +208,8 @@ export function ReplyForm({ withMentions(withNostrEvent(withImages(withReact(createEditor())))), ); + const { t } = useTranslation(); + const filters = contacts ?.filter((c) => c?.name?.toLowerCase().startsWith(search.toLowerCase())) ?.slice(0, 10); @@ -334,7 +337,7 @@ export function ReplyForm({ autoCorrect="none" spellCheck={false} renderElement={(props) => } - placeholder="Post your reply" + placeholder={t("editor.replyPlaceholder")} className="focus:outline-none h-28" /> {target && filters.length > 0 && ( @@ -383,7 +386,7 @@ export function ReplyForm({ {loading ? ( ) : ( - "Post" + t("global.post") )}
diff --git a/packages/ui/src/emptyFeed.tsx b/packages/ui/src/emptyFeed.tsx index 2238f088..58a8a87a 100644 --- a/packages/ui/src/emptyFeed.tsx +++ b/packages/ui/src/emptyFeed.tsx @@ -1,11 +1,14 @@ import { InfoIcon } from "@lume/icons"; import { cn } from "@lume/utils"; +import { useTranslation } from "react-i18next"; export function EmptyFeed({ text, subtext, className, }: { text?: string; subtext?: string; className?: string }) { + const { t } = useTranslation(); + return (

- {text ? text : "This feed is empty"} + {text ? text : t("global.emptyFeedTitle")}

- {subtext - ? subtext - : "You can follow more users to build up your timeline"} + {subtext ? subtext : t("global.emptyFeedSubtitle")}

diff --git a/packages/ui/src/mentions.tsx b/packages/ui/src/mentions.tsx index 608c7e91..b217c3f9 100644 --- a/packages/ui/src/mentions.tsx +++ b/packages/ui/src/mentions.tsx @@ -10,6 +10,7 @@ import { import { NDKCacheUserProfile } from "@lume/types"; import { cn } from "@lume/utils"; +import { useTranslation } from "react-i18next"; type MentionListRef = { onKeyDown: (props: { event: Event }) => boolean; @@ -22,6 +23,7 @@ const List = ( }, ref: Ref, ) => { + const [t] = useTranslation(); const [selectedIndex, setSelectedIndex] = useState(0); const selectItem = (index) => { @@ -107,7 +109,9 @@ const List = ( )) ) : ( -
No result
+
+ {t("global.noResult")} +
)}
); diff --git a/packages/ui/src/nip05.tsx b/packages/ui/src/nip05.tsx deleted file mode 100644 index 1e0e286d..00000000 --- a/packages/ui/src/nip05.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { UnverifiedIcon, VerifiedIcon } from "@lume/icons"; -import { cn } from "@lume/utils"; -import { useQuery } from "@tanstack/react-query"; -import { fetch } from "@tauri-apps/plugin-http"; -import { memo } from "react"; - -interface NIP05 { - names: { - [key: string]: string; - }; -} - -export const NIP05 = memo(function NIP05({ - pubkey, - nip05, - className, -}: { - pubkey: string; - nip05: string; - className?: string; -}) { - const { status, data } = useQuery({ - queryKey: ["nip05", nip05], - queryFn: async ({ signal }: { signal: AbortSignal }) => { - try { - const localPath = nip05.split("@")[0]; - const service = nip05.split("@")[1]; - const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`; - - const res = await fetch(verifyURL, { - method: "GET", - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - signal, - }); - - if (!res.ok) - throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); - - const data: NIP05 = await res.json(); - if (data.names) { - if (data.names[localPath.toLowerCase()] === pubkey) return true; - if (data.names[localPath] === pubkey) return true; - return false; - } - return false; - } catch (e) { - throw new Error(`Failed to verify NIP-05, error: ${e}`); - } - }, - refetchOnMount: false, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - staleTime: Infinity, - }); - - if (status === "pending") { -
; - } - - return ( -
-

- {nip05.startsWith("_@") ? nip05.replace("_@", "") : nip05} -

- {data === true ? ( - - ) : ( - - )} -
- ); -}); diff --git a/packages/ui/src/replyList.tsx b/packages/ui/src/replyList.tsx index 52d066d4..6c52fc16 100644 --- a/packages/ui/src/replyList.tsx +++ b/packages/ui/src/replyList.tsx @@ -4,6 +4,7 @@ import { NDKEventWithReplies } from "@lume/types"; import { cn } from "@lume/utils"; import { NDKKind, type NDKSubscription } from "@nostr-dev-kit/ndk"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { ReplyForm } from "./editor/replyForm"; export function ReplyList({ @@ -11,6 +12,8 @@ export function ReplyList({ className, }: { eventId: string; className?: string }) { const ark = useArk(); + + const [t] = useTranslation(); const [data, setData] = useState(null); useEffect(() => { @@ -68,7 +71,7 @@ export function ReplyList({

👋

- Be the first to Reply! + {t("note.reply.empty")}

diff --git a/packages/ui/src/routes/suggest.tsx b/packages/ui/src/routes/suggest.tsx index 1630953c..d16ef695 100644 --- a/packages/ui/src/routes/suggest.tsx +++ b/packages/ui/src/routes/suggest.tsx @@ -1,6 +1,7 @@ import { User } from "@lume/ark"; import { ArrowLeftIcon, ArrowRightIcon, LoaderIcon } from "@lume/icons"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { WindowVirtualizer } from "virtua"; @@ -28,6 +29,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) { const queryClient = useQueryClient(); const navigate = useNavigate(); + const { t } = useTranslation(); const { isLoading, isError, data } = useQuery({ queryKey: ["trending-users"], queryFn: async ({ signal }: { signal: AbortSignal }) => { @@ -71,7 +73,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
-

Suggested Follows

+

{t("suggestion.title")}

{isLoading ? ( @@ -80,7 +82,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
) : isError ? (
- Error. Cannot get trending users + {t("suggestion.error")}
) : ( data?.profiles.map((item: { pubkey: string }) => ( @@ -115,7 +117,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) { onClick={submit} className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl dark:shadow-none shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-44 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed" > - Save & Go back + {t("suggestion.button")}
diff --git a/packages/ui/src/routes/user.tsx b/packages/ui/src/routes/user.tsx index 2ff5b49c..2cdeb60a 100644 --- a/packages/ui/src/routes/user.tsx +++ b/packages/ui/src/routes/user.tsx @@ -9,6 +9,7 @@ import { FETCH_LIMIT } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { WindowVirtualizer } from "virtua"; @@ -17,6 +18,7 @@ export function UserRoute() { const navigate = useNavigate(); const { id } = useParams(); + const { t } = useTranslation(); const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ["user-posts", id], @@ -107,7 +109,7 @@ export function UserRoute() {

- Latest posts + {t("user.latestPosts")}

{isLoading ? ( @@ -130,7 +132,7 @@ export function UserRoute() { ) : ( <> - Load more + {t("global.loadMore")} )} diff --git a/packages/ui/src/search/dialog.tsx b/packages/ui/src/search/dialog.tsx index c39e2341..af739f2f 100644 --- a/packages/ui/src/search/dialog.tsx +++ b/packages/ui/src/search/dialog.tsx @@ -4,17 +4,20 @@ import { COL_TYPES, searchAtom } from "@lume/utils"; import { type NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useAtom } from "jotai"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useDebounce } from "use-debounce"; import { Command } from "../cmdk"; export function SearchDialog() { + const ark = useArk(); + const [open, setOpen] = useAtom(searchAtom); const [loading, setLoading] = useState(false); const [events, setEvents] = useState([]); const [search, setSearch] = useState(""); const [value] = useDebounce(search, 1200); - const ark = useArk(); + const { t } = useTranslation(); const { vlistRef, columns, addColumn } = useColumnContext(); const searchEvents = async () => { @@ -90,7 +93,7 @@ export function SearchDialog() {
@@ -101,7 +104,7 @@ export function SearchDialog() { ) : !events.length ? ( - No results found. + {t("global.noResult")} ) : ( <> @@ -161,7 +164,7 @@ export function SearchDialog() {
- Try searching for people, notes, or keywords + {t("search.empty")}
) : null} diff --git a/src-tauri/locales/en.json b/src-tauri/locales/en.json index 85043d89..8c0e10df 100644 --- a/src-tauri/locales/en.json +++ b/src-tauri/locales/en.json @@ -7,7 +7,16 @@ "moveLeft": "Move Left", "moveRight": "Move Right", "newColumn": "New Column", - "inspect": "Inspect" + "inspect": "Inspect", + "loadMore": "Load more", + "delete": "Delete", + "refresh": "Refresh", + "cancel": "Cancel", + "save": "Save", + "post": "Post", + "noResult": "No results found.", + "emptyFeedTitle": "This feed is empty", + "emptyFeedSubtitle": "You can follow more users to build up your timeline" }, "nip89": { "unsupported": "Lume isn't support this event", @@ -49,9 +58,32 @@ }, "reply": { "single": "reply", - "plural": "replies" + "plural": "replies", + "empty": "Be the first to Reply!" } }, + "user": { + "follow": "Follow", + "unfollow": "Unfollow", + "latestPosts": "Latest posts", + "avatarButton": "Change avatar", + "coverButton": "Change cover", + "editProfile": "Edit profile", + "settings": "Settings", + "logout": "Log out", + "logoutConfirmTitle": "Are you sure!", + "logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account." + }, + "editor": { + "title": "New Post", + "placeholder": "What are you up to?", + "successMessage": "Your note has been published successfully.", + "replyPlaceholder": "Post your reply" + }, + "search": { + "placeholder": "Type something to search...", + "empty": "Try searching for people, notes, or keywords" + }, "welcome": { "title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.", "signup": "Join Nostr", @@ -133,5 +165,17 @@ "payment": "Open payment website", "paymentNote": "You need to make a payment to connect this relay" } + }, + "suggestion": { + "title": "Suggested Follows", + "error": "Error. Cannot get trending users", + "button": "Save & Go back" + }, + "interests": { + "title": "Interests", + "subtitle": "Pick things you'd like to see in your home feed.", + "edit": "Edit Interest", + "followAll": "Follow All", + "unfollowAll": "Unfollow All" } }