From 25ae4f22012d1eda0f40a3c4b9c52d697cbb8a3e Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 29 Jan 2024 09:12:44 +0700 Subject: [PATCH 1/8] feat: add multi-lang --- apps/desktop/package.json | 2 + apps/desktop/src/app.tsx | 24 ++-- apps/desktop/src/i18n.ts | 33 ++++++ .../src/routes/auth/create-address.tsx | 17 +-- apps/desktop/src/routes/auth/create-keys.tsx | 15 +-- apps/desktop/src/routes/auth/create.tsx | 18 +-- apps/desktop/src/routes/auth/login-key.tsx | 21 ++-- .../src/routes/auth/login-nsecbunker.tsx | 6 +- apps/desktop/src/routes/auth/login-oauth.tsx | 8 +- apps/desktop/src/routes/auth/login.tsx | 13 ++- apps/desktop/src/routes/auth/onboarding.tsx | 32 +++--- apps/desktop/src/routes/auth/welcome.tsx | 15 +-- packages/utils/src/constants.ts | 8 +- pnpm-lock.yaml | 43 +++++++ src-tauri/locales/en.json | 70 ++++++++++++ src-tauri/locales/ja.json | 3 + src-tauri/resources/.keep | 0 src-tauri/resources/config.toml | 108 ------------------ src-tauri/tauri.conf.json | 4 +- 19 files changed, 256 insertions(+), 184 deletions(-) create mode 100644 apps/desktop/src/i18n.ts create mode 100644 src-tauri/locales/en.json create mode 100644 src-tauri/locales/ja.json create mode 100644 src-tauri/resources/.keep delete mode 100644 src-tauri/resources/config.toml diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 785f5cfd..f5db5003 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -36,6 +36,7 @@ "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.17.19", "framer-motion": "^10.18.0", + "i18next": "^23.8.0", "jotai": "^2.6.3", "minidenticons": "^4.2.0", "nanoid": "^5.0.4", @@ -45,6 +46,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..61c6f437 --- /dev/null +++ b/apps/desktop/src/i18n.ts @@ -0,0 +1,33 @@ +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 { initReactI18next } from "react-i18next"; + +const enFilePath = await resolveResource("locales/en.json"); +const jaFilePath = await resolveResource("locales/ja.json"); + +const enLocale = JSON.parse(await readTextFile(enFilePath)); +const jaLocale = JSON.parse(await readTextFile(jaFilePath)); + +const osLocale = (await locale()).slice(0, 2); + +const resources = { + en: { + translation: enLocale, + }, + ja: { + translation: jaLocale, + }, +}; + +i18n.use(initReactI18next).init({ + lng: osLocale, + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, + resources, +}); + +export default i18n; 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..7e90e3e8 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,9 +54,14 @@ export function LoginWithKey() {
-

Enter your Private Key

-

- Lume will put your private key to{" "} +

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

+ + Lume will put your private key to {storage.platform === "macos" ? "Apple Keychain" @@ -62,10 +69,8 @@ export function LoginWithKey() { ? "Credential Manager" : "Secret Service"} - . -
- It will be secured by your OS. -

+ . It will be secured by your OS. +
) : ( - "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..016c5249 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,7 +41,7 @@ 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{" "} 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")}{" "} =16.17.0'} dev: false + /i18next@23.8.0: + resolution: {integrity: sha512-1H+39doU9dQZrRprpnZ2aZetbX9I1N3bM/YGHN/ZkMJ//wJqrxDEqgI5mmSsh/rglsFBiNxI6UtFZfUO2A6XbA==} + dependencies: + '@babel/runtime': 7.23.9 + dev: false + /iconv-lite@0.4.23: resolution: {integrity: sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==} engines: {node: '>=0.10.0'} @@ -6970,6 +6988,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-i18next@14.0.1(i18next@23.8.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-TMV8hFismBmpMdIehoFHin/okfvgjFhp723RYgIqB4XyhDobVMyukyM3Z8wtTRmajyFMZrBl/OaaXF2P6WjUAw==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.23.9 + html-parse-stringify: 3.0.1 + i18next: 23.8.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-remove-scroll-bar@2.3.4(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} @@ -8594,6 +8632,11 @@ packages: vite: 5.0.12(@types/node@20.11.8) dev: false + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + /volar-service-css@0.0.17(@volar/language-service@1.11.1): resolution: {integrity: sha512-bEDJykygMzn2+a9ud6KwZZLli9eqarxApAXZuf2CqJJh6Trw1elmbBCo9SlPfqMrIhpFnwV0Sa+Xoc9x5WPeGw==} peerDependencies: diff --git a/src-tauri/locales/en.json b/src-tauri/locales/en.json new file mode 100644 index 00000000..61b32f5e --- /dev/null +++ b/src-tauri/locales/en.json @@ -0,0 +1,70 @@ +{ + "global": { + "continue": "Continue", + "loading": "Loading" + }, + "welcome": { + "title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.", + "signup": "Join Nostr", + "login": "Login", + "footer": "Before joining Nostr, you can take time to learn more about Nostr" + }, + "login": { + "title": "Welcome back, anon!", + "loginWithAddress": "Login with Nostr Address", + "loginWithBunker": "Login with nsecBunker", + "or": "Or continue with", + "loginWithPrivkey": "Login with Private Key" + }, + "loginWithAddress": { + "title": "Enter your Nostr Address" + }, + "loginWithBunker": { + "title": "Enter your nsecbunker token" + }, + "loginWithPrivkey": { + "title": "Enter your Private Key", + "subtitle": "Lume will put your private key to <1>{{service}}.\nIt will be secured by your OS." + }, + "signup": { + "title": "Let's Get Started", + "subtitle": "Choose one of methods below to create your account", + "selfManageMethod": "Self-Managed", + "selfManageMethodDescription": "You create your keys and keep them safe.", + "providerMethod": "Managed by Provider", + "providerMethodDescription": "A 3rd party provider will handle your sign in keys for you." + }, + "signupWithSelfManage": { + "title": "This is your new Account Key", + "subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.", + "confirm1": "I understand the risk of lost private key.", + "confirm2": "I will make sure keep it safe and not sharing with anyone.", + "confirm3": "I understand I cannot recover private key.", + "button": "Save key & Continue" + }, + "signupWithProvider": { + "title": "Let's set up your account on Nostr", + "username": "Username *", + "chooseProvider": "Choose a Provider", + "usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account", + "email": "Backup Email (optional)", + "emailFooter": "Use for recover your account if you lose your password" + }, + "onboardingSettings": { + "title": "You're almost ready to use Lume.", + "subtitle": "Let's start personalizing your experience.", + "notification": { + "title": "Push notification", + "subtitle": "Enabling push notifications will allow you to receive notifications from Lume." + }, + "lowPower": { + "title": "Low Power Mode", + "subtitle": "Limited relay connection and hide all media, sustainable for low network environment." + }, + "translation": { + "title": "Translation (nostr.wine)", + "subtitle": "Translate text to your preferred language, powered by Nostr Wine." + }, + "footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later." + } +} diff --git a/src-tauri/locales/ja.json b/src-tauri/locales/ja.json new file mode 100644 index 00000000..1ab7f45d --- /dev/null +++ b/src-tauri/locales/ja.json @@ -0,0 +1,3 @@ +{ + "title": "こんにちは世界" +} diff --git a/src-tauri/resources/.keep b/src-tauri/resources/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src-tauri/resources/config.toml b/src-tauri/resources/config.toml deleted file mode 100644 index 8cf06f1d..00000000 --- a/src-tauri/resources/config.toml +++ /dev/null @@ -1,108 +0,0 @@ -[info] -relay_url = "" -name = "depot" -description = "Nostr Relay inside Lume. Powered by nostr-rs-relay" -pubkey = "" -favicon = "favicon.ico" -relay_icon = "https://example.test/img.png" -#contact = "mailto:contact@example.com" - -[diagnostics] -#tracing = false - -[database] -engine = "sqlite" -data_directory = "." -max_conn = 8 -min_conn = 0 - -[logging] -#folder_path = "./log" -#file_prefix = "nostr-relay" - -[network] -address = "0.0.0.0" -port = 6090 -#remote_ip_header = "x-forwarded-for" -#remote_ip_header = "cf-connecting-ip" -#ping_interval = 300 - -[options] -reject_future_seconds = 1800 - -[limits] -messages_per_sec = 10 -subscriptions_per_min = 10 -limit_scrapers = false - -[authorization] -pubkey_whitelist = [] -nip42_auth = true -nip42_dms = true - -[verified_users] -mode = "passive" -#domain_blacklist = ["wellorder.net"] -#domain_whitelist = ["example.com"] -verify_expiration = "1 week" -#verify_update_frequency = "24 hours" -max_consecutive_failures = 3 - -[grpc] -# gRPC interfaces for externalized decisions and other extensions to -# functionality. -# -# Events can be authorized through an external service, by providing -# the URL below. In the event the server is not accessible, events -# will be permitted. The protobuf3 schema used is available in -# `proto/nauthz.proto`. -# event_admission_server = "http://[::1]:50051" - -# If the event admission server denies writes -# in any case (excluding spam filtering). -# This is reflected in the relay information document. -# restricts_write = true - -[pay_to_relay] -# Enable pay to relay -#enabled = false - -# The cost to be admitted to relay -#admission_cost = 4200 - -# The cost in sats per post -#cost_per_event = 0 - -# Url of lnbits api -#node_url = "" - -# LNBits api secret -#api_secret = "" - -# Nostr direct message on signup -#direct_message=false - -# Terms of service -#terms_message = """ -#This service (and supporting services) are provided "as is", without warranty of any kind, express or implied. -# -#By using this service, you agree: -#* Not to engage in spam or abuse the relay service -#* Not to disseminate illegal content -#* That requests to delete content cannot be guaranteed -#* To use the service in compliance with all applicable laws -#* To grant necessary rights to your content for unlimited time -#* To be of legal age and have capacity to use this service -#* That the service may be terminated at any time without notice -#* That the content you publish may be removed at any time without notice -#* To have your IP address collected to detect abuse or misuse -#* To cooperate with the relay to combat abuse or misuse -#* You may be exposed to content that you might find triggering or distasteful -#* The relay operator is not liable for content produced by users of the relay -#""" - -# Whether or not new sign ups should be allowed -#sign_ups = false - -# optional if `direct_message=false` -#secret_key = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bc04f7e0..7aa50cfa 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -34,7 +34,7 @@ }, "shell": { "open": true, - "scope": [{ "name": "bin/depot", "sidecar": true, "args": true }] + "scope": [] }, "updater": { "endpoints": [ @@ -51,7 +51,7 @@ "depends": [] }, "externalBin": [], - "resources": ["resources/*"], + "resources": ["resources/*", "./locales/*"], "icon": [ "icons/32x32.png", "icons/128x128.png", From b97676dd3e5da48debc58686dd0d046f2bfcf8bd Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 29 Jan 2024 09:30:33 +0700 Subject: [PATCH 2/8] feat: add translation to relay screen --- apps/desktop/src/routes/home/index.tsx | 9 +- .../relays/components/relayEventList.tsx | 13 ++- .../routes/relays/components/relayItem.tsx | 7 +- .../routes/relays/components/relayList.tsx | 91 ------------------- .../src/routes/relays/components/sidebar.tsx | 6 +- apps/desktop/src/routes/relays/index.tsx | 7 +- apps/desktop/src/routes/relays/url.tsx | 57 ++++++------ src-tauri/locales/en.json | 25 ++++- 8 files changed, 78 insertions(+), 137 deletions(-) delete mode 100644 apps/desktop/src/routes/relays/components/relayList.tsx 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/src-tauri/locales/en.json b/src-tauri/locales/en.json index 61b32f5e..0127616a 100644 --- a/src-tauri/locales/en.json +++ b/src-tauri/locales/en.json @@ -1,7 +1,12 @@ { "global": { + "relay": "Relay", "continue": "Continue", - "loading": "Loading" + "loading": "Loading", + "moveLeft": "Move Left", + "moveRight": "Move Right", + "newColumn": "New Column", + "inspect": "Inspect" }, "welcome": { "title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.", @@ -66,5 +71,23 @@ "subtitle": "Translate text to your preferred language, powered by Nostr Wine." }, "footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later." + }, + "relays": { + "global": "Global", + "follows": "Follows", + "sidebar": { + "title": "Connected relays", + "empty": "Empty." + }, + "relayView": { + "empty": "Could not load relay information 😬", + "owner": "Owner", + "contact": "Contact", + "software": "Software", + "nips": "Supported NIPs", + "limit": "Limitation", + "payment": "Open payment website", + "paymentNote": "You need to make a payment to connect this relay" + } } } From 698bd78684f855209ce8019854a610d3e2349bb3 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 29 Jan 2024 10:22:55 +0700 Subject: [PATCH 3/8] feat: migrate note component to i18n --- packages/ark/package.json | 1 + .../ark/src/components/note/buttons/pin.tsx | 7 ++- .../ark/src/components/note/buttons/reply.tsx | 5 ++- .../src/components/note/buttons/repost.tsx | 8 ++-- .../ark/src/components/note/buttons/zap.tsx | 24 +++++----- packages/ark/src/components/note/child.tsx | 7 +-- .../ark/src/components/note/mentions/note.tsx | 6 ++- .../ark/src/components/note/mentions/user.tsx | 8 ++-- packages/ark/src/components/note/menu.tsx | 20 +++++---- packages/ark/src/components/note/nip89.tsx | 10 +++-- .../src/components/note/primitives/reply.tsx | 6 ++- .../src/components/note/primitives/repost.tsx | 10 ++--- packages/ark/src/components/note/thread.tsx | 4 +- packages/ark/src/components/user/nip05.tsx | 2 +- packages/ark/src/hooks/useProfile.ts | 3 +- packages/ui/package.json | 1 + pnpm-lock.yaml | 6 +++ src-tauri/locales/en.json | 44 +++++++++++++++++++ 18 files changed, 125 insertions(+), 47 deletions(-) diff --git a/packages/ark/package.json b/packages/ark/package.json index 95aafc8f..4e14134d 100644 --- a/packages/ark/package.json +++ b/packages/ark/package.json @@ -32,6 +32,7 @@ "re-resizable": "^6.9.11", "react": "^18.2.0", "react-currency-input-field": "^3.6.14", + "react-i18next": "^14.0.1", "react-router-dom": "^6.21.3", "react-string-replace": "^1.1.1", "sonner": "^1.3.1", diff --git a/packages/ark/src/components/note/buttons/pin.tsx b/packages/ark/src/components/note/buttons/pin.tsx index 4b8f8848..6125f4e3 100644 --- a/packages/ark/src/components/note/buttons/pin.tsx +++ b/packages/ark/src/components/note/buttons/pin.tsx @@ -1,11 +1,14 @@ import { PinIcon } from "@lume/icons"; import { COL_TYPES } from "@lume/utils"; import * as Tooltip from "@radix-ui/react-tooltip"; +import { useTranslation } from "react-i18next"; import { useColumnContext } from "../../column/provider"; import { useNoteContext } from "../provider"; export function NotePin() { const event = useNoteContext(); + + const { t } = useTranslation(); const { addColumn } = useColumnContext(); return ( @@ -24,12 +27,12 @@ export function NotePin() { className="inline-flex items-center justify-center gap-2 pl-2 pr-3 text-sm font-medium rounded-full h-7 w-max bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:bg-neutral-900" > - Pin + {t("note.buttons.pin")} - Pin note + {t("note.buttons.pinTooltip")} diff --git a/packages/ark/src/components/note/buttons/reply.tsx b/packages/ark/src/components/note/buttons/reply.tsx index 26b1d7e7..1371e4ac 100644 --- a/packages/ark/src/components/note/buttons/reply.tsx +++ b/packages/ark/src/components/note/buttons/reply.tsx @@ -1,5 +1,6 @@ import { ReplyIcon } from "@lume/icons"; import * as Tooltip from "@radix-ui/react-tooltip"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useNoteContext } from "../provider"; @@ -7,6 +8,8 @@ export function NoteReply() { const event = useNoteContext(); const navigate = useNavigate(); + const { t } = useTranslation(); + return ( @@ -21,7 +24,7 @@ export function NoteReply() { - View thread + {t("note.menu.viewThread")} diff --git a/packages/ark/src/components/note/buttons/repost.tsx b/packages/ark/src/components/note/buttons/repost.tsx index 8f5218a9..9cc7f07e 100644 --- a/packages/ark/src/components/note/buttons/repost.tsx +++ b/packages/ark/src/components/note/buttons/repost.tsx @@ -5,6 +5,7 @@ import * as Tooltip from "@radix-ui/react-tooltip"; import { useSetAtom } from "jotai"; import { nip19 } from "nostr-tools"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useNoteContext } from "../provider"; @@ -13,6 +14,7 @@ export function NoteRepost() { const setEditorValue = useSetAtom(editorValueAtom); const setIsEditorOpen = useSetAtom(editorAtom); + const [t] = useTranslation(); const [loading, setLoading] = useState(false); const [isRepost, setIsRepost] = useState(false); const [open, setOpen] = useState(false); @@ -81,7 +83,7 @@ export function NoteRepost() { - Repost + {t("note.buttons.repost")} @@ -96,7 +98,7 @@ export function NoteRepost() { 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" > - Repost + {t("note.buttons.repost")} @@ -106,7 +108,7 @@ export function NoteRepost() { 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" > - Quote + {t("note.buttons.quote")} diff --git a/packages/ark/src/components/note/buttons/zap.tsx b/packages/ark/src/components/note/buttons/zap.tsx index 90073c6e..c27e3505 100644 --- a/packages/ark/src/components/note/buttons/zap.tsx +++ b/packages/ark/src/components/note/buttons/zap.tsx @@ -8,6 +8,7 @@ import * as Tooltip from "@radix-ui/react-tooltip"; import { QRCodeSVG } from "qrcode.react"; import { useState } from "react"; import CurrencyInput from "react-currency-input-field"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useProfile } from "../../../hooks/useProfile"; import { useNoteContext } from "../provider"; @@ -23,6 +24,7 @@ export function NoteZap() { const [isLoading, setIsLoading] = useState(false); const [invoice, setInvoice] = useState(null); + const { t } = useTranslation(); const { user } = useProfile(event.pubkey); const createZapRequest = async (instant?: boolean) => { @@ -99,7 +101,7 @@ export function NoteZap() { - Zap + {t("note.zap.tooltip")} @@ -124,7 +126,7 @@ export function NoteZap() { - Zap + {t("note.zap.tooltip")} @@ -145,7 +147,7 @@ export function NoteZap() {
- Send zap to{" "} + {t("note.zap.modalTitle")}{" "} {user?.name || user?.displayName || displayNpub(event.pubkey, 16)} @@ -217,7 +219,7 @@ export function NoteZap() { autoComplete="off" autoCorrect="off" autoCapitalize="off" - placeholder="Enter message (optional)" + placeholder={t("note.zap.messagePlaceholder")} className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400" />
@@ -227,10 +229,10 @@ export function NoteZap() { className="inline-flex items-center justify-center w-full pb-[2px] font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800" > {isCompleted - ? "Zapped" + ? t("note.zap.buttonFinish") : isLoading - ? "Processing..." - : "Zap"} + ? t("note.zap.buttonLoading") + : t("note.zap.zap")}
@@ -241,11 +243,11 @@ export function NoteZap() {
-

Scan to zap

+

+ {t("note.zap.invoiceButton")} +

- You must use Bitcoin wallet which support Lightning -
- such as: Blue Wallet, Bitkit, Phoenix,... + {t("note.zap.invoiceFooter")}
diff --git a/packages/ark/src/components/note/child.tsx b/packages/ark/src/components/note/child.tsx index 10753543..ef839800 100644 --- a/packages/ark/src/components/note/child.tsx +++ b/packages/ark/src/components/note/child.tsx @@ -1,7 +1,7 @@ import { NOSTR_MENTIONS } from "@lume/utils"; import { nanoid } from "nanoid"; -import { nip19 } from "nostr-tools"; import { ReactNode, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import reactStringReplace from "react-string-replace"; import { useEvent } from "../../hooks/useEvent"; @@ -13,6 +13,7 @@ export function NoteChild({ eventId, isRoot, }: { eventId: string; isRoot?: boolean }) { + const { t } = useTranslation(); const { isLoading, isError, data } = useEvent(eventId); const richContent = useMemo(() => { @@ -91,7 +92,7 @@ export function NoteChild({ return (
- Failed to fetch event + {t("note.error")}
); @@ -111,7 +112,7 @@ export function NoteChild({
- {isRoot ? "posted:" : "replied:"} + {isRoot ? t("note.posted") : t("note.replied")}:
diff --git a/packages/ark/src/components/note/mentions/note.tsx b/packages/ark/src/components/note/mentions/note.tsx index 4a6b2172..14e0a354 100644 --- a/packages/ark/src/components/note/mentions/note.tsx +++ b/packages/ark/src/components/note/mentions/note.tsx @@ -1,6 +1,7 @@ import { PinIcon } from "@lume/icons"; import { COL_TYPES, NOSTR_MENTIONS } from "@lume/utils"; import { ReactNode, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import reactStringReplace from "react-string-replace"; import { useEvent } from "../../../hooks/useEvent"; @@ -13,6 +14,7 @@ export function MentionNote({ eventId, openable = true, }: { eventId: string; openable?: boolean }) { + const { t } = useTranslation(); const { addColumn } = useColumnContext(); const { isLoading, isError, data } = useEvent(eventId); @@ -98,7 +100,7 @@ export function MentionNote({ contentEditable={false} className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900" > - Failed to fetch event. + {t("note.error")}
); } @@ -127,7 +129,7 @@ export function MentionNote({ to={`/events/${data.id}`} className="text-sm text-blue-500 hover:text-blue-600" > - Show more + {t("note.showMore")} diff --git a/packages/ark/src/components/note/menu.tsx b/packages/ark/src/components/note/menu.tsx index 5f7fd79f..75f2955d 100644 --- a/packages/ark/src/components/note/menu.tsx +++ b/packages/ark/src/components/note/menu.tsx @@ -5,6 +5,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { nip19 } from "nostr-tools"; import { type EventPointer } from "nostr-tools/lib/types/nip19"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { useColumnContext } from "../column/provider"; @@ -13,7 +14,10 @@ import { useNoteContext } from "./provider"; export function NoteMenu() { const event = useNoteContext(); const navigate = useNavigate(); + + const { t } = useTranslation(); const { addColumn } = useColumnContext(); + const [open, setOpen] = useState(false); const copyID = async () => { @@ -67,7 +71,7 @@ export function NoteMenu() { onClick={() => copyLink()} 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" > - View thread + {t("note.menu.viewThread")} @@ -76,7 +80,7 @@ export function NoteMenu() { onClick={() => navigate(`/events/${event.id}`)} 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" > - Copy shareable link + {t("note.menu.copyLink")} @@ -85,7 +89,7 @@ export function NoteMenu() { onClick={() => copyID()} 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" > - Copy note ID + {t("note.menu.copyNoteId")} @@ -94,7 +98,7 @@ export function NoteMenu() { onClick={() => copyNpub()} 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" > - Copy author ID + {t("note.menu.copyAuthorId")} @@ -102,7 +106,7 @@ export function NoteMenu() { to={`/users/${event.pubkey}`} 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" > - View author + {t("note.menu.viewAuthor")} @@ -117,7 +121,7 @@ export function NoteMenu() { } 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" > - Pin author + {t("note.menu.pinAuthor")} @@ -127,7 +131,7 @@ export function NoteMenu() { onClick={() => copyRaw()} 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" > - Copy raw event + {t("note.menu.copyRaw")} @@ -136,7 +140,7 @@ export function NoteMenu() { onClick={muteUser} 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" > - Mute + {t("note.menu.mute")} diff --git a/packages/ark/src/components/note/nip89.tsx b/packages/ark/src/components/note/nip89.tsx index c747b2e9..0399a68b 100644 --- a/packages/ark/src/components/note/nip89.tsx +++ b/packages/ark/src/components/note/nip89.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { useArk } from "../../hooks/useArk"; import { AppHandler } from "./appHandler"; import { useNoteContext } from "./provider"; @@ -7,6 +8,7 @@ export function NIP89({ className }: { className?: string }) { const ark = useArk(); const event = useNoteContext(); + const { t } = useTranslation(); const { isLoading, isError, data } = useQuery({ queryKey: ["app-recommend", event.id], queryFn: () => { @@ -33,7 +35,7 @@ export function NIP89({ className }: { className?: string }) {

- Lume isn't support this event + {t("nip89.unsupported")}

{event.kind} @@ -41,10 +43,10 @@ export function NIP89({ className }: { className?: string }) {

- Open with + {t("nip89.openWith")} - {data.map((item, index) => ( - + {data.map((item) => ( + ))}
diff --git a/packages/ark/src/components/note/primitives/reply.tsx b/packages/ark/src/components/note/primitives/reply.tsx index a1b5e461..0aa447b9 100644 --- a/packages/ark/src/components/note/primitives/reply.tsx +++ b/packages/ark/src/components/note/primitives/reply.tsx @@ -3,6 +3,7 @@ import { NDKEventWithReplies } from "@lume/types"; import { cn } from "@lume/utils"; import * as Collapsible from "@radix-ui/react-collapsible"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Note } from ".."; import { ChildReply } from "./childReply"; @@ -11,6 +12,7 @@ export function Reply({ }: { event: NDKEventWithReplies; }) { + const [t] = useTranslation(); const [open, setOpen] = useState(false); return ( @@ -30,7 +32,9 @@ export function Reply({ className={cn("size-5", open ? "rotate-180 transform" : "")} /> {`${event.replies?.length} ${ - event.replies?.length === 1 ? "reply" : "replies" + event.replies?.length === 1 + ? t("note.reply.single") + : t("note.reply.plural") }`}
diff --git a/packages/ark/src/components/note/primitives/repost.tsx b/packages/ark/src/components/note/primitives/repost.tsx index 4d95a018..e2ffb0ee 100644 --- a/packages/ark/src/components/note/primitives/repost.tsx +++ b/packages/ark/src/components/note/primitives/repost.tsx @@ -2,6 +2,7 @@ import { RepostIcon } from "@lume/icons"; import { cn } from "@lume/utils"; import { NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk"; import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { Note } from ".."; import { useArk } from "../../../hooks/useArk"; import { User } from "../../user"; @@ -12,6 +13,7 @@ export function RepostNote({ }: { event: NDKEvent; className?: string }) { const ark = useArk(); + const { t } = useTranslation(); const { isLoading, isError, @@ -51,7 +53,7 @@ export function RepostNote({
- reposted + {t("note.reposted")}
@@ -59,10 +61,6 @@ export function RepostNote({

Failed to get event

-

- You can consider enable Outbox in Settings for better event - discovery. -

@@ -85,7 +83,7 @@ export function RepostNote({
- reposted + {t("note.reposted")}
diff --git a/packages/ark/src/components/note/thread.tsx b/packages/ark/src/components/note/thread.tsx index 05e0e395..a112ad9b 100644 --- a/packages/ark/src/components/note/thread.tsx +++ b/packages/ark/src/components/note/thread.tsx @@ -1,5 +1,6 @@ import { PinIcon } from "@lume/icons"; import { COL_TYPES, cn } from "@lume/utils"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { Note } from "."; import { useArk } from "../../hooks/useArk"; @@ -18,6 +19,7 @@ export function NoteThread({ tags: event.tags, }); + const { t } = useTranslation(); const { addColumn } = useColumnContext(); if (!thread) return null; @@ -36,7 +38,7 @@ export function NoteThread({ to={`/events/${thread?.rootEventId || thread?.replyEventId}`} className="self-start text-blue-500 hover:text-blue-600" > - Show thread + {t("note.showThread")} {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" } } From 23482531c573e10904bd027bff5eb5effdc230ce Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 29 Jan 2024 14:57:00 +0700 Subject: [PATCH 5/8] feat: migrate settings screen to i18n --- apps/desktop/src/router.tsx | 9 -- apps/desktop/src/routes/settings/about.tsx | 8 +- apps/desktop/src/routes/settings/advanced.tsx | 10 +- apps/desktop/src/routes/settings/backup.tsx | 8 +- .../settings/components/avatarUpload.tsx | 5 +- .../settings/components/contactCard.tsx | 45 -------- .../settings/components/coverUpload.tsx | 5 +- .../routes/settings/components/postCard.tsx | 60 ---------- .../settings/components/profileCard.tsx | 77 ------------- .../routes/settings/components/relayCard.tsx | 46 -------- .../settings/components/walletConnectForm.tsx | 62 ----------- .../routes/settings/components/zapCard.tsx | 51 --------- .../src/routes/settings/editContact.tsx | 34 ------ apps/desktop/src/routes/settings/general.tsx | 104 ++++++------------ apps/desktop/src/routes/settings/nwc.tsx | 16 +-- apps/desktop/src/routes/settings/profile.tsx | 25 ++--- packages/ui/src/layouts/settings.tsx | 17 ++- src-tauri/locales/en.json | 83 +++++++++++++- 18 files changed, 175 insertions(+), 490 deletions(-) delete mode 100644 apps/desktop/src/routes/settings/components/contactCard.tsx delete mode 100644 apps/desktop/src/routes/settings/components/postCard.tsx delete mode 100644 apps/desktop/src/routes/settings/components/profileCard.tsx delete mode 100644 apps/desktop/src/routes/settings/components/relayCard.tsx delete mode 100644 apps/desktop/src/routes/settings/components/walletConnectForm.tsx delete mode 100644 apps/desktop/src/routes/settings/components/zapCard.tsx delete mode 100644 apps/desktop/src/routes/settings/editContact.tsx 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/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 ( -
-