Felix Zieger commited on
Commit
831f7e7
·
1 Parent(s): 34f302f

multi language support

Browse files
src/App.tsx CHANGED
@@ -3,22 +3,25 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
3
  import { TooltipProvider } from "@/components/ui/tooltip";
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
  import { BrowserRouter, Routes, Route } from "react-router-dom";
 
6
  import Index from "./pages/Index";
7
 
8
  const queryClient = new QueryClient();
9
 
10
  const App = () => (
11
  <QueryClientProvider client={queryClient}>
12
- <TooltipProvider>
13
- <Toaster />
14
- <Sonner />
15
- <BrowserRouter>
16
- <Routes>
17
- <Route path="/" element={<Index />} />
18
- </Routes>
19
- </BrowserRouter>
20
- </TooltipProvider>
 
 
21
  </QueryClientProvider>
22
  );
23
 
24
- export default App;
 
3
  import { TooltipProvider } from "@/components/ui/tooltip";
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
  import { BrowserRouter, Routes, Route } from "react-router-dom";
6
+ import { LanguageProvider } from "./contexts/LanguageContext";
7
  import Index from "./pages/Index";
8
 
9
  const queryClient = new QueryClient();
10
 
11
  const App = () => (
12
  <QueryClientProvider client={queryClient}>
13
+ <LanguageProvider>
14
+ <TooltipProvider>
15
+ <Toaster />
16
+ <Sonner />
17
+ <BrowserRouter>
18
+ <Routes>
19
+ <Route path="/" element={<Index />} />
20
+ </Routes>
21
+ </BrowserRouter>
22
+ </TooltipProvider>
23
+ </LanguageProvider>
24
  </QueryClientProvider>
25
  );
26
 
27
+ export default App;
src/components/GameContainer.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, KeyboardEvent, useEffect } from "react";
2
  import { getRandomWord } from "@/lib/words";
3
  import { motion } from "framer-motion";
4
  import { generateAIResponse, guessWord } from "@/services/mistralService";
@@ -9,6 +9,8 @@ import { ThemeSelector } from "./game/ThemeSelector";
9
  import { SentenceBuilder } from "./game/SentenceBuilder";
10
  import { GuessDisplay } from "./game/GuessDisplay";
11
  import { GameOver } from "./game/GameOver";
 
 
12
 
13
  type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess" | "game-over";
14
 
@@ -24,6 +26,8 @@ export const GameContainer = () => {
24
  const [totalWords, setTotalWords] = useState<number>(0);
25
  const [usedWords, setUsedWords] = useState<string[]>([]);
26
  const { toast } = useToast();
 
 
27
 
28
  useEffect(() => {
29
  const handleKeyPress = (e: KeyboardEvent) => {
@@ -52,13 +56,15 @@ export const GameContainer = () => {
52
  const handleThemeSelect = async (theme: string) => {
53
  setCurrentTheme(theme);
54
  try {
55
- const word = theme === "standard" ? getRandomWord() : await getThemedWord(theme, usedWords);
 
 
56
  setCurrentWord(word);
57
  setGameState("building-sentence");
58
  setSuccessfulRounds(0);
59
  setTotalWords(0);
60
- setUsedWords([word]); // Initialize used words with the first word
61
- console.log("Game started with word:", word, "theme:", theme);
62
  } catch (error) {
63
  console.error('Error getting themed word:', error);
64
  toast({
@@ -88,8 +94,8 @@ export const GameContainer = () => {
88
  } catch (error) {
89
  console.error('Error in AI turn:', error);
90
  toast({
91
- title: "AI Response Delayed",
92
- description: "The AI is currently busy. Please try adding another word or wait a moment.",
93
  variant: "default",
94
  });
95
  } finally {
@@ -224,4 +230,4 @@ export const GameContainer = () => {
224
  </motion.div>
225
  </div>
226
  );
227
- };
 
1
+ import { useState, KeyboardEvent, useEffect, useContext } from "react";
2
  import { getRandomWord } from "@/lib/words";
3
  import { motion } from "framer-motion";
4
  import { generateAIResponse, guessWord } from "@/services/mistralService";
 
9
  import { SentenceBuilder } from "./game/SentenceBuilder";
10
  import { GuessDisplay } from "./game/GuessDisplay";
11
  import { GameOver } from "./game/GameOver";
12
+ import { useTranslation } from "@/hooks/useTranslation";
13
+ import { LanguageContext } from "@/contexts/LanguageContext";
14
 
15
  type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess" | "game-over";
16
 
 
26
  const [totalWords, setTotalWords] = useState<number>(0);
27
  const [usedWords, setUsedWords] = useState<string[]>([]);
28
  const { toast } = useToast();
29
+ const t = useTranslation();
30
+ const { language } = useContext(LanguageContext);
31
 
32
  useEffect(() => {
33
  const handleKeyPress = (e: KeyboardEvent) => {
 
56
  const handleThemeSelect = async (theme: string) => {
57
  setCurrentTheme(theme);
58
  try {
59
+ const word = theme === "standard" ?
60
+ getRandomWord() :
61
+ await getThemedWord(theme, usedWords, language);
62
  setCurrentWord(word);
63
  setGameState("building-sentence");
64
  setSuccessfulRounds(0);
65
  setTotalWords(0);
66
+ setUsedWords([word]);
67
+ console.log("Game started with word:", word, "theme:", theme, "language:", language);
68
  } catch (error) {
69
  console.error('Error getting themed word:', error);
70
  toast({
 
94
  } catch (error) {
95
  console.error('Error in AI turn:', error);
96
  toast({
97
+ title: t.game.aiThinking,
98
+ description: t.game.aiDelayed,
99
  variant: "default",
100
  });
101
  } finally {
 
230
  </motion.div>
231
  </div>
232
  );
233
+ };
src/components/HighScoreBoard.tsx CHANGED
@@ -20,6 +20,7 @@ import {
20
  PaginationNext,
21
  PaginationPrevious,
22
  } from "@/components/ui/pagination";
 
23
 
24
  interface HighScore {
25
  id: string;
@@ -62,6 +63,7 @@ export const HighScoreBoard = ({
62
  const [hasSubmitted, setHasSubmitted] = useState(false);
63
  const [currentPage, setCurrentPage] = useState(1);
64
  const { toast } = useToast();
 
65
 
66
  const { data: highScores, refetch } = useQuery({
67
  queryKey: ["highScores"],
@@ -78,11 +80,10 @@ export const HighScoreBoard = ({
78
  });
79
 
80
  const handleSubmitScore = async () => {
81
- // Validate player name (only alphanumeric characters allowed)
82
  if (!playerName.trim() || !/^[a-zA-Z0-9]+$/.test(playerName.trim())) {
83
  toast({
84
- title: "Error",
85
- description: "Please enter a valid name (only letters and numbers allowed)",
86
  variant: "destructive",
87
  });
88
  return;
@@ -90,8 +91,8 @@ export const HighScoreBoard = ({
90
 
91
  if (currentScore < 1) {
92
  toast({
93
- title: "Error",
94
- description: "You need to complete at least one round to submit a score",
95
  variant: "destructive",
96
  });
97
  return;
@@ -99,8 +100,8 @@ export const HighScoreBoard = ({
99
 
100
  if (hasSubmitted) {
101
  toast({
102
- title: "Error",
103
- description: "You have already submitted your score for this game",
104
  variant: "destructive",
105
  });
106
  return;
@@ -108,7 +109,6 @@ export const HighScoreBoard = ({
108
 
109
  setIsSubmitting(true);
110
  try {
111
- // Check if player already exists
112
  const { data: existingScores } = await supabase
113
  .from("high_scores")
114
  .select("*")
@@ -117,7 +117,6 @@ export const HighScoreBoard = ({
117
  const existingScore = existingScores?.[0];
118
 
119
  if (existingScore) {
120
- // Only update if the new score is better
121
  if (currentScore > existingScore.score) {
122
  const { error } = await supabase
123
  .from("high_scores")
@@ -130,20 +129,20 @@ export const HighScoreBoard = ({
130
  if (error) throw error;
131
 
132
  toast({
133
- title: "New High Score!",
134
- description: `You beat your previous record of ${existingScore.score} rounds!`,
135
  });
136
  } else {
137
  toast({
138
- title: "Score Not Updated",
139
- description: `Your current score (${currentScore}) is not higher than your best score (${existingScore.score})`,
 
140
  variant: "destructive",
141
  });
142
  setIsSubmitting(false);
143
  return;
144
  }
145
  } else {
146
- // Insert new score
147
  const { error } = await supabase.from("high_scores").insert({
148
  player_name: playerName.trim(),
149
  score: currentScore,
@@ -151,11 +150,6 @@ export const HighScoreBoard = ({
151
  });
152
 
153
  if (error) throw error;
154
-
155
- toast({
156
- title: "Success!",
157
- description: "Your score has been recorded",
158
- });
159
  }
160
 
161
  setHasSubmitted(true);
@@ -164,8 +158,8 @@ export const HighScoreBoard = ({
164
  } catch (error) {
165
  console.error("Error submitting score:", error);
166
  toast({
167
- title: "Error",
168
- description: "Failed to submit score. Please try again.",
169
  variant: "destructive",
170
  });
171
  } finally {
@@ -212,20 +206,19 @@ export const HighScoreBoard = ({
212
  return (
213
  <div className="space-y-6">
214
  <div className="text-center">
215
- <h2 className="text-2xl font-bold mb-2">Leaderboard</h2>
216
  <p className="text-gray-600">
217
- Your score: {currentScore} rounds
218
- {currentScore > 0 && ` (${avgWordsPerRound.toFixed(1)} words/round)`}
219
  </p>
220
  </div>
221
 
222
  {!hasSubmitted && currentScore > 0 && (
223
  <div className="flex gap-4 mb-6">
224
  <Input
225
- placeholder="Enter your name (letters and numbers only)"
226
  value={playerName}
227
  onChange={(e) => {
228
- // Only allow alphanumeric input
229
  const value = e.target.value.replace(/[^a-zA-Z0-9]/g, '');
230
  setPlayerName(value);
231
  }}
@@ -237,7 +230,7 @@ export const HighScoreBoard = ({
237
  onClick={handleSubmitScore}
238
  disabled={isSubmitting || !playerName.trim() || hasSubmitted}
239
  >
240
- {isSubmitting ? "Submitting..." : "Submit Score"}
241
  </Button>
242
  </div>
243
  )}
@@ -246,10 +239,10 @@ export const HighScoreBoard = ({
246
  <Table>
247
  <TableHeader>
248
  <TableRow>
249
- <TableHead>Rank</TableHead>
250
- <TableHead>Player</TableHead>
251
- <TableHead>Rounds</TableHead>
252
- <TableHead>Avg Words/Round</TableHead>
253
  </TableRow>
254
  </TableHeader>
255
  <TableBody>
@@ -271,7 +264,7 @@ export const HighScoreBoard = ({
271
  {!paginatedScores?.length && (
272
  <TableRow>
273
  <TableCell colSpan={4} className="text-center">
274
- No high scores yet. Be the first!
275
  </TableCell>
276
  </TableRow>
277
  )}
@@ -287,7 +280,7 @@ export const HighScoreBoard = ({
287
  onClick={handlePreviousPage}
288
  className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
289
  >
290
- <span className="hidden sm:inline">Previous</span>
291
  <span className="text-xs text-muted-foreground ml-1">←</span>
292
  </PaginationPrevious>
293
  </PaginationItem>
@@ -306,19 +299,13 @@ export const HighScoreBoard = ({
306
  onClick={handleNextPage}
307
  className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
308
  >
309
- <span className="hidden sm:inline">Next</span>
310
  <span className="text-xs text-muted-foreground ml-1">→</span>
311
  </PaginationNext>
312
  </PaginationItem>
313
  </PaginationContent>
314
  </Pagination>
315
  )}
316
-
317
- <div className="flex justify-end">
318
- <Button variant="outline" onClick={onClose}>
319
- Close <span className="text-xs text-muted-foreground ml-1">Esc</span>
320
- </Button>
321
- </div>
322
  </div>
323
  );
324
  };
 
20
  PaginationNext,
21
  PaginationPrevious,
22
  } from "@/components/ui/pagination";
23
+ import { useTranslation } from "@/hooks/useTranslation";
24
 
25
  interface HighScore {
26
  id: string;
 
63
  const [hasSubmitted, setHasSubmitted] = useState(false);
64
  const [currentPage, setCurrentPage] = useState(1);
65
  const { toast } = useToast();
66
+ const t = useTranslation();
67
 
68
  const { data: highScores, refetch } = useQuery({
69
  queryKey: ["highScores"],
 
80
  });
81
 
82
  const handleSubmitScore = async () => {
 
83
  if (!playerName.trim() || !/^[a-zA-Z0-9]+$/.test(playerName.trim())) {
84
  toast({
85
+ title: t.leaderboard.error.invalidName,
86
+ description: t.leaderboard.error.invalidName,
87
  variant: "destructive",
88
  });
89
  return;
 
91
 
92
  if (currentScore < 1) {
93
  toast({
94
+ title: t.leaderboard.error.noRounds,
95
+ description: t.leaderboard.error.noRounds,
96
  variant: "destructive",
97
  });
98
  return;
 
100
 
101
  if (hasSubmitted) {
102
  toast({
103
+ title: t.leaderboard.error.alreadySubmitted,
104
+ description: t.leaderboard.error.alreadySubmitted,
105
  variant: "destructive",
106
  });
107
  return;
 
109
 
110
  setIsSubmitting(true);
111
  try {
 
112
  const { data: existingScores } = await supabase
113
  .from("high_scores")
114
  .select("*")
 
117
  const existingScore = existingScores?.[0];
118
 
119
  if (existingScore) {
 
120
  if (currentScore > existingScore.score) {
121
  const { error } = await supabase
122
  .from("high_scores")
 
129
  if (error) throw error;
130
 
131
  toast({
132
+ title: t.leaderboard.error.newHighScore,
133
+ description: t.leaderboard.error.beatRecord.replace("{score}", String(existingScore.score)),
134
  });
135
  } else {
136
  toast({
137
+ title: t.leaderboard.error.notHigher
138
+ .replace("{current}", String(currentScore))
139
+ .replace("{best}", String(existingScore.score)),
140
  variant: "destructive",
141
  });
142
  setIsSubmitting(false);
143
  return;
144
  }
145
  } else {
 
146
  const { error } = await supabase.from("high_scores").insert({
147
  player_name: playerName.trim(),
148
  score: currentScore,
 
150
  });
151
 
152
  if (error) throw error;
 
 
 
 
 
153
  }
154
 
155
  setHasSubmitted(true);
 
158
  } catch (error) {
159
  console.error("Error submitting score:", error);
160
  toast({
161
+ title: t.leaderboard.error.submitError,
162
+ description: t.leaderboard.error.submitError,
163
  variant: "destructive",
164
  });
165
  } finally {
 
206
  return (
207
  <div className="space-y-6">
208
  <div className="text-center">
209
+ <h2 className="text-2xl font-bold mb-2">{t.leaderboard.title}</h2>
210
  <p className="text-gray-600">
211
+ {t.leaderboard.yourScore}: {currentScore} {t.leaderboard.roundCount}
212
+ {currentScore > 0 && ` (${avgWordsPerRound.toFixed(1)} ${t.leaderboard.wordsPerRound})`}
213
  </p>
214
  </div>
215
 
216
  {!hasSubmitted && currentScore > 0 && (
217
  <div className="flex gap-4 mb-6">
218
  <Input
219
+ placeholder={t.leaderboard.enterName}
220
  value={playerName}
221
  onChange={(e) => {
 
222
  const value = e.target.value.replace(/[^a-zA-Z0-9]/g, '');
223
  setPlayerName(value);
224
  }}
 
230
  onClick={handleSubmitScore}
231
  disabled={isSubmitting || !playerName.trim() || hasSubmitted}
232
  >
233
+ {isSubmitting ? t.leaderboard.submitting : t.leaderboard.submit}
234
  </Button>
235
  </div>
236
  )}
 
239
  <Table>
240
  <TableHeader>
241
  <TableRow>
242
+ <TableHead>{t.leaderboard.rank}</TableHead>
243
+ <TableHead>{t.leaderboard.player}</TableHead>
244
+ <TableHead>{t.leaderboard.roundsColumn}</TableHead>
245
+ <TableHead>{t.leaderboard.avgWords}</TableHead>
246
  </TableRow>
247
  </TableHeader>
248
  <TableBody>
 
264
  {!paginatedScores?.length && (
265
  <TableRow>
266
  <TableCell colSpan={4} className="text-center">
267
+ {t.leaderboard.noScores}
268
  </TableCell>
269
  </TableRow>
270
  )}
 
280
  onClick={handlePreviousPage}
281
  className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
282
  >
283
+ <span className="hidden sm:inline">{t.leaderboard.previous}</span>
284
  <span className="text-xs text-muted-foreground ml-1">←</span>
285
  </PaginationPrevious>
286
  </PaginationItem>
 
299
  onClick={handleNextPage}
300
  className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
301
  >
302
+ <span className="hidden sm:inline">{t.leaderboard.next}</span>
303
  <span className="text-xs text-muted-foreground ml-1">→</span>
304
  </PaginationNext>
305
  </PaginationItem>
306
  </PaginationContent>
307
  </Pagination>
308
  )}
 
 
 
 
 
 
309
  </div>
310
  );
311
  };
src/components/game/GuessDisplay.tsx CHANGED
@@ -7,6 +7,7 @@ import {
7
  } from "@/components/ui/dialog";
8
  import { HighScoreBoard } from "@/components/HighScoreBoard";
9
  import { useState } from "react";
 
10
 
11
  interface GuessDisplayProps {
12
  sentence: string[];
@@ -29,6 +30,7 @@ export const GuessDisplay = ({
29
  }: GuessDisplayProps) => {
30
  const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
31
  const [isDialogOpen, setIsDialogOpen] = useState(false);
 
32
 
33
  return (
34
  <motion.div
@@ -36,21 +38,19 @@ export const GuessDisplay = ({
36
  animate={{ opacity: 1 }}
37
  className="text-center"
38
  >
39
- <h2 className="mb-4 text-2xl font-semibold text-gray-900">AI's Guess</h2>
40
  <div className="mb-6 rounded-lg bg-gray-50 p-4">
41
  <p className="mb-4 text-lg text-gray-800">
42
- Your sentence: {sentence.join(" ")}
 
 
 
43
  </p>
44
- <p className="text-xl font-bold text-primary">AI guessed: {aiGuess}</p>
45
  <p className="mt-4 text-lg">
46
  {isGuessCorrect() ? (
47
- <span className="text-green-600">
48
- Correct guess! 🎉 Ready for the next round? Press Enter
49
- </span>
50
  ) : (
51
- <span className="text-red-600">
52
- Game Over! Press Enter to play again
53
- </span>
54
  )}
55
  </p>
56
  </div>
@@ -60,7 +60,7 @@ export const GuessDisplay = ({
60
  onClick={onNextRound}
61
  className="w-full bg-primary text-lg hover:bg-primary/90"
62
  >
63
- Next Round
64
  </Button>
65
  ) : (
66
  <>
@@ -69,7 +69,7 @@ export const GuessDisplay = ({
69
  <Button
70
  className="w-full bg-secondary text-lg hover:bg-secondary/90"
71
  >
72
- View Leaderboard 🏆
73
  </Button>
74
  </DialogTrigger>
75
  <DialogContent className="max-w-md bg-white">
@@ -88,11 +88,11 @@ export const GuessDisplay = ({
88
  onClick={onPlayAgain}
89
  className="w-full bg-primary text-lg hover:bg-primary/90"
90
  >
91
- Play Again
92
  </Button>
93
  </>
94
  )}
95
  </div>
96
  </motion.div>
97
  );
98
- };
 
7
  } from "@/components/ui/dialog";
8
  import { HighScoreBoard } from "@/components/HighScoreBoard";
9
  import { useState } from "react";
10
+ import { useTranslation } from "@/hooks/useTranslation";
11
 
12
  interface GuessDisplayProps {
13
  sentence: string[];
 
30
  }: GuessDisplayProps) => {
31
  const isGuessCorrect = () => aiGuess.toLowerCase() === currentWord.toLowerCase();
32
  const [isDialogOpen, setIsDialogOpen] = useState(false);
33
+ const t = useTranslation();
34
 
35
  return (
36
  <motion.div
 
38
  animate={{ opacity: 1 }}
39
  className="text-center"
40
  >
41
+ <h2 className="mb-4 text-2xl font-semibold text-gray-900">{t.guess.title}</h2>
42
  <div className="mb-6 rounded-lg bg-gray-50 p-4">
43
  <p className="mb-4 text-lg text-gray-800">
44
+ {t.guess.sentence}: {sentence.join(" ")}
45
+ </p>
46
+ <p className="text-xl font-bold text-primary">
47
+ {t.guess.aiGuessed}: {aiGuess}
48
  </p>
 
49
  <p className="mt-4 text-lg">
50
  {isGuessCorrect() ? (
51
+ <span className="text-green-600">{t.guess.correct}</span>
 
 
52
  ) : (
53
+ <span className="text-red-600">{t.guess.incorrect}</span>
 
 
54
  )}
55
  </p>
56
  </div>
 
60
  onClick={onNextRound}
61
  className="w-full bg-primary text-lg hover:bg-primary/90"
62
  >
63
+ {t.guess.nextRound}
64
  </Button>
65
  ) : (
66
  <>
 
69
  <Button
70
  className="w-full bg-secondary text-lg hover:bg-secondary/90"
71
  >
72
+ {t.guess.viewLeaderboard} 🏆
73
  </Button>
74
  </DialogTrigger>
75
  <DialogContent className="max-w-md bg-white">
 
88
  onClick={onPlayAgain}
89
  className="w-full bg-primary text-lg hover:bg-primary/90"
90
  >
91
+ {t.guess.playAgain}
92
  </Button>
93
  </>
94
  )}
95
  </div>
96
  </motion.div>
97
  );
98
+ };
src/components/game/LanguageSelector.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { useContext } from "react";
3
+ import { LanguageContext } from "@/contexts/LanguageContext";
4
+ import { Language } from "@/i18n/translations";
5
+
6
+ const languages: { code: Language; name: string; flag: string }[] = [
7
+ { code: 'en', name: 'English', flag: '🇬🇧' },
8
+ { code: 'fr', name: 'Français', flag: '🇫🇷' },
9
+ { code: 'de', name: 'Deutsch', flag: '🇩🇪' },
10
+ { code: 'it', name: 'Italiano', flag: '🇮🇹' },
11
+ { code: 'es', name: 'Español', flag: '🇪🇸' },
12
+ ];
13
+
14
+ export const LanguageSelector = () => {
15
+ const { language, setLanguage } = useContext(LanguageContext);
16
+
17
+ return (
18
+ <div className="flex flex-wrap justify-center gap-2 mb-4">
19
+ {languages.map(({ code, name, flag }) => (
20
+ <Button
21
+ key={code}
22
+ variant={language === code ? "default" : "outline"}
23
+ onClick={() => setLanguage(code)}
24
+ className="flex items-center gap-2"
25
+ >
26
+ <span>{flag}</span>
27
+ <span>{name}</span>
28
+ </Button>
29
+ ))}
30
+ </div>
31
+ );
32
+ };
src/components/game/SentenceBuilder.tsx CHANGED
@@ -3,6 +3,7 @@ import { Input } from "@/components/ui/input";
3
  import { motion } from "framer-motion";
4
  import { KeyboardEvent, useRef, useEffect, useState } from "react";
5
  import { useToast } from "@/hooks/use-toast";
 
6
 
7
  interface SentenceBuilderProps {
8
  currentWord: string;
@@ -29,6 +30,7 @@ export const SentenceBuilder = ({
29
  const [imageLoaded, setImageLoaded] = useState(false);
30
  const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
31
  const { toast } = useToast();
 
32
 
33
  useEffect(() => {
34
  const img = new Image();
@@ -37,14 +39,12 @@ export const SentenceBuilder = ({
37
  console.log("Attempting to load image:", imagePath);
38
  }, [imagePath]);
39
 
40
- // Focus input on initial render
41
  useEffect(() => {
42
  setTimeout(() => {
43
  inputRef.current?.focus();
44
  }, 100);
45
  }, []);
46
 
47
- // Focus input after AI finishes thinking
48
  useEffect(() => {
49
  if (!isAiThinking && sentence.length > 0 && sentence.length % 2 === 0) {
50
  setTimeout(() => {
@@ -59,7 +59,6 @@ export const SentenceBuilder = ({
59
  if (playerInput.trim()) {
60
  handleSubmit(e as any);
61
  }
62
- // Make the guess immediately without waiting for AI response
63
  onMakeGuess();
64
  }
65
  };
@@ -69,11 +68,10 @@ export const SentenceBuilder = ({
69
  const input = playerInput.trim().toLowerCase();
70
  const target = currentWord.toLowerCase();
71
 
72
- // Check if the input contains only letters
73
  if (!/^[a-zA-Z]+$/.test(input)) {
74
  toast({
75
- title: "Invalid Word",
76
- description: "Please use only letters (no numbers or special characters)",
77
  variant: "destructive",
78
  });
79
  return;
@@ -81,8 +79,8 @@ export const SentenceBuilder = ({
81
 
82
  if (input.includes(target)) {
83
  toast({
84
- title: "Invalid Word",
85
- description: `You cannot use words that contain "${currentWord}"`,
86
  variant: "destructive",
87
  });
88
  return;
@@ -98,10 +96,10 @@ export const SentenceBuilder = ({
98
  className="text-center"
99
  >
100
  <h2 className="mb-4 text-2xl font-semibold text-gray-900">
101
- Build a Description
102
  </h2>
103
  <p className="mb-6 text-sm text-gray-600">
104
- Take turns with AI to describe your word without using the word itself!
105
  </p>
106
  <div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
107
  {imageLoaded && (
@@ -117,7 +115,7 @@ export const SentenceBuilder = ({
117
  </div>
118
  <div className="mb-6 rounded-lg bg-gray-50 p-4">
119
  <p className="text-lg text-gray-800">
120
- {sentence.length > 0 ? sentence.join(" ") : "Start your sentence..."}
121
  </p>
122
  </div>
123
  <form onSubmit={handleSubmit} className="mb-4">
@@ -126,12 +124,11 @@ export const SentenceBuilder = ({
126
  type="text"
127
  value={playerInput}
128
  onChange={(e) => {
129
- // Only allow letters in the input
130
  const value = e.target.value.replace(/[^a-zA-Z]/g, '');
131
  onInputChange(value);
132
  }}
133
  onKeyDown={handleKeyDown}
134
- placeholder="Enter your word (letters only)..."
135
  className="mb-4"
136
  disabled={isAiThinking}
137
  />
@@ -141,7 +138,7 @@ export const SentenceBuilder = ({
141
  className="flex-1 bg-primary text-lg hover:bg-primary/90"
142
  disabled={!playerInput.trim() || isAiThinking}
143
  >
144
- {isAiThinking ? "AI is thinking..." : "Add Word ⏎"}
145
  </Button>
146
  <Button
147
  type="button"
@@ -149,7 +146,7 @@ export const SentenceBuilder = ({
149
  className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
150
  disabled={(!sentence.length && !playerInput.trim()) || isAiThinking}
151
  >
152
- {isAiThinking ? "AI is thinking..." : "Make AI Guess ⇧⏎"}
153
  </Button>
154
  </div>
155
  </form>
 
3
  import { motion } from "framer-motion";
4
  import { KeyboardEvent, useRef, useEffect, useState } from "react";
5
  import { useToast } from "@/hooks/use-toast";
6
+ import { useTranslation } from "@/hooks/useTranslation";
7
 
8
  interface SentenceBuilderProps {
9
  currentWord: string;
 
30
  const [imageLoaded, setImageLoaded] = useState(false);
31
  const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
32
  const { toast } = useToast();
33
+ const t = useTranslation();
34
 
35
  useEffect(() => {
36
  const img = new Image();
 
39
  console.log("Attempting to load image:", imagePath);
40
  }, [imagePath]);
41
 
 
42
  useEffect(() => {
43
  setTimeout(() => {
44
  inputRef.current?.focus();
45
  }, 100);
46
  }, []);
47
 
 
48
  useEffect(() => {
49
  if (!isAiThinking && sentence.length > 0 && sentence.length % 2 === 0) {
50
  setTimeout(() => {
 
59
  if (playerInput.trim()) {
60
  handleSubmit(e as any);
61
  }
 
62
  onMakeGuess();
63
  }
64
  };
 
68
  const input = playerInput.trim().toLowerCase();
69
  const target = currentWord.toLowerCase();
70
 
 
71
  if (!/^[a-zA-Z]+$/.test(input)) {
72
  toast({
73
+ title: t.game.invalidWord,
74
+ description: t.game.lettersOnly,
75
  variant: "destructive",
76
  });
77
  return;
 
79
 
80
  if (input.includes(target)) {
81
  toast({
82
+ title: t.game.invalidWord,
83
+ description: `${t.game.cantUseTargetWord} "${currentWord}"`,
84
  variant: "destructive",
85
  });
86
  return;
 
96
  className="text-center"
97
  >
98
  <h2 className="mb-4 text-2xl font-semibold text-gray-900">
99
+ {t.game.buildDescription}
100
  </h2>
101
  <p className="mb-6 text-sm text-gray-600">
102
+ {t.game.buildSubtitle}
103
  </p>
104
  <div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
105
  {imageLoaded && (
 
115
  </div>
116
  <div className="mb-6 rounded-lg bg-gray-50 p-4">
117
  <p className="text-lg text-gray-800">
118
+ {sentence.length > 0 ? sentence.join(" ") : t.game.startSentence}
119
  </p>
120
  </div>
121
  <form onSubmit={handleSubmit} className="mb-4">
 
124
  type="text"
125
  value={playerInput}
126
  onChange={(e) => {
 
127
  const value = e.target.value.replace(/[^a-zA-Z]/g, '');
128
  onInputChange(value);
129
  }}
130
  onKeyDown={handleKeyDown}
131
+ placeholder={t.game.inputPlaceholder}
132
  className="mb-4"
133
  disabled={isAiThinking}
134
  />
 
138
  className="flex-1 bg-primary text-lg hover:bg-primary/90"
139
  disabled={!playerInput.trim() || isAiThinking}
140
  >
141
+ {isAiThinking ? t.game.aiThinking : t.game.addWord}
142
  </Button>
143
  <Button
144
  type="button"
 
146
  className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
147
  disabled={(!sentence.length && !playerInput.trim()) || isAiThinking}
148
  >
149
+ {isAiThinking ? t.game.aiThinking : t.game.makeGuess}
150
  </Button>
151
  </div>
152
  </form>
src/components/game/ThemeSelector.tsx CHANGED
@@ -2,8 +2,11 @@ import { useState, useEffect, useRef } from "react";
2
  import { Button } from "@/components/ui/button";
3
  import { Input } from "@/components/ui/input";
4
  import { motion } from "framer-motion";
 
 
 
5
 
6
- type Theme = "standard" | "sports" | "food" | "custom";
7
 
8
  interface ThemeSelectorProps {
9
  onThemeSelect: (theme: string) => void;
@@ -14,14 +17,26 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
14
  const [customTheme, setCustomTheme] = useState("");
15
  const [isGenerating, setIsGenerating] = useState(false);
16
  const inputRef = useRef<HTMLInputElement>(null);
 
 
 
 
 
 
 
 
17
 
18
  useEffect(() => {
19
  const handleKeyPress = (e: KeyboardEvent) => {
20
- if (e.target instanceof HTMLInputElement) return; // Ignore when typing in input
21
 
22
  switch(e.key.toLowerCase()) {
23
  case 'a':
24
- setSelectedTheme("standard");
 
 
 
 
25
  break;
26
  case 'b':
27
  setSelectedTheme("sports");
@@ -30,7 +45,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
30
  setSelectedTheme("food");
31
  break;
32
  case 'd':
33
- e.preventDefault(); // Prevent 'd' from being entered in the input
34
  setSelectedTheme("custom");
35
  break;
36
  case 'enter':
@@ -43,7 +58,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
43
 
44
  window.addEventListener('keydown', handleKeyPress);
45
  return () => window.removeEventListener('keydown', handleKeyPress);
46
- }, [selectedTheme, customTheme]);
47
 
48
  useEffect(() => {
49
  if (selectedTheme === "custom") {
@@ -77,25 +92,35 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
77
  className="space-y-6"
78
  >
79
  <div className="text-center space-y-2">
80
- <h2 className="text-2xl font-bold text-gray-900">Choose a Theme</h2>
81
- <p className="text-gray-600">Select a theme for your word-guessing adventure</p>
82
  </div>
83
 
84
  <div className="space-y-4">
85
- <Button
86
- variant={selectedTheme === "standard" ? "default" : "outline"}
87
- className="w-full justify-between"
88
- onClick={() => setSelectedTheme("standard")}
89
- >
90
- Standard <span className="text-sm opacity-50">Press A</span>
91
- </Button>
 
 
 
 
 
 
 
 
 
 
92
 
93
  <Button
94
  variant={selectedTheme === "sports" ? "default" : "outline"}
95
  className="w-full justify-between"
96
  onClick={() => setSelectedTheme("sports")}
97
  >
98
- Sports <span className="text-sm opacity-50">Press B</span>
99
  </Button>
100
 
101
  <Button
@@ -103,7 +128,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
103
  className="w-full justify-between"
104
  onClick={() => setSelectedTheme("food")}
105
  >
106
- Food <span className="text-sm opacity-50">Press C</span>
107
  </Button>
108
 
109
  <Button
@@ -111,7 +136,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
111
  className="w-full justify-between"
112
  onClick={() => setSelectedTheme("custom")}
113
  >
114
- Choose your theme <span className="text-sm opacity-50">Press D</span>
115
  </Button>
116
 
117
  {selectedTheme === "custom" && (
@@ -124,7 +149,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
124
  <Input
125
  ref={inputRef}
126
  type="text"
127
- placeholder="Enter a theme (e.g., Animals, Movies)"
128
  value={customTheme}
129
  onChange={(e) => setCustomTheme(e.target.value)}
130
  onKeyPress={handleInputKeyPress}
@@ -139,7 +164,7 @@ export const ThemeSelector = ({ onThemeSelect }: ThemeSelectorProps) => {
139
  className="w-full"
140
  disabled={selectedTheme === "custom" && !customTheme.trim() || isGenerating}
141
  >
142
- {isGenerating ? "Generating themed words..." : "Continue ⏎"}
143
  </Button>
144
  </motion.div>
145
  );
 
2
  import { Button } from "@/components/ui/button";
3
  import { Input } from "@/components/ui/input";
4
  import { motion } from "framer-motion";
5
+ import { useTranslation } from "@/hooks/useTranslation";
6
+ import { useContext } from "react";
7
+ import { LanguageContext } from "@/contexts/LanguageContext";
8
 
9
+ type Theme = "standard" | "technology" | "sports" | "food" | "custom";
10
 
11
  interface ThemeSelectorProps {
12
  onThemeSelect: (theme: string) => void;
 
17
  const [customTheme, setCustomTheme] = useState("");
18
  const [isGenerating, setIsGenerating] = useState(false);
19
  const inputRef = useRef<HTMLInputElement>(null);
20
+ const t = useTranslation();
21
+ const { language } = useContext(LanguageContext);
22
+
23
+ useEffect(() => {
24
+ if (language !== 'en') {
25
+ setSelectedTheme("technology");
26
+ }
27
+ }, [language]);
28
 
29
  useEffect(() => {
30
  const handleKeyPress = (e: KeyboardEvent) => {
31
+ if (e.target instanceof HTMLInputElement) return;
32
 
33
  switch(e.key.toLowerCase()) {
34
  case 'a':
35
+ if (language === 'en') {
36
+ setSelectedTheme("standard");
37
+ } else {
38
+ setSelectedTheme("technology");
39
+ }
40
  break;
41
  case 'b':
42
  setSelectedTheme("sports");
 
45
  setSelectedTheme("food");
46
  break;
47
  case 'd':
48
+ e.preventDefault();
49
  setSelectedTheme("custom");
50
  break;
51
  case 'enter':
 
58
 
59
  window.addEventListener('keydown', handleKeyPress);
60
  return () => window.removeEventListener('keydown', handleKeyPress);
61
+ }, [selectedTheme, customTheme, language]);
62
 
63
  useEffect(() => {
64
  if (selectedTheme === "custom") {
 
92
  className="space-y-6"
93
  >
94
  <div className="text-center space-y-2">
95
+ <h2 className="text-2xl font-bold text-gray-900">{t.themes.title}</h2>
96
+ <p className="text-gray-600">{t.themes.subtitle}</p>
97
  </div>
98
 
99
  <div className="space-y-4">
100
+ {language === 'en' ? (
101
+ <Button
102
+ variant={selectedTheme === "standard" ? "default" : "outline"}
103
+ className="w-full justify-between"
104
+ onClick={() => setSelectedTheme("standard")}
105
+ >
106
+ {t.themes.standard} <span className="text-sm opacity-50">{t.themes.pressKey} A</span>
107
+ </Button>
108
+ ) : (
109
+ <Button
110
+ variant={selectedTheme === "technology" ? "default" : "outline"}
111
+ className="w-full justify-between"
112
+ onClick={() => setSelectedTheme("technology")}
113
+ >
114
+ {t.themes.technology} <span className="text-sm opacity-50">{t.themes.pressKey} A</span>
115
+ </Button>
116
+ )}
117
 
118
  <Button
119
  variant={selectedTheme === "sports" ? "default" : "outline"}
120
  className="w-full justify-between"
121
  onClick={() => setSelectedTheme("sports")}
122
  >
123
+ {t.themes.sports} <span className="text-sm opacity-50">{t.themes.pressKey} B</span>
124
  </Button>
125
 
126
  <Button
 
128
  className="w-full justify-between"
129
  onClick={() => setSelectedTheme("food")}
130
  >
131
+ {t.themes.food} <span className="text-sm opacity-50">{t.themes.pressKey} C</span>
132
  </Button>
133
 
134
  <Button
 
136
  className="w-full justify-between"
137
  onClick={() => setSelectedTheme("custom")}
138
  >
139
+ {t.themes.custom} <span className="text-sm opacity-50">{t.themes.pressKey} D</span>
140
  </Button>
141
 
142
  {selectedTheme === "custom" && (
 
149
  <Input
150
  ref={inputRef}
151
  type="text"
152
+ placeholder={t.themes.customPlaceholder}
153
  value={customTheme}
154
  onChange={(e) => setCustomTheme(e.target.value)}
155
  onKeyPress={handleInputKeyPress}
 
164
  className="w-full"
165
  disabled={selectedTheme === "custom" && !customTheme.trim() || isGenerating}
166
  >
167
+ {isGenerating ? t.themes.generating : `${t.themes.continue} ⏎`}
168
  </Button>
169
  </motion.div>
170
  );
src/components/game/WelcomeScreen.tsx CHANGED
@@ -3,7 +3,10 @@ import { motion } from "framer-motion";
3
  import { useState } from "react";
4
  import { HighScoreBoard } from "../HighScoreBoard";
5
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
6
- import { Card, CardContent } from "@/components/ui/card";
 
 
 
7
 
8
  interface WelcomeScreenProps {
9
  onStart: () => void;
@@ -12,30 +15,8 @@ interface WelcomeScreenProps {
12
  export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
13
  const [showHighScores, setShowHighScores] = useState(false);
14
  const [showHowToPlay, setShowHowToPlay] = useState(false);
15
-
16
- const HowToPlayContent = () => (
17
- <div className="space-y-6">
18
- <div className="grid gap-4 text-gray-600">
19
- <div>
20
- <h3 className="font-medium text-gray-800">The Setup</h3>
21
- <p>You'll work with two AIs: one as your partner giving clues, and another trying to guess the word.</p>
22
- </div>
23
- <div>
24
- <h3 className="font-medium text-gray-800">Your Goal</h3>
25
- <p>Help the AI guess the secret word using one-word clues. Each correct guess earns you a point!</p>
26
- </div>
27
- <div>
28
- <h3 className="font-medium text-gray-800">The Rules</h3>
29
- <ul className="list-disc list-inside space-y-1">
30
- <li>One word per clue only</li>
31
- <li>No parts of the secret word or translations</li>
32
- <li>Clues must relate to the word (be creative!)</li>
33
- <li>No spelling out the answer</li>
34
- </ul>
35
- </div>
36
- </div>
37
- </div>
38
- );
39
 
40
  return (
41
  <>
@@ -44,10 +25,12 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
44
  animate={{ opacity: 1 }}
45
  className="max-w-2xl mx-auto text-center space-y-8"
46
  >
 
 
47
  <div>
48
- <h1 className="mb-4 text-4xl font-bold text-gray-900">Think in Sync</h1>
49
  <p className="text-lg text-gray-600">
50
- In this game you team up with AI to guess secret words!
51
  </p>
52
  </div>
53
 
@@ -56,7 +39,7 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
56
  onClick={onStart}
57
  className="w-full bg-primary text-lg hover:bg-primary/90"
58
  >
59
- Start Game
60
  </Button>
61
  <div className="grid grid-cols-2 gap-4">
62
  <Button
@@ -64,14 +47,14 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
64
  variant="outline"
65
  className="text-lg"
66
  >
67
- How to Play 📖
68
  </Button>
69
  <Button
70
  onClick={() => setShowHighScores(true)}
71
  variant="outline"
72
  className="text-lg"
73
  >
74
- Leaderboard 🏆
75
  </Button>
76
  </div>
77
  </div>
@@ -91,11 +74,30 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
91
  <Dialog open={showHowToPlay} onOpenChange={setShowHowToPlay}>
92
  <DialogContent className="sm:max-w-[600px]">
93
  <DialogHeader>
94
- <DialogTitle className="text-xl font-semibold text-primary">How to Play</DialogTitle>
95
  </DialogHeader>
96
- <HowToPlayContent />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </DialogContent>
98
  </Dialog>
99
  </>
100
  );
101
- };
 
3
  import { useState } from "react";
4
  import { HighScoreBoard } from "../HighScoreBoard";
5
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
6
+ import { LanguageSelector } from "./LanguageSelector";
7
+ import { useTranslation } from "@/hooks/useTranslation";
8
+ import { useContext } from "react";
9
+ import { LanguageContext } from "@/contexts/LanguageContext";
10
 
11
  interface WelcomeScreenProps {
12
  onStart: () => void;
 
15
  export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
16
  const [showHighScores, setShowHighScores] = useState(false);
17
  const [showHowToPlay, setShowHowToPlay] = useState(false);
18
+ const t = useTranslation();
19
+ const { language } = useContext(LanguageContext);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  return (
22
  <>
 
25
  animate={{ opacity: 1 }}
26
  className="max-w-2xl mx-auto text-center space-y-8"
27
  >
28
+ <LanguageSelector />
29
+
30
  <div>
31
+ <h1 className="mb-4 text-4xl font-bold text-gray-900">{t.welcome.title}</h1>
32
  <p className="text-lg text-gray-600">
33
+ {t.welcome.subtitle}
34
  </p>
35
  </div>
36
 
 
39
  onClick={onStart}
40
  className="w-full bg-primary text-lg hover:bg-primary/90"
41
  >
42
+ {t.welcome.startButton}
43
  </Button>
44
  <div className="grid grid-cols-2 gap-4">
45
  <Button
 
47
  variant="outline"
48
  className="text-lg"
49
  >
50
+ {t.welcome.howToPlay} 📖
51
  </Button>
52
  <Button
53
  onClick={() => setShowHighScores(true)}
54
  variant="outline"
55
  className="text-lg"
56
  >
57
+ {t.welcome.leaderboard} 🏆
58
  </Button>
59
  </div>
60
  </div>
 
74
  <Dialog open={showHowToPlay} onOpenChange={setShowHowToPlay}>
75
  <DialogContent className="sm:max-w-[600px]">
76
  <DialogHeader>
77
+ <DialogTitle className="text-xl font-semibold text-primary">{t.welcome.howToPlay}</DialogTitle>
78
  </DialogHeader>
79
+ <div className="space-y-6">
80
+ <div className="grid gap-4 text-gray-600">
81
+ <div>
82
+ <h3 className="font-medium text-gray-800">{t.howToPlay.setup.title}</h3>
83
+ <p>{t.howToPlay.setup.description}</p>
84
+ </div>
85
+ <div>
86
+ <h3 className="font-medium text-gray-800">{t.howToPlay.goal.title}</h3>
87
+ <p>{t.howToPlay.goal.description}</p>
88
+ </div>
89
+ <div>
90
+ <h3 className="font-medium text-gray-800">{t.howToPlay.rules.title}</h3>
91
+ <ul className="list-disc list-inside space-y-1">
92
+ {t.howToPlay.rules.items.map((rule, index) => (
93
+ <li key={index}>{rule}</li>
94
+ ))}
95
+ </ul>
96
+ </div>
97
+ </div>
98
+ </div>
99
  </DialogContent>
100
  </Dialog>
101
  </>
102
  );
103
+ };
src/contexts/LanguageContext.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useState, useEffect, ReactNode } from 'react';
2
+ import { Language } from '@/i18n/translations';
3
+
4
+ interface LanguageContextType {
5
+ language: Language;
6
+ setLanguage: (lang: Language) => void;
7
+ }
8
+
9
+ export const LanguageContext = createContext<LanguageContextType>({
10
+ language: 'en',
11
+ setLanguage: () => {},
12
+ });
13
+
14
+ interface LanguageProviderProps {
15
+ children: ReactNode;
16
+ }
17
+
18
+ export const LanguageProvider = ({ children }: LanguageProviderProps) => {
19
+ const [language, setLanguage] = useState<Language>('en');
20
+
21
+ useEffect(() => {
22
+ const savedLang = localStorage.getItem('language') as Language;
23
+ if (savedLang && ['en', 'fr', 'de', 'it', 'es'].includes(savedLang)) {
24
+ setLanguage(savedLang);
25
+ }
26
+ }, []);
27
+
28
+ const handleSetLanguage = (lang: Language) => {
29
+ setLanguage(lang);
30
+ localStorage.setItem('language', lang);
31
+ };
32
+
33
+ return (
34
+ <LanguageContext.Provider value={{ language, setLanguage: handleSetLanguage }}>
35
+ {children}
36
+ </LanguageContext.Provider>
37
+ );
38
+ };
src/hooks/useTranslation.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { useContext } from 'react';
2
+ import { LanguageContext } from '@/contexts/LanguageContext';
3
+ import { translations } from '@/i18n/translations';
4
+
5
+ export const useTranslation = () => {
6
+ const { language } = useContext(LanguageContext);
7
+ return translations[language];
8
+ };
src/i18n/translations/de.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const de = {
2
+ welcome: {
3
+ title: "Denken im Einklang",
4
+ subtitle: "In diesem Spiel arbeiten Sie mit KI zusammen, um geheime Wörter zu erraten!",
5
+ startButton: "Spiel Starten",
6
+ howToPlay: "Spielanleitung",
7
+ leaderboard: "Bestenliste"
8
+ },
9
+ howToPlay: {
10
+ setup: {
11
+ title: "Der Aufbau",
12
+ description: "Sie arbeiten mit zwei KIs: eine als Partner, der Hinweise gibt, und eine andere, die versucht, das Wort zu erraten."
13
+ },
14
+ goal: {
15
+ title: "Ihr Ziel",
16
+ description: "Helfen Sie der KI, das geheime Wort mit Einwort-Hinweisen zu erraten. Jede richtige Vermutung bringt Ihnen einen Punkt!"
17
+ },
18
+ rules: {
19
+ title: "Die Regeln",
20
+ items: [
21
+ "Nur ein Wort pro Hinweis",
22
+ "Keine Teile des geheimen Wortes oder Übersetzungen",
23
+ "Hinweise müssen sich auf das Wort beziehen (seien Sie kreativ!)",
24
+ "Keine Buchstabierung der Antwort"
25
+ ]
26
+ }
27
+ },
28
+ game: {
29
+ buildDescription: "Beschreibung Erstellen",
30
+ buildSubtitle: "Wechseln Sie sich mit der KI ab, um Ihr Wort zu beschreiben, ohne das Wort selbst zu verwenden!",
31
+ startSentence: "Beginnen Sie Ihren Satz...",
32
+ inputPlaceholder: "Geben Sie Ihr Wort ein (nur Buchstaben)...",
33
+ addWord: "Wort Hinzufügen",
34
+ makeGuess: "KI Raten Lassen",
35
+ aiThinking: "KI denkt nach...",
36
+ aiDelayed: "Die KI ist derzeit beschäftigt. Bitte versuchen Sie es in einem Moment erneut.",
37
+ invalidWord: "Ungültiges Wort",
38
+ cantUseTargetWord: "Sie können keine Wörter verwenden, die enthalten",
39
+ lettersOnly: "Bitte verwenden Sie nur Buchstaben (keine Zahlen oder Sonderzeichen)"
40
+ },
41
+ guess: {
42
+ title: "KI-Vermutung",
43
+ sentence: "Ihr Satz",
44
+ aiGuessed: "KI hat geraten",
45
+ correct: "Richtig geraten! 🎉 Bereit für die nächste Runde? Drücken Sie Enter",
46
+ incorrect: "Spiel vorbei! Drücken Sie Enter zum Neustart",
47
+ nextRound: "Nächste Runde",
48
+ playAgain: "Erneut Spielen",
49
+ viewLeaderboard: "Bestenliste Anzeigen"
50
+ },
51
+ gameOver: {
52
+ title: "Spiel Vorbei!",
53
+ completedRounds: "Sie haben {count} Runden erfolgreich abgeschlossen!",
54
+ playAgain: "Erneut Spielen"
55
+ },
56
+ themes: {
57
+ title: "Thema Wählen",
58
+ subtitle: "Wählen Sie ein Thema für Ihr Wörter-Rate-Abenteuer",
59
+ standard: "",
60
+ technology: "Technologie",
61
+ sports: "Sport",
62
+ food: "Essen",
63
+ custom: "Wählen Sie Ihr Thema",
64
+ customPlaceholder: "Geben Sie ein Thema ein (z.B. Tiere, Filme)",
65
+ continue: "Weiter",
66
+ generating: "Generiere thematische Wörter...",
67
+ pressKey: "Drücken Sie"
68
+ },
69
+ leaderboard: {
70
+ title: "Bestenliste",
71
+ yourScore: "Ihre Punktzahl",
72
+ roundCount: "Runden",
73
+ wordsPerRound: "Wörter/Runde",
74
+ enterName: "Geben Sie Ihren Namen ein (nur Buchstaben und Zahlen)",
75
+ submit: "Punktzahl Einreichen",
76
+ submitting: "Wird eingereicht...",
77
+ rank: "Rang",
78
+ player: "Spieler",
79
+ roundsColumn: "Runden",
80
+ avgWords: "Durchschn. Wörter/Runde",
81
+ noScores: "Noch keine Highscores. Seien Sie der Erste!",
82
+ previous: "Zurück",
83
+ next: "Weiter",
84
+ error: {
85
+ invalidName: "Bitte geben Sie einen gültigen Namen ein (nur Buchstaben und Zahlen)",
86
+ noRounds: "Sie müssen mindestens eine Runde abschließen, um eine Punktzahl einzureichen",
87
+ alreadySubmitted: "Sie haben Ihre Punktzahl für dieses Spiel bereits eingereicht",
88
+ newHighScore: "Neuer Highscore!",
89
+ beatRecord: "Sie haben Ihren bisherigen Rekord von {score} Runden übertroffen!",
90
+ notHigher: "Ihre aktuelle Punktzahl ({current}) ist nicht höher als Ihre beste Punktzahl ({best})",
91
+ submitError: "Fehler beim Einreichen der Punktzahl. Bitte versuchen Sie es erneut."
92
+ }
93
+ }
94
+ };
src/i18n/translations/en.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const en = {
2
+ welcome: {
3
+ title: "Think in Sync",
4
+ subtitle: "In this game you team up with AI to guess secret words!",
5
+ startButton: "Start Game",
6
+ howToPlay: "How to Play",
7
+ leaderboard: "Leaderboard"
8
+ },
9
+ howToPlay: {
10
+ setup: {
11
+ title: "The Setup",
12
+ description: "You'll work with two AIs: one as your partner giving clues, and another trying to guess the word."
13
+ },
14
+ goal: {
15
+ title: "Your Goal",
16
+ description: "Help the AI guess the secret word using one-word clues. Each correct guess earns you a point!"
17
+ },
18
+ rules: {
19
+ title: "The Rules",
20
+ items: [
21
+ "One word per clue only",
22
+ "No parts of the secret word or translations",
23
+ "Clues must relate to the word (be creative!)",
24
+ "No spelling out the answer"
25
+ ]
26
+ }
27
+ },
28
+ game: {
29
+ buildDescription: "Build a Description",
30
+ buildSubtitle: "Take turns with AI to describe your word without using the word itself!",
31
+ startSentence: "Start your sentence...",
32
+ inputPlaceholder: "Enter your word (letters only)...",
33
+ addWord: "Add Word",
34
+ makeGuess: "Make AI Guess",
35
+ aiThinking: "AI is thinking...",
36
+ aiDelayed: "The AI is currently busy. Please try again in a moment.",
37
+ invalidWord: "Invalid Word",
38
+ cantUseTargetWord: "You cannot use words that contain",
39
+ lettersOnly: "Please use only letters (no numbers or special characters)"
40
+ },
41
+ guess: {
42
+ title: "AI's Guess",
43
+ sentence: "Your sentence",
44
+ aiGuessed: "AI guessed",
45
+ correct: "Correct guess! 🎉 Ready for the next round? Press Enter",
46
+ incorrect: "Game Over! Press Enter to play again",
47
+ nextRound: "Next Round",
48
+ playAgain: "Play Again",
49
+ viewLeaderboard: "View Leaderboard"
50
+ },
51
+ gameOver: {
52
+ title: "Game Over!",
53
+ completedRounds: "You completed {count} rounds successfully!",
54
+ playAgain: "Play Again"
55
+ },
56
+ themes: {
57
+ title: "Choose a Theme",
58
+ subtitle: "Select a theme for your word-guessing adventure",
59
+ standard: "Standard",
60
+ technology: "Technology",
61
+ sports: "Sports",
62
+ food: "Food",
63
+ custom: "Choose your theme",
64
+ customPlaceholder: "Enter a theme (e.g., Animals, Movies)",
65
+ continue: "Continue",
66
+ generating: "Generating themed words...",
67
+ pressKey: "Press"
68
+ },
69
+ leaderboard: {
70
+ title: "Leaderboard",
71
+ yourScore: "Your score",
72
+ roundCount: "rounds",
73
+ wordsPerRound: "words/round",
74
+ enterName: "Enter your name (letters and numbers only)",
75
+ submit: "Submit Score",
76
+ submitting: "Submitting...",
77
+ rank: "Rank",
78
+ player: "Player",
79
+ roundsColumn: "Rounds",
80
+ avgWords: "Avg Words/Round",
81
+ noScores: "No high scores yet. Be the first!",
82
+ previous: "Previous",
83
+ next: "Next",
84
+ error: {
85
+ invalidName: "Please enter a valid name (only letters and numbers allowed)",
86
+ noRounds: "You need to complete at least one round to submit a score",
87
+ alreadySubmitted: "You have already submitted your score for this game",
88
+ newHighScore: "New High Score!",
89
+ beatRecord: "You beat your previous record of {score} rounds!",
90
+ notHigher: "Your current score ({current}) is not higher than your best score ({best})",
91
+ submitError: "Failed to submit score. Please try again."
92
+ }
93
+ }
94
+ };
src/i18n/translations/es.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const es = {
2
+ welcome: {
3
+ title: "Pensar en Sintonía",
4
+ subtitle: "¡En este juego te unes a la IA para adivinar palabras secretas!",
5
+ startButton: "Comenzar Juego",
6
+ howToPlay: "Cómo Jugar",
7
+ leaderboard: "Tabla de Posiciones"
8
+ },
9
+ howToPlay: {
10
+ setup: {
11
+ title: "La Configuración",
12
+ description: "Trabajarás con dos IAs: una como compañera dando pistas y otra intentando adivinar la palabra."
13
+ },
14
+ goal: {
15
+ title: "Tu Objetivo",
16
+ description: "Ayuda a la IA a adivinar la palabra secreta usando pistas de una sola palabra. ¡Cada adivinanza correcta te da un punto!"
17
+ },
18
+ rules: {
19
+ title: "Las Reglas",
20
+ items: [
21
+ "Solo una palabra por pista",
22
+ "No usar partes de la palabra secreta ni traducciones",
23
+ "Las pistas deben relacionarse con la palabra (¡sé creativo!)",
24
+ "No deletrear la respuesta"
25
+ ]
26
+ }
27
+ },
28
+ game: {
29
+ buildDescription: "Construye una Descripción",
30
+ buildSubtitle: "¡Alterna con la IA para describir tu palabra sin usar la palabra misma!",
31
+ startSentence: "Comienza tu frase...",
32
+ inputPlaceholder: "Ingresa tu palabra (solo letras)...",
33
+ addWord: "Agregar Palabra",
34
+ makeGuess: "Hacer que la IA Adivine",
35
+ aiThinking: "La IA está pensando...",
36
+ aiDelayed: "La IA está ocupada en este momento. Por favor, inténtalo de nuevo en un momento.",
37
+ invalidWord: "Palabra Inválida",
38
+ cantUseTargetWord: "No puedes usar palabras que contengan",
39
+ lettersOnly: "Por favor usa solo letras (sin números ni caracteres especiales)"
40
+ },
41
+ guess: {
42
+ title: "Intento de la IA",
43
+ sentence: "Tu frase",
44
+ aiGuessed: "La IA adivinó",
45
+ correct: "¡Adivinanza correcta! 🎉 ¿Listo para la siguiente ronda? Presiona Enter",
46
+ incorrect: "¡Juego terminado! Presiona Enter para jugar de nuevo",
47
+ nextRound: "Siguiente Ronda",
48
+ playAgain: "Jugar de Nuevo",
49
+ viewLeaderboard: "Ver Tabla de Posiciones"
50
+ },
51
+ gameOver: {
52
+ title: "¡Juego Terminado!",
53
+ completedRounds: "¡Completaste {count} rondas exitosamente!",
54
+ playAgain: "Jugar de Nuevo"
55
+ },
56
+ themes: {
57
+ title: "Elegir un Tema",
58
+ subtitle: "Selecciona un tema para tu aventura de adivinanzas",
59
+ standard: "",
60
+ technology: "Tecnología",
61
+ sports: "Deportes",
62
+ food: "Comida",
63
+ custom: "Elige tu tema",
64
+ customPlaceholder: "Ingresa un tema (ej: Animales, Películas)",
65
+ continue: "Continuar",
66
+ generating: "Generando palabras temáticas...",
67
+ pressKey: "Presiona"
68
+ },
69
+ leaderboard: {
70
+ title: "Tabla de Posiciones",
71
+ yourScore: "Tu puntaje",
72
+ roundCount: "rondas",
73
+ wordsPerRound: "palabras/ronda",
74
+ enterName: "Ingresa tu nombre (solo letras y números)",
75
+ submit: "Enviar Puntaje",
76
+ submitting: "Enviando...",
77
+ rank: "Posición",
78
+ player: "Jugador",
79
+ roundsColumn: "Rondas",
80
+ avgWords: "Prom. Palabras/Ronda",
81
+ noScores: "Aún no hay puntajes altos. ¡Sé el primero!",
82
+ previous: "Anterior",
83
+ next: "Siguiente",
84
+ error: {
85
+ invalidName: "Por favor ingresa un nombre válido (solo letras y números)",
86
+ noRounds: "Necesitas completar al menos una ronda para enviar un puntaje",
87
+ alreadySubmitted: "Ya has enviado tu puntaje para este juego",
88
+ newHighScore: "¡Nuevo Récord!",
89
+ beatRecord: "¡Superaste tu récord anterior de {score} rondas!",
90
+ notHigher: "Tu puntaje actual ({current}) no es mayor que tu mejor puntaje ({best})",
91
+ submitError: "Error al enviar el puntaje. Por favor intenta de nuevo."
92
+ }
93
+ }
94
+ };
src/i18n/translations/fr.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const fr = {
2
+ welcome: {
3
+ title: "Penser en Sync",
4
+ subtitle: "Dans ce jeu, vous faites équipe avec l'IA pour deviner des mots secrets !",
5
+ startButton: "Commencer",
6
+ howToPlay: "Comment Jouer",
7
+ leaderboard: "Classement"
8
+ },
9
+ howToPlay: {
10
+ setup: {
11
+ title: "La Configuration",
12
+ description: "Vous travaillerez avec deux IA : une comme partenaire donnant des indices, et une autre essayant de deviner le mot."
13
+ },
14
+ goal: {
15
+ title: "Votre Objectif",
16
+ description: "Aidez l'IA à deviner le mot secret en utilisant des indices d'un seul mot. Chaque devinette correcte vous rapporte un point !"
17
+ },
18
+ rules: {
19
+ title: "Les Règles",
20
+ items: [
21
+ "Un seul mot par indice",
22
+ "Pas de parties du mot secret ni de traductions",
23
+ "Les indices doivent être liés au mot (soyez créatif !)",
24
+ "Ne pas épeler la réponse"
25
+ ]
26
+ }
27
+ },
28
+ game: {
29
+ buildDescription: "Construire une Description",
30
+ buildSubtitle: "Alternez avec l'IA pour décrire votre mot sans utiliser le mot lui-même !",
31
+ startSentence: "Commencez votre phrase...",
32
+ inputPlaceholder: "Entrez votre mot (lettres uniquement)...",
33
+ addWord: "Ajouter un Mot",
34
+ makeGuess: "Faire Deviner l'IA",
35
+ aiThinking: "L'IA réfléchit...",
36
+ aiDelayed: "L'IA est actuellement occupée. Veuillez réessayer dans un moment.",
37
+ invalidWord: "Mot Invalide",
38
+ cantUseTargetWord: "Vous ne pouvez pas utiliser des mots qui contiennent",
39
+ lettersOnly: "Veuillez utiliser uniquement des lettres (pas de chiffres ni de caractères spéciaux)"
40
+ },
41
+ guess: {
42
+ title: "Devinette de l'IA",
43
+ sentence: "Votre phrase",
44
+ aiGuessed: "L'IA a deviné",
45
+ correct: "Devinette correcte ! 🎉 Prêt pour le prochain tour ? Appuyez sur Entrée",
46
+ incorrect: "Partie terminée ! Appuyez sur Entrée pour rejouer",
47
+ nextRound: "Tour Suivant",
48
+ playAgain: "Rejouer",
49
+ viewLeaderboard: "Voir le Classement"
50
+ },
51
+ gameOver: {
52
+ title: "Partie Terminée !",
53
+ completedRounds: "Vous avez complété {count} tours avec succès !",
54
+ playAgain: "Rejouer"
55
+ },
56
+ themes: {
57
+ title: "Choisir un Thème",
58
+ subtitle: "Sélectionnez un thème pour votre aventure de devinettes",
59
+ standard: "",
60
+ technology: "Technologie",
61
+ sports: "Sports",
62
+ food: "Nourriture",
63
+ custom: "Choisissez votre thème",
64
+ customPlaceholder: "Entrez un thème (ex: Animaux, Films)",
65
+ continue: "Continuer",
66
+ generating: "Génération des mots thématiques...",
67
+ pressKey: "Appuyez sur"
68
+ },
69
+ leaderboard: {
70
+ title: "Classement",
71
+ yourScore: "Votre score",
72
+ roundCount: "tours",
73
+ wordsPerRound: "mots/tour",
74
+ enterName: "Entrez votre nom (lettres et chiffres uniquement)",
75
+ submit: "Soumettre le Score",
76
+ submitting: "Soumission...",
77
+ rank: "Rang",
78
+ player: "Joueur",
79
+ roundsColumn: "Tours",
80
+ avgWords: "Moy. Mots/Tour",
81
+ noScores: "Pas encore de scores. Soyez le premier !",
82
+ previous: "Précédent",
83
+ next: "Suivant",
84
+ error: {
85
+ invalidName: "Veuillez entrer un nom valide (uniquement lettres et chiffres)",
86
+ noRounds: "Vous devez compléter au moins un tour pour soumettre un score",
87
+ alreadySubmitted: "Vous avez déjà soumis votre score pour cette partie",
88
+ newHighScore: "Nouveau Record !",
89
+ beatRecord: "Vous avez battu votre record précédent de {score} tours !",
90
+ notHigher: "Votre score actuel ({current}) n'est pas supérieur à votre meilleur score ({best})",
91
+ submitError: "Échec de la soumission du score. Veuillez réessayer."
92
+ }
93
+ }
94
+ };
src/i18n/translations/index.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { en } from './en';
2
+ import { fr } from './fr';
3
+ import { de } from './de';
4
+ import { it } from './it';
5
+ import { es } from './es';
6
+
7
+ export const translations = {
8
+ en,
9
+ fr,
10
+ de,
11
+ it,
12
+ es
13
+ } as const;
14
+
15
+ export type Language = keyof typeof translations;
16
+ export type Translation = typeof en;
src/i18n/translations/it.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const it = {
2
+ welcome: {
3
+ title: "Pensare in Sincronia",
4
+ subtitle: "In questo gioco fai squadra con l'IA per indovinare parole segrete!",
5
+ startButton: "Inizia Gioco",
6
+ howToPlay: "Come Giocare",
7
+ leaderboard: "Classifica"
8
+ },
9
+ howToPlay: {
10
+ setup: {
11
+ title: "La Configurazione",
12
+ description: "Lavorerai con due IA: una come partner che fornisce indizi e un'altra che cerca di indovinare la parola."
13
+ },
14
+ goal: {
15
+ title: "Il tuo Obiettivo",
16
+ description: "Aiuta l'IA a indovinare la parola segreta usando indizi di una sola parola. Ogni risposta corretta ti fa guadagnare un punto!"
17
+ },
18
+ rules: {
19
+ title: "Le Regole",
20
+ items: [
21
+ "Solo una parola per indizio",
22
+ "Niente parti della parola segreta o traduzioni",
23
+ "Gli indizi devono essere correlati alla parola (sii creativo!)",
24
+ "Non si può sillabare la risposta"
25
+ ]
26
+ }
27
+ },
28
+ game: {
29
+ buildDescription: "Costruisci una Descrizione",
30
+ buildSubtitle: "Alternati con l'IA per descrivere la tua parola senza usare la parola stessa!",
31
+ startSentence: "Inizia la tua frase...",
32
+ inputPlaceholder: "Inserisci la tua parola (solo lettere)...",
33
+ addWord: "Aggiungi Parola",
34
+ makeGuess: "Fai Indovinare l'IA",
35
+ aiThinking: "L'IA sta pensando...",
36
+ aiDelayed: "L'IA è attualmente occupata. Riprova tra un momento.",
37
+ invalidWord: "Parola Non Valida",
38
+ cantUseTargetWord: "Non puoi usare parole che contengono",
39
+ lettersOnly: "Per favore usa solo lettere (no numeri o caratteri speciali)"
40
+ },
41
+ guess: {
42
+ title: "Tentativo dell'IA",
43
+ sentence: "La tua frase",
44
+ aiGuessed: "L'IA ha indovinato",
45
+ correct: "Indovinato correttamente! 🎉 Pronto per il prossimo round? Premi Invio",
46
+ incorrect: "Game Over! Premi Invio per giocare di nuovo",
47
+ nextRound: "Prossimo Round",
48
+ playAgain: "Gioca Ancora",
49
+ viewLeaderboard: "Vedi Classifica"
50
+ },
51
+ gameOver: {
52
+ title: "Game Over!",
53
+ completedRounds: "Hai completato {count} round con successo!",
54
+ playAgain: "Gioca Ancora"
55
+ },
56
+ themes: {
57
+ title: "Scegli un Tema",
58
+ subtitle: "Seleziona un tema per la tua avventura di indovinelli",
59
+ standard: "",
60
+ technology: "Tecnologia",
61
+ sports: "Sport",
62
+ food: "Cibo",
63
+ custom: "Scegli il tuo tema",
64
+ customPlaceholder: "Inserisci un tema (es: Animali, Film)",
65
+ continue: "Continua",
66
+ generating: "Generazione parole tematiche...",
67
+ pressKey: "Premi"
68
+ },
69
+ leaderboard: {
70
+ title: "Classifica",
71
+ yourScore: "Il tuo punteggio",
72
+ roundCount: "round",
73
+ wordsPerRound: "parole/round",
74
+ enterName: "Inserisci il tuo nome (solo lettere e numeri)",
75
+ submit: "Invia Punteggio",
76
+ submitting: "Invio in corso...",
77
+ rank: "Posizione",
78
+ player: "Giocatore",
79
+ roundsColumn: "Round",
80
+ avgWords: "Media Parole/Round",
81
+ noScores: "Ancora nessun punteggio. Sii il primo!",
82
+ previous: "Precedente",
83
+ next: "Successivo",
84
+ error: {
85
+ invalidName: "Inserisci un nome valido (solo lettere e numeri)",
86
+ noRounds: "Devi completare almeno un round per inviare un punteggio",
87
+ alreadySubmitted: "Hai già inviato il tuo punteggio per questa partita",
88
+ newHighScore: "Nuovo Record!",
89
+ beatRecord: "Hai battuto il tuo record precedente di {score} round!",
90
+ notHigher: "Il tuo punteggio attuale ({current}) non è superiore al tuo miglior punteggio ({best})",
91
+ submitError: "Errore nell'invio del punteggio. Riprova."
92
+ }
93
+ }
94
+ };
src/services/themeService.ts CHANGED
@@ -1,14 +1,15 @@
1
  import { supabase } from "@/integrations/supabase/client";
 
2
 
3
- export const getThemedWord = async (theme: string, usedWords: string[] = []): Promise<string> => {
4
  if (theme === "standard") {
5
  throw new Error("Standard theme should use the words list");
6
  }
7
 
8
- console.log('Getting themed word for:', theme, 'excluding:', usedWords);
9
 
10
  const { data, error } = await supabase.functions.invoke('generate-themed-word', {
11
- body: { theme, usedWords }
12
  });
13
 
14
  if (error) {
 
1
  import { supabase } from "@/integrations/supabase/client";
2
+ import { Language } from "@/i18n/translations";
3
 
4
+ export const getThemedWord = async (theme: string, usedWords: string[] = [], language: Language = 'en'): Promise<string> => {
5
  if (theme === "standard") {
6
  throw new Error("Standard theme should use the words list");
7
  }
8
 
9
+ console.log('Getting themed word for:', theme, 'language:', language, 'excluding:', usedWords);
10
 
11
  const { data, error } = await supabase.functions.invoke('generate-themed-word', {
12
+ body: { theme, usedWords, language }
13
  });
14
 
15
  if (error) {
supabase/functions/generate-themed-word/index.ts CHANGED
@@ -7,39 +7,54 @@ const corsHeaders = {
7
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8
  };
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  serve(async (req) => {
11
  if (req.method === 'OPTIONS') {
12
  return new Response(null, { headers: corsHeaders });
13
  }
14
 
15
  try {
16
- const { theme, usedWords = [] } = await req.json();
17
- console.log('Generating word for theme:', theme, 'excluding:', usedWords);
18
 
19
  const client = new Mistral({
20
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
21
  });
22
 
 
 
23
  const response = await client.chat.complete({
24
  model: "mistral-large-latest",
25
  messages: [
26
  {
27
  role: "system",
28
- content: `You are helping generate words for a word-guessing game. Generate a single word related to the theme "${theme}".
29
- The word should be:
30
- - A single word (no spaces or hyphens)
31
- - Common enough that people would know it
32
- - Specific enough to be interesting
33
- - Related to the theme "${theme}"
34
- - Between 4 and 12 letters
35
- - A noun
36
- - NOT be any of these previously used words: ${usedWords.join(', ')}
37
-
38
- Respond with just the word in UPPERCASE, nothing else.`
39
  }
40
  ],
41
- maxTokens: 10,
42
- temperature: 0.9
43
  });
44
 
45
  const word = response.choices[0].message.content.trim();
 
7
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8
  };
9
 
10
+ const languagePrompts = {
11
+ en: {
12
+ systemPrompt: "You are helping generate words for a word-guessing game. Generate a single word in English related to the theme",
13
+ requirements: "The word should be:\n- A single word (no spaces or hyphens)\n- Common enough that people would know it\n- Specific enough to be interesting\n- Related to the theme\n- Between 4 and 12 letters\n- A noun\n- NOT be any of these previously used words:"
14
+ },
15
+ fr: {
16
+ systemPrompt: "Vous aidez à générer des mots pour un jeu de devinettes. Générez un seul mot en français lié au thème",
17
+ requirements: "Le mot doit être :\n- Un seul mot (pas d'espaces ni de traits d'union)\n- Assez courant pour que les gens le connaissent\n- Suffisamment spécifique pour être intéressant\n- En rapport avec le thème\n- Entre 4 et 12 lettres\n- Un nom\n- NE PAS être l'un de ces mots déjà utilisés :"
18
+ },
19
+ de: {
20
+ systemPrompt: "Sie helfen bei der Generierung von Wörtern für ein Worträtselspiel. Generieren Sie ein einzelnes Wort auf Deutsch zum Thema",
21
+ requirements: "Das Wort sollte:\n- Ein einzelnes Wort sein (keine Leerzeichen oder Bindestriche)\n- Häufig genug, dass Menschen es kennen\n- Spezifisch genug, um interessant zu sein\n- Zum Thema passen\n- Zwischen 4 und 12 Buchstaben lang sein\n- Ein Substantiv sein\n- NICHT eines dieser bereits verwendeten Wörter sein:"
22
+ },
23
+ it: {
24
+ systemPrompt: "Stai aiutando a generare parole per un gioco di indovinelli. Genera una singola parola in italiano legata al tema",
25
+ requirements: "La parola deve essere:\n- Una singola parola (senza spazi o trattini)\n- Abbastanza comune da essere conosciuta\n- Sufficientemente specifica da essere interessante\n- Correlata al tema\n- Tra 4 e 12 lettere\n- Un sostantivo\n- NON essere una di queste parole già utilizzate:"
26
+ },
27
+ es: {
28
+ systemPrompt: "Estás ayudando a generar palabras para un juego de adivinanzas. Genera una sola palabra en español relacionada con el tema",
29
+ requirements: "La palabra debe ser:\n- Una sola palabra (sin espacios ni guiones)\n- Lo suficientemente común para que la gente la conozca\n- Lo suficientemente específica para ser interesante\n- Relacionada con el tema\n- Entre 4 y 12 letras\n- Un sustantivo\n- NO ser ninguna de estas palabras ya utilizadas:"
30
+ }
31
+ };
32
+
33
  serve(async (req) => {
34
  if (req.method === 'OPTIONS') {
35
  return new Response(null, { headers: corsHeaders });
36
  }
37
 
38
  try {
39
+ const { theme, usedWords = [], language = 'en' } = await req.json();
40
+ console.log('Generating word for theme:', theme, 'language:', language, 'excluding:', usedWords);
41
 
42
  const client = new Mistral({
43
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
44
  });
45
 
46
+ const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
47
+
48
  const response = await client.chat.complete({
49
  model: "mistral-large-latest",
50
  messages: [
51
  {
52
  role: "system",
53
+ content: `${prompts.systemPrompt} "${theme}".\n${prompts.requirements} ${usedWords.join(', ')}\n\nRespond with just the word in UPPERCASE, nothing else.`
 
 
 
 
 
 
 
 
 
 
54
  }
55
  ],
56
+ maxTokens: 15,
57
+ temperature: 0.99
58
  });
59
 
60
  const word = response.choices[0].message.content.trim();
supabase/functions/generate-word/index.ts CHANGED
@@ -6,23 +6,50 @@ const corsHeaders = {
6
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
7
  };
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  serve(async (req) => {
10
  if (req.method === 'OPTIONS') {
11
  return new Response(null, { headers: corsHeaders });
12
  }
13
 
14
  try {
15
- const { currentWord, currentSentence } = await req.json();
16
- console.log('Generating word for:', { currentWord, currentSentence });
17
 
18
- // currentSentence is already a string from the client
19
  const existingSentence = currentSentence || '';
 
20
 
21
  const client = new Mistral({
22
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
23
  });
24
 
25
- // Add retry logic with exponential backoff
26
  const maxRetries = 3;
27
  let retryCount = 0;
28
  let lastError = null;
@@ -34,25 +61,21 @@ serve(async (req) => {
34
  messages: [
35
  {
36
  role: "system",
37
- content: `You are helping in a word game. The secret word is "${currentWord}".
38
- Your task is to find a sentence to describe this word without using it directly.
39
- Answer with a complete, grammatically correct sentence that starts with "${existingSentence}".
40
- Do not add quotes or backticks. Just answer with the sentence.`
41
  }
42
  ],
43
- maxTokens: 10,
44
  temperature: 0.5
45
  });
46
 
47
  const aiResponse = response.choices[0].message.content.trim();
48
  console.log('AI full response:', aiResponse);
49
 
50
- // Extract the new word by comparing with the existing sentence
51
  const newWord = aiResponse
52
  .slice(existingSentence.length)
53
  .trim()
54
  .split(' ')[0]
55
- .replace(/[.,!?]$/, ''); // Remove any punctuation at the end
56
 
57
  console.log('Extracted new word:', newWord);
58
 
@@ -64,27 +87,23 @@ serve(async (req) => {
64
  console.error(`Attempt ${retryCount + 1} failed:`, error);
65
  lastError = error;
66
 
67
- // If it's a rate limit error, wait before retrying
68
  if (error.message?.includes('rate limit') || error.status === 429) {
69
- const waitTime = Math.pow(2, retryCount) * 1000; // Exponential backoff: 1s, 2s, 4s
70
  console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
71
  await new Promise(resolve => setTimeout(resolve, waitTime));
72
  retryCount++;
73
  continue;
74
  }
75
 
76
- // If it's not a rate limit error, throw immediately
77
  throw error;
78
  }
79
  }
80
 
81
- // If we've exhausted all retries
82
  throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
83
 
84
  } catch (error) {
85
  console.error('Error generating word:', error);
86
 
87
- // Provide a more user-friendly error message
88
  const errorMessage = error.message?.includes('rate limit')
89
  ? "The AI service is currently busy. Please try again in a few moments."
90
  : "Sorry, there was an error generating the word. Please try again.";
 
6
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
7
  };
8
 
9
+ const languagePrompts = {
10
+ en: {
11
+ systemPrompt: "You are helping in a word game. The secret word is",
12
+ task: "Your task is to find a sentence to describe this word without using it directly.",
13
+ instruction: "Answer with a complete, grammatically correct sentence that starts with"
14
+ },
15
+ fr: {
16
+ systemPrompt: "Vous aidez dans un jeu de mots. Le mot secret est",
17
+ task: "Votre tâche est de trouver une phrase pour décrire ce mot sans l'utiliser directement.",
18
+ instruction: "Répondez avec une phrase complète et grammaticalement correcte qui commence par"
19
+ },
20
+ de: {
21
+ systemPrompt: "Sie helfen bei einem Wortspiel. Das geheime Wort ist",
22
+ task: "Ihre Aufgabe ist es, einen Satz zu finden, der dieses Wort beschreibt, ohne es direkt zu verwenden.",
23
+ instruction: "Antworten Sie mit einem vollständigen, grammatikalisch korrekten Satz, der beginnt mit"
24
+ },
25
+ it: {
26
+ systemPrompt: "Stai aiutando in un gioco di parole. La parola segreta è",
27
+ task: "Il tuo compito è trovare una frase per descrivere questa parola senza usarla direttamente.",
28
+ instruction: "Rispondi con una frase completa e grammaticalmente corretta che inizia con"
29
+ },
30
+ es: {
31
+ systemPrompt: "Estás ayudando en un juego de palabras. La palabra secreta es",
32
+ task: "Tu tarea es encontrar una frase para describir esta palabra sin usarla directamente.",
33
+ instruction: "Responde con una frase completa y gramaticalmente correcta que comience con"
34
+ }
35
+ };
36
+
37
  serve(async (req) => {
38
  if (req.method === 'OPTIONS') {
39
  return new Response(null, { headers: corsHeaders });
40
  }
41
 
42
  try {
43
+ const { currentWord, currentSentence, language = 'en' } = await req.json();
44
+ console.log('Generating word for:', { currentWord, currentSentence, language });
45
 
 
46
  const existingSentence = currentSentence || '';
47
+ const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
48
 
49
  const client = new Mistral({
50
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
51
  });
52
 
 
53
  const maxRetries = 3;
54
  let retryCount = 0;
55
  let lastError = null;
 
61
  messages: [
62
  {
63
  role: "system",
64
+ content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}". Do not add quotes or backticks. Just answer with the sentence.`
 
 
 
65
  }
66
  ],
67
+ maxTokens: 200,
68
  temperature: 0.5
69
  });
70
 
71
  const aiResponse = response.choices[0].message.content.trim();
72
  console.log('AI full response:', aiResponse);
73
 
 
74
  const newWord = aiResponse
75
  .slice(existingSentence.length)
76
  .trim()
77
  .split(' ')[0]
78
+ .replace(/[.,!?]$/, '');
79
 
80
  console.log('Extracted new word:', newWord);
81
 
 
87
  console.error(`Attempt ${retryCount + 1} failed:`, error);
88
  lastError = error;
89
 
 
90
  if (error.message?.includes('rate limit') || error.status === 429) {
91
+ const waitTime = Math.pow(2, retryCount) * 1000;
92
  console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
93
  await new Promise(resolve => setTimeout(resolve, waitTime));
94
  retryCount++;
95
  continue;
96
  }
97
 
 
98
  throw error;
99
  }
100
  }
101
 
 
102
  throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
103
 
104
  } catch (error) {
105
  console.error('Error generating word:', error);
106
 
 
107
  const errorMessage = error.message?.includes('rate limit')
108
  ? "The AI service is currently busy. Please try again in a few moments."
109
  : "Sorry, there was an error generating the word. Please try again.";
supabase/functions/guess-word/index.ts CHANGED
@@ -6,20 +6,44 @@ const corsHeaders = {
6
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
7
  };
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  serve(async (req) => {
10
  if (req.method === 'OPTIONS') {
11
  return new Response(null, { headers: corsHeaders });
12
  }
13
 
14
  try {
15
- const { sentence } = await req.json();
16
- console.log('Trying to guess word from sentence:', sentence);
17
 
18
  const client = new Mistral({
19
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
20
  });
21
 
22
- // Add retry logic with exponential backoff
 
23
  const maxRetries = 3;
24
  let retryCount = 0;
25
  let lastError = null;
@@ -31,13 +55,11 @@ serve(async (req) => {
31
  messages: [
32
  {
33
  role: "system",
34
- content: `You are playing a word guessing game. Given a descriptive sentence, your task is to guess the single word being described.
35
- Respond with ONLY the word you think is being described, in uppercase letters.
36
- Do not add any explanation or punctuation.`
37
  },
38
  {
39
  role: "user",
40
- content: `Based on this description, what single word is being described: "${sentence}"`
41
  }
42
  ],
43
  maxTokens: 10,
@@ -55,27 +77,23 @@ serve(async (req) => {
55
  console.error(`Attempt ${retryCount + 1} failed:`, error);
56
  lastError = error;
57
 
58
- // If it's a rate limit error, wait before retrying
59
  if (error.message?.includes('rate limit') || error.status === 429) {
60
- const waitTime = Math.pow(2, retryCount) * 1000; // Exponential backoff: 1s, 2s, 4s
61
  console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
62
  await new Promise(resolve => setTimeout(resolve, waitTime));
63
  retryCount++;
64
  continue;
65
  }
66
 
67
- // If it's not a rate limit error, throw immediately
68
  throw error;
69
  }
70
  }
71
 
72
- // If we've exhausted all retries
73
  throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
74
 
75
  } catch (error) {
76
  console.error('Error generating guess:', error);
77
 
78
- // Provide a more user-friendly error message
79
  const errorMessage = error.message?.includes('rate limit')
80
  ? "The AI service is currently busy. Please try again in a few moments."
81
  : "Sorry, there was an error generating the guess. Please try again.";
 
6
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
7
  };
8
 
9
+ const languagePrompts = {
10
+ en: {
11
+ systemPrompt: "You are playing a word guessing game in English. Given a descriptive sentence, your task is to guess the single word being described.",
12
+ instruction: "Based on this description, what single word is being described:"
13
+ },
14
+ fr: {
15
+ systemPrompt: "Vous jouez à un jeu de devinettes de mots en français. À partir d'une phrase descriptive, votre tâche est de deviner le mot unique décrit.",
16
+ instruction: "D'après cette description, quel mot unique est décrit :"
17
+ },
18
+ de: {
19
+ systemPrompt: "Sie spielen ein Worträtselspiel auf Deutsch. Anhand eines beschreibenden Satzes ist es Ihre Aufgabe, das beschriebene einzelne Wort zu erraten.",
20
+ instruction: "Welches einzelne Wort wird basierend auf dieser Beschreibung beschrieben:"
21
+ },
22
+ it: {
23
+ systemPrompt: "Stai giocando a un gioco di indovinelli in italiano. Data una frase descrittiva, il tuo compito è indovinare la singola parola descritta.",
24
+ instruction: "In base a questa descrizione, quale singola parola viene descritta:"
25
+ },
26
+ es: {
27
+ systemPrompt: "Estás jugando a un juego de adivinanzas de palabras en español. Dada una frase descriptiva, tu tarea es adivinar la única palabra que se describe.",
28
+ instruction: "Según esta descripción, ¿qué palabra única se está describiendo:"
29
+ }
30
+ };
31
+
32
  serve(async (req) => {
33
  if (req.method === 'OPTIONS') {
34
  return new Response(null, { headers: corsHeaders });
35
  }
36
 
37
  try {
38
+ const { sentence, language = 'en' } = await req.json();
39
+ console.log('Trying to guess word from sentence:', sentence, 'language:', language);
40
 
41
  const client = new Mistral({
42
  apiKey: Deno.env.get('MISTRAL_API_KEY'),
43
  });
44
 
45
+ const prompts = languagePrompts[language as keyof typeof languagePrompts] || languagePrompts.en;
46
+
47
  const maxRetries = 3;
48
  let retryCount = 0;
49
  let lastError = null;
 
55
  messages: [
56
  {
57
  role: "system",
58
+ content: `${prompts.systemPrompt} Respond with ONLY the word you think is being described, in uppercase letters. Do not add any explanation or punctuation.`
 
 
59
  },
60
  {
61
  role: "user",
62
+ content: `${prompts.instruction} "${sentence}"`
63
  }
64
  ],
65
  maxTokens: 10,
 
77
  console.error(`Attempt ${retryCount + 1} failed:`, error);
78
  lastError = error;
79
 
 
80
  if (error.message?.includes('rate limit') || error.status === 429) {
81
+ const waitTime = Math.pow(2, retryCount) * 1000;
82
  console.log(`Rate limit hit, waiting ${waitTime}ms before retry`);
83
  await new Promise(resolve => setTimeout(resolve, waitTime));
84
  retryCount++;
85
  continue;
86
  }
87
 
 
88
  throw error;
89
  }
90
  }
91
 
 
92
  throw new Error(`Failed after ${maxRetries} attempts. Last error: ${lastError?.message}`);
93
 
94
  } catch (error) {
95
  console.error('Error generating guess:', error);
96
 
 
97
  const errorMessage = error.message?.includes('rate limit')
98
  ? "The AI service is currently busy. Please try again in a few moments."
99
  : "Sorry, there was an error generating the guess. Please try again.";