feat: migrate ui components to i18n

This commit is contained in:
reya 2024-01-29 13:38:22 +07:00
parent 698bd78684
commit cfda9ba899
16 changed files with 122 additions and 124 deletions

View File

@ -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"
>
<RefreshIcon className="size-4" />
Refresh
{t("global.refresh")}
</button>
</DropdownMenu.Item>
{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"
>
<MoveLeftIcon className="size-4" />
Move left
{t("global.moveLeft")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -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"
>
<MoveRightIcon className="size-4" />
Move right
{t("global.moveRight")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
@ -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"
>
<TrashIcon className="size-4" />
Delete
{t("global.Delete")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@ -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({
) : (
<>
<EditInterestIcon className="size-4" />
Edit interest
{t("interests.edit")}
</>
)}
</Dialog.Trigger>
@ -80,7 +82,7 @@ export function InterestModal({
<div className="w-full h-full flex flex-col">
<div className="h-16 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex w-full items-center justify-between">
<div className="flex flex-col">
<h3 className="font-semibold">Edit Interest</h3>
<h3 className="font-semibold">{t("interests.edit")}</h3>
</div>
</div>
<div className="w-full flex-1 min-h-0 flex flex-col justify-between">
@ -104,7 +106,7 @@ export function InterestModal({
onClick={() => toggleAll(topic.content)}
className="text-sm font-medium text-blue-500"
>
Follow All
{t("interests.followAll")}
</button>
</div>
<div className="flex flex-wrap items-center gap-3">
@ -131,7 +133,7 @@ export function InterestModal({
<div className="h-16 shrink-0 w-full flex items-center px-8 justify-center gap-2 border-t border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
<Dialog.Close className="inline-flex h-9 flex-1 gap-2 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium dark:bg-neutral-900 dark:hover:bg-neutral-800 hover:bg-blue-200">
<ArrowLeftIcon className="size-4" />
Cancel
{t("global.cancel")}
</Dialog.Close>
<button
type="button"
@ -141,7 +143,7 @@ export function InterestModal({
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Save"
t("global.save")
)}
</button>
</div>

View File

@ -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 ? (
<LoaderIcon className="size-4 animate-spin" />
) : followed ? (
"Unfollow"
t("user.unfollow")
) : (
"Follow"
t("user.follow")
)}
</button>
);

View File

@ -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"
>
<UserIcon className="size-4" />
Edit profile
{t("user.editProfile")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -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"
>
<SettingsIcon className="size-4" />
Settings
{t("user.settings")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />

View File

@ -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"
>
<LogoutIcon className="size-4" />
Logout
{t("user.logout")}
</button>
</AlertDialog.Trigger>
<AlertDialog.Portal>
@ -47,11 +50,10 @@ export function Logout() {
<div className="relative h-min w-full max-w-md rounded-xl bg-neutral-100 dark:bg-neutral-900">
<div className="flex flex-col gap-1 border-b border-white/5 px-5 py-4">
<AlertDialog.Title className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Are you sure!
{t("user.logoutConfirmTitle")}
</AlertDialog.Title>
<AlertDialog.Description className="text-sm leading-tight text-neutral-600 dark:text-neutral-400">
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")}
</AlertDialog.Description>
</div>
<div className="flex justify-end gap-2 px-5 py-3">
@ -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")}
</button>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
@ -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")}
</button>
</AlertDialog.Action>
</div>

View File

@ -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<SetStateAction<string>>;
}) {
const ark = useArk();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const uploadAvatar = async () => {
@ -36,7 +39,7 @@ export function AvatarUploadButton({
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Change avatar"
t("user.avatarButton")
)}
</button>
);

View File

@ -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() {
>
<div className="flex items-center justify-between h-16 pl-7 pr-3 border-b shrink-0 border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
<div>
<h3 className="font-medium">New Post</h3>
<h3 className="font-medium">{t("editor.title")}</h3>
</div>
<div className="flex items-center">
<div className="inline-flex items-center gap-2">
@ -336,7 +336,7 @@ export function EditorForm() {
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Post"
t("global.post")
)}
</button>
</div>
@ -349,7 +349,7 @@ export function EditorForm() {
autoCorrect="none"
spellCheck={false}
renderElement={(props) => <Element {...props} />}
placeholder="What are you up to?"
placeholder={t("editor.placeholder")}
className="focus:outline-none"
/>
{target && filters.length > 0 && (

View File

@ -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) => <Element {...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 ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Post"
t("global.post")
)}
</button>
</div>

View File

@ -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 (
<div
className={cn(
@ -16,12 +19,10 @@ export function EmptyFeed({
<InfoIcon className="size-8 text-blue-500" />
<div className="text-center">
<p className="font-semibold text-lg">
{text ? text : "This feed is empty"}
{text ? text : t("global.emptyFeedTitle")}
</p>
<p className="leading-tight text-sm">
{subtext
? subtext
: "You can follow more users to build up your timeline"}
{subtext ? subtext : t("global.emptyFeedSubtitle")}
</p>
</div>
</div>

View File

@ -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<unknown>,
) => {
const [t] = useTranslation();
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => {
@ -107,7 +109,9 @@ const List = (
</button>
))
) : (
<div className="text-center text-sm font-medium">No result</div>
<div className="text-center text-sm font-medium">
{t("global.noResult")}
</div>
)}
</div>
);

View File

@ -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") {
<div className="h-4 w-4 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />;
}
return (
<div className="inline-flex items-center gap-1">
<p className={cn("text-sm font-medium", className)}>
{nip05.startsWith("_@") ? nip05.replace("_@", "") : nip05}
</p>
{data === true ? (
<VerifiedIcon className="h-4 w-4 text-teal-500" />
) : (
<UnverifiedIcon className="h-4 w-4 text-red-500" />
)}
</div>
);
});

View File

@ -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 | NDKEventWithReplies[]>(null);
useEffect(() => {
@ -68,7 +71,7 @@ export function ReplyList({
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
Be the first to Reply!
{t("note.reply.empty")}
</p>
</div>
</div>

View File

@ -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[] }) {
</div>
<div className="relative px-3">
<div className="flex items-center h-16">
<h3 className="font-semibold text-xl">Suggested Follows</h3>
<h3 className="font-semibold text-xl">{t("suggestion.title")}</h3>
</div>
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
{isLoading ? (
@ -80,7 +82,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
</div>
) : isError ? (
<div className="flex h-44 w-full items-center justify-center">
Error. Cannot get trending users
{t("suggestion.error")}
</div>
) : (
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")}
</button>
</div>
</div>

View File

@ -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() {
</User.Provider>
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Latest posts
{t("user.latestPosts")}
</h3>
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
{isLoading ? (
@ -130,7 +132,7 @@ export function UserRoute() {
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
{t("global.loadMore")}
</>
)}
</button>

View File

@ -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<NDKEvent[]>([]);
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() {
<Command.Input
value={search}
onValueChange={setSearch}
placeholder="Type something to search..."
placeholder={t("search.placeholder")}
className="w-full h-12 bg-neutral-100 dark:bg-neutral-900 rounded-xl border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600"
/>
</div>
@ -101,7 +104,7 @@ export function SearchDialog() {
</Command.Loading>
) : !events.length ? (
<Command.Empty className="flex items-center justify-center h-full text-sm">
No results found.
{t("global.noResult")}
</Command.Empty>
) : (
<>
@ -161,7 +164,7 @@ export function SearchDialog() {
<div className="size-16 bg-blue-100 dark:bg-blue-900 rounded-full inline-flex items-center justify-center text-blue-500">
<SearchIcon className="size-6" />
</div>
Try searching for people, notes, or keywords
{t("search.empty")}
</div>
) : null}
</Command.List>

View File

@ -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"
}
}