Files
scrabble-game/ui/src/lib/i18n/en.ts
T
Ilia Denisov 77a690fcf6
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Failing after 13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
feat(ui): move in-game status to the board — highlight, score badge, full-width rack
Remove the under-board status strip and relocate its signals:
- bag count -> a badge on the exchange/pass control + the foot of the move table
- whose-turn / win-lose -> a thin strip above the score plaques
- the tentative-move caption -> the board itself: staged tiles tint green (legal) or
  pink (illegal), the board tiles a formed word runs through go a shade darker, and an
  orange score badge sits on the main word (digit sized like a tile value, clamped on-board)

The word geometry (covered cells + badge anchor) is a new pure client-side helper
(ui/src/lib/formed.ts), independent of the move evaluator, so it works on the local or
network preview path alike; the badge's number still comes from the preview score.

Rack: a seven-column grid filling the tray width in both layouts — square, full-width
tiles — with the confirm control in the fixed 7th slot.

Settings: a touch-only "Zoom the board" toggle (default on, device-local) gates the
tile-placement auto-zoom; taking a hint while zoomed in now zooms out so the highlighted
hint word is never left off-screen.

Docs (FUNCTIONAL +_ru, UI_DESIGN, ARCHITECTURE) and e2e/unit tests updated.
2026-07-10 14:58:32 +02:00

465 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// English message catalog (authoritative). Keys are flat dotted strings; ru.ts must
// provide exactly the same keys (enforced by its type and a Vitest parity test).
// {name} placeholders are filled by t(key, params).
export const en = {
'app.title': 'Scrabble',
'app.brand': 'Erudit',
'dict.loading': 'Loading…',
'connection.connecting': 'Connecting…',
'maintenance.title': 'Under maintenance',
'maintenance.body': 'Scrabble is briefly down for an update. It will be back in a moment — no need to reload the page.',
'maintenance.retry': 'Try again',
'blocked.title': 'Account blocked',
'blocked.permanent': 'Your account is blocked.',
'blocked.temporary': 'Your account is blocked until {until}.',
'blocked.reason': 'Reason:',
'boot.errorTitle': "Couldn't load the game",
'boot.errorBody': 'Please try again in a moment.',
'launch.errorTitle': "Can't open in Telegram",
'launch.errorBody': 'Screenshot or share this with the developer.',
'launch.share': 'Share',
'launch.copied': 'Copied',
'common.back': 'Back',
'common.cancel': 'Cancel',
'common.ok': 'OK',
'common.close': 'Close',
'common.loading': 'Loading…',
'common.retry': 'Retry',
'common.you': 'You',
'common.save': 'Save',
'login.title': 'Sign in',
'login.guest': 'Play as guest',
'login.email': 'Email',
'login.emailPlaceholder': 'you@example.com',
'login.sendCode': 'Send code',
'login.codePlaceholder': '6-digit code',
'login.signIn': 'Sign in',
'confirm.busy': 'Confirming your email…',
'confirm.linked': 'Email confirmed.',
'confirm.merge': 'Almost done — finish the merge in the app.',
'confirm.close': 'You can close this window and return to the game.',
'confirm.expired': 'This link is invalid or has expired. Request a new code.',
'login.codeSent': 'We sent a code to {email}.',
'lobby.activeGames': 'Active games',
'lobby.finishedGames': 'Finished games',
'lobby.noActive': 'No active games yet.',
'lobby.noFinished': 'No finished games yet.',
'lobby.limitReached': "You've reached the simultaneous games limit.",
'lobby.new': 'Play',
'lobby.stats': 'Stats',
'lobby.profile': 'Profile',
'lobby.settings': 'Settings',
'lobby.about': 'About',
'lobby.yourTurn': 'Your turn',
'lobby.theirTurn': 'Their turn',
'lobby.hideGame': 'Remove from list',
'lobby.vs': 'vs {opponents}',
'new.title': 'New game',
'new.english': 'Scrabble',
'new.russian': 'Скрэббл',
'new.erudit': 'Erudite',
'new.find': 'Find a game',
'new.searching': 'Looking for an opponent…',
'new.rulesEnglish': '100 tiles · bingo +50',
'new.rulesRussian': '104 tiles · ё is a letter · bingo +50',
'new.rulesErudit': '131 tiles · ё = е · no centre ×2 · bonus +15',
'new.moveLimit': 'Move time: {n} h 00 min',
'new.searchHint':
'Finding an opponent can sometimes take a while. If you do not want to wait, close the app after starting the game and come back in a couple of minutes.',
// First-run coachmark hints (components/Coachmark.svelte). The leading emoji in each comment
// names the UI element the bubble's tail points at.
'onboarding.lobbySettings': 'Friends, game & profile settings, feedback', // ⚙️
'onboarding.lobbyStats': 'Stats refresh after each game with an opponent', // ✏️
'onboarding.lobbyNew': 'Practise against the AI', // 🎲
'onboarding.gameHeader': 'Move history, chat and word check', // scoreboard strip
'onboarding.gameTurn': 'Pass a turn, swap tiles', // 🔄
'onboarding.gameHints': 'Available hints', // 🛟
'onboarding.gameShuffle': 'Shuffle your tiles', // 🔀
'onboarding.gameRack': 'Pick a tile and place it on the board', // rack
'game.bag': '{n} in the bag',
'game.bagEmpty': 'Bag is empty',
'game.hints': 'Hints {n}',
'game.yourTurn': 'Your turn',
'game.yourTurnBy': '{name}: Your turn!',
'game.opponentsTurn': "Opponent's turn",
'game.searchingForOpponent': 'Waiting for opponent…',
'game.waiting': "Waiting for {name}",
'game.makeMove': 'Make move',
'game.reset': 'Reset',
'game.draw': 'Exchange/Pass',
'game.shuffle': 'Shuffle',
'game.hint': 'Hint',
'game.hintLockedIn': 'Available in {n} min.',
'game.chat': 'Chat',
'game.checkWord': 'Check word',
'game.dictionary': 'Dictionary',
'game.dropGame': 'Drop game',
'game.oneWordRule': 'One word per turn',
'game.chooseBlank': 'Choose a letter for the blank',
'game.exchangeTitle': 'Exchange or pass',
'game.exchangeConfirm': 'Exchange {n}',
'game.passNoExchange': 'Pass without exchanging',
'game.confirmResign': 'Resign this game?',
'game.hintShown': 'Best move: {word} for {n}',
'game.won': 'You won',
'game.lost': 'You lost',
'game.tied': 'Draw',
'game.abortedNote': 'This game could not be continued by the organizer and was ended in a draw.',
'game.checkWordPrompt': 'Enter a word',
'game.wordLegal': '“{word}” is valid',
'game.wordIllegal': '“{word}” is not valid',
'game.complain': 'Disagree',
'game.lookup': 'Look it up',
'game.complaintSent': 'Thanks, sent for review.',
'game.check': 'Check',
'game.checkWait': 'Please wait a moment.',
'game.noHintOptions': 'No options with your letters.',
'game.thinking': 'thinking…',
'move.pass': 'pass',
'move.exchange': 'exchange',
'move.resign': 'resign',
'move.timeout': 'timeout',
'result.victory': 'Victory',
'result.defeat': 'Defeat',
'result.draw': 'Draw',
'result.place2': 'II place',
'result.place3': 'III place',
'result.place4': 'IV place',
'result.yourMove': 'Your move',
'result.oppMove': "Opponent's move",
'chat.placeholder': 'Quick message…',
'chat.send': 'Send',
'chat.nudge': 'Waiting for your move 🤭',
'chat.nudgeBy': '{name}: Waiting for your move 🤭',
'chat.nudgeAction': 'Nudge',
'chat.awaitingReply': "Let's be patient",
'chat.empty': 'No messages yet.',
'chat.nudged': '{name} nudged you',
'chat.sentThisTurn': 'You can write again next turn.',
'profile.title': 'Profile',
'profile.language': 'Language',
'profile.timezone': 'Time zone',
'profile.hintBalance': 'Hint balance',
'profile.guest': 'Guest account',
'profile.edit': 'Edit profile',
'profile.displayName': 'Display name',
'profile.awayWindow': 'Away window',
'profile.awayHint': 'You are not auto-resigned during these hours.',
'profile.from': 'From',
'profile.to': 'To',
'profile.blockChat': 'Disable chat',
'profile.blockFriendRequests': 'Disable friend requests',
'profile.notificationsInAppOnly': 'Notifications in the app only',
'profile.preferences': 'Preferences',
'profile.preferencesHint': 'The game variants you can be matched into. Pick at least one.',
'profile.email': 'Email',
'profile.bindEmail': 'Bind email',
'profile.emailCode': 'Confirmation code',
'profile.emailSent': 'We sent a code to {email}.',
'profile.emailBound': 'Email confirmed.',
'profile.saved': 'Profile saved.',
'profile.guestLocked': 'Sign in with email to manage your profile.',
'profile.linkAccount': 'Link an account',
'profile.linkTelegram': 'Link Telegram',
'profile.linkVK': 'Link VK',
'profile.linked': 'Account linked.',
'profile.merged': 'Accounts merged.',
'profile.mergeTitle': 'Merge accounts?',
'profile.mergeBody': 'This identity already belongs to “{name}” ({games} games, {friends} friends).',
'profile.mergeIrreversible': 'Merging combines both accounts into this one and cannot be undone.',
'profile.mergeConfirm': 'Merge',
'profile.accountsTitle': 'Sign-in methods',
'profile.changeEmail': 'Change',
'profile.newEmailPlaceholder': 'New email address',
'profile.emailChanged': 'Email updated.',
'profile.emailChangeTaken': 'Check the address or contact support.',
'profile.unlink': 'Unlink',
'profile.unlinked': 'Account unlinked.',
'profile.unlinkTitle': 'Unlink account?',
'profile.unlinkBody': 'Remove {provider} as a sign-in method? You can link it again later.',
'profile.deleteAccount': 'Delete account',
'profile.deleteTitle': 'Delete your account?',
'profile.deleteWarn': 'This removes your account and frees your sign-in methods. It cannot be undone.',
'profile.deleteEmailPrompt': 'We sent a code to your email. Enter it to delete your account.',
'profile.deletePhrasePrompt': 'Type DELETE to confirm.',
'profile.deletePhrasePlaceholder': 'DELETE',
'profile.deleteConfirm': 'Delete permanently',
'deleted.title': 'Account deleted',
'deleted.body': 'Your account has been removed. Reopening the app just creates a new account.',
'settings.title': 'Settings',
'settings.theme': 'Theme',
'settings.themeAuto': 'Auto',
'settings.themeLight': 'Light',
'settings.themeDark': 'Dark',
'settings.language': 'Interface language',
'settings.boardStyle': 'Board style',
'settings.boardLabels': 'Bonus labels',
'settings.labelsBeginner': 'Beginner',
'settings.labelsClassic': 'Classic',
'settings.labelsNone': 'None',
'settings.reduceMotion': 'Reduce motion',
'settings.zoomBoard': 'Zoom the board',
'settings.offlineMode': 'Play mode',
'settings.online': 'Online',
'settings.offline': 'Offline',
'settings.offlineChecking': 'Loading dictionaries…',
'settings.offlineNeedsData': 'Not enough data on the device. Internet access is needed to download it.',
'offline.preloadWarning': 'Poor internet connection. Some features may be unavailable.',
'offline.promptTitle': 'No connection. Enable offline mode?',
'offline.promptYes': 'Enable',
'offline.promptNo': 'Keep trying',
// --- wallet ---
'wallet.title': 'Wallet',
'wallet.tab': 'Wallet',
'wallet.balance': 'Balance',
'wallet.noChips': 'No chips yet',
'wallet.source.vk': 'VK',
'wallet.source.telegram': 'Telegram',
'wallet.source.direct': 'Web',
'wallet.viewOnly': 'view only',
'wallet.benefits': 'Benefits',
'wallet.noAdsUntil': 'No ads until {date}',
'wallet.noAdsForever': 'No ads forever',
'wallet.adsOn': 'Ads are on',
'wallet.hints': 'Hints {n}',
'wallet.store': 'Store',
'wallet.empty': 'The store is empty for now',
'wallet.buy': 'Buy',
'wallet.rewardWatch': 'Watch an ad for {n} 🪙',
'wallet.rewardCta': 'Watch',
'wallet.rewardCredited': '+{n} 🪙 for watching',
'wallet.rewardCapped': "You've reached today's reward limit",
'wallet.rewardFailed': 'The ad could not be shown',
'wallet.offer': 'Public offer',
'wallet.platformNoBuy': 'Purchases are not available on this platform',
'wallet.iosBlockedPre': 'Purchases on iOS are prohibited by Apple policy. Try the ',
'wallet.iosBlockedLink': 'other version',
'wallet.iosBlockedPost': ' of the game.',
'wallet.gpStub': 'To buy chips, install the RuStore build.',
'wallet.cur.RUB': '₽',
'wallet.cur.VOTE': 'votes',
'wallet.cur.XTR': '⭐',
'wallet.warnTitle': 'Web-only purchase',
'wallet.warnBody':
'This spends your VK/Telegram chips, and the benefit will work on the web and in the app only — because of store rules.',
'wallet.warnConfirm': 'Continue',
'wallet.warnCancel': 'Cancel',
'about.title': 'About',
'about.tab': 'Info',
'about.description': 'A multiplatform Scrabble game.',
'about.version': 'Version {v}',
'feedback.title': 'Feedback',
'feedback.placeholder': 'Describe the problem or share your suggestion…',
'feedback.attach': 'Attach file',
'feedback.removeFile': 'Remove',
'feedback.send': 'Send',
'feedback.fileRejected': 'This file cant be attached.',
'feedback.sent': 'Your message has been sent.',
'feedback.waiting': 'We are reviewing your last message.',
'feedback.banned': 'Feedback submission is unavailable.',
'feedback.replyTitle': 'Reply to your last message',
'landing.tagline': 'Play Scrabble with friends, random opponent or an AI.',
'landing.playTelegram': 'Play in Telegram',
'landing.playVK': 'Play on VK',
'landing.playWeb': 'Play in your browser',
'landing.captionTelegram': 'Telegram',
'landing.captionVK': 'VK',
'landing.captionWeb': 'Web',
'landing.offer': 'Public offer',
'install.title': 'Install the app',
'install.subtitle': 'Put the app icon on your desktop (home screen) to open the game in one tap.',
'install.iosTitle': 'Add to Home Screen',
'install.iosStep1': 'Tap the Share button in the browser toolbar.',
'install.iosStep2': 'Choose “Add to Home Screen”.',
'install.iosStep3': 'Tap “Add”.',
'lang.en': 'English',
'lang.ru': 'Русский',
'error.not_your_turn': "It is not your turn.",
'error.nudge_own_turn': 'It is your turn — there is no one to nudge.',
'error.illegal_play': 'That is not a legal play.',
'error.hint_unavailable': 'No hints available.',
'error.no_hint_available': 'No options with your letters.',
'error.chat_rejected': 'Message rejected (too long or contains contact info).',
'error.nudge_too_soon': "Please don't rush your opponent so often.",
'error.chat_not_your_turn': 'You can chat only on your turn.',
'error.chat_already_sent': 'You can send only one message per turn.',
'error.game_finished': 'This game is finished.',
'error.not_a_player': 'You are not a player in this game.',
'error.already_queued': 'You are already in the queue.',
'error.email_taken': 'That email belongs to another account.',
'error.code_invalid': 'Invalid or expired code.',
'error.invalid_email': 'Enter a valid email address.',
'error.invalid_config': 'Invalid game settings.',
'error.not_found': 'Not found.',
'error.session_invalid': 'Your session expired. Please sign in again.',
'error.unauthenticated': 'Please sign in.',
'error.rate_limited': 'Too many requests, slow down.',
'error.unavailable': 'Connection problem. Retrying…',
'error.internal': 'Something went wrong.',
'error.generic': 'Something went wrong.',
'lobby.invitations': 'Invitations',
'lobby.friends': 'Friends',
'friends.title': 'Friends',
'friends.yours': 'Your friends',
'friends.none': 'No friends yet.',
'friends.incoming': 'Friend requests',
'friends.accept': 'Accept',
'friends.decline': 'Decline',
'friends.unfriend': 'Remove',
'friends.block': 'Block',
'friends.actions': 'Actions',
'friends.blockConfirm': 'Block this player?',
'friends.unfriendConfirm': 'Remove from friends?',
'friends.add': 'Add a friend',
'friends.addFromGame': 'Add to friends',
'friends.blockFromGame': 'Block player',
'friends.requestSent': 'Friend request sent.',
'friends.getCode': 'Show my code',
'friends.codeHint': 'Give this code to a friend within 12 hours.',
'friends.codeExpires': 'Expires at {time}',
'friends.enterCode': 'Have a code? Add a friend',
'friends.codePlaceholder': '6-digit code',
'friends.redeem': 'Add',
'friends.copy': 'Copy',
'friends.codeCopied': 'Code copied.',
'friends.shareTelegram': 'Share',
'friends.inviteText': "Let's play Scrabble!",
'friends.linkCopied': 'Link copied.',
'friends.selfInvite': "Hopefully you've been friends with yourself for a while ☺️",
'friends.staleInviteTitle': 'Link expired',
'friends.staleInvite': 'You opened the game from an outdated link. Open the bot {bot} to play and get notifications.',
'friends.welcomeRedeemTitle': 'Welcome!',
'friends.welcomeRedeem': 'Welcome, {name}!\n\nUse {bot} to join games the most convenient way.',
'friends.added': 'Added {name}.',
'friends.blockedList': 'Blocked players',
'friends.unblock': 'Unblock',
'friends.noneBlocked': 'No blocked players.',
'invitations.none': 'No invitations.',
'invitations.from': 'From {name}',
'invitations.with': 'With {names}',
'invitations.accept': 'Accept',
'invitations.decline': 'Decline',
'invitations.declineConfirm': 'Decline invitation?',
'invitations.cancel': 'Cancel',
'invitations.waiting': 'Waiting for replies',
'new.auto': 'Quick match',
'new.withFriends': 'Play with friends',
'new.pickFriends': 'Choose who to invite',
'new.searchFriends': 'Search friends',
'new.gameType': 'Variant',
'new.invite': 'Send invitation',
'new.moveTime': 'Move time',
'new.hintsPerPlayer': 'Hints',
'new.multipleWordsPerTurn': 'Multiple words per turn',
'new.start': 'Start game',
'new.invited': 'Invitation sent.',
'new.limitGuestTitle': 'More games?',
'new.limitGuestBody': 'Sign in or create an account to use all the game features.',
'new.limitDurableTitle': 'Game limit',
'new.limitDurableBody': 'You have reached the limit. Finish your active games to start a new one.',
'new.limitLogin': 'Sign in',
'new.noFriends': 'Add friends first to invite them.',
'new.opponentAI': 'AI',
'new.opponentRandom': 'Random player',
'new.aiInactiveLimit': 'Loss after 7 days of inactivity',
'stats.title': 'Statistics',
'stats.wins': 'Wins',
'stats.losses': 'Losses',
'stats.draws': 'Draws',
'stats.played': 'Games',
'stats.moves': 'Moves',
'stats.hintShare': 'Hint share',
'stats.winRate': 'Win rate',
'stats.maxGame': 'Best game',
'stats.maxWord': 'Best move',
'stats.guestHint': 'Sign in to track your statistics.',
'game.exportGcg': 'Export game',
'game.exportChoice': 'Export the finished game as:',
'game.exportImageOpt': 'Image (PNG)',
'game.exportGcgOpt': 'GCG file',
'game.gcgActiveOnly': 'Available once the game is finished.',
'game.gcgCopied': 'GCG copied to the clipboard.',
'game.addFriendShort': 'Add friend?',
'game.blockShort': 'Block?',
'time.minutes': '{n} min',
'time.hours': '{n} h',
'error.self_relation': 'You cannot do that to yourself.',
'error.request_exists': 'A request or friendship already exists.',
'error.request_blocked': 'This player is not accepting requests.',
'error.request_not_found': 'No matching friend request.',
'error.no_shared_game': 'You can only add someone you have played with.',
'error.request_declined': 'This player declined your request.',
'error.friend_code_invalid': 'That friend code is invalid or expired.',
'error.invalid_invitation': 'Invalid invitation.',
'error.invitation_blocked': 'You cannot invite this player.',
'error.invitation_not_found': 'Invitation not found.',
'error.invitation_not_pending': 'This invitation is no longer open.',
'error.invitation_expired': 'This invitation has expired.',
'error.not_invited': 'You were not invited.',
'error.already_responded': 'You already responded.',
'error.not_inviter': 'Only the inviter can do that.',
'error.game_active': 'Available only after the game is finished.',
'error.invalid_profile': 'Some profile fields are invalid.',
'error.already_confirmed': 'This email is already confirmed.',
'pin.enter': 'Enter PIN',
'pin.create': 'Create a PIN',
'pin.repeat': 'Repeat the PIN',
'pin.enterCurrent': 'Enter current PIN',
'pin.mismatch': "PINs don't match",
'pin.wrong': 'Wrong PIN',
'pin.setNew': 'Set a new PIN',
'pin.remove': 'Remove PIN',
'hotseat.hostPin': 'Host password',
'hotseat.setPin': 'Password',
'hotseat.changePin': 'Change',
'hotseat.playerName': 'Player name',
'hotseat.addPlayer': 'Add player',
'hotseat.removePlayer': 'Remove player',
'hotseat.hostPlaysTitle': 'Are you playing too?',
'hotseat.hostPlaysYes': 'Yes, I play',
'hotseat.hostPlaysNo': 'No',
'hotseat.unlock': 'Unlock',
'hotseat.unlockTitle': 'Unlock: {name}',
'hotseat.turnOf': 'Turn: {name}',
'hotseat.host': 'Host',
'hotseat.skip': 'Skip the current turn',
'hotseat.exclude': 'Exclude a player:',
'hotseat.terminate': 'End the game',
'hotseat.askSkip': "Skip {name}'s turn?",
'hotseat.askExclude': 'Remove {name} from the game?',
'hotseat.askTerminate': 'End the game with no result?',
} as const;
export type MessageKey = keyof typeof en;