Felix Zieger commited on
Commit
e9a2815
·
1 Parent(s): 47fea40

input validation

Browse files
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: blue
5
  colorTo: pink
6
  sdk: docker
7
  app_port: 8080
8
- pinned: false
9
  ---
10
  # Think in Sync
11
 
 
5
  colorTo: pink
6
  sdk: docker
7
  app_port: 8080
8
+ pinned: true
9
  ---
10
  # Think in Sync
11
 
src/components/game/SentenceBuilder.tsx CHANGED
@@ -1,10 +1,5 @@
1
- import { Button } from "@/components/ui/button";
2
- 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
- import { useTranslation } from "@/hooks/useTranslation";
7
- import { House } from "lucide-react";
8
  import {
9
  AlertDialog,
10
  AlertDialogAction,
@@ -15,6 +10,11 @@ import {
15
  AlertDialogHeader,
16
  AlertDialogTitle,
17
  } from "@/components/ui/alert-dialog";
 
 
 
 
 
18
 
19
  interface SentenceBuilderProps {
20
  currentWord: string;
@@ -39,92 +39,25 @@ export const SentenceBuilder = ({
39
  onMakeGuess,
40
  onBack,
41
  }: SentenceBuilderProps) => {
42
- const inputRef = useRef<HTMLInputElement>(null);
43
- const [imageLoaded, setImageLoaded] = useState(false);
44
  const [showConfirmDialog, setShowConfirmDialog] = useState(false);
45
  const [hasMultipleWords, setHasMultipleWords] = useState(false);
46
- const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
47
- const { toast } = useToast();
48
  const t = useTranslation();
49
 
50
- useEffect(() => {
51
- const img = new Image();
52
- img.onload = () => setImageLoaded(true);
53
- img.src = imagePath;
54
- console.log("Attempting to load image:", imagePath);
55
- }, [imagePath]);
56
-
57
- useEffect(() => {
58
- setTimeout(() => {
59
- inputRef.current?.focus();
60
- }, 100);
61
- }, []);
62
-
63
- useEffect(() => {
64
- if (!isAiThinking && sentence.length > 0 && sentence.length % 2 === 0) {
65
- setTimeout(() => {
66
- inputRef.current?.focus();
67
- }, 100);
68
- }
69
- }, [isAiThinking, sentence.length]);
70
-
71
- useEffect(() => {
72
- // Check if input contains multiple words
73
- setHasMultipleWords(playerInput.trim().split(/\s+/).length > 1);
74
- }, [playerInput]);
75
-
76
- const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
77
- if (e.shiftKey && e.key === 'Enter') {
78
- e.preventDefault();
79
- // Only trigger if buttons are not disabled and either we have a sentence or valid input
80
- if (!hasMultipleWords && !isAiThinking && (sentence.length > 0 || playerInput.trim())) {
81
- onMakeGuess();
82
- }
83
- }
84
  };
85
 
86
- const handleSubmit = (e: React.FormEvent) => {
87
- e.preventDefault();
88
- const input = playerInput.trim().toLowerCase();
89
- const target = currentWord.toLowerCase();
90
-
91
- if (hasMultipleWords) {
92
- toast({
93
- title: t.game.invalidWord,
94
- description: t.game.singleWordOnly,
95
- variant: "destructive",
96
- });
97
- return;
98
- }
99
-
100
- if (!/^[\p{L}]+$/u.test(input)) {
101
- toast({
102
- title: t.game.invalidWord,
103
- description: t.game.lettersOnly,
104
- variant: "destructive",
105
- });
106
- return;
107
- }
108
-
109
- if (input.includes(target)) {
110
- toast({
111
- title: t.game.invalidWord,
112
- description: `${t.game.cantUseTargetWord} "${currentWord}"`,
113
- variant: "destructive",
114
- });
115
- return;
116
- }
117
-
118
- onSubmitWord(e);
119
  };
120
 
121
- const handleHomeClick = () => {
122
- if (successfulRounds > 0) {
123
- setShowConfirmDialog(true);
124
- } else {
125
- onBack?.();
126
- }
127
- };
128
 
129
  return (
130
  <motion.div
@@ -132,88 +65,27 @@ export const SentenceBuilder = ({
132
  animate={{ opacity: 1 }}
133
  className="text-center relative"
134
  >
135
- <div className="absolute right-0 top-0 bg-primary/10 px-3 py-1 rounded-lg">
136
- <span className="text-sm font-medium text-primary">
137
- {t.game.round} {successfulRounds + 1}
138
- </span>
139
- </div>
140
-
141
- <Button
142
- variant="ghost"
143
- size="icon"
144
- className="absolute left-0 top-0 text-gray-600 hover:text-primary"
145
- onClick={handleHomeClick}
146
- >
147
- <House className="h-5 w-5" />
148
- </Button>
149
-
150
- <h2 className="mb-4 text-2xl font-semibold text-gray-900">
151
- Think in Sync
152
- </h2>
153
- <div>
154
- <p className="mb-1 text-sm text-gray-600">
155
- {t.game.describeWord}
156
- </p>
157
- <div className="mb-6 overflow-hidden rounded-lg bg-secondary/10">
158
- {imageLoaded && (
159
- <img
160
- src={imagePath}
161
- alt={currentWord}
162
- className="mx-auto h-48 w-full object-cover"
163
- />
164
- )}
165
- <p className="p-4 text-2xl font-bold tracking-wider text-secondary">
166
- {currentWord}
167
- </p>
168
- </div>
169
- </div>
170
- <form onSubmit={handleSubmit} className="mb-4">
171
- {sentence.length > 0 && (
172
- <motion.div
173
- initial={{ opacity: 0, y: -10 }}
174
- animate={{ opacity: 1, y: 0 }}
175
- className="mb-4 text-left p-3 rounded-lg bg-gray-50"
176
- >
177
- <p className="text-gray-700">
178
- {sentence.join(" ")}
179
- </p>
180
- </motion.div>
181
- )}
182
- <div className="relative mb-4">
183
- <Input
184
- ref={inputRef}
185
- type="text"
186
- value={playerInput}
187
- onChange={(e) => onInputChange(e.target.value)}
188
- onKeyDown={handleKeyDown}
189
- placeholder={t.game.inputPlaceholder}
190
- className={`w-full ${hasMultipleWords ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
191
- disabled={isAiThinking}
192
- />
193
- {hasMultipleWords && (
194
- <p className="text-sm text-red-500 mt-1">
195
- {t.game.singleWordOnly}
196
- </p>
197
- )}
198
- </div>
199
- <div className="flex gap-4">
200
- <Button
201
- type="submit"
202
- className="flex-1 bg-primary text-lg hover:bg-primary/90"
203
- disabled={!playerInput.trim() || isAiThinking || hasMultipleWords}
204
- >
205
- {isAiThinking ? t.game.aiThinking : `${t.game.addWord} ⏎`}
206
- </Button>
207
- <Button
208
- type="button"
209
- onClick={onMakeGuess}
210
- className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
211
- disabled={(!sentence.length && !playerInput.trim()) || isAiThinking || hasMultipleWords}
212
- >
213
- {isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
214
- </Button>
215
- </div>
216
- </form>
217
 
218
  <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
219
  <AlertDialogContent>
@@ -233,4 +105,4 @@ export const SentenceBuilder = ({
233
  </AlertDialog>
234
  </motion.div>
235
  );
236
- };
 
1
+ import { useState } from "react";
 
2
  import { motion } from "framer-motion";
 
 
 
 
3
  import {
4
  AlertDialog,
5
  AlertDialogAction,
 
10
  AlertDialogHeader,
11
  AlertDialogTitle,
12
  } from "@/components/ui/alert-dialog";
13
+ import { useTranslation } from "@/hooks/useTranslation";
14
+ import { RoundHeader } from "./sentence-builder/RoundHeader";
15
+ import { WordDisplay } from "./sentence-builder/WordDisplay";
16
+ import { SentenceDisplay } from "./sentence-builder/SentenceDisplay";
17
+ import { InputForm } from "./sentence-builder/InputForm";
18
 
19
  interface SentenceBuilderProps {
20
  currentWord: string;
 
39
  onMakeGuess,
40
  onBack,
41
  }: SentenceBuilderProps) => {
 
 
42
  const [showConfirmDialog, setShowConfirmDialog] = useState(false);
43
  const [hasMultipleWords, setHasMultipleWords] = useState(false);
44
+ const [containsTargetWord, setContainsTargetWord] = useState(false);
 
45
  const t = useTranslation();
46
 
47
+ // Input validation
48
+ const validateInput = (input: string) => {
49
+ setHasMultipleWords(input.trim().split(/\s+/).length > 1);
50
+ setContainsTargetWord(
51
+ input.toLowerCase().includes(currentWord.toLowerCase())
52
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  };
54
 
55
+ const handleInputChange = (value: string) => {
56
+ validateInput(value);
57
+ onInputChange(value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  };
59
 
60
+ const isValidInput = !playerInput || /^[\p{L} ]+$/u.test(playerInput);
 
 
 
 
 
 
61
 
62
  return (
63
  <motion.div
 
65
  animate={{ opacity: 1 }}
66
  className="text-center relative"
67
  >
68
+ <RoundHeader
69
+ successfulRounds={successfulRounds}
70
+ onBack={onBack}
71
+ showConfirmDialog={showConfirmDialog}
72
+ setShowConfirmDialog={setShowConfirmDialog}
73
+ />
74
+
75
+ <WordDisplay currentWord={currentWord} />
76
+
77
+ <SentenceDisplay sentence={sentence} />
78
+
79
+ <InputForm
80
+ playerInput={playerInput}
81
+ onInputChange={handleInputChange}
82
+ onSubmitWord={onSubmitWord}
83
+ onMakeGuess={onMakeGuess}
84
+ isAiThinking={isAiThinking}
85
+ hasMultipleWords={hasMultipleWords}
86
+ containsTargetWord={containsTargetWord}
87
+ isValidInput={isValidInput}
88
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
91
  <AlertDialogContent>
 
105
  </AlertDialog>
106
  </motion.div>
107
  );
108
+ };
src/components/game/sentence-builder/InputForm.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { KeyboardEvent, useRef, useEffect } from "react";
2
+ import { Input } from "@/components/ui/input";
3
+ import { Button } from "@/components/ui/button";
4
+ import { useTranslation } from "@/hooks/useTranslation";
5
+
6
+ interface InputFormProps {
7
+ playerInput: string;
8
+ onInputChange: (value: string) => void;
9
+ onSubmitWord: (e: React.FormEvent) => void;
10
+ onMakeGuess: () => void;
11
+ isAiThinking: boolean;
12
+ hasMultipleWords: boolean;
13
+ containsTargetWord: boolean;
14
+ isValidInput: boolean;
15
+ }
16
+
17
+ export const InputForm = ({
18
+ playerInput,
19
+ onInputChange,
20
+ onSubmitWord,
21
+ onMakeGuess,
22
+ isAiThinking,
23
+ hasMultipleWords,
24
+ containsTargetWord,
25
+ isValidInput
26
+ }: InputFormProps) => {
27
+ const inputRef = useRef<HTMLInputElement>(null);
28
+ const t = useTranslation();
29
+
30
+ useEffect(() => {
31
+ setTimeout(() => {
32
+ inputRef.current?.focus();
33
+ }, 100);
34
+ }, []);
35
+
36
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
37
+ if (e.shiftKey && e.key === 'Enter') {
38
+ e.preventDefault();
39
+ if (!hasMultipleWords && !containsTargetWord && !isAiThinking && isValidInput) {
40
+ onMakeGuess();
41
+ }
42
+ }
43
+ };
44
+
45
+ const getInputError = () => {
46
+ if (hasMultipleWords) return t.game.singleWordOnly;
47
+ if (containsTargetWord) return t.game.cantUseTargetWord;
48
+ if (!isValidInput && playerInput) return t.game.lettersOnly;
49
+ return null;
50
+ };
51
+
52
+ const error = getInputError();
53
+
54
+ return (
55
+ <form onSubmit={onSubmitWord} className="mb-4">
56
+ <div className="relative mb-4">
57
+ <Input
58
+ ref={inputRef}
59
+ type="text"
60
+ value={playerInput}
61
+ onChange={(e) => onInputChange(e.target.value)}
62
+ onKeyDown={handleKeyDown}
63
+ placeholder={t.game.inputPlaceholder}
64
+ className={`w-full ${error ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
65
+ disabled={isAiThinking}
66
+ />
67
+ {error && (
68
+ <p className="text-sm text-red-500 mt-1">
69
+ {error}
70
+ </p>
71
+ )}
72
+ </div>
73
+ <div className="flex gap-4">
74
+ <Button
75
+ type="submit"
76
+ className="flex-1 bg-primary text-lg hover:bg-primary/90"
77
+ disabled={!playerInput.trim() || isAiThinking || hasMultipleWords || containsTargetWord || !isValidInput}
78
+ >
79
+ {isAiThinking ? t.game.aiThinking : `${t.game.addWord} ⏎`}
80
+ </Button>
81
+ <Button
82
+ type="button"
83
+ onClick={onMakeGuess}
84
+ className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
85
+ disabled={(!playerInput.trim() && !playerInput.trim()) || isAiThinking || hasMultipleWords || containsTargetWord || !isValidInput}
86
+ >
87
+ {isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
88
+ </Button>
89
+ </div>
90
+ </form>
91
+ );
92
+ };
src/components/game/sentence-builder/RoundHeader.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { House } from "lucide-react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { useTranslation } from "@/hooks/useTranslation";
4
+
5
+ interface RoundHeaderProps {
6
+ successfulRounds: number;
7
+ onBack?: () => void;
8
+ showConfirmDialog: boolean;
9
+ setShowConfirmDialog: (show: boolean) => void;
10
+ }
11
+
12
+ export const RoundHeader = ({
13
+ successfulRounds,
14
+ onBack,
15
+ showConfirmDialog,
16
+ setShowConfirmDialog
17
+ }: RoundHeaderProps) => {
18
+ const t = useTranslation();
19
+
20
+ const handleHomeClick = () => {
21
+ if (successfulRounds > 0) {
22
+ setShowConfirmDialog(true);
23
+ } else {
24
+ onBack?.();
25
+ }
26
+ };
27
+
28
+ return (
29
+ <div className="relative">
30
+ <div className="absolute right-0 top-0 bg-primary/10 px-3 py-1 rounded-lg">
31
+ <span className="text-sm font-medium text-primary">
32
+ {t.game.round} {successfulRounds + 1}
33
+ </span>
34
+ </div>
35
+
36
+ <Button
37
+ variant="ghost"
38
+ size="icon"
39
+ className="absolute left-0 top-0 text-gray-600 hover:text-primary"
40
+ onClick={handleHomeClick}
41
+ >
42
+ <House className="h-5 w-5" />
43
+ </Button>
44
+
45
+ <h2 className="mb-4 text-2xl font-semibold text-gray-900">
46
+ Think in Sync
47
+ </h2>
48
+ </div>
49
+ );
50
+ };
src/components/game/sentence-builder/SentenceDisplay.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from "framer-motion";
2
+
3
+ interface SentenceDisplayProps {
4
+ sentence: string[];
5
+ }
6
+
7
+ export const SentenceDisplay = ({ sentence }: SentenceDisplayProps) => {
8
+ if (!sentence.length) return null;
9
+
10
+ return (
11
+ <motion.div
12
+ initial={{ opacity: 0, y: -10 }}
13
+ animate={{ opacity: 1, y: 0 }}
14
+ className="mb-4 text-left p-3 rounded-lg bg-gray-50"
15
+ >
16
+ <p className="text-gray-700">
17
+ {sentence.join(" ")}
18
+ </p>
19
+ </motion.div>
20
+ );
21
+ };
src/components/game/sentence-builder/WordDisplay.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { useTranslation } from "@/hooks/useTranslation";
3
+
4
+ interface WordDisplayProps {
5
+ currentWord: string;
6
+ }
7
+
8
+ export const WordDisplay = ({ currentWord }: WordDisplayProps) => {
9
+ const [imageLoaded, setImageLoaded] = useState(false);
10
+ const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
11
+ const t = useTranslation();
12
+
13
+ useEffect(() => {
14
+ const img = new Image();
15
+ img.onload = () => setImageLoaded(true);
16
+ img.src = imagePath;
17
+ console.log("Attempting to load image:", imagePath);
18
+ }, [imagePath]);
19
+
20
+ return (
21
+ <div>
22
+ <p className="mb-1 text-sm text-gray-600">
23
+ {t.game.describeWord}
24
+ </p>
25
+ <div className="mb-6 overflow-hidden rounded-lg bg-secondary/10">
26
+ {imageLoaded && (
27
+ <img
28
+ src={imagePath}
29
+ alt={currentWord}
30
+ className="mx-auto h-48 w-full object-cover"
31
+ />
32
+ )}
33
+ <p className="p-4 text-2xl font-bold tracking-wider text-secondary">
34
+ {currentWord}
35
+ </p>
36
+ </div>
37
+ </div>
38
+ );
39
+ };
src/i18n/translations/de.ts CHANGED
@@ -10,7 +10,7 @@ export const de = {
10
  aiThinking: "KI denkt nach...",
11
  aiDelayed: "Die KI ist derzeit beschäftigt. Bitte versuche es gleich noch einmal.",
12
  invalidWord: "Ungültiges Wort",
13
- cantUseTargetWord: "Du kannst das Zielwort nicht verwenden",
14
  lettersOnly: "Bitte nur Buchstaben verwenden",
15
  singleWordOnly: "Bitte nur ein Wort eingeben",
16
  leaveGameTitle: "Spiel verlassen?",
 
10
  aiThinking: "KI denkt nach...",
11
  aiDelayed: "Die KI ist derzeit beschäftigt. Bitte versuche es gleich noch einmal.",
12
  invalidWord: "Ungültiges Wort",
13
+ cantUseTargetWord: "Verwende nicht das geheime Wort",
14
  lettersOnly: "Bitte nur Buchstaben verwenden",
15
  singleWordOnly: "Bitte nur ein Wort eingeben",
16
  leaveGameTitle: "Spiel verlassen?",
src/i18n/translations/en.ts CHANGED
@@ -10,7 +10,7 @@ export const en = {
10
  aiThinking: "AI is thinking...",
11
  aiDelayed: "The AI is currently busy. Please try again in a moment.",
12
  invalidWord: "Invalid Word",
13
- cantUseTargetWord: "You can't use the target word",
14
  lettersOnly: "Please use letters only",
15
  singleWordOnly: "Please enter only one word",
16
  leaveGameTitle: "Leave Game?",
 
10
  aiThinking: "AI is thinking...",
11
  aiDelayed: "The AI is currently busy. Please try again in a moment.",
12
  invalidWord: "Invalid Word",
13
+ cantUseTargetWord: "Do not use the secret word",
14
  lettersOnly: "Please use letters only",
15
  singleWordOnly: "Please enter only one word",
16
  leaveGameTitle: "Leave Game?",
src/i18n/translations/es.ts CHANGED
@@ -10,7 +10,7 @@ export const es = {
10
  aiThinking: "La IA está pensando...",
11
  aiDelayed: "La IA está ocupada en este momento. Por favor, inténtalo de nuevo en un momento.",
12
  invalidWord: "Palabra inválida",
13
- cantUseTargetWord: "No puedes usar la palabra objetivo",
14
  lettersOnly: "Por favor, usa solo letras",
15
  singleWordOnly: "Por favor, ingresa solo una palabra",
16
  leaveGameTitle: "¿Salir del juego?",
 
10
  aiThinking: "La IA está pensando...",
11
  aiDelayed: "La IA está ocupada en este momento. Por favor, inténtalo de nuevo en un momento.",
12
  invalidWord: "Palabra inválida",
13
+ cantUseTargetWord: "No uses la palabra secreta",
14
  lettersOnly: "Por favor, usa solo letras",
15
  singleWordOnly: "Por favor, ingresa solo una palabra",
16
  leaveGameTitle: "¿Salir del juego?",
src/i18n/translations/fr.ts CHANGED
@@ -9,7 +9,7 @@ export const fr = {
9
  aiThinking: "L'IA réfléchit...",
10
  aiDelayed: "L'IA est actuellement occupée. Veuillez réessayer dans un moment.",
11
  invalidWord: "Mot invalide",
12
- cantUseTargetWord: "Vous ne pouvez pas utiliser le mot cible",
13
  lettersOnly: "Veuillez utiliser uniquement des lettres",
14
  singleWordOnly: "Veuillez entrer un seul mot",
15
  leaveGameTitle: "Quitter le jeu ?",
 
9
  aiThinking: "L'IA réfléchit...",
10
  aiDelayed: "L'IA est actuellement occupée. Veuillez réessayer dans un moment.",
11
  invalidWord: "Mot invalide",
12
+ cantUseTargetWord: "N'utilisez pas le mot secret",
13
  lettersOnly: "Veuillez utiliser uniquement des lettres",
14
  singleWordOnly: "Veuillez entrer un seul mot",
15
  leaveGameTitle: "Quitter le jeu ?",
src/i18n/translations/it.ts CHANGED
@@ -10,7 +10,7 @@ export const it = {
10
  aiThinking: "L'IA sta pensando...",
11
  aiDelayed: "L'IA è attualmente occupata. Riprova tra un momento.",
12
  invalidWord: "Parola non valida",
13
- cantUseTargetWord: "Non puoi usare la parola obiettivo",
14
  lettersOnly: "Usa solo lettere",
15
  singleWordOnly: "Inserisci una sola parola",
16
  leaveGameTitle: "Lasciare il gioco?",
 
10
  aiThinking: "L'IA sta pensando...",
11
  aiDelayed: "L'IA è attualmente occupata. Riprova tra un momento.",
12
  invalidWord: "Parola non valida",
13
+ cantUseTargetWord: "Non usare la parola segreta",
14
  lettersOnly: "Usa solo lettere",
15
  singleWordOnly: "Inserisci una sola parola",
16
  leaveGameTitle: "Lasciare il gioco?",