diff --git a/app/(core)/components/Header.jsx b/app/(core)/components/Header.jsx
index 12456db..6f9d682 100644
--- a/app/(core)/components/Header.jsx
+++ b/app/(core)/components/Header.jsx
@@ -10,7 +10,7 @@ import { useSticky } from "../hooks/useSticky";
import { useTheme } from "../hooks/useTheme";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHamburger } from "@fortawesome/free-solid-svg-icons";
-import GoogleTranslator from "./GoogleTranslator.jsx";
+import LanguageSwitcher from "./LanguageSwitcher.jsx";
import { usePathname } from "next/navigation.js";
export default function Header() {
@@ -129,8 +129,8 @@ export default function Header() {
-
+
diff --git a/app/(core)/components/LanguageSwitcher.jsx b/app/(core)/components/LanguageSwitcher.jsx
new file mode 100644
index 0000000..807d55f
--- /dev/null
+++ b/app/(core)/components/LanguageSwitcher.jsx
@@ -0,0 +1,131 @@
+"use client";
+
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faLanguage, faChevronDown } from "@fortawesome/free-solid-svg-icons";
+import useTranslation from "../hooks/useTranslation.ts";
+import "../styles/translator.css";
+
+const LANGUAGES = {
+ en: "English",
+ ru: "Русский",
+};
+
+const getStoredLang = () => {
+ if (typeof document === "undefined") return "en";
+ const value = `; ${document.cookie}`;
+ const parts = value.split("; googtrans=");
+ if (parts.length === 2) {
+ const cookie = parts.pop()?.split(";").shift();
+ if (cookie) {
+ const segments = cookie.split("/");
+ if (segments.length >= 3 && LANGUAGES[segments[2]]) {
+ return segments[2];
+ }
+ }
+ }
+ return "en";
+};
+
+const setLangCookie = (languageCode) => {
+ const hostname = window.location.hostname;
+ const isLocal = hostname === "localhost" || hostname === "127.0.0.1";
+ const domainAttr = isLocal ? "" : `; domain=.${hostname}`;
+ document.cookie = `googtrans=/en/${languageCode}; path=/${domainAttr}; SameSite=Lax`;
+};
+
+export default function LanguageSwitcher() {
+ const { t, meta } = useTranslation();
+ const isCompleted = meta?.completed || false;
+ const [currentLanguage, setCurrentLanguage] = useState("en");
+ const [open, setOpen] = useState(false);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ setCurrentLanguage(getStoredLang());
+ }, []);
+
+ useEffect(() => {
+ if (typeof document === "undefined") return;
+ document.documentElement.lang = currentLanguage === "ru" ? "ru" : "en";
+ }, [currentLanguage]);
+
+ const changeLanguage = useCallback((languageCode) => {
+ setCurrentLanguage(languageCode);
+ setLangCookie(languageCode);
+ setOpen(false);
+
+ window.dispatchEvent(
+ new CustomEvent("gtrans:languagechange", {
+ detail: { lang: languageCode },
+ })
+ );
+ }, []);
+
+ useEffect(() => {
+ if (!open) return;
+
+ const handleClickOutside = (event) => {
+ if (
+ containerRef.current &&
+ !containerRef.current.contains(event.target)
+ ) {
+ setOpen(false);
+ }
+ };
+
+ const handleEscape = (event) => {
+ if (event.key === "Escape") setOpen(false);
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("keydown", handleEscape);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleEscape);
+ };
+ }, [open]);
+
+ return (
+
+
+
+ {open ? (
+
+ {Object.entries(LANGUAGES).map(([code, name]) => (
+ -
+
+
+ ))}
+
+ ) : null}
+
+ );
+}
diff --git a/app/(core)/styles/index.css b/app/(core)/styles/index.css
index b102e9a..4b627d5 100644
--- a/app/(core)/styles/index.css
+++ b/app/(core)/styles/index.css
@@ -650,6 +650,13 @@ nav a:hover::after {
}
.controls {
+ display: flex;
+ gap: 0.45rem;
+ position: relative;
+ z-index: 2200;
+ }
+
+ .github-header-badge {
display: none;
}
diff --git a/app/(core)/styles/translator.css b/app/(core)/styles/translator.css
index d8abd83..b36a4a6 100644
--- a/app/(core)/styles/translator.css
+++ b/app/(core)/styles/translator.css
@@ -117,3 +117,114 @@ body {
transform: scale(1);
}
}
+
+/* Compact language switcher button + dropdown */
+.lang-switcher {
+ position: relative;
+ flex-shrink: 0;
+ z-index: 2200;
+}
+
+.lang-switcher__trigger {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.35rem;
+ height: 2.75rem;
+ min-width: 2.75rem;
+ padding: 0 0.65rem;
+ border: 1px solid var(--border-color);
+ border-radius: 0.75rem;
+ background: var(--bg-color);
+ color: var(--ph-fg-muted, var(--text-color));
+ cursor: pointer;
+ box-shadow: 0 10px 20px rgba(15, 23, 42, 0.12);
+ transition:
+ border-color 0.2s ease,
+ box-shadow 0.2s ease,
+ background 0.2s ease;
+}
+
+.lang-switcher__trigger:hover,
+.lang-switcher__trigger:focus-visible {
+ border-color: var(--accent-color);
+ box-shadow: 0 0 0 2px rgba(0, 230, 230, 0.25);
+ outline: none;
+}
+
+.lang-switcher__icon {
+ font-size: 1rem;
+ color: var(--accent-color);
+ pointer-events: none;
+}
+
+.lang-switcher__code {
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ line-height: 1;
+ pointer-events: none;
+}
+
+.lang-switcher__chevron {
+ font-size: 0.55rem;
+ opacity: 0.7;
+ transition: transform 0.2s ease;
+ pointer-events: none;
+}
+
+.lang-switcher__chevron.is-open {
+ transform: rotate(180deg);
+}
+
+.lang-switcher__menu {
+ position: absolute;
+ top: calc(100% + 0.4rem);
+ right: 0;
+ z-index: 2300;
+ min-width: 8.5rem;
+ margin: 0;
+ padding: 0.35rem;
+ list-style: none;
+ border: 1px solid var(--border-color);
+ border-radius: 0.75rem;
+ background: var(--bg-color);
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25);
+}
+
+.lang-switcher__option {
+ display: block;
+ width: 100%;
+ padding: 0.55rem 0.75rem;
+ border: none;
+ border-radius: 0.5rem;
+ background: transparent;
+ color: var(--ph-fg-muted, var(--text-color));
+ font-size: 0.9rem;
+ text-align: left;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.lang-switcher__option:hover,
+.lang-switcher__option:focus-visible {
+ background: rgba(0, 230, 230, 0.1);
+ outline: none;
+}
+
+.lang-switcher__option.is-active {
+ color: var(--accent-color);
+ font-weight: 600;
+ background: rgba(0, 230, 230, 0.08);
+}
+
+@media (max-width: 840px) {
+ .lang-switcher__code {
+ display: none;
+ }
+
+ .lang-switcher__trigger {
+ padding: 0 0.55rem;
+ min-width: 2.75rem;
+ }
+}