diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 785f5cfd..7e27cc66 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -36,6 +36,8 @@ "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.17.19", "framer-motion": "^10.18.0", + "i18next": "^23.8.0", + "i18next-resources-to-backend": "^1.2.0", "jotai": "^2.6.3", "minidenticons": "^4.2.0", "nanoid": "^5.0.4", @@ -45,6 +47,7 @@ "react-currency-input-field": "^3.6.14", "react-dom": "^18.2.0", "react-hook-form": "^7.49.3", + "react-i18next": "^14.0.1", "react-router-dom": "^6.21.3", "smol-toml": "^1.1.4", "sonner": "^1.3.1", diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx index 64a7f3c3..4cb8af63 100644 --- a/apps/desktop/src/app.tsx +++ b/apps/desktop/src/app.tsx @@ -1,7 +1,9 @@ import { ColumnProvider, LumeProvider } from "@lume/ark"; import { StorageProvider } from "@lume/storage"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { I18nextProvider } from "react-i18next"; import { Toaster } from "sonner"; +import i18n from "./i18n"; import Router from "./router"; const queryClient = new QueryClient({ @@ -14,15 +16,17 @@ const queryClient = new QueryClient({ export default function App() { return ( - - - - - - - - - - + + + + + + + + + + + + ); } diff --git a/apps/desktop/src/i18n.ts b/apps/desktop/src/i18n.ts new file mode 100644 index 00000000..080f7339 --- /dev/null +++ b/apps/desktop/src/i18n.ts @@ -0,0 +1,26 @@ +import { resolveResource } from "@tauri-apps/api/path"; +import { readTextFile } from "@tauri-apps/plugin-fs"; +import { locale } from "@tauri-apps/plugin-os"; +import i18n from "i18next"; +import resourcesToBackend from "i18next-resources-to-backend"; +import { initReactI18next } from "react-i18next"; + +const currentLocale = (await locale()).slice(0, 2); + +i18n + .use( + resourcesToBackend(async (language: string) => { + const file_path = await resolveResource(`locales/${language}.json`); + return JSON.parse(await readTextFile(file_path)); + }), + ) + .use(initReactI18next) + .init({ + lng: currentLocale, + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; diff --git a/apps/desktop/src/router.tsx b/apps/desktop/src/router.tsx index a840b26f..23844d38 100644 --- a/apps/desktop/src/router.tsx +++ b/apps/desktop/src/router.tsx @@ -59,15 +59,6 @@ export default function Router() { return { Component: ProfileSettingScreen }; }, }, - { - path: "edit-contact", - async lazy() { - const { EditContactScreen } = await import( - "./routes/settings/editContact" - ); - return { Component: EditContactScreen }; - }, - }, { path: "backup", async lazy() { diff --git a/apps/desktop/src/routes/activty/components/activityRepost.tsx b/apps/desktop/src/routes/activty/components/activityRepost.tsx index 3386ba6d..cc577bd0 100644 --- a/apps/desktop/src/routes/activty/components/activityRepost.tsx +++ b/apps/desktop/src/routes/activty/components/activityRepost.tsx @@ -1,8 +1,11 @@ import { User } from "@lume/ark"; import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; export function ActivityRepost({ event }: { event: NDKEvent }) { + const { t } = useTranslation(); + return (
-

reposted

+

{t("activity.repost")}

-

mention you

+

{t("activity.mention")}

- zapped {compactNumber.format(invoice.amount)} sats + {t("activity.zap")} {compactNumber.format(invoice.amount)} sats

diff --git a/apps/desktop/src/routes/activty/components/list.tsx b/apps/desktop/src/routes/activty/components/list.tsx index 61755fa5..02d6ff2f 100644 --- a/apps/desktop/src/routes/activty/components/list.tsx +++ b/apps/desktop/src/routes/activty/components/list.tsx @@ -4,6 +4,7 @@ import { FETCH_LIMIT } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { ActivityRepost } from "./activityRepost"; import { ActivityText } from "./activityText"; import { ActivityZap } from "./activityZap"; @@ -12,6 +13,7 @@ export function ActivityList() { const ark = useArk(); const queryClient = useQueryClient(); + const { t } = useTranslation(); const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ["activity"], @@ -86,7 +88,7 @@ export function ActivityList() { ) : !allEvents.length ? (

🎉

-

Yo! Nothing new yet.

+

{t("activity.empty")}

) : ( allEvents.map((event) => renderEvenKind(event)) @@ -104,7 +106,7 @@ export function ActivityList() { ) : ( <> - Load more + {t("global.loadMore")} )} diff --git a/apps/desktop/src/routes/activty/components/singleRepost.tsx b/apps/desktop/src/routes/activty/components/singleRepost.tsx index f975d3aa..c97e876b 100644 --- a/apps/desktop/src/routes/activty/components/singleRepost.tsx +++ b/apps/desktop/src/routes/activty/components/singleRepost.tsx @@ -1,16 +1,20 @@ import { User } from "@lume/ark"; import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { useTranslation } from "react-i18next"; import { ActivityRootNote } from "./rootNote"; export function ActivitySingleRepost({ event }: { event: NDKEvent }) { + const { t } = useTranslation(); const repostId = event.tags.find((el) => el[0] === "e")[1]; return (
-

Boost

+

+ {t("activity.boost")} +

- @ Someone has reposted to your note + {t("activity.boostSubtitle")}

@@ -22,7 +26,7 @@ export function ActivitySingleRepost({ event }: { event: NDKEvent }) {
-

Reposted

+

{t("activity.repost")}

diff --git a/apps/desktop/src/routes/activty/components/singleText.tsx b/apps/desktop/src/routes/activty/components/singleText.tsx index 52c53cb7..d0c6d734 100644 --- a/apps/desktop/src/routes/activty/components/singleText.tsx +++ b/apps/desktop/src/routes/activty/components/singleText.tsx @@ -1,5 +1,6 @@ import { Note, useArk } from "@lume/ark"; import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { useTranslation } from "react-i18next"; import { ActivityRootNote } from "./rootNote"; export function ActivitySingleText({ event }: { event: NDKEvent }) { @@ -9,14 +10,16 @@ export function ActivitySingleText({ event }: { event: NDKEvent }) { tags: event.tags, }); + const { t } = useTranslation(); + return (

- Conversation + {t("activity.conversation")}

- @ Someone has replied to your note + {t("activity.conversationSubtitle")}

@@ -33,7 +36,9 @@ export function ActivitySingleText({ event }: { event: NDKEvent }) { ) : null}
-

New reply

+

+ {t("activity.newReply")} +

diff --git a/apps/desktop/src/routes/activty/index.tsx b/apps/desktop/src/routes/activty/index.tsx index 5a13bf81..1dc3e54c 100644 --- a/apps/desktop/src/routes/activty/index.tsx +++ b/apps/desktop/src/routes/activty/index.tsx @@ -1,10 +1,12 @@ import { activityUnreadAtom } from "@lume/utils"; import { useSetAtom } from "jotai"; import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { Outlet } from "react-router-dom"; import { ActivityList } from "./components/list"; export function ActivityScreen() { + const { t } = useTranslation(); const setUnreadActivity = useSetAtom(activityUnreadAtom); useEffect(() => { @@ -15,7 +17,7 @@ export function ActivityScreen() {
- Activity + {t("activity.title")}
diff --git a/apps/desktop/src/routes/auth/create-address.tsx b/apps/desktop/src/routes/auth/create-address.tsx index c99ac938..dcf8833a 100644 --- a/apps/desktop/src/routes/auth/create-address.tsx +++ b/apps/desktop/src/routes/auth/create-address.tsx @@ -14,6 +14,7 @@ import { Window } from "@tauri-apps/api/window"; import { useSetAtom } from "jotai"; import { useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { useLoaderData, useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -43,6 +44,7 @@ export function CreateAccountAddress() { const [serviceId, setServiceId] = useState(services?.[0]?.id); const [loading, setIsLoading] = useState(false); + const { t } = useTranslation(); const { register, handleSubmit, @@ -156,7 +158,7 @@ export function CreateAccountAddress() {

- Let's set up your account on Nostr + {t("signupWithProvider.title")}

{!services ? ( @@ -174,7 +176,7 @@ export function CreateAccountAddress() { htmlFor="username" className="text-sm font-semibold uppercase text-neutral-600" > - Username * + {t("signupWithProvider.username")}
@@ -203,7 +205,7 @@ export function CreateAccountAddress() { - Choose a Provider + {t("signupWithProvider.chooseProvider")} {services.map((service) => ( @@ -215,8 +217,7 @@ export function CreateAccountAddress() {
- Use to login to Lume and other Nostr apps. You can choose - provider you trust to manage your account + {t("signupWithProvider.usernameFooter")}
@@ -226,7 +227,7 @@ export function CreateAccountAddress() { htmlFor="email" className="text-sm font-semibold uppercase text-neutral-600" > - Backup Email (optional) + {t("signupWithProvider.email")}
- Use for recover your account if you lose your password + {t("signupWithProvider.emailFooter")}
@@ -251,7 +252,7 @@ export function CreateAccountAddress() { {loading ? ( ) : ( - "Continue" + t("global.continue") )}
diff --git a/apps/desktop/src/routes/auth/create-keys.tsx b/apps/desktop/src/routes/auth/create-keys.tsx index 46cb4347..b0bd4358 100644 --- a/apps/desktop/src/routes/auth/create-keys.tsx +++ b/apps/desktop/src/routes/auth/create-keys.tsx @@ -11,6 +11,7 @@ import { useSetAtom } from "jotai"; import { nanoid } from "nanoid"; import { getPublicKey, nip19 } from "nostr-tools"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -20,6 +21,7 @@ export function CreateAccountKeys() { const setOnboarding = useSetAtom(onboardingAtom); const navigate = useNavigate(); + const [t] = useTranslation(); const [key, setKey] = useState(""); const [loading, setLoading] = useState(false); const [showKey, setShowKey] = useState(false); @@ -76,11 +78,10 @@ export function CreateAccountKeys() {

- This is your new Account Key + {t("signupWithSelfManage.title")}

- Keep your key in safe place. If you lose this key, you will lose - access to your account. + {t("signupWithSelfManage.subtitle")}

@@ -122,7 +123,7 @@ export function CreateAccountKeys() { className="text-sm leading-none text-neutral-500" htmlFor="confirm1" > - I understand the risk of lost private key. + {t("signupWithSelfManage.confirm1")}
@@ -142,7 +143,7 @@ export function CreateAccountKeys() { className="text-sm leading-none text-neutral-500" htmlFor="confirm2" > - I will make sure keep it safe and not sharing with anyone. + {t("signupWithSelfManage.confirm2")}
@@ -162,7 +163,7 @@ export function CreateAccountKeys() { className="text-sm leading-none text-neutral-500" htmlFor="confirm3" > - I understand I cannot recover private key. + {t("signupWithSelfManage.confirm3")}
@@ -176,7 +177,7 @@ export function CreateAccountKeys() { {loading ? ( ) : ( - "Save key & Continue" + t("signupWithSelfManage.button") )}
diff --git a/apps/desktop/src/routes/auth/create.tsx b/apps/desktop/src/routes/auth/create.tsx index c0b5a301..c982c3bb 100644 --- a/apps/desktop/src/routes/auth/create.tsx +++ b/apps/desktop/src/routes/auth/create.tsx @@ -1,11 +1,13 @@ import { LoaderIcon } from "@lume/icons"; import { cn } from "@lume/utils"; import { useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; export function CreateAccountScreen() { const navigate = useNavigate(); + const [t] = useTranslation(); const [method, setMethod] = useState<"self" | "managed">("self"); const [loading, setLoading] = useState(false); @@ -23,9 +25,9 @@ export function CreateAccountScreen() {
-

Let's Get Started

+

{t("signup.title")}

- Choose one of methods below to create your account + {t("signup.subtitle")}

@@ -37,9 +39,9 @@ export function CreateAccountScreen() { method === "self" ? "ring-1 ring-teal-500" : "", )} > -

Self-Managed

+

{t("signup.selfManageMethod")}

- You create your keys and keep them safe. + {t("signup.selfManageMethodDescription")}

diff --git a/apps/desktop/src/routes/auth/login-key.tsx b/apps/desktop/src/routes/auth/login-key.tsx index 871d26de..7de52444 100644 --- a/apps/desktop/src/routes/auth/login-key.tsx +++ b/apps/desktop/src/routes/auth/login-key.tsx @@ -4,6 +4,7 @@ import { useStorage } from "@lume/storage"; import { getPublicKey, nip19 } from "nostr-tools"; import { useState } from "react"; import { useForm } from "react-hook-form"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -15,6 +16,7 @@ export function LoginWithKey() { const [showKey, setShowKey] = useState(false); const [loading, setLoading] = useState(false); + const { t } = useTranslation("loginWithPrivkey.subtitle"); const { register, handleSubmit, @@ -52,19 +54,21 @@ export function LoginWithKey() {
-

Enter your Private Key

-

- Lume will put your private key to{" "} - - {storage.platform === "macos" - ? "Apple Keychain" - : storage.platform === "windows" - ? "Credential Manager" - : "Secret Service"} - - . -
- It will be secured by your OS. +

+ {t("loginWithPrivkey.title")} +

+

+ + Lume will put your private key to{" "} + + {storage.platform === "macos" + ? "Apple Keychain" + : storage.platform === "windows" + ? "Credential Manager" + : "Secret Service"} + + . It will be secured by your OS. +

@@ -107,7 +111,7 @@ export function LoginWithKey() { {loading ? ( ) : ( - "Continue" + t("global.continue") )} diff --git a/apps/desktop/src/routes/auth/login-nsecbunker.tsx b/apps/desktop/src/routes/auth/login-nsecbunker.tsx index 4679ab76..fb38b39d 100644 --- a/apps/desktop/src/routes/auth/login-nsecbunker.tsx +++ b/apps/desktop/src/routes/auth/login-nsecbunker.tsx @@ -5,6 +5,7 @@ import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; import { useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -15,6 +16,7 @@ export function LoginWithNsecbunker() { const [loading, setLoading] = useState(false); + const { t } = useTranslation(); const { register, handleSubmit, @@ -69,7 +71,7 @@ export function LoginWithNsecbunker() {

- Enter your nsecbunker token + {t("loginWithBunker.title")}

@@ -101,7 +103,7 @@ export function LoginWithNsecbunker() { {loading ? ( ) : ( - "Continue" + t("global.continue") )} diff --git a/apps/desktop/src/routes/auth/login-oauth.tsx b/apps/desktop/src/routes/auth/login-oauth.tsx index 84591e7d..60981a82 100644 --- a/apps/desktop/src/routes/auth/login-oauth.tsx +++ b/apps/desktop/src/routes/auth/login-oauth.tsx @@ -7,6 +7,7 @@ import { Window } from "@tauri-apps/api/window"; import { fetch } from "@tauri-apps/plugin-http"; import { useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -19,6 +20,7 @@ export function LoginWithOAuth() { const [loading, setLoading] = useState(false); + const { t } = useTranslation(); const { register, handleSubmit, @@ -130,7 +132,9 @@ export function LoginWithOAuth() {
-

Enter your Nostr Address

+

+ {t("loginWithAddress.title")} +

) : ( - "Continue" + t("global.continue") )}
diff --git a/apps/desktop/src/routes/auth/login.tsx b/apps/desktop/src/routes/auth/login.tsx index 63a6ca3d..a08c9c84 100644 --- a/apps/desktop/src/routes/auth/login.tsx +++ b/apps/desktop/src/routes/auth/login.tsx @@ -1,11 +1,14 @@ +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; export function LoginScreen() { + const { t } = useTranslation(); + return (
-

Welcome back, anon!

+

{t("login.title")}

@@ -13,13 +16,13 @@ export function LoginScreen() { to="/auth/login-oauth" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600" > - Login with Nostr Address + {t("login.loginWithAddress")} - Login with nsecBunker + {t("login.loginWithBunker")}
@@ -29,7 +32,7 @@ export function LoginScreen() {
- Or continue with + {t("login.or")}
@@ -38,13 +41,10 @@ export function LoginScreen() { to="/auth/login-key" className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900" > - Login with Private Key + {t("login.loginWithPrivkey")}

- Lume will put your Private Key in{" "} - Secure Storage depended - on your OS Platform. It will be secured by Password or Biometric - ID + {t("login.footer")}

diff --git a/apps/desktop/src/routes/auth/onboarding.tsx b/apps/desktop/src/routes/auth/onboarding.tsx index e771f621..d6e8ca00 100644 --- a/apps/desktop/src/routes/auth/onboarding.tsx +++ b/apps/desktop/src/routes/auth/onboarding.tsx @@ -8,6 +8,7 @@ import { requestPermission, } from "@tauri-apps/plugin-notification"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -16,6 +17,7 @@ export function OnboardingScreen() { const storage = useStorage(); const navigate = useNavigate(); + const [t] = useTranslation(); const [loading, setLoading] = useState(false); const [apiKey, setAPIKey] = useState(""); const [settings, setSettings] = useState({ @@ -91,10 +93,10 @@ export function OnboardingScreen() {

- You're almost ready to use Lume. + {t("onboardingSettings.title")}

- Let's start personalizing your experience. + {t("onboardingSettings.subtitle")}

@@ -107,10 +109,11 @@ export function OnboardingScreen() {
-

Push notification

+

+ {t("onboardingSettings.notification.title")} +

- Enabling push notifications will allow you to receive - notifications from Lume. + {t("onboardingSettings.notification.subtitle")}

@@ -123,10 +126,11 @@ export function OnboardingScreen() {
-

Low Power Mode

+

+ {t("onboardingSettings.lowPower.title")} +

- Limited relay connection and hide all media, sustainable for low - network environment. + {t("onboardingSettings.lowPower.subtitle")}

@@ -140,11 +144,10 @@ export function OnboardingScreen() {

- Translation (nostr.wine) + {t("onboardingSettings.translation.title")}

- Translate text to your preferred language, powered by Nostr - Wine. + {t("onboardingSettings.translation.subtitle")}

@@ -175,10 +178,7 @@ export function OnboardingScreen() { ) : null}
-

- There are many more settings you can configure from the - "Settings" screen. Be sure to visit it later. -

+

{t("onboardingSettings.footer")}

diff --git a/apps/desktop/src/routes/auth/welcome.tsx b/apps/desktop/src/routes/auth/welcome.tsx index 83789082..2ca0437a 100644 --- a/apps/desktop/src/routes/auth/welcome.tsx +++ b/apps/desktop/src/routes/auth/welcome.tsx @@ -1,6 +1,9 @@ +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; export function WelcomeScreen() { + const { t } = useTranslation(); + return (
@@ -12,10 +15,8 @@ export function WelcomeScreen() { alt="lume" className="w-2/3" /> -

- Lume is a magnificent client for Nostr to meet, explore -
- and freely share your thoughts with everyone. +

+ {t("welcome.title")}

@@ -23,19 +24,19 @@ export function WelcomeScreen() { to="/auth/create" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600" > - Join Nostr + {t("welcome.signup")} - Login + {t("welcome.login")}

- Before joining Nostr, you can take time to learn more about Nostr{" "} + {t("welcome.footer")}{" "}

@@ -95,7 +95,7 @@ export function ErrorScreen() {
- 3. Report this issue to Lume's Devs + 3. Report this issue to Lume

- While waiting for Lume's Devs to release the bug fixes, - you always can use other Nostr clients with your account: + While waiting for Lume release the bug fixes, you always can + use other Nostr clients with your account:

@@ -134,15 +134,15 @@ export function ErrorScreen() { - primal.net + nostter diff --git a/apps/desktop/src/routes/home/index.tsx b/apps/desktop/src/routes/home/index.tsx index 2fc80ce4..9af90aac 100644 --- a/apps/desktop/src/routes/home/index.tsx +++ b/apps/desktop/src/routes/home/index.tsx @@ -18,10 +18,13 @@ import { TutorialModal } from "@lume/ui/src/tutorial/modal"; import { COL_TYPES } from "@lume/utils"; import * as Tooltip from "@radix-ui/react-tooltip"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { VList } from "virtua"; export function HomeScreen() { + const { t } = useTranslation(); const { columns, vlistRef, addColumn } = useColumnContext(); + const [selectedIndex, setSelectedIndex] = useState(-1); const renderItem = (column: IColumn) => { @@ -124,7 +127,7 @@ export function HomeScreen() { - Move Left + {t("global.moveLeft")} @@ -151,7 +154,7 @@ export function HomeScreen() { - Move Right + {t("global.moveRight")} @@ -174,7 +177,7 @@ export function HomeScreen() { - New Column + {t("global.newColum")} diff --git a/apps/desktop/src/routes/relays/components/relayEventList.tsx b/apps/desktop/src/routes/relays/components/relayEventList.tsx index 88bc4923..bc637764 100644 --- a/apps/desktop/src/routes/relays/components/relayEventList.tsx +++ b/apps/desktop/src/routes/relays/components/relayEventList.tsx @@ -4,10 +4,13 @@ import { FETCH_LIMIT } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { VList } from "virtua"; export function RelayEventList({ relayUrl }: { relayUrl: string }) { const ark = useArk(); + + const { t } = useTranslation(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ["relay-events", relayUrl], @@ -37,14 +40,10 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) { if (!lastEvent) return; return lastEvent.created_at - 1; }, + select: (data) => data?.pages.flatMap((page) => page), refetchOnWindowFocus: false, }); - const allEvents = useMemo( - () => (data ? data.pages.flatMap((page) => page) : []), - [data], - ); - const renderItem = useCallback( (event: NDKEvent) => { switch (event.kind) { @@ -64,7 +63,7 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) { {status === "pending" ? ( ) : ( - allEvents.map((item) => renderItem(item)) + data.map((item) => renderItem(item)) )}
{hasNextPage ? ( @@ -79,7 +78,7 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) { ) : ( <> - Load more + {t("global.loading")} )} diff --git a/apps/desktop/src/routes/relays/components/relayItem.tsx b/apps/desktop/src/routes/relays/components/relayItem.tsx index 72754927..001c73a2 100644 --- a/apps/desktop/src/routes/relays/components/relayItem.tsx +++ b/apps/desktop/src/routes/relays/components/relayItem.tsx @@ -1,17 +1,20 @@ import { User, useRelaylist } from "@lume/ark"; import { PlusIcon, SearchIcon } from "@lume/icons"; import { normalizeRelayUrl } from "nostr-fetch"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; export function RelayItem({ url, users }: { url: string; users?: string[] }) { const domain = new URL(url).hostname; + + const { t } = useTranslation(); const { connectRelay } = useRelaylist(); return (
- Relay:{" "} + {t("global.relay")}:{" "} {url} @@ -39,7 +42,7 @@ export function RelayItem({ url, users }: { url: string; users?: string[] }) { className="inline-flex h-8 items-center justify-center gap-2 rounded-lg bg-neutral-100 px-2 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" > - Inspect + {t("global.inspect")} - -
-
- - Relay:{" "} - - - {key} - -
-
-
- {value.slice(0, 4).map((item) => ( - - ))} - {value.length > 4 ? ( -
- +{value.length} -
- ) : null} -
-
- ))} - - )} -
- ); -} diff --git a/apps/desktop/src/routes/relays/components/sidebar.tsx b/apps/desktop/src/routes/relays/components/sidebar.tsx index 89b49315..b92c2381 100644 --- a/apps/desktop/src/routes/relays/components/sidebar.tsx +++ b/apps/desktop/src/routes/relays/components/sidebar.tsx @@ -3,11 +3,13 @@ import { CancelIcon, LoaderIcon, RefreshIcon } from "@lume/icons"; import { cn } from "@lume/utils"; import { NDKKind, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk"; import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { RelayForm } from "./relayForm"; export function RelaySidebar({ className }: { className?: string }) { const ark = useArk(); + const { t } = useTranslation(); const { removeRelay } = useRelaylist(); const { status, data, isRefetching, refetch } = useQuery({ queryKey: ["relay-personal"], @@ -40,7 +42,7 @@ export function RelaySidebar({ className }: { className?: string }) { )} >
-

Connected relays

+

{t("relays.sidebar.title")}

) : !data.length ? (
-

Empty.

+

{t("relays.sidebar.empty")}

) : ( data.map((item) => ( diff --git a/apps/desktop/src/routes/relays/index.tsx b/apps/desktop/src/routes/relays/index.tsx index 222bf705..7e9a7af1 100644 --- a/apps/desktop/src/routes/relays/index.tsx +++ b/apps/desktop/src/routes/relays/index.tsx @@ -1,8 +1,11 @@ import { cn } from "@lume/utils"; +import { useTranslation } from "react-i18next"; import { NavLink, Outlet } from "react-router-dom"; import { RelaySidebar } from "./components/sidebar"; export function RelaysScreen() { + const { t } = useTranslation(); + return (
@@ -20,7 +23,7 @@ export function RelaysScreen() { ) } > - Global + {t("relays.global")} - Follows + {t("relays.follows")}
diff --git a/apps/desktop/src/routes/relays/url.tsx b/apps/desktop/src/routes/relays/url.tsx index 91238c58..7c5a7ee0 100644 --- a/apps/desktop/src/routes/relays/url.tsx +++ b/apps/desktop/src/routes/relays/url.tsx @@ -1,15 +1,16 @@ -import { ArrowLeftIcon, LoaderIcon } from "@lume/icons"; +import { LoaderIcon } from "@lume/icons"; import { NIP11 } from "@lume/types"; import { User } from "@lume/ui"; import { Suspense } from "react"; -import { Await, useLoaderData, useNavigate, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Await, useLoaderData, useParams } from "react-router-dom"; import { RelayEventList } from "./components/relayEventList"; export function RelayUrlScreen() { + const { t } = useTranslation(); const { url } = useParams(); const data: { relay?: { [key: string]: string } } = useLoaderData(); - const navigate = useNavigate(); const getSoftwareName = (url: string) => { const filename = url.substring(url.lastIndexOf("/") + 1); @@ -32,7 +33,7 @@ export function RelayUrlScreen() { fallback={
- Loading... + {t("global.loading")}
} > @@ -40,7 +41,7 @@ export function RelayUrlScreen() { resolve={data.relay} errorElement={
-

Could not load relay information 😬

+

{t("relays.relayView.empty")}

} > @@ -55,7 +56,7 @@ export function RelayUrlScreen() { {resolvedRelay.pubkey ? (
- Owner: + {t("relays.relayView.owner")}:
@@ -65,7 +66,7 @@ export function RelayUrlScreen() { {resolvedRelay.contact ? (
- Contact: + {t("relays.relayView.contact")}:
- Software: + {t("relays.relayView.software")}:
- Supported NIPs: + {t("relays.relayView.nips")}:
{resolvedRelay.supported_nips.map((item) => ( @@ -113,26 +114,24 @@ export function RelayUrlScreen() { {resolvedRelay.limitation ? (
- Limitation + {t("relays.relayView.limit")}
- {Object.keys(resolvedRelay.limitation).map( - (key, index) => { - return ( -
-

- {titleCase(key)}: -

-

- {resolvedRelay.limitation[key].toString()} -

-
- ); - }, - )} + {Object.keys(resolvedRelay.limitation).map((key) => { + return ( +
+

+ {titleCase(key)}: +

+

+ {resolvedRelay.limitation[key].toString()} +

+
+ ); + })}
) : null} @@ -144,10 +143,10 @@ export function RelayUrlScreen() { rel="noreferrer" className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium hover:bg-blue-600" > - Open payment website + {t("relays.relayView.payment")}
- You need to make a payment to connect this relay + {t("relays.relayView.paymentNote")}
) : null} diff --git a/apps/desktop/src/routes/settings/about.tsx b/apps/desktop/src/routes/settings/about.tsx index 784eb1fe..db0b386c 100644 --- a/apps/desktop/src/routes/settings/about.tsx +++ b/apps/desktop/src/routes/settings/about.tsx @@ -2,10 +2,12 @@ import { getVersion } from "@tauri-apps/api/app"; import { relaunch } from "@tauri-apps/plugin-process"; import { Update, check } from "@tauri-apps/plugin-updater"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { toast } from "sonner"; export function AboutScreen() { + const [t] = useTranslation(); const [version, setVersion] = useState(""); const [newUpdate, setNewUpdate] = useState(null); @@ -34,7 +36,7 @@ export function AboutScreen() {

Lume

- Version {version} + {t("settings.about.version")} {version}

@@ -44,7 +46,7 @@ export function AboutScreen() { onClick={() => checkUpdate()} className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600" > - Check for update + {t("settings.about.checkUpdate")} ) : ( )} { await storage.clearCache(); @@ -13,16 +15,18 @@ export function AdvancedSettingScreen() {
- Cache + {t("settings.advanced.cache.title")} +
+
+ {t("settings.advanced.cache.subtitle")}
-
Use for boost up nostr connection
diff --git a/apps/desktop/src/routes/settings/backup.tsx b/apps/desktop/src/routes/settings/backup.tsx index 129d46bc..aa095b44 100644 --- a/apps/desktop/src/routes/settings/backup.tsx +++ b/apps/desktop/src/routes/settings/backup.tsx @@ -3,11 +3,13 @@ import { EyeOffIcon } from "@lume/icons"; import { useStorage } from "@lume/storage"; import { nip19 } from "nostr-tools"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; export function BackupSettingScreen() { const ark = useArk(); const storage = useStorage(); + const [t] = useTranslation(); const [privkey, setPrivkey] = useState(null); const [showPassword, setShowPassword] = useState(false); @@ -29,7 +31,9 @@ export function BackupSettingScreen() {
{privkey ? (
-
Private key
+
+ {t("settings.backup.privkey.title")} +
removePrivkey()} className="mt-2 inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-red-200 dark:bg-red-800 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white" > - Remove private key + {t("settings.backup.privkey.button")}
) : null} diff --git a/apps/desktop/src/routes/settings/components/avatarUpload.tsx b/apps/desktop/src/routes/settings/components/avatarUpload.tsx index 54ae1618..601f43f6 100644 --- a/apps/desktop/src/routes/settings/components/avatarUpload.tsx +++ b/apps/desktop/src/routes/settings/components/avatarUpload.tsx @@ -1,10 +1,13 @@ import { useArk } from "@lume/ark"; import { LoaderIcon } from "@lume/icons"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; export function AvatarUpload({ setPicture }) { const ark = useArk(); + + const [t] = useTranslation(); const [loading, setLoading] = useState(false); const upload = async () => { @@ -36,7 +39,7 @@ export function AvatarUpload({ setPicture }) { {loading ? ( ) : ( - "Change avatar" + t("user.avatarButton") )} ); diff --git a/apps/desktop/src/routes/settings/components/contactCard.tsx b/apps/desktop/src/routes/settings/components/contactCard.tsx deleted file mode 100644 index 9af13390..00000000 --- a/apps/desktop/src/routes/settings/components/contactCard.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useArk } from "@lume/ark"; -import { EditIcon, LoaderIcon } from "@lume/icons"; -import { compactNumber } from "@lume/utils"; -import { useQuery } from "@tanstack/react-query"; -import { Link } from "react-router-dom"; - -export function ContactCard() { - const ark = useArk(); - const { status, data } = useQuery({ - queryKey: ["contacts"], - queryFn: async () => { - const contacts = await ark.getUserContacts(); - return contacts; - }, - refetchOnWindowFocus: false, - }); - - return ( -
- {status === "pending" ? ( -
- -
- ) : ( -
-

- {compactNumber.format(data.length)} -

-
-

- Contacts -

- - - Edit - -
-
- )} -
- ); -} diff --git a/apps/desktop/src/routes/settings/components/coverUpload.tsx b/apps/desktop/src/routes/settings/components/coverUpload.tsx index 88f1ebf8..bdeb7868 100644 --- a/apps/desktop/src/routes/settings/components/coverUpload.tsx +++ b/apps/desktop/src/routes/settings/components/coverUpload.tsx @@ -1,10 +1,13 @@ import { useArk } from "@lume/ark"; import { LoaderIcon } from "@lume/icons"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; export function CoverUpload({ setBanner }) { const ark = useArk(); + + const [t] = useTranslation(); const [loading, setLoading] = useState(false); const upload = async () => { @@ -36,7 +39,7 @@ export function CoverUpload({ setBanner }) { {loading ? ( ) : ( - "Change cover" + t("user.coverButton") )} ); diff --git a/apps/desktop/src/routes/settings/components/postCard.tsx b/apps/desktop/src/routes/settings/components/postCard.tsx deleted file mode 100644 index f828d4a0..00000000 --- a/apps/desktop/src/routes/settings/components/postCard.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { compactNumber } from "@lume/utils"; -import { useQuery } from "@tanstack/react-query"; -import { fetch } from "@tauri-apps/plugin-http"; -import { Link } from "react-router-dom"; - -export function PostCard() { - const ark = useArk(); - const { status, data } = useQuery({ - queryKey: ["user-stats", ark.account.pubkey], - queryFn: async ({ signal }: { signal: AbortSignal }) => { - const res = await fetch( - `https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`, - { - signal, - }, - ); - - if (!res.ok) { - throw new Error("Error"); - } - - return await res.json(); - }, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - staleTime: Infinity, - }); - - return ( -
- {status === "pending" ? ( -
- -
- ) : ( -
-

- {compactNumber.format( - data.stats[ark.account.pubkey].pub_note_count, - )} -

-
-

- Posts -

- - View - -
-
- )} -
- ); -} diff --git a/apps/desktop/src/routes/settings/components/profileCard.tsx b/apps/desktop/src/routes/settings/components/profileCard.tsx deleted file mode 100644 index 7b949df4..00000000 --- a/apps/desktop/src/routes/settings/components/profileCard.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useArk, useProfile } from "@lume/ark"; -import { EditIcon, LoaderIcon } from "@lume/icons"; -import { displayNpub } from "@lume/utils"; -import * as Avatar from "@radix-ui/react-avatar"; -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { minidenticon } from "minidenticons"; -import { nip19 } from "nostr-tools"; -import { Link } from "react-router-dom"; - -export function ProfileCard() { - const ark = useArk(); - const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent( - minidenticon(ark.account.pubkey, 90, 50), - )}`; - - const { isLoading, user } = useProfile(ark.account.pubkey); - - const copyNpub = async () => { - return await writeText(nip19.npubEncode(ark.account.pubkey)); - }; - - return ( -
- {isLoading ? ( -
- -
- ) : ( -
-
- - - - Edit - -
-
- - - - {ark.account.pubkey} - - -
-

- {user?.display_name || user?.name} -

-

- {user?.nip05 || displayNpub(ark.account.pubkey, 16)} -

-
-
-
- )} -
- ); -} diff --git a/apps/desktop/src/routes/settings/components/relayCard.tsx b/apps/desktop/src/routes/settings/components/relayCard.tsx deleted file mode 100644 index c170a2cc..00000000 --- a/apps/desktop/src/routes/settings/components/relayCard.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useArk } from "@lume/ark"; -import { EditIcon, LoaderIcon } from "@lume/icons"; -import { compactNumber } from "@lume/utils"; -import { useQuery } from "@tanstack/react-query"; -import { Link } from "react-router-dom"; - -export function RelayCard() { - const ark = useArk(); - - const { status, data } = useQuery({ - queryKey: ["relays", ark.account.pubkey], - queryFn: async () => { - const relays = await ark.getUserRelays({}); - return relays; - }, - refetchOnWindowFocus: false, - }); - - return ( -
- {status === "pending" ? ( -
- -
- ) : ( -
-

- {compactNumber.format(data?.relays?.length || 0)} -

-
-

- Relays -

- - - Edit - -
-
- )} -
- ); -} diff --git a/apps/desktop/src/routes/settings/components/walletConnectForm.tsx b/apps/desktop/src/routes/settings/components/walletConnectForm.tsx deleted file mode 100644 index 6553b72f..00000000 --- a/apps/desktop/src/routes/settings/components/walletConnectForm.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { useState } from "react"; -import { toast } from "sonner"; - -export function NWCForm({ setWalletConnectURL }) { - const ark = useArk(); - const storage = useStorage(); - - const [uri, setUri] = useState(""); - const [loading, setLoading] = useState(false); - - const submit = async () => { - try { - setLoading(true); - - if (!uri.startsWith("nostr+walletconnect:")) { - toast.error( - "Connect URI is required and must start with format nostr+walletconnect:, please check again", - ); - setLoading(false); - return; - } - - const uriObj = new URL(uri); - const params = new URLSearchParams(uriObj.search); - - if (params.has("relay") && params.has("secret")) { - await storage.createPrivkey(`${ark.account.pubkey}-nwc`, uri); - setWalletConnectURL(uri); - setLoading(false); - } else { - setLoading(false); - toast.error("Connect URI is not valid, please check again"); - return; - } - } catch (e) { - setLoading(false); - toast.error(String(e)); - } - }; - - return ( -
-