diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx
new file mode 100644
index 00000000..a702357b
--- /dev/null
+++ b/src/components/LanguageSwitcher.tsx
@@ -0,0 +1,97 @@
+import { Languages } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { SUPPORTED_LANGUAGES, changeAppLanguage, isRTLLanguage } from '@/i18n';
+import { cn } from '@/lib/utils';
+
+/**
+ * Resolve the live i18next language code to one of `SUPPORTED_LANGUAGES`'
+ * codes so the right row shows as selected.
+ *
+ * Mirrors the logic in `LanguageSettingsPage` so the top-nav switcher and the
+ * settings page agree on which language is active:
+ * 1. Exact match (case-insensitive) — `zh-Hant` → `zh-Hant`, `en` → `en`.
+ * 2. Traditional-Chinese aliases — `zh-TW` / `zh-HK` resolve to `zh-Hant`
+ * because `i18n.ts` registers them as resource aliases.
+ * 3. Base-code fallback — `en-US` → `en`, `pt-BR` → `pt`.
+ */
+function resolveCurrentLng(rawLng: string): string {
+ const lower = rawLng.toLowerCase();
+ const exact = SUPPORTED_LANGUAGES.find((l) => l.code.toLowerCase() === lower);
+ if (exact) return exact.code;
+ if (lower === 'zh-tw' || lower === 'zh-hk') return 'zh-Hant';
+ return lower.split('-')[0];
+}
+
+interface LanguageSwitcherProps {
+ /** Extra classes for the trigger button. */
+ className?: string;
+}
+
+/**
+ * Compact language picker for the top nav. A globe/languages icon opens a
+ * dropdown of every supported language by its own native name, so a visitor
+ * who can't read the current UI language can still recognize and pick theirs.
+ *
+ * Switching happens in place — `changeAppLanguage` swaps the active locale and
+ * the app re-renders translated — so the user never loses their scroll
+ * position, the campaign they're reading, or any in-progress form state. The
+ * full `/settings/language` page remains the canonical deep-link.
+ *
+ * Each row renders in its own language and direction (`lang` + `dir`) so the
+ * native names read correctly regardless of the current UI direction.
+ */
+export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
+ const { t, i18n: i18nInstance } = useTranslation();
+ const currentLng = resolveCurrentLng(i18nInstance.language ?? 'en');
+
+ return (
+
+
+
+
+
+ {t('language.title')}
+
+ {
+ if (code !== currentLng) void changeAppLanguage(code);
+ }}
+ >
+ {SUPPORTED_LANGUAGES.map((language) => (
+
+ {language.nativeName}
+
+ ))}
+
+
+
+ );
+}
+
+export default LanguageSwitcher;
diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx
index bbb13914..9e5a3f48 100644
--- a/src/components/TopNav.tsx
+++ b/src/components/TopNav.tsx
@@ -19,6 +19,7 @@ import { nip19 } from 'nostr-tools';
import { Capacitor } from '@capacitor/core';
import { LoginArea } from '@/components/auth/LoginArea';
+import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import { LogoIcon } from '@/components/icons/LogoIcon';
import { Sheet, SheetContent } from '@/components/ui/sheet';
import { useAppContext } from '@/hooks/useAppContext';
@@ -120,6 +121,11 @@ export function TopNav() {
{/* Right cluster */}