diff --git a/package-lock.json b/package-lock.json index 6bcb341..ccc88cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,8 @@ "framer-motion": "^11.14.4", "heroicons": "^2.2.0", "html2canvas": "^1.4.1", + "i18next": "^24.1.2", + "i18next-browser-languagedetector": "^8.0.2", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -53,6 +55,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-i18next": "^15.2.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", @@ -4771,6 +4774,15 @@ "integrity": "sha512-yOwvztmNiBWqR946t+JdgZmyzEmnRMC2nxvHFC90bF1SUttwB6yJKYeme1JeEcBfobdOs827nCyiWBS2z/brog==", "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -4784,6 +4796,46 @@ "node": ">=8.0.0" } }, + "node_modules/i18next": { + "version": "24.1.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.2.tgz", + "integrity": "sha512-th/075GW0Ub1gYDMHLiZXMGSfGv1aP1VqjT3fma/12hNHCNlH8oJMftvlDzycT/R+KoULWk+xLU8H1JRwV85qw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", + "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5683,6 +5735,28 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", + "integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -6370,7 +6444,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6605,6 +6679,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index fffbac5..a841a60 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "framer-motion": "^11.14.4", "heroicons": "^2.2.0", "html2canvas": "^1.4.1", + "i18next": "^24.1.2", + "i18next-browser-languagedetector": "^8.0.2", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -57,6 +59,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-i18next": "^15.2.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", diff --git a/public/audio/briefings/alert.mp3 b/public/audio/briefings/alert-en.mp3 similarity index 100% rename from public/audio/briefings/alert.mp3 rename to public/audio/briefings/alert-en.mp3 diff --git a/public/audio/briefings/april.mp3 b/public/audio/briefings/april-en.mp3 similarity index 100% rename from public/audio/briefings/april.mp3 rename to public/audio/briefings/april-en.mp3 diff --git a/public/audio/briefings/intro.mp3 b/public/audio/briefings/intro-en.mp3 similarity index 100% rename from public/audio/briefings/intro.mp3 rename to public/audio/briefings/intro-en.mp3 diff --git a/public/audio/briefings/january.mp3 b/public/audio/briefings/january-en.mp3 similarity index 100% rename from public/audio/briefings/january.mp3 rename to public/audio/briefings/january-en.mp3 diff --git a/public/audio/briefings/july.mp3 b/public/audio/briefings/july-en.mp3 similarity index 100% rename from public/audio/briefings/july.mp3 rename to public/audio/briefings/july-en.mp3 diff --git a/public/audio/briefings/march.mp3 b/public/audio/briefings/march-en.mp3 similarity index 100% rename from public/audio/briefings/march.mp3 rename to public/audio/briefings/march-en.mp3 diff --git a/public/audio/briefings/may.mp3 b/public/audio/briefings/may-en.mp3 similarity index 100% rename from public/audio/briefings/may.mp3 rename to public/audio/briefings/may-en.mp3 diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..42ce147 --- /dev/null +++ b/src/components/LanguageSwitcher.tsx @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import { Button } from './ui/button'; +import { Languages } from 'lucide-react'; + +export const LanguageSwitcher = () => { + const { i18n } = useTranslation(); + + const toggleLanguage = () => { + const newLang = i18n.language === 'en' ? 'ro' : 'en'; + i18n.changeLanguage(newLang); + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/MonthTransition.tsx b/src/components/MonthTransition.tsx index eee194e..a1fcdd5 100644 --- a/src/components/MonthTransition.tsx +++ b/src/components/MonthTransition.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; +import { useTranslation } from "react-i18next"; export enum TransitionStyle { FADE = "fade", @@ -10,82 +11,101 @@ export enum TransitionStyle { } interface MonthTransitionProps { - month: string; + monthIndex: number; onComplete: () => void; style: TransitionStyle; } +// Helper function to translate month name +const useTranslatedMonth = (monthIndex: number) => { + const { t } = useTranslation(); + const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'exposé']; + return t(`months.${monthKeys[monthIndex]}`); +}; + // Create separate components for each style -const FadeTransition = ({ month }: { month: string }) => ( - - -
- {month} -
-
-
-); - -const TypewriterTransition = ({ month }: { month: string }) => ( -
-
- {month} -
-
-); - -const SplitScreenTransition = ({ month }: { month: string }) => ( - <> -
-
-
-
-
- {month} -
- -); - -const MatrixTransition = ({ month }: { month: string }) => ( - <> -
- {[...Array(20)].map((_, i) => ( -
- 2+2=5 +const FadeTransition = ({ monthIndex }: { monthIndex: number }) => { + const translatedMonth = useTranslatedMonth(monthIndex); + return ( + + +
+ {translatedMonth}
- ))} -
-
-
- {month} + + + ); +}; + +const TypewriterTransition = ({ monthIndex }: { monthIndex: number }) => { + const translatedMonth = useTranslatedMonth(monthIndex); + return ( +
+
+ {translatedMonth}
- -); + ); +}; -const NumberCycleTransition = ({ month }: { month: string }) => { +const SplitScreenTransition = ({ monthIndex }: { monthIndex: number }) => { + const translatedMonth = useTranslatedMonth(monthIndex); + return ( + <> +
+
+
+
+
+ {translatedMonth} +
+ + ); +}; + +const MatrixTransition = ({ monthIndex }: { monthIndex: number }) => { + const translatedMonth = useTranslatedMonth(monthIndex); + return ( + <> +
+ {[...Array(20)].map((_, i) => ( +
+ 2+2=5 +
+ ))} +
+
+
+ {translatedMonth} +
+
+ + ); +}; + +const NumberCycleTransition = ({ monthIndex }: { monthIndex: number }) => { + const translatedMonth = useTranslatedMonth(monthIndex); const [displayText, setDisplayText] = useState( - Array(month.length).fill('0').join('') + Array(translatedMonth.length).fill('0').join('') ); useEffect(() => { let cycleCount = 0; - const maxCycles = 15; // Reduced from 20 to make crystallization start sooner + const maxCycles = 15; const interval = setInterval(() => { cycleCount++; if (cycleCount >= maxCycles) { - // Start crystallizing the text setDisplayText(prev => { - const monthArray = month.split(''); + const monthArray = translatedMonth.split(''); const currentArray = prev.split(''); const remainingIndices = currentArray.reduce((acc, char, i) => { @@ -104,9 +124,8 @@ const NumberCycleTransition = ({ month }: { month: string }) => { return currentArray.join(''); }); } else { - // Random number phase setDisplayText(prev => - Array(month.length) + Array(translatedMonth.length) .fill(0) .map(() => Math.floor(Math.random() * 10).toString()) .join('') @@ -115,7 +134,7 @@ const NumberCycleTransition = ({ month }: { month: string }) => { }, 100); return () => clearInterval(interval); - }, [month]); + }, [translatedMonth]); return (
@@ -123,7 +142,7 @@ const NumberCycleTransition = ({ month }: { month: string }) => {
{char} @@ -133,26 +152,26 @@ const NumberCycleTransition = ({ month }: { month: string }) => { ); }; -export const MonthTransition = ({ month, onComplete, style }: MonthTransitionProps) => { +export const MonthTransition = ({ monthIndex, onComplete, style }: MonthTransitionProps) => { useEffect(() => { - const timer = setTimeout(onComplete, 3500); // Increased from 3000 to 3500ms + const timer = setTimeout(onComplete, 3500); return () => clearTimeout(timer); }, [onComplete]); const renderTransition = () => { switch (style) { case TransitionStyle.FADE: - return ; + return ; case TransitionStyle.TYPEWRITER: - return ; + return ; case TransitionStyle.SPLIT_SCREEN: - return ; + return ; case TransitionStyle.MATRIX: - return ; + return ; case TransitionStyle.NUMBER_CYCLE: - return ; + return ; default: - return ; + return ; } }; diff --git a/src/components/game/BriefingAudio.tsx b/src/components/game/BriefingAudio.tsx index 186234a..f40194c 100644 --- a/src/components/game/BriefingAudio.tsx +++ b/src/components/game/BriefingAudio.tsx @@ -1,8 +1,9 @@ -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Play, Pause } from "lucide-react"; +import { useEffect, useRef, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Play, Pause } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useTranslation } from 'react-i18next'; import { playRecordingSound } from "@/utils/audio"; -import { cn } from "@/lib/utils"; interface BriefingAudioProps { stage: string; @@ -12,8 +13,9 @@ interface BriefingAudioProps { export const BriefingAudio = ({ stage, audioRef, className }: BriefingAudioProps) => { const [isPlaying, setIsPlaying] = useState(false); + const { t, i18n } = useTranslation(); - const togglePlayback = () => { + const handlePlayPause = () => { if (!audioRef.current) return; if (isPlaying) { @@ -22,47 +24,65 @@ export const BriefingAudio = ({ stage, audioRef, className }: BriefingAudioProps playRecordingSound(); audioRef.current.play(); } - setIsPlaying(!isPlaying); }; useEffect(() => { - const audio = audioRef.current; - if (!audio) return; + if (!audioRef.current) return; - const handleEnded = () => setIsPlaying(false); - audio.addEventListener('ended', handleEnded); + const handleEnded = () => { + setIsPlaying(false); + }; + + const handlePlay = () => { + setIsPlaying(true); + }; + + const handlePause = () => { + setIsPlaying(false); + }; + + audioRef.current.addEventListener('ended', handleEnded); + audioRef.current.addEventListener('play', handlePlay); + audioRef.current.addEventListener('pause', handlePause); return () => { - audio.removeEventListener('ended', handleEnded); + if (!audioRef.current) return; + audioRef.current.removeEventListener('ended', handleEnded); + audioRef.current.removeEventListener('play', handlePlay); + audioRef.current.removeEventListener('pause', handlePause); }; }, [audioRef]); const getAudioFileName = (stage: string) => { + const currentLanguage = i18n.language; + const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'expose']; + // Handle special stages - if (stage === "ALERT" || stage === "INTRO") { - return `${stage.toLowerCase()}.mp3`; + if (stage === "INTRO") { + return `intro-${currentLanguage}.mp3`; } - // Handle monthly stages - const month = stage.split(':')[0].toLowerCase().trim(); - return `${month}.mp3`; + // For all other stages (including ALERT), use the month-based naming + const monthIndex = parseInt(stage); + const monthKey = monthKeys[monthIndex]; + return `${monthKey}-${currentLanguage}.mp3`; }; - // Skip rendering for special stages that don't have audio - if (stage === "ALERT" || stage === "INTRO") { + // Only skip rendering for INTRO stage + if (stage === "INTRO") { return null; } return ( -
+