diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
new file mode 100644
index 0000000..887dbe6
--- /dev/null
+++ b/frontend/src/i18n.ts
@@ -0,0 +1,15 @@
+import i18n from 'i18next'
+import { initReactI18next } from 'react-i18next'
+import LanguageDetector from 'i18next-browser-languagedetector'
+import en from './locales/en.json'
+
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources: { en: { translation: en } },
+ fallbackLng: 'en',
+ interpolation: { escapeValue: false },
+ })
+
+export default i18n
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
new file mode 100644
index 0000000..48dab23
--- /dev/null
+++ b/frontend/src/locales/en.json
@@ -0,0 +1,1067 @@
+{
+ "app": {
+ "name": "cruciverb",
+ "tagline": "a crossword a day",
+ "generating": "Generating puzzle...",
+ "offline": "Offline - playing from cache",
+ "streakKeepGoing": "{{count}} day streak - keep it going",
+ "skipToContent": "Skip to content",
+ "crosswordGrid": "Crossword grid",
+ "clues": "Clues",
+ "gameControls": "Game controls"
+ },
+ "common": {
+ "loading": "Loading...",
+ "close": "Close",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "submit": "Submit",
+ "delete": "Delete",
+ "anonymous": "Anonymous",
+ "on": "on",
+ "off": "off",
+ "notSet": "not set",
+ "signIn": "Sign in",
+ "signOut": "Sign out",
+ "report": "Report",
+ "reported": "Reported",
+ "dismiss": "Dismiss",
+ "copied": "Copied!",
+ "copyLink": "Copy link",
+ "export": "Export",
+ "import": "Import",
+ "add": "Add",
+ "connect": "Connect",
+ "block": "Block",
+ "unblock": "Unblock",
+ "approve": "Approve",
+ "reject": "Reject",
+ "test": "Test",
+ "publish": "Publish",
+ "create": "Create",
+ "save": "Save",
+ "undo": "Undo",
+ "more": "More",
+ "tryAgain": "try again",
+ "back": "Back"
+ },
+ "game": {
+ "across": "Across",
+ "down": "Down",
+ "solved": "Puzzle complete",
+ "timeUp": "Time's up",
+ "fasterThan": "Faster than {{percent}}% of solvers - #{{rank}} of {{total}}",
+ "solvedHint": "Know a better clue? Click the speech bubble next to any clue to suggest one.",
+ "marathonComplete": "Puzzle #{{count}} complete - next one loading...",
+ "marathonOver": "Marathon over - {{count}} puzzles solved",
+ "check": "Check",
+ "reveal": "Reveal",
+ "word": "Word",
+ "validate": "Validate",
+ "checkTooltip": "Check cell - counts as a hint",
+ "revealTooltip": "Reveal letter - counts as a hint",
+ "wordTooltip": "Reveal word - counts as a hint",
+ "validateTooltip": "Validate solution",
+ "hintsUsed": "{{count}} hint used",
+ "hintsUsed_plural": "{{count}} hints used",
+ "suggestClue": "Suggest alternative clue",
+ "clueSubmitPlaceholder": "Your alternative clue...",
+ "submit": "Submit",
+ "selectCellToBegin": "Select a cell to begin",
+ "clickToSwitchDirection": "Click to switch direction",
+ "undo": "Undo",
+ "redo": "Redo",
+ "undoTooltip": "Undo (Ctrl+Z)",
+ "redoTooltip": "Redo (Ctrl+Y)",
+ "pencilToggle": "Toggle pencil mode",
+ "pencilTooltip": "Pencil mode - tentative letters",
+ "autoCheckToggle": "Toggle auto-check",
+ "autoCheckTooltip": "Auto-check - marks wrong letters as you type",
+ "shareResult": "Share result"
+ },
+ "grid": {
+ "ariaLabel": "Crossword puzzle, {{width}} by {{height}}",
+ "blackCell": "Black cell",
+ "cellLabel": "Row {{row}}, Column {{col}}",
+ "cellNumber": ", number {{num}}",
+ "cellLetter": ", letter {{letter}}",
+ "cellEmpty": ", empty",
+ "handwritingCanvas": "Handwriting input area"
+ },
+ "welcome": {
+ "todaysPuzzles": "today's puzzles",
+ "newPuzzlesIn": "new puzzles in {{countdown}}",
+ "customPuzzle": "custom puzzle",
+ "clueStyle": "clue style",
+ "standard": "standard",
+ "cryptic": "cryptic",
+ "difficulty": "difficulty",
+ "auto": "auto",
+ "veryEasy": "very easy",
+ "easy": "easy",
+ "medium": "medium",
+ "hard": "hard",
+ "gameMode": "game mode",
+ "speedRun": "speed run",
+ "blind": "blind",
+ "marathon": "marathon",
+ "theme": "theme",
+ "none": "none",
+ "wordPack": "word pack",
+ "startMarathon": "Start marathon",
+ "marathonDesc": "endless 5x5 - 2 min each",
+ "importPuz": "Import .puz / .ipuz",
+ "communityPuzzles": "community puzzles",
+ "federatedPuzzles": "federated puzzles",
+ "wordOfTheDay": "word of the day",
+ "crypticExplainer": "British-style wordplay clues where each clue has a definition and a wordplay element (anagram, hidden word, reversal, etc.) that both lead to the answer. Much harder than standard clues.",
+ "blindExplainer": "Clues are hidden until you select a word. You can only see one clue at a time. Completed words reveal their clues. Forces you to work from crossings.",
+ "marathonExplainer": "Endless stream of Quick (5x5) puzzles. Each solved puzzle immediately loads the next. You have 2 minutes per puzzle - if time runs out, the marathon ends."
+ },
+ "header": {
+ "share": "Share",
+ "playTogether": "Play together",
+ "communityClues": "Community clues",
+ "leaderboard": "Leaderboard",
+ "more": "More",
+ "puzzleEditor": "Puzzle editor",
+ "achievements": "Achievements",
+ "myStats": "My stats",
+ "settings": "Settings",
+ "aboutInstance": "About this instance",
+ "signInPasskey": "Sign in with passkey",
+ "keyboardShortcuts": "Keyboard shortcuts",
+ "narrator": "Narrator",
+ "narratorActive": "Narrator active",
+ "newPuzzle": "New puzzle",
+ "startNewPuzzle": "Start a new puzzle",
+ "custom": "Custom",
+ "size": "Size",
+ "fill": "Fill",
+ "generate": "Generate",
+ "presetGlyph": "Glyph",
+ "presetRune": "Rune",
+ "presetScroll": "Scroll",
+ "presetCodex": "Codex",
+ "presetGrimoire": "Grimoire",
+ "presetGlyphDesc": "5x5 very easy",
+ "presetRuneDesc": "5x5 easy",
+ "presetScrollDesc": "9x9 medium",
+ "presetCodexDesc": "15x15 hard",
+ "presetGrimoireDesc": "21x21 expert",
+ "history": "History",
+ "archive": "Archive",
+ "clueTrainer": "Clue trainer",
+ "freezeTooltip": "{{count}} streak freeze available - protects your streak if you miss a day"
+ },
+ "shortcuts": {
+ "title": "Keyboard shortcuts",
+ "subtitle": "Available while the grid is focused",
+ "navigation": "Navigation",
+ "editing": "Editing",
+ "narrator": "Narrator mode",
+ "arrows": "Move between cells",
+ "tabNext": "Jump to next clue",
+ "tabPrev": "Jump to previous clue",
+ "space": "Toggle across / down",
+ "escape": "Deselect cell",
+ "letters": "Enter a letter",
+ "backspace": "Clear current cell",
+ "delete": "Clear and move back",
+ "ctrlZ": "Undo last entry",
+ "ctrlY": "Redo last entry",
+ "question": "Re-read current clue",
+ "ctrlH": "Puzzle overview",
+ "f1": "Puzzle overview"
+ },
+ "difficulty": {
+ "veryEasy": "Very easy",
+ "easy": "Easy",
+ "medium": "Medium",
+ "hard": "Hard"
+ },
+ "achievements": {
+ "title": "Achievements",
+ "count": "{{unlocked}}/{{total}}"
+ },
+ "community": {
+ "title": "Community clues",
+ "browseSubmit": "Browse & submit",
+ "mySubmissions": "My submissions",
+ "enterWord": "Enter a word...",
+ "writeClueFor": "Write a clue for {{word}}...",
+ "noCluesYet": "No community clues for {{word}} yet. Be the first!",
+ "noSubmissions": "You haven't submitted any clues yet.",
+ "reportClue": "Report this clue",
+ "reasonPlaceholder": "Reason (optional)",
+ "submitReport": "Submit report",
+ "deleteSubmission": "Delete submission"
+ },
+ "communityToday": {
+ "title": "Today's community puzzles",
+ "subtitle": "Puzzles published by the community in the last 24 hours.",
+ "none": "No community puzzles published today yet."
+ },
+ "leaderboard": {
+ "title": "Leaderboard",
+ "tabs": {
+ "daily": "Daily",
+ "alltime": "All-time",
+ "weekly": "Weekly",
+ "streaks": "Streaks",
+ "total": "Total",
+ "global": "Global"
+ },
+ "columns": {
+ "rank": "#",
+ "player": "Player",
+ "time": "Time",
+ "hints": "Hints",
+ "date": "Date",
+ "current": "Current",
+ "best": "Best",
+ "solves": "Solves",
+ "instance": "Instance"
+ },
+ "cleanOnly": "clean only (0 hints)",
+ "yourTime": "Your time: {{time}} ({{detail}})",
+ "hintCount": "{{count}} hint",
+ "hintCount_plural": "{{count}} hints"
+ },
+ "rating": {
+ "label": "Rate this puzzle",
+ "avg": "{{avg}} avg ({{count}} {{unit}})",
+ "rating": "rating",
+ "ratings": "ratings",
+ "star": "Rate {{n}} out of 5"
+ },
+ "receipt": {
+ "claimTitle": "Claim this solve?",
+ "solvedOn": "You solved a puzzle on {{instance}}",
+ "time": "Time: {{time}}s",
+ "hints": "Hints: {{hints}}",
+ "date": "Date: {{date}}",
+ "room": "Room: {{code}} ({{mode}})",
+ "with": "With: {{players}}",
+ "claiming": "Claiming...",
+ "claim": "Claim",
+ "qrLabel": "QR code for receipt link"
+ },
+ "share": {
+ "title": "Share puzzle",
+ "copyEmbed": "Copy embed code",
+ "embedOnWebsite": "Embed on your website",
+ "timer": "Timer",
+ "hintsLabel": "Hints",
+ "puzzle": "Puzzle",
+ "solution": "Solution",
+ "qrLabel": "QR code for puzzle link"
+ },
+ "embed": {
+ "solved": "Puzzle complete",
+ "brand": "cruciverb",
+ "noPuzzleId": "No puzzle ID",
+ "notFound": "Puzzle not found"
+ },
+ "onboarding": {
+ "welcomeTo": "Welcome to cruciverb",
+ "chooseName": "Choose a name",
+ "chooseNameDesc": "This is how you'll appear on leaderboards. Leave it blank to stay anonymous. You can change it anytime in settings.",
+ "progressSaved": "Your progress is saved in this browser",
+ "progressSavedDesc": "Your solves, streaks, and achievements are tied to a cookie in this browser on this device. If you switch browsers or clear cookies, you'll start fresh.",
+ "rememberFor": "Remember me for",
+ "playAcrossDevices": "Play across devices with a passkey",
+ "playAcrossDevicesDesc": "A passkey is a secure, passwordless credential stored on your device or password manager. Set one up to access your progress from any browser or device - no account or password needed.",
+ "settingUp": "Setting up...",
+ "setupPasskey": "Set up passkey",
+ "passkeySaved": "Passkey saved",
+ "passkeyFailed": "Passkey registration failed",
+ "comingFrom": "Coming from",
+ "anotherInstance": "another instance?",
+ "done": "Done",
+ "startPlaying": "Start playing",
+ "imported": "Imported {{count}} items.",
+ "skipped": "Skipped: {{items}}",
+ "importFailed": "Import failed"
+ },
+ "analytics": {
+ "title": "My Stats",
+ "couldNotLoad": "Could not load analytics.",
+ "noPuzzlesSolved": "No puzzles solved yet. Complete a puzzle to see your stats here.",
+ "solved": "Solved",
+ "cleanRate": "Clean rate",
+ "totalTime": "Total time",
+ "streak": "Streak",
+ "bestStreak": "Best streak",
+ "avgTime": "Avg time",
+ "activity": "Activity",
+ "improvement": "Improvement",
+ "fasterThanLastMonth": "{{pct}}% faster than last month",
+ "slowerThanLastMonth": "{{pct}}% slower than last month",
+ "needMoreSolves": "need more solves to compare",
+ "completionRate": "Completion rate",
+ "completion": "completion",
+ "total": "total",
+ "solveTimeTrend": "Solve time trend",
+ "timeOfDay": "Time of day",
+ "consistency": "Consistency",
+ "veryConsistent": "very consistent",
+ "consistent": "consistent",
+ "variable": "variable",
+ "unpredictable": "unpredictable",
+ "variance": "variance",
+ "notEnoughData": "not enough data",
+ "notEnoughDataYet": "not enough data yet",
+ "dayOfWeek": "Day of week",
+ "byPreset": "By preset",
+ "presetMix": "Preset mix",
+ "hintsTrend": "Hints trend",
+ "cleanSolveTrend": "Clean solve trend",
+ "monthlyVolume": "Monthly volume",
+ "solveTimeDistribution": "Solve time distribution",
+ "difficultyProgression": "Difficulty progression",
+ "peakPerformance": "Peak performance",
+ "fastestAt": "Fastest at {{hour}}:00 - avg {{time}}",
+ "streakProgress": "Streak progress",
+ "daysCurrent": "{{count}} day current",
+ "daysCurrent_plural": "{{count}} days current",
+ "daysBest": "{{count}} day best",
+ "daysBest_plural": "{{count}} days best",
+ "bestStreakNow": "You are on your best streak right now",
+ "personalRecords": "Personal records",
+ "recentSolves": "Recent solves",
+ "solvesInYear": "{{count}} solve in the last year",
+ "solvesInYear_plural": "{{count}} solves in the last year",
+ "dayNames": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+ },
+ "instance": {
+ "aboutTitle": "About this instance",
+ "tagline": "self-hosted crossword puzzles",
+ "license": "open source, CC0 public domain",
+ "federationActive": "Federation active",
+ "connectedInstances": "{{count}} connected instance",
+ "connectedInstances_plural": "{{count}} connected instances",
+ "contact": "Contact",
+ "rules": "Rules",
+ "features": "Features",
+ "featureList": {
+ "dailyPuzzles": "Daily puzzles in 4 difficulty levels",
+ "crypticMode": "Cryptic crossword mode with 660K clues",
+ "puzzleEditor": "Puzzle editor with auto-fill",
+ "communityClues": "Community clue contributions and voting",
+ "themedCategories": "11 themed word categories",
+ "gameModes": "Speed run, blind, and marathon game modes",
+ "achievements": "Achievements and streaks",
+ "pwa": "PWA offline support",
+ "federation": "ActivityPub federation"
+ },
+ "federationHealth": "Federation health",
+ "deliveryRate": "delivery rate",
+ "peers": "peers",
+ "queueDepth": "queue depth",
+ "status": "status",
+ "frozen": "Frozen",
+ "active": "Active",
+ "rssFeed": "RSS feed",
+ "atomFeed": "Atom feed",
+ "jsonFeed": "JSON feed",
+ "healthTitle": "Instance health",
+ "healthy": "Healthy",
+ "server": "server"
+ },
+ "report": {
+ "selectReason": "Select reason",
+ "offensive": "Offensive content",
+ "spam": "Spam",
+ "broken": "Broken / incorrect",
+ "other": "Other"
+ },
+ "datepicker": {
+ "months": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+ "weekdays": ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
+ "calendarLabel": "Calendar - {{month}} {{year}}",
+ "prevMonth": "Previous month",
+ "nextMonth": "Next month"
+ },
+ "zoom": {
+ "tryAgain": "try again"
+ },
+ "party": {
+ "title": "Party mode",
+ "roomCode": "Room code",
+ "players": "Players ({{count}})",
+ "waitingForPlayers": "Waiting for more players to join...",
+ "needMorePlayers": "Need 2+ players",
+ "startGame": "Start game",
+ "waitingForHost": "Waiting for the host to start...",
+ "leaveRoom": "Leave room",
+ "leave": "Leave",
+ "copied": "Copied",
+ "leaveEndGame": "Leaving will end the game for everyone.",
+ "leaveConfirm": "Leave this game?",
+ "cancel": "Cancel",
+ "endAndLeave": "End game and leave",
+ "round": "Round {{current}} / {{total}}",
+ "scores": "Scores",
+ "results": "Results",
+ "vote": "vote",
+ "votes": "votes",
+ "voteLocked": "Vote locked in",
+ "lockedIn": "Locked in",
+ "correct": "Correct!",
+ "submittedWaiting": "Submitted - waiting for others...",
+ "typeAnswer": "Type the answer - be fast!",
+ "go": "Go",
+ "yourAnswer": "Your answer...",
+ "pickDefinition": "Pick the real definition",
+ "submit": "Submit",
+ "subModes": {
+ "wrong": "Wrong answers",
+ "bluff": "Bluff",
+ "reverse": "Reverse clue",
+ "speed": "Speed",
+ "roulette": "Roulette"
+ },
+ "prompts": {
+ "wrongAnswer": "Write a wrong answer",
+ "fakeDefinition": "Write a fake definition",
+ "writeClue": "Write a clue for this word",
+ "voteFunniest": "Vote for the funniest",
+ "realDefinition": "Which is the real definition?",
+ "voteBestClue": "Vote for the best clue"
+ },
+ "placeholders": {
+ "funniestWrong": "Your funniest wrong answer...",
+ "soundReal": "Make it sound real...",
+ "bestClue": "Your best clue..."
+ },
+ "resultLabels": {
+ "realAnswer": "Real answer",
+ "realDefinition": "Real definition",
+ "originalClue": "Original clue"
+ },
+ "firstPlace": "1st place",
+ "phaseAnnounce": "{{phase}} phase - round {{current}} of {{total}}",
+ "resultsAnnounce": "Results - the answer was {{answer}}",
+ "gameOver": "Game over"
+ },
+ "multiplayer": {
+ "playTogether": "Play together",
+ "createRoom": "Create a room",
+ "coop": "Co-op",
+ "race": "Race",
+ "partyLabel": "Party",
+ "createRoomBtn": "Create room",
+ "startParty": "Start party",
+ "loadPuzzleFirst": "Load a puzzle first",
+ "creating": "Creating...",
+ "joinRoom": "Join a room",
+ "roomCode": "Room code",
+ "recentRooms": "Recent rooms",
+ "copied": "Copied",
+ "turnBased": "turn-based",
+ "wrongAnswersOnly": "wrong answers only",
+ "bluff": "bluff",
+ "reverseClue": "reverse clue",
+ "speedRound": "speed round",
+ "roulette": "roulette",
+ "yourTurn": "Your turn",
+ "playerTurn": "{{name}}'s turn",
+ "waitingToStart": "Waiting to start",
+ "playing": "playing",
+ "you": "you",
+ "waitingForPlayersEllipsis": "Waiting for players...",
+ "passTurn": "Pass turn",
+ "needMorePlayers": "Need 2+ players",
+ "startGame": "Start game",
+ "leaveRoom": "Leave room",
+ "disconnected": "Disconnected",
+ "joinFailed": "Could not find that room. Check the code and try again.",
+ "createFailed": "Could not create the room. Try again.",
+ "partyModes": {
+ "wrongDesc": "Write funny wrong answers to real crossword clues and vote on the funniest one. The real answer is hidden among the fakes as a trap.",
+ "bluffDesc": "An obscure word is shown - everyone writes a fake definition. The real definition is mixed in. Fool others with yours, or spot the real one.",
+ "reverseDesc": "You see the answer word - now write the cleverest clue for it. Everyone votes on their favorite.",
+ "speedDesc": "A clue appears and everyone races to type the correct answer. First three to get it right score points.",
+ "rouletteDesc": "A word is shown with four possible definitions - only one is real. The other three are definitions for completely different words. Pick the right one."
+ }
+ },
+ "settings": {
+ "title": "Settings",
+ "tabs": {
+ "profile": "Profile",
+ "display": "Display",
+ "session": "Session",
+ "visibility": "Visibility",
+ "data": "Data",
+ "federation": "Federation",
+ "accessibility": "Accessibility",
+ "account": "Account"
+ },
+ "accessibility": {
+ "colorblindHint": "Adjust the color palette for different types of color vision",
+ "textSizeHint": "Scale all text up or down to match your preference"
+ },
+ "profile": {
+ "displayName": "Display name",
+ "placeholder": "Anonymous",
+ "hint": "This is how you appear on leaderboards and in community features",
+ "sessionLabel": "Session",
+ "hasPasskey": "You have a passkey set up - your progress is portable across browsers.",
+ "noPasskey": "No passkey yet. Add one in the Session tab to access your progress from other devices."
+ },
+ "display": {
+ "theme": "Theme",
+ "system": "System",
+ "light": "Light",
+ "dark": "Dark",
+ "inputMode": "Input mode",
+ "inputAuto": "Auto (keyboard + pen)",
+ "inputKeyboard": "Keyboard",
+ "inputStylus": "Stylus",
+ "colorblind": "Colorblind mode",
+ "colorblindNone": "None",
+ "protanopia": "Protanopia (red-blind)",
+ "deuteranopia": "Deuteranopia (green-blind)",
+ "tritanopia": "Tritanopia (blue-blind)",
+ "textSize": "Text size: {{scale}}%",
+ "torFriendly": "Tor-friendly mode",
+ "torHint": "Uses system fonts - no external requests to Google",
+ "language": "Language",
+ "pushNotifications": "Push notifications",
+ "pushHint": "Get notified when the daily puzzle is ready",
+ "pushComingSoon": "Coming soon"
+ },
+ "narrator": {
+ "title": "Narrator",
+ "narratorMode": "Narrator mode",
+ "hint": "Speaks cell positions, clues, and game events aloud",
+ "speechRate": "Speech rate: {{rate}}x",
+ "pitch": "Pitch: {{pitch}}x",
+ "voice": "Voice",
+ "defaultVoice": "Default",
+ "verbosity": "Verbosity",
+ "brief": "Brief",
+ "normal": "Normal",
+ "verbose": "Verbose",
+ "soundEffects": "Sound effects",
+ "notSupported": "Your browser does not support speech synthesis"
+ },
+ "session": {
+ "duration": "Session duration",
+ "durationHint": "How long this browser remembers your progress",
+ "passkeys": "Passkeys",
+ "noPasskeys": "No passkeys yet. Add one to access your progress from other browsers or devices.",
+ "addPasskey": "Add passkey",
+ "registering": "Registering...",
+ "noSupport": "Your browser does not support passkeys.",
+ "sessionBinding": "Session binding",
+ "sessionBindingHint": "Lock this session to your current browser - invalidates if user-agent changes",
+ "requiresPasskey": "Requires a passkey to be set up",
+ "thisSession": "This session",
+ "oneDay": "1 day",
+ "oneWeek": "1 week",
+ "thirtyDays": "30 days",
+ "ninetyDays": "90 days",
+ "oneYear": "1 year"
+ },
+ "visibility": {
+ "quickSetup": "Quick setup",
+ "open": "Open",
+ "openDesc": "Everything visible, all features on",
+ "private": "Private",
+ "privateDesc": "Hidden from leaderboards, anonymous, local only",
+ "fortress": "Fortress",
+ "fortressDesc": "Maximum privacy, encrypted, auto-purge",
+ "visibilityLabel": "Visibility",
+ "hideLeaderboards": "Hide from leaderboards",
+ "hideLeaderboardsHint": "Your solve times won't appear on any leaderboard",
+ "privateProfile": "Private profile",
+ "privateProfileHint": "Your display name is hidden - you appear as Anonymous",
+ "anonSolve": "Anonymous solve mode",
+ "anonSolveHint": "No achievements triggered, no stats recorded for your profile"
+ },
+ "data": {
+ "title": "Data protection",
+ "noTrack": "No-track mode",
+ "noTrackHint": "Solves are not recorded on the server at all",
+ "anonTimes": "Anonymize solve times",
+ "anonTimesHint": "Rounded to nearest minute to prevent fingerprinting",
+ "encrypt": "Encrypt solve data",
+ "encryptHint": "Progress stored as AES-256-GCM ciphertext at rest - the server needs DATA_ENC_KEY set for this to take effect",
+ "ephemeral": "Ephemeral session",
+ "ephemeralHint": "All server-side data deleted when your cookie expires",
+ "autoPurge": "Auto-purge old data",
+ "purgeAfter": "Purge after",
+ "purgeDays": {
+ "7": "7 days",
+ "30": "30 days",
+ "60": "60 days",
+ "90": "90 days"
+ }
+ },
+ "federation": {
+ "fedContent": "Federated content",
+ "showAll": "Show all content",
+ "approvedOnly": "Approved content only",
+ "localOnly": "Local content only",
+ "blockedInstances": "Blocked instances",
+ "block": "Block",
+ "exportBlocklist": "Export blocklist",
+ "importBlocklist": "Import blocklist"
+ },
+ "account": {
+ "yourData": "Your data",
+ "dataHint": "Download a copy of everything cruciverb knows about you, or restore from a previous export. Achievements, streaks, settings, and clue submissions carry over. Solve records and puzzle ratings are instance-specific and can't be transferred.",
+ "export": "Export",
+ "import": "Import",
+ "claimSolve": "Claim a solve",
+ "claimHint": "Paste a receipt URL or receipt JSON from another instance to claim a solve on this one.",
+ "claim": "Claim",
+ "claiming": "Claiming...",
+ "solveClaimed": "Solve claimed",
+ "failedToClaim": "Failed to claim",
+ "moveInstance": "Move to another instance",
+ "moveHint": "Migrate to a different cruciverb instance. This notifies all connected peers and exports your data.",
+ "migrate": "Migrate",
+ "migrating": "Migrating...",
+ "migrateSuccess": "Your data has been exported. Upload this file on your new instance to restore everything.",
+ "migrateFailed": "Migration failed",
+ "signOut": "Sign out",
+ "signOutHint": "Clears your session cookie and starts fresh. Your data remains on the server unless you delete your account.",
+ "signOutEverywhere": "Sign out everywhere",
+ "deleteAccount": "Delete account",
+ "deleteHint": "Permanently delete all your data - solves, streaks, achievements, clue submissions, settings, passkeys, and this session. This cannot be undone.",
+ "confirmDelete": "Are you absolutely sure?",
+ "yesDelete": "Yes, delete everything"
+ }
+ },
+ "editor": {
+ "title": "Puzzle editor",
+ "createNew": "Create new",
+ "customSize": "Custom size",
+ "yourPuzzles": "Your puzzles",
+ "importExport": "Import / Export",
+ "exportAll": "Export all",
+ "untitled": "Untitled",
+ "puzzleTitle": "Puzzle title...",
+ "design": "Design",
+ "fill": "Fill",
+ "clues": "Clues",
+ "exitTest": "Exit test",
+ "autoFill": "Auto-fill",
+ "filling": "Filling...",
+ "undoCtrlZ": "Undo (Ctrl+Z)",
+ "clearLetters": "Clear letters",
+ "clearEverything": "Clear everything",
+ "exportIpuz": "Export as .ipuz",
+ "unpublish": "Unpublish",
+ "unpublished": "Unpublished - puzzle is now a draft",
+ "designHint": "click cells to toggle black/white",
+ "fillHint": "filling {{dir}} - click same cell to switch",
+ "testHint": "solve the puzzle to test it",
+ "clueHint": "click a cell or clue to edit",
+ "rotationalSymmetry": "Rotational symmetry",
+ "mirrorSymmetry": "Mirror symmetry",
+ "noSymmetry": "No symmetry",
+ "checkAnswers": "Check answers",
+ "cluesClickToEdit": "Clues (click to edit)",
+ "writeClue": "Write a clue...",
+ "clickToAddClue": "click to add clue...",
+ "emptyHint1": "Design your grid pattern, then click Auto-fill to populate words and clues.",
+ "emptyHint2": "Or switch to Fill mode to type your own words, then click Clues to write clues.",
+ "publishTitle": "Publish your puzzle?",
+ "publishDesc": "Your puzzle will be visible to everyone on the welcome screen under \"community puzzles.\" Anyone can play it - it loads just like a regular puzzle with timer, hints, and validation.",
+ "publishNote": "You can unpublish later by changing the status back to draft from your puzzle list.",
+ "isThemed": "This is a themed puzzle",
+ "themePlaceholder": "Theme name (e.g. Space, Movies, Food)",
+ "dontShowAgain": "Don't show this again",
+ "published": "Published! Your puzzle is now available for everyone to play.",
+ "autoFillFailed": "Auto-fill failed",
+ "titleRequired": "Give your puzzle a title before publishing",
+ "cluesRequired": "All clues must be filled in before publishing",
+ "testPassed": "Test passed - puzzle solves correctly",
+ "testFailed": "Some answers are incorrect",
+ "twoLetterSlots": "{{count}} two-letter slot (min 3 letters)",
+ "twoLetterSlots_plural": "{{count}} two-letter slots (min 3 letters)",
+ "offensiveContent": "\"{{word}}\" contains offensive content",
+ "unpublishToDelete": "unpublish to delete",
+ "yesDelete": "yes, delete"
+ },
+ "admin": {
+ "title": "cruciverb admin",
+ "invalidCredentials": "Invalid credentials",
+ "username": "Username",
+ "password": "Password",
+ "setup": {
+ "title": "Create admin account",
+ "desc": "No admin account exists yet. Create one to get started.",
+ "confirmPassword": "Confirm password",
+ "createAccount": "Create admin account",
+ "usernameTooShort": "Username must be at least 3 characters",
+ "passwordTooShort": "Password must be at least 8 characters",
+ "passwordMismatch": "Passwords do not match",
+ "success": "Admin account created - you can now sign in.",
+ "failed": "Setup failed"
+ },
+ "tabs": {
+ "overview": "Overview",
+ "content": "Content",
+ "users": "Users",
+ "achievements": "Achievements",
+ "integrations": "Integrations",
+ "federation": "Federation",
+ "auditLog": "Audit log"
+ },
+ "overview": {
+ "totalUsers": "total users",
+ "activeToday": "{{count}} active today",
+ "puzzlesSolved": "puzzles solved",
+ "today": "{{count}} today",
+ "avgSolveTime": "avg solve time",
+ "fastest": "fastest: {{time}}",
+ "avgPuzzleRating": "avg puzzle rating",
+ "ratingCount": "{{count}} ratings",
+ "puzzlesGenerated": "puzzles generated",
+ "dailyCount": "{{count}} daily",
+ "communityPuzzles": "community puzzles",
+ "drafts": "{{count}} drafts",
+ "communityClues": "community clues",
+ "pending": "{{count}} pending",
+ "badgesEarned": "badges earned",
+ "passkeys": "passkeys",
+ "webhooks": "webhooks",
+ "solveStatsByDifficulty": "Solve stats by difficulty",
+ "preset": "Preset",
+ "solves": "Solves",
+ "last7Days": "Last 7 days",
+ "topClueContributors": "Top clue contributors",
+ "user": "User",
+ "submitted": "Submitted",
+ "approved": "Approved",
+ "rate": "Rate",
+ "system": "System",
+ "dictionaryWords": "dictionary words",
+ "clueDbEntries": "clue DB entries",
+ "crypticClues": "cryptic clues",
+ "cachedInMemory": "cached in memory"
+ },
+ "content": {
+ "puzzles": "Puzzles",
+ "id": "ID",
+ "type": "Type",
+ "size": "Size",
+ "rating": "Rating",
+ "created": "Created",
+ "random": "random",
+ "pendingClues": "Pending clues",
+ "noCluesPending": "No clues pending moderation",
+ "reports": "Reports",
+ "noPendingReports": "No pending reports",
+ "removeContent": "Remove content",
+ "blockedWords": "Blocked words",
+ "blockedWordsDesc": "Words added here are excluded from puzzle generation. Built-in words from the server wordlist cannot be removed from here.",
+ "blockedWordPlaceholder": "Word to block...",
+ "wordBlocked": "Word added to blocklist",
+ "wordBlockFailed": "Failed to add word",
+ "filter_all": "All",
+ "filter_daily": "Daily",
+ "filter_custom": "Custom",
+ "deleteSelected": "Delete selected",
+ "deleteSelectedMsg": "Delete {{count}} selected items? This cannot be undone.",
+ "puzzlesDeleted": "{{count}} puzzles deleted",
+ "communityPuzzles": "Community puzzles",
+ "noCommunityPuzzles": "No published community puzzles",
+ "title": "Title",
+ "author": "Author",
+ "deleteCommunityPuzzle": "Delete community puzzle",
+ "deleteCommunityPuzzleMsg": "Delete this community puzzle? This cannot be undone.",
+ "communityPuzzleDeleted": "Community puzzle deleted"
+ },
+ "users": {
+ "searchPlaceholder": "Search by name or token...",
+ "purgeInactive": "Purge inactive",
+ "userCount": "{{count}} users",
+ "name": "Name",
+ "solves": "Solves",
+ "streak": "Streak",
+ "lastActive": "Last active",
+ "created": "Created",
+ "status": "Status",
+ "banned": "banned",
+ "activeStatus": "active",
+ "ban": "Ban",
+ "unban": "Unban",
+ "banUser": "Ban user",
+ "banMsg": "Ban {{name}}? This removes their published content and prevents future submissions.",
+ "userBanned": "User banned",
+ "userUnbanned": "User unbanned",
+ "deleteSession": "Delete session",
+ "deleteSessionMsg": "This will remove the user session and their settings. Solve records and achievements will remain.",
+ "sessionDeleted": "Session deleted",
+ "purgeTitle": "Purge inactive sessions",
+ "purgeMsg": "Delete all sessions that have been inactive for more than this many days:",
+ "purged": "Purged {{count}} sessions"
+ },
+ "achievements": {
+ "badge": "Badge",
+ "description": "Description",
+ "unlockedBy": "Unlocked by",
+ "playerCount": "{{count}} player",
+ "playerCount_plural": "{{count}} players"
+ },
+ "integrations": {
+ "webhooks": "Webhooks",
+ "webhookUrl": "Webhook URL",
+ "signingSecret": "Signing secret",
+ "noWebhooks": "No webhooks configured",
+ "url": "URL",
+ "events": "Events",
+ "format": "Format",
+ "failures": "Failures",
+ "webhookCreated": "Webhook created",
+ "webhookDeleted": "Webhook deleted",
+ "testSent": "Test event sent",
+ "deleteWebhook": "Delete webhook",
+ "deleteWebhookMsg": "Remove this webhook? It will no longer receive events.",
+ "dailyPublished": "Daily puzzle published",
+ "puzzleCompleted": "Puzzle completed",
+ "achievementUnlocked": "Achievement unlocked",
+ "allEvents": "All events",
+ "environment": "Environment",
+ "instanceRules": "Instance rules",
+ "instanceRulesDesc": "Rules shown on the about page. Supports basic markdown formatting.",
+ "adminPathDesc": "URL path to admin panel (requires restart)",
+ "preview": "Preview",
+ "mdBold": "Bold",
+ "mdItalic": "Italic",
+ "mdHeading": "Heading",
+ "mdList": "Bullet list"
+ },
+ "federation": {
+ "status": "Status",
+ "active": "Active",
+ "notConfigured": "Not configured",
+ "followers": "followers",
+ "deliveryQueue": "delivery queue",
+ "inboundActivities": "inbound activities",
+ "outboundDeliveries": "outbound deliveries",
+ "blockedInstances": "blocked instances",
+ "frozen": "Frozen",
+ "federationPaused": "federation paused",
+ "inboxReturns503": "inbox returns 503",
+ "keyChanges": "key changes detected",
+ "checkAuditLog": "check audit log",
+ "controls": "Controls",
+ "unfreeze": "Unfreeze",
+ "freezeFederation": "Freeze federation",
+ "freezeTitle": "Freeze federation",
+ "unfreezeTitle": "Unfreeze federation",
+ "freezeMsg": "This will pause all inbound/outbound federation. Inbox returns 503, delivery and discovery workers pause.",
+ "unfreezeMsg": "Resume all federation activity?",
+ "federationFrozen": "Federation frozen",
+ "federationUnfrozen": "Federation unfrozen",
+ "frozenWarning": "Federation is frozen - inbox returns 503, delivery and discovery paused",
+ "allowlistMode": "Allowlist mode ON",
+ "followerApproval": "Follower approval ON",
+ "newPeerReview": "New peer review ON",
+ "outboxLimit": "Outbox limit: {{limit}}",
+ "peersTitle": "Peers",
+ "peerDomainPlaceholder": "Domain (e.g., other-cruciverb.example.com)",
+ "domain": "Domain",
+ "lastSeen": "Last seen",
+ "never": "never",
+ "peerConnected": "Peer connected",
+ "disconnectPeer": "Disconnect peer",
+ "disconnectMsg": "Stop federating with {{domain}}? This removes the peer and any pending deliveries.",
+ "peerDisconnected": "Peer disconnected",
+ "followersTitle": "Followers",
+ "approvedFollowers": "{{count}} approved follower",
+ "approvedFollowers_plural": "{{count}} approved followers",
+ "pendingApproval": "{{count}} pending approval:",
+ "actor": "Actor",
+ "requested": "Requested",
+ "followerApproved": "Follower approved",
+ "followerRejected": "Follower rejected",
+ "security": "Security",
+ "domainToBlock": "Domain to block",
+ "reasonOptional": "Reason (optional)",
+ "blocked": "Blocked",
+ "reason": "Reason",
+ "blockTitle": "Block instance",
+ "blockMsg": "Block {{domain}}? This removes all federated content from this domain and prevents future federation.",
+ "instanceBlocked": "Instance blocked",
+ "unblockTitle": "Unblock instance",
+ "unblockMsg": "Allow {{domain}} to federate again?",
+ "instanceUnblocked": "Instance unblocked",
+ "exportBlocklist": "Export blocklist",
+ "importBlocklist": "Import blocklist",
+ "importedBlocked": "Imported {{count}} blocked domains",
+ "reputationDomain": "Domain",
+ "reputationScore": "Score",
+ "reputationClues": "Clues",
+ "reputationRejected": "Rejected",
+ "reputationReports": "Reports",
+ "reputationUpdated": "Updated",
+ "profile": "Profile",
+ "actorUsername": "Actor display name",
+ "bio": "Bio / summary",
+ "bioPlaceholder": "Daily crossword puzzles in five sizes, four game modes, cryptic clues, multiplayer rooms, party games, a built-in puzzle editor for constructors, and a clue trainer for the stubborn.",
+ "avatar": "Avatar",
+ "banner": "Banner",
+ "saveProfile": "Save profile",
+ "profileSaved": "Profile saved",
+ "uploading": "Uploading...",
+ "uploadSuccess": "Image uploaded",
+ "uploadFailed": "Upload failed",
+ "configuration": "Configuration",
+ "activityPubTitle": "ActivityPub federation",
+ "activityPubSetup": "Set AP_DOMAIN to enable. Required env vars:",
+ "activityPubDomain": "your public domain",
+ "activityPubActor": "optional, defaults to \"cruciverb\"",
+ "activityPubReady": "Database tables are ready. RSA keys generate on first startup.",
+ "apHumor": "AP post humor",
+ "apHumorDesc": "Custom taglines, sign-offs, and community middles added to daily ActivityPub posts. One entry per line.",
+ "dailyTaglines": "Daily taglines",
+ "dailySignoffs": "Daily sign-offs",
+ "communityMiddles": "Community digest middles",
+ "onePerLine": "One per line...",
+ "humorSaved": "AP humor lines saved",
+ "entries": "entries"
+ },
+ "audit": {
+ "noEntries": "No audit log entries",
+ "action": "Action",
+ "target": "Target",
+ "detail": "Detail",
+ "time": "Time"
+ },
+ "dialog": {
+ "deletePuzzle": "Delete puzzle",
+ "deletePuzzleMsg": "This will permanently delete the puzzle and all associated solve records. This cannot be undone.",
+ "puzzleDeleted": "Puzzle deleted",
+ "reportRemoved": "Report removed",
+ "reportDismissed": "Report dismissed"
+ }
+ },
+ "history": {
+ "title": "Puzzle history",
+ "empty": "No solves yet. Complete a puzzle to see it here.",
+ "hint": "hint",
+ "hints": "hints",
+ "playAgain": "Play again"
+ },
+ "archive": {
+ "title": "Puzzle archive",
+ "all": "All",
+ "solved": "Solved",
+ "loadMore": "Load more",
+ "empty": "No daily puzzles found.",
+ "play": "Play"
+ },
+ "challenge": {
+ "todaysChallenge": "Today's challenge",
+ "completed": "Challenge completed - {{challenge}}",
+ "notMet": "Challenge not met - {{challenge}}",
+ "types": {
+ "purist": "Purist",
+ "speed_demon": "Speed demon",
+ "surgeon": "Surgeon",
+ "no_regrets": "No regrets",
+ "iron_will": "Iron will",
+ "trust_fall": "Trust fall",
+ "inkwell": "Inkwell",
+ "night_owl": "Night owl",
+ "early_bird": "Early bird",
+ "rush_hour": "Rush hour",
+ "night_shift": "Night shift",
+ "afternoon_tea": "Afternoon tea",
+ "contrarian": "Contrarian",
+ "sunday_stroll": "Sunday stroll",
+ "blitz": "Blitz",
+ "opera_singer": "Opera singer",
+ "tongue_twister": "Tongue twister",
+ "mic_drop": "Mic drop",
+ "jackrabbit": "Jackrabbit",
+ "metronome": "Metronome",
+ "quickdraw": "Quickdraw",
+ "bookends": "Bookends",
+ "long_haul": "Long haul",
+ "fast_fingers": "Fast fingers",
+ "three_in_a_row": "Three in a row",
+ "alphabetical": "Alphabetical",
+ "lucky_seven": "Lucky seven",
+ "even_steven": "Even Steven",
+ "minimalist": "Minimalist",
+ "overtime": "Overtime",
+ "no_hints": "Purist",
+ "clean_sweep": "Surgeon",
+ "first_try": "Surgeon"
+ },
+ "descriptions": {
+ "purist": "Solve without using any hints, reveals, or checks",
+ "speed_demon": "Finish before the speedrun time limit",
+ "surgeon": "Complete without entering a single wrong letter",
+ "no_regrets": "Never delete, backspace, or undo any letter",
+ "iron_will": "Solve without pausing",
+ "trust_fall": "Solve without using auto-check",
+ "inkwell": "Solve entirely in pen mode, never use pencil",
+ "night_owl": "Solve between midnight and 5am",
+ "early_bird": "Solve between 5am and 8am",
+ "rush_hour": "Solve between 12pm and 1pm",
+ "night_shift": "Solve between 8pm and midnight",
+ "afternoon_tea": "Solve between 3pm and 5pm",
+ "contrarian": "First cell you select must be in the bottom-right quadrant",
+ "sunday_stroll": "Take at least 3 minutes to solve regardless of size",
+ "blitz": "Solve in under 60 seconds",
+ "opera_singer": "At some point type 5 vowels in a row",
+ "tongue_twister": "At some point type 6 consonants in a row",
+ "mic_drop": "The final letter you type completes the puzzle",
+ "jackrabbit": "Fill 10 cells in the first 30 seconds",
+ "metronome": "Never go more than 15 seconds between typing letters",
+ "quickdraw": "Never spend more than 10 seconds on a single cell",
+ "bookends": "The first and last letter you type must both be correct",
+ "long_haul": "Spend at least 30 seconds studying clues before typing your first letter",
+ "fast_fingers": "Fill the first row completely within 20 seconds",
+ "three_in_a_row": "Type the same letter three times consecutively",
+ "alphabetical": "The first letter you type must be A, B, C, D, or E",
+ "lucky_seven": "Solve in exactly 7 minutes (within 10 seconds)",
+ "even_steven": "Your solve time in seconds must be an even number",
+ "minimalist": "Use fewer than 3 hint checks (not zero, but almost)",
+ "overtime": "Take longer than 10 minutes to solve",
+ "no_hints": "Solve without using any hints, reveals, or checks",
+ "clean_sweep": "Complete without entering a single wrong letter",
+ "first_try": "Complete without entering a single wrong letter"
+ }
+ },
+ "heatmap": {
+ "title": "Solve heatmap",
+ "show": "View heatmap",
+ "hide": "Hide heatmap",
+ "clean": "Clean",
+ "hinted": "Hinted",
+ "wrong": "Wrong"
+ },
+ "constructorAnalytics": {
+ "title": "Puzzle analytics",
+ "totalSolvers": "Total solvers",
+ "avgTime": "Avg solve time",
+ "completionRate": "Completion rate",
+ "hintHeatmap": "Hint heatmap",
+ "heatmapDesc": "Cells where solvers needed the most hints",
+ "noData": "No solve data yet. Wait for people to play your puzzle."
+ },
+ "trainer": {
+ "title": "Clue trainer",
+ "easy": "Easy",
+ "medium": "Medium",
+ "hard": "Hard",
+ "score": "{{correct}} / {{total}} correct",
+ "letters": "{{count}} letters",
+ "placeholder": "Type your answer...",
+ "next": "Next",
+ "correctMsg": "Correct!",
+ "wrongMsg": "Wrong - the answer was {{answer}}",
+ "noClues": "No clues available"
+ }
+}