diff --git a/package.json b/package.json
index 2532f3c1..4be52717 100644
--- a/package.json
+++ b/package.json
@@ -49,11 +49,13 @@
"dockview-react": "8.2.0",
"emoji-mart": "5.6.0",
"flexlayout-react": "0.10.8",
+ "i18next": "^26.4.2",
"lucide-react": "1.34.0",
"mdast-util-from-markdown": "2.0.3",
"nostr-tools": "2.25.0",
"react": "19.2.8",
"react-dom": "19.2.8",
+ "react-i18next": "^17.0.13",
"react-markdown": "10.1.0",
"remark-breaks": "4.0.0",
"remark-gfm": "4.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d64a91f9..cdcafed6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -53,6 +53,9 @@ importers:
flexlayout-react:
specifier: 0.10.8
version: 0.10.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ i18next:
+ specifier: ^26.4.2
+ version: 26.4.2(typescript@7.0.2)
lucide-react:
specifier: 1.34.0
version: 1.34.0(react@19.2.8)
@@ -68,6 +71,9 @@ importers:
react-dom:
specifier: 19.2.8
version: 19.2.8(react@19.2.8)
+ react-i18next:
+ specifier: ^17.0.13
+ version: 17.0.13(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.2.18)(react@19.2.8)
@@ -961,9 +967,20 @@ packages:
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+ html-parse-stringify@4.0.1:
+ resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
+ i18next@26.4.2:
+ resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==}
+ peerDependencies:
+ typescript: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
@@ -1343,6 +1360,22 @@ packages:
peerDependencies:
react: ^19.2.8
+ react-i18next@17.0.13:
+ resolution: {integrity: sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q==}
+ peerDependencies:
+ i18next: '>= 26.2.0'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ typescript: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+
react-markdown@10.1.0:
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
peerDependencies:
@@ -2215,8 +2248,14 @@ snapshots:
dependencies:
'@types/hast': 3.0.5
+ html-parse-stringify@4.0.1: {}
+
html-url-attributes@3.0.1: {}
+ i18next@26.4.2(typescript@7.0.2):
+ optionalDependencies:
+ typescript: 7.0.2
+
inline-style-parser@0.2.7: {}
is-alphabetical@2.0.1: {}
@@ -2752,6 +2791,17 @@ snapshots:
react: 19.2.8
scheduler: 0.27.0
+ react-i18next@17.0.13(i18next@26.4.2(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2):
+ dependencies:
+ '@babel/runtime': 7.29.7
+ html-parse-stringify: 4.0.1
+ i18next: 26.4.2(typescript@7.0.2)
+ react: 19.2.8
+ use-sync-external-store: 1.6.0(react@19.2.8)
+ optionalDependencies:
+ react-dom: 19.2.8(react@19.2.8)
+ typescript: 7.0.2
+
react-markdown@10.1.0(@types/react@19.2.18)(react@19.2.8):
dependencies:
'@types/hast': 3.0.5
diff --git a/src/app/LanguageSettings.tsx b/src/app/LanguageSettings.tsx
new file mode 100644
index 00000000..f20743bd
--- /dev/null
+++ b/src/app/LanguageSettings.tsx
@@ -0,0 +1,49 @@
+import { useTranslation } from "react-i18next";
+import {
+ appLocales,
+ currentLocale,
+ setAppLocale,
+ type AppLocale,
+} from "../features/localization/service";
+
+export function LanguageSettings() {
+ const { t } = useTranslation();
+ return (
+
+
+ {t("language.title")}
+
+
+
+
+ {t("language.description")}
+
+
+
+
+ );
+}
diff --git a/src/app/Settings.tsx b/src/app/Settings.tsx
index 04a899d5..1850747a 100644
--- a/src/app/Settings.tsx
+++ b/src/app/Settings.tsx
@@ -1,6 +1,13 @@
import { useEffect, useState, useSyncExternalStore } from "react";
import { RecoveryScreen } from "./RecoveryScreen";
-import { Blocks, Settings2, UserRound, Palette, Bell } from "lucide-react";
+import {
+ Blocks,
+ Settings2,
+ UserRound,
+ Palette,
+ Bell,
+ Languages,
+} from "lucide-react";
import type { PluginManager } from "../plugins/manager";
import type { Communities } from "../features/communities/service";
import { PluginImport } from "./PluginImport";
@@ -8,14 +15,17 @@ import { ProfileSettings } from "./ProfileSettings";
import type { Appearance } from "../shared/theme/service";
import { AppearanceSettings } from "./AppearanceSettings";
+import { LanguageSettings } from "./LanguageSettings";
+import { useTranslation } from "react-i18next";
import { NotificationSettings } from "./NotificationSettings";
import type { NotificationsService } from "../features/notifications/service";
const sections = [
- { id: "profile", label: "Profile", icon: UserRound },
- { id: "plugins", label: "Plugins", icon: Blocks },
- { id: "appearance", label: "Appearance", icon: Palette },
- { id: "notifications", label: "Notifications", icon: Bell },
+ { id: "profile", label: "settings.profile", icon: UserRound },
+ { id: "plugins", label: "settings.plugins", icon: Blocks },
+ { id: "appearance", label: "settings.appearance", icon: Palette },
+ { id: "notifications", label: "settings.notifications", icon: Bell },
+ { id: "language", label: "settings.language", icon: Languages },
] as const;
export function Settings({
@@ -35,6 +45,7 @@ export function Settings({
| undefined;
onSection?: (section: string) => void;
}) {
+ const { t } = useTranslation();
const [selected, setSelected] =
useState<(typeof sections)[number]["id"]>("profile");
const requestedSection =
@@ -68,13 +79,13 @@ export function Settings({
id="settings-title"
className="m-0 text-3xl font-medium tracking-tight"
>
- Settings
+ {t("settings.title")}
@@ -104,6 +115,9 @@ export function Settings({
+
+
+
- Skip to content
+ {t("shell.skipToContent")}
-