Explorar o código

refactor: extract all English UI strings to messages/en.ts

- All user-facing strings in single file for future i18n
- Keyboard layout (ROWS array) also externalized
- Components import from messages/en.ts

To add a language: copy en.ts → fr.ts, translate values, swap import.

Co-Authored-By: Claude <noreply@anthropic.com>
Oleg Panashchenko hai 2 semanas
pai
achega
71112156b3

+ 7 - 11
client/src/components/Keyboard.tsx

@@ -1,15 +1,11 @@
+import { en } from '../messages/en.js';
+
 const COLOR_MAP: Record<string, string> = {
   g: '#6aaa64',
   y: '#c9b458',
   x: '#787c7e',
 };
 
-const ROWS = [
-  ['q','w','e','r','t','y','u','i','o','p'],
-  ['a','s','d','f','g','h','j','k','l'],
-  ['Enter','z','x','c','v','b','n','m','⌫'],
-];
-
 interface KeyboardProps {
   letterColors: Record<string, string | undefined>;
   onKeyPress: (key: string) => void;
@@ -18,22 +14,22 @@ interface KeyboardProps {
 export function Keyboard({ letterColors, onKeyPress }: KeyboardProps) {
   return (
     <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', alignItems: 'center', marginTop: '12px', width: '95vw', maxWidth: '380px', margin: '12px auto 0' }}>
-      {ROWS.map((row, rowIdx) => (
+      {en.keyboardRows.map((row, rowIdx) => (
         <div key={rowIdx} style={{ display: 'flex', gap: '4px', width: '100%', justifyContent: 'center' }}>
           {row.map((key) => {
-            const isSpecial = key === 'Enter' || key === '⌫';
-            const backspace = key === '⌫';
+            const isSpecial = key === en.keyEnter || key === en.keyBackspace;
+            const isBackspace = key === en.keyBackspace;
             const letter = isSpecial ? '' : key;
             const color = isSpecial ? undefined : letterColors[letter];
             const bg = color ? COLOR_MAP[color] : '#d3d6da';
             const fg = color ? '#fff' : '#000';
-            const label = backspace ? '⌫' : key.toUpperCase();
+            const label = isBackspace ? en.backspaceLabel : key.toUpperCase();
             const flexBasis = isSpecial ? '15%' : '9%';
 
             return (
               <div
                 key={key}
-                onClick={() => onKeyPress(backspace ? 'Backspace' : key)}
+                onClick={() => onKeyPress(isBackspace ? en.keyBackspace : key)}
                 style={{
                   flex: `0 0 ${flexBasis}`,
                   height: '48px',

+ 2 - 1
client/src/hooks/useGame.ts

@@ -1,5 +1,6 @@
 import { useState, useCallback } from 'react';
 import type { GuessEntry, GuessResponse, SolverReplay } from '@wordle/shared';
+import { en } from '../messages/en.js';
 
 const BASE = '/api';
 
@@ -52,7 +53,7 @@ export function useGame(initialGuesses: GuessEntry[]): UseGameReturn {
     const data: GuessResponse = await res.json();
 
     if (!data.valid) {
-      setMessage('Unknown word');
+      setMessage(en.game.unknownWord);
       return false;
     }
 

+ 54 - 0
client/src/messages/en.ts

@@ -0,0 +1,54 @@
+/** English (en) messages for the Wordle client. */
+
+export const en = {
+  /** Keyboard layout — three rows of key labels. Special: 'Enter' and 'Backspace'. */
+  keyboardRows: [
+    ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
+    ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
+    ['Enter', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'Backspace'],
+  ],
+
+  /** Backspace key display label */
+  backspaceLabel: '⌫',
+
+  title: 'Wordle',
+
+  /** Screen 1 — Rules */
+  rules: {
+    heading: 'How to Play',
+    bullets: [
+      'Guess the 5-letter word in 6 attempts.',
+      'After each guess, letters light up with colors:',
+    ],
+    greenDesc: 'Green — correct letter, correct position',
+    yellowDesc: 'Yellow — letter in word, wrong position',
+    grayDesc: 'Gray — letter not in word',
+    playButton: 'Play',
+  },
+
+  /** Screen 2 — Game */
+  game: {
+    hint: 'Type a 5-letter word and press Enter',
+    unknownWord: 'Unknown word',
+  },
+
+  /** Screen 3 — Results */
+  results: {
+    congratulations: 'Congratulations!',
+    betterLuck: 'Better luck next time',
+    solvedIn: (n: number) => `Solved in ${n} ${n === 1 ? 'attempt' : 'attempts'}`,
+    theWordWas: (w: string) => `The word was: ${w.toUpperCase()}`,
+    statistics: 'Statistics',
+    played: 'Played',
+    winRate: 'Win Rate',
+    streak: 'Streak',
+    maxStreak: 'Max Streak',
+    noGames: 'No games yet.',
+    solverCaption: 'How the solvers cracked this word:',
+    playAgain: 'Play Again',
+  },
+
+  /** Shared labels */
+  keyEnter: 'Enter',
+  keyBackspace: 'Backspace',
+} as const;

+ 12 - 8
client/src/screens/Screen1Rules.tsx

@@ -1,29 +1,33 @@
+import { en } from '../messages/en.js';
+
 interface Screen1RulesProps {
   onPlay: () => void;
 }
 
 export function Screen1Rules({ onPlay }: Screen1RulesProps) {
+  const { rules, title } = en;
+
   return (
     <div style={{ maxWidth: '400px', margin: '40px auto', padding: '16px', fontFamily: 'sans-serif' }}>
-      <h1 style={{ textAlign: 'center', fontSize: '2rem', marginBottom: '24px' }}>Wordle</h1>
+      <h1 style={{ textAlign: 'center', fontSize: '2rem', marginBottom: '24px' }}>{title}</h1>
 
-      <h2 style={{ fontSize: '1.2rem', marginBottom: '8px' }}>How to Play</h2>
+      <h2 style={{ fontSize: '1.2rem', marginBottom: '8px' }}>{rules.heading}</h2>
       <ul style={{ lineHeight: '1.8', paddingLeft: '20px' }}>
-        <li>Guess the <strong>5-letter word</strong> in <strong>6 attempts</strong>.</li>
-        <li>After each guess, letters light up with colors:</li>
+        <li>{rules.bullets[0]}</li>
+        <li>{rules.bullets[1]}</li>
       </ul>
 
       <div style={{ display: 'flex', gap: '8px', margin: '12px 0' }}>
         <div style={{ width: '40px', height: '40px', backgroundColor: '#6aaa64', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', borderRadius: '4px' }}>G</div>
-        <span style={{ lineHeight: '40px' }}>Green — correct letter, correct position</span>
+        <span style={{ lineHeight: '40px' }}>{rules.greenDesc}</span>
       </div>
       <div style={{ display: 'flex', gap: '8px', margin: '12px 0' }}>
         <div style={{ width: '40px', height: '40px', backgroundColor: '#c9b458', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', borderRadius: '4px' }}>Y</div>
-        <span style={{ lineHeight: '40px' }}>Yellow — letter in word, wrong position</span>
+        <span style={{ lineHeight: '40px' }}>{rules.yellowDesc}</span>
       </div>
       <div style={{ display: 'flex', gap: '8px', margin: '12px 0' }}>
         <div style={{ width: '40px', height: '40px', backgroundColor: '#787c7e', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', borderRadius: '4px' }}>X</div>
-        <span style={{ lineHeight: '40px' }}>Gray — letter not in word</span>
+        <span style={{ lineHeight: '40px' }}>{rules.grayDesc}</span>
       </div>
 
       <div style={{ textAlign: 'center', marginTop: '32px' }}>
@@ -40,7 +44,7 @@ export function Screen1Rules({ onPlay }: Screen1RulesProps) {
             cursor: 'pointer',
           }}
         >
-          Play
+          {rules.playButton}
         </button>
       </div>
     </div>

+ 2 - 1
client/src/screens/Screen2Game.tsx

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
 import type { GuessEntry } from '@wordle/shared';
 import { Grid } from '../components/Grid.js';
 import { Keyboard } from '../components/Keyboard.js';
+import { en } from '../messages/en.js';
 
 interface Screen2GameProps {
   guesses: GuessEntry[];
@@ -71,7 +72,7 @@ export function Screen2Game({ guesses, letterColors, message, onGuess, onDismiss
 
   return (
     <div style={{ maxWidth: '400px', margin: '20px auto', padding: '8px', fontFamily: 'sans-serif' }}>
-      <h1 style={{ textAlign: 'center', fontSize: '1.6rem', marginBottom: '16px' }}>Wordle</h1>
+      <h1 style={{ textAlign: 'center', fontSize: '1.6rem', marginBottom: '16px' }}>{en.title}</h1>
 
       <div style={{ position: 'relative' }}>
         <Grid guesses={[...guesses, currentGuess ? { guess: currentGuess, colors: [] } : null].filter(Boolean) as GuessEntry[]} />

+ 41 - 37
client/src/screens/Screen3Results.tsx

@@ -1,4 +1,5 @@
 import type { GameStats, SolverReplay } from '@wordle/shared';
+import { en } from '../messages/en.js';
 
 interface Screen3ResultsProps {
   won: boolean;
@@ -9,28 +10,6 @@ interface Screen3ResultsProps {
   onPlayAgain?: () => void;
 }
 
-function Histogram({ stats }: { stats: GameStats }) {
-  if (stats.gamesPlayed === 0) return <p style={{ color: '#787c7e' }}>No games yet.</p>;
-
-  const maxCount = Math.max(...stats.histogram, 1);
-
-  return (
-    <div style={{ maxWidth: '300px', margin: '0 auto' }}>
-      {stats.histogram.map((count, idx) => {
-        const label = idx < 6 ? `${idx + 1}` : 'X';
-        const pct = (count / maxCount) * 100;
-        return (
-          <div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
-            <span style={{ width: '20px', textAlign: 'right', fontWeight: 'bold', color: '#555' }}>{label}</span>
-            <div style={{ flex: 1, height: '20px', backgroundColor: count > 0 ? '#6aaa64' : '#d3d6da', width: `${Math.max(pct, count > 0 ? 5 : 0)}%`, minWidth: count > 0 ? '12px' : '4px', borderRadius: '2px' }} />
-            <span style={{ width: '24px', fontSize: '0.8rem', color: '#555' }}>{count}</span>
-          </div>
-        );
-      })}
-    </div>
-  );
-}
-
 const MINI_COLORS: Record<string, string> = { g: '#6aaa64', y: '#c9b458', x: '#787c7e' };
 
 function MiniBoard({ replay }: { replay: SolverReplay }) {
@@ -43,12 +22,11 @@ function MiniBoard({ replay }: { replay: SolverReplay }) {
         <div key={rowIdx} style={{ display: 'flex', gap: '2px', justifyContent: 'center', marginBottom: '2px' }}>
           {step.guess.split('').map((letter, colIdx) => {
             const c = step.colors[colIdx];
-            const bg = MINI_COLORS[c] ?? '#d3d6da';
             return (
               <div key={colIdx} style={{
                 width: '20px', height: '20px',
-                backgroundColor: bg,
-                color: c === 'x' || !c ? '#fff' : '#fff',
+                backgroundColor: MINI_COLORS[c] ?? '#d3d6da',
+                color: '#fff',
                 display: 'flex', alignItems: 'center', justifyContent: 'center',
                 fontSize: '0.55rem', fontWeight: 'bold',
                 textTransform: 'uppercase', fontFamily: 'monospace',
@@ -64,24 +42,50 @@ function MiniBoard({ replay }: { replay: SolverReplay }) {
   );
 }
 
+function Histogram({ stats }: { stats: GameStats }) {
+  if (stats.gamesPlayed === 0) return <p style={{ color: '#787c7e' }}>{en.results.noGames}</p>;
+
+  const maxCount = Math.max(...stats.histogram, 1);
+
+  return (
+    <div style={{ maxWidth: '300px', margin: '0 auto' }}>
+      {stats.histogram.map((count, idx) => {
+        const label = idx < 6 ? `${idx + 1}` : 'X';
+        const pct = (count / maxCount) * 100;
+        return (
+          <div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
+            <span style={{ width: '20px', textAlign: 'right', fontWeight: 'bold', color: '#555' }}>{label}</span>
+            <div style={{ flex: 1, height: '20px', backgroundColor: count > 0 ? '#6aaa64' : '#d3d6da', width: `${Math.max(pct, count > 0 ? 5 : 0)}%`, minWidth: count > 0 ? '12px' : '4px', borderRadius: '2px' }} />
+            <span style={{ width: '24px', fontSize: '0.8rem', color: '#555' }}>{count}</span>
+          </div>
+        );
+      })}
+    </div>
+  );
+}
+
 export function Screen3Results({ won, attemptCount, revealedWord, stats, replays, onPlayAgain }: Screen3ResultsProps) {
+  const r = en.results;
+
   return (
     <div style={{ maxWidth: '400px', margin: '24px auto', padding: '16px', fontFamily: 'sans-serif', textAlign: 'center' }}>
-      <h1 style={{ fontSize: '1.6rem', marginBottom: '12px' }}>{won ? '🎉 Congratulations!' : 'Better luck next time'}</h1>
+      <h1 style={{ fontSize: '1.6rem', marginBottom: '12px' }}>
+        {won ? r.congratulations : r.betterLuck}
+      </h1>
 
       {won ? (
-        <p style={{ fontSize: '1.1rem', marginBottom: '8px' }}>Solved in {attemptCount} {attemptCount === 1 ? 'attempt' : 'attempts'}</p>
+        <p style={{ fontSize: '1.1rem', marginBottom: '8px' }}>{r.solvedIn(attemptCount)}</p>
       ) : (
-        revealedWord && <p style={{ fontSize: '1.1rem', marginBottom: '8px' }}>The word was: <strong>{revealedWord.toUpperCase()}</strong></p>
+        revealedWord && <p style={{ fontSize: '1.1rem', marginBottom: '8px' }}>{r.theWordWas(revealedWord)}</p>
       )}
 
       <div style={{ margin: '24px 0' }}>
-        <h2 style={{ fontSize: '1.1rem', marginBottom: '12px' }}>Statistics</h2>
+        <h2 style={{ fontSize: '1.1rem', marginBottom: '12px' }}>{r.statistics}</h2>
         <div style={{ display: 'flex', justifyContent: 'center', gap: '24px', marginBottom: '16px' }}>
-          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.gamesPlayed}</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>Played</div></div>
-          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.gamesPlayed > 0 ? Math.round((stats.wins / stats.gamesPlayed) * 100) : 0}%</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>Win Rate</div></div>
-          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.currentStreak}</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>Streak</div></div>
-          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.maxStreak}</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>Max Streak</div></div>
+          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.gamesPlayed}</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>{r.played}</div></div>
+          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.gamesPlayed > 0 ? Math.round((stats.wins / stats.gamesPlayed) * 100) : 0}%</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>{r.winRate}</div></div>
+          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.currentStreak}</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>{r.streak}</div></div>
+          <div><div style={{ fontSize: '1.4rem', fontWeight: 'bold' }}>{stats.maxStreak}</div><div style={{ fontSize: '0.75rem', color: '#787c7e' }}>{r.maxStreak}</div></div>
         </div>
         <Histogram stats={stats} />
       </div>
@@ -89,11 +93,11 @@ export function Screen3Results({ won, attemptCount, revealedWord, stats, replays
       {replays && replays.length > 0 && (
         <div style={{ margin: '24px 0' }}>
           <p style={{ fontSize: '0.85rem', color: '#787c7e', marginBottom: '12px' }}>
-            How the solvers cracked this word:
+            {r.solverCaption}
           </p>
           <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', maxWidth: '320px', margin: '0 auto' }}>
-            {replays.map((r) => (
-              <MiniBoard key={r.solver} replay={r} />
+            {replays.map((rp) => (
+              <MiniBoard key={rp.solver} replay={rp} />
             ))}
           </div>
         </div>
@@ -113,7 +117,7 @@ export function Screen3Results({ won, attemptCount, revealedWord, stats, replays
             cursor: 'pointer',
           }}
         >
-          Play Again
+          {r.playAgain}
         </button>
       )}
     </div>