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 }) => (
-
-);
-
-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 (
+
- >
-);
+ );
+};
-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 (
-
+