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")}
) : (
- "Save"
+ t("global.save")
)}
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"
}
}