Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/(core)/components/ContributorsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export default function ContributorsSection() {
/>
</a>
<div className="contributor-info">
<p className="contributor-name">{c.login}</p>
<p className="contributor-name" translate="no">
{c.login}
</p>
<p className="contributor-data">
{c.contributions} {c.contributions === 1 ? "commit" : "commits"}
</p>
Expand Down
123 changes: 103 additions & 20 deletions app/(core)/components/GoogleTranslator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import {

const LANGUAGES = {
en: "English",
it: "Italian",
fr: "French",
de: "German",
es: "Spanish",
it: "Italiano",
fr: "Français",
de: "Deutsch",
es: "Español",
ar: "العربية",
};

const ICON_FRAMES = [
Expand All @@ -33,47 +34,96 @@ const LANGUAGE_ICONS = {
fr: faGlobeEurope,
de: faGlobeEurope,
es: faGlobeAmericas,
ar: faGlobeAsia,
default: faGlobeEurope,
};

// Helper to extract language from googtrans cookie
const getGoogleTransLang = () => {
const getCookie = (name) => {
if (typeof document === "undefined") return null;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(";").shift();
return null;
};
const cookie = getCookie("googtrans");
if (cookie) {
const parts = cookie.split("/");
if (parts.length >= 3) {
return parts[2]; // e.g., /en/it -> it
}
}
return "en";
};

export default function GoogleTranslator() {
const pathname = usePathname();
const [currentLanguage, setCurrentLanguage] = useState("en");
const [icon, setIcon] = useState(LANGUAGE_ICONS["en"]);
const translateElementRef = useRef(null);
const scriptLoaded = useRef(false);

useEffect(() => {
if (
!scriptLoaded.current &&
window.location.hostname !== "localhost" &&
window.location.hostname !== "127.0.0.1"
) {
const script = document.createElement("script");
script.src =
"https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
script.async = true;
document.body.appendChild(script);
scriptLoaded.current = true;
// State and ref for widget readiness and initial sync
const [widgetReady, setWidgetReady] = useState(false);
const initializedRef = useRef(false);

// Load Google Translate script once
useEffect(() => {
if (!scriptLoaded.current) {
window.googleTranslateElementInit = () => {
new window.google.translate.TranslateElement(
{
pageLanguage: "en",
includedLanguages: "en,it,fr,de,es",
includedLanguages: "en,it,fr,de,es,ar",
autoDisplay: false,
},
"google_translate_element"
);
};

const script = document.createElement("script");
script.src =
"https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
script.async = true;
document.body.appendChild(script);
scriptLoaded.current = true;
}
}, []);

// Detect when the Google Translate widget is ready (select element exists)
useEffect(() => {
let intervalId;
const checkWidget = () => {
const select = document.querySelector(".goog-te-combo");
if (select) {
setWidgetReady(true);
clearInterval(intervalId);
}
};
intervalId = setInterval(checkWidget, 200);
return () => clearInterval(intervalId);
}, []);

// Listen for retranslate requests from useTranslation
useEffect(() => {
const handler = (e) => {
const select = document.querySelector(".goog-te-combo");
if (!select) return;
select.value = e.detail.lang;
select.dispatchEvent(new Event("change"));
};

window.addEventListener("gtrans:retranslate", handler);
return () => window.removeEventListener("gtrans:retranslate", handler);
}, []);

const changeLanguage = useCallback(
(languageCode) => {
if (languageCode === currentLanguage) return;
setCurrentLanguage(languageCode);

// Animate icon
let frame = 0;
const interval = setInterval(() => {
setIcon(ICON_FRAMES[frame % ICON_FRAMES.length]);
Expand All @@ -84,17 +134,45 @@ export default function GoogleTranslator() {
}
}, 100);

// Trigger Google Translate
const selectElement = document.querySelector(".goog-te-combo");
if (selectElement) {
selectElement.value = languageCode;
selectElement.dispatchEvent(new Event("change"));
}
document.cookie = `googtrans=/en/${languageCode}; path=/; domain=${window.location.hostname}`;

// Update cookie
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}`;

// Notify useTranslation
window.dispatchEvent(
new CustomEvent("gtrans:languagechange", {
detail: { lang: languageCode },
})
);
},
[currentLanguage]
);

// Reset translation when route changes
// Sync the selector with the stored language on first widget ready
useEffect(() => {
if (widgetReady && !initializedRef.current) {
initializedRef.current = true;
const initialLang = getGoogleTransLang();
if (initialLang !== "en") {
changeLanguage(initialLang);
} else {
// For English we just set the state directly (no need to trigger widget)
setCurrentLanguage(initialLang);
setIcon(LANGUAGE_ICONS[initialLang] || LANGUAGE_ICONS.default);
}
}
}, [widgetReady, changeLanguage]);

// Reset translation on route change (keep the same language)
useEffect(() => {
if (currentLanguage !== "en") {
const timeout = setTimeout(() => {
Expand All @@ -114,7 +192,12 @@ export default function GoogleTranslator() {
onChange={(e) => changeLanguage(e.target.value)}
>
{Object.entries(LANGUAGES).map(([code, name]) => (
<option key={code} value={code}>
<option
key={code}
value={code}
translate="no"
className="notranslate"
>
{name}
</option>
))}
Expand Down
23 changes: 14 additions & 9 deletions app/(core)/components/Hero.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import chaptersData from "../data/chapters.js";
import { motion, useReducedMotion } from "framer-motion";
import useTranslation from "../hooks/useTranslation.ts";

// Container variant for staggered child animations
const containerVariants = (rm) => ({
Expand Down Expand Up @@ -86,6 +87,9 @@ const glowVariant = {

export function Hero() {
const reduceMotion = useReducedMotion();
const { t, meta } = useTranslation();

const isCompleted = meta?.completed || false;

// Compute chapters count
const chaptersCount = Array.isArray(chaptersData)
Expand All @@ -95,13 +99,13 @@ export function Hero() {
: 0;

// Split heading into words
const titleWords = "PhysicsHub – Best website to learn physics easily.".split(
" "
);
const titleWords = t(
"PhysicsHub – Best website to learn physics easily."
).split(" ");

return (
<motion.div
className="ph-hero__container"
className={`ph-hero__container ${isCompleted ? "notranslate" : ""}`}
variants={containerVariants(reduceMotion)}
initial="hidden"
animate="show"
Expand All @@ -128,29 +132,30 @@ export function Hero() {

{/* Subtitle */}
<motion.p className="ph-hero__subtitle" variants={fadeUp(reduceMotion)}>
Experience physics in real time, uncover the concepts behind the
formulas, and instantly see how they apply to the real world.
{t(
"Experience physics in real time, uncover the concepts behind the formulas, and instantly see how they apply to the real world."
)}
</motion.p>

{/* CTA buttons */}
<motion.div className="ph-hero__ctas" variants={fadeUp(reduceMotion)}>
<motion.div variants={buttonVariant} whileHover="hover" whileTap="tap">
<Link className="ph-btn ph-btn--primary main-btn" href="/simulations">
Go to Simulations
{t("Go to Simulations")}
<FontAwesomeIcon icon={faArrowRight} style={{ marginLeft: 8 }} />
</Link>
</motion.div>
<motion.div variants={buttonVariant} whileHover="hover" whileTap="tap">
<Link className="ph-btn ph-btn--ghost main-btn" href="/contribute">
Contribute
{t("Contribute")}
<FontAwesomeIcon icon={faGithub} style={{ marginLeft: 8 }} />
</Link>
</motion.div>
</motion.div>

{/* Info text */}
<motion.p className="ph-hero__info" variants={fadeUp(reduceMotion)}>
Currently {chaptersCount} chapters available.
{t("Currently")} {chaptersCount} {t("chapters available.")}
</motion.p>
</motion.div>
);
Expand Down
Loading