0c5d3808d7
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) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
A Mini App launch on /telegram/ without sign-in data (empty initData) used to
location.replace('/') — bouncing the visitor to the marketing landing. That
destroyed all diagnosability and, on some Android clients that reach the entry
with no initData, simply looked like the app refusing to open.
Replace the bounce with a compact, privacy-safe launch-error screen
(screens/TelegramLaunchError.svelte) that renders a one-screenshot diagnostic
snapshot captured at the moment of failure: SDK/WebApp presence, Telegram
platform/version, whether initData is empty, whether the URL fragment carried
tgWebAppData, the initData field NAMES present/missing (never the signed
values, never an IP), and OS/mobile/browser via User-Agent Client Hints plus
the full User-Agent. A Share button delivers it through the OS share sheet
(clipboard copy on desktop, reusing the GCG no-webview-strand guard); Retry
re-checks in place to recover a late initData without a reload (which would
discard the launch fragment).
This is the instrument to root-cause the Android empty-initData failure, which
is not reproducible in Playwright.
Tests: collectTelegramDiag + shareText/pickTextShare unit tests; the /telegram/
e2e now asserts the diagnostic screen, not a redirect. Docs: ARCHITECTURE.md +
UI_DESIGN.md updated.
328 lines
18 KiB
TypeScript
328 lines
18 KiB
TypeScript
// Russian message catalog. Typed as Record<MessageKey, string> so it must cover every
|
||
// key the English catalog defines (a Vitest test asserts parity too).
|
||
|
||
import type { MessageKey } from './en';
|
||
|
||
export const ru: Record<MessageKey, string> = {
|
||
'app.title': 'Scrabble',
|
||
'connection.connecting': 'Подключение…',
|
||
|
||
'blocked.title': 'Учётная запись заблокирована',
|
||
'blocked.permanent': 'Ваша учётная запись заблокирована.',
|
||
'blocked.temporary': 'Ваша учётная запись заблокирована до {until}.',
|
||
'blocked.reason': 'Причина:',
|
||
|
||
'boot.errorTitle': 'Не удалось загрузить игру',
|
||
'boot.errorBody': 'Попробуйте ещё раз или зайдите позже.',
|
||
|
||
'launch.errorTitle': 'Не открывается в Telegram',
|
||
'launch.errorBody': 'Сделайте скриншот или поделитесь с разработчиком.',
|
||
'launch.share': 'Поделиться',
|
||
'launch.copied': 'Скопировано',
|
||
|
||
'common.back': 'Назад',
|
||
'common.cancel': 'Отмена',
|
||
'common.ok': 'ОК',
|
||
'common.close': 'Закрыть',
|
||
'common.loading': 'Загрузка…',
|
||
'common.retry': 'Повторить',
|
||
'common.you': 'Вы',
|
||
'common.save': 'Сохранить',
|
||
|
||
'login.title': 'Вход',
|
||
'login.guest': 'Играть как гость',
|
||
'login.email': 'Эл. почта',
|
||
'login.emailPlaceholder': 'you@example.com',
|
||
'login.sendCode': 'Отправить код',
|
||
'login.codePlaceholder': 'Код из 6 цифр',
|
||
'login.signIn': 'Войти',
|
||
'login.codeSent': 'Мы отправили код на {email}.',
|
||
|
||
'lobby.activeGames': 'Активные игры',
|
||
'lobby.finishedGames': 'Завершённые игры',
|
||
'lobby.noActive': 'Пока нет активных игр.',
|
||
'lobby.noFinished': 'Пока нет завершённых игр.',
|
||
'lobby.limitReached': 'Вы достигли лимита одновременных партий',
|
||
'lobby.new': 'Играть',
|
||
'lobby.stats': 'Цифры',
|
||
'lobby.profile': 'Профиль',
|
||
'lobby.settings': 'Настройки',
|
||
'lobby.about': 'О программе',
|
||
'lobby.yourTurn': 'Ваш ход',
|
||
'lobby.theirTurn': 'Ход соперника',
|
||
'lobby.hideGame': 'Убрать из списка',
|
||
'lobby.vs': 'против {opponents}',
|
||
|
||
'new.title': 'Новая игра',
|
||
'new.english': 'Scrabble',
|
||
'new.russian': 'Скрэббл',
|
||
'new.erudit': 'Эрудит',
|
||
'new.find': 'Найти игру',
|
||
'new.searching': 'Ищем соперника…',
|
||
'new.rulesEnglish': '100 фишек · бинго +50',
|
||
'new.rulesRussian': '104 фишки · ё — отдельная буква · бинго +50',
|
||
'new.rulesErudit': '131 фишка · ё = е · центр не удваивает · бонус +15',
|
||
'new.moveLimit': 'Время на ход: {n} ч. 00 мин.',
|
||
'new.searchHint':
|
||
'Иногда поиск соперника может занять некоторое время. Если не захотите ждать после начала игры, можете вернуться в приложение через несколько минут.',
|
||
|
||
'game.bag': '{n} в мешке',
|
||
'game.bagEmpty': 'Мешок пуст',
|
||
'game.hints': 'Подсказки {n}',
|
||
'game.yourTurn': 'Ваш ход',
|
||
'game.yourTurnBy': '{name}: Ваш ход!',
|
||
'game.opponentsTurn': 'Ход соперника',
|
||
'game.searchingForOpponent': 'Ждём соперника…',
|
||
'game.waiting': 'Ожидаем {name}',
|
||
'game.makeMove': 'Сделать ход',
|
||
'game.reset': 'Сброс',
|
||
'game.draw': 'Обмен/Пас',
|
||
'game.shuffle': 'Перемешать',
|
||
'game.hint': 'Подсказка',
|
||
'game.chat': 'Чат',
|
||
'game.checkWord': 'Проверить слово',
|
||
'game.dictionary': 'Словарь',
|
||
'game.dropGame': 'Покинуть игру',
|
||
'game.previewWords': '{words}: {n}',
|
||
'game.previewIllegal': 'Недопустимый ход',
|
||
'game.oneWordRule': 'Одно слово за ход',
|
||
'game.chooseBlank': 'Выберите букву для бланка',
|
||
'game.exchangeTitle': 'Обмен или пас',
|
||
'game.exchangeConfirm': 'Обменять {n}',
|
||
'game.passNoExchange': 'Пас без обмена',
|
||
'game.confirmResign': 'Сдаться в этой игре?',
|
||
'game.hintShown': 'Лучший ход: {word} на {n}',
|
||
'game.won': 'Вы выиграли',
|
||
'game.lost': 'Вы проиграли',
|
||
'game.tied': 'Ничья',
|
||
'game.abortedNote': 'Партия не может быть продолжена по вине организатора и завершена ничьёй.',
|
||
'game.checkWordPrompt': 'Введите слово',
|
||
'game.wordLegal': '«{word}» допустимо',
|
||
'game.wordIllegal': '«{word}» недопустимо',
|
||
'game.complain': 'Возражаю',
|
||
'game.lookup': 'Поискать в словаре',
|
||
'game.complaintSent': 'Спасибо, отправлено на проверку.',
|
||
'game.check': 'Проверить',
|
||
'game.checkWait': 'Секунду, пожалуйста.',
|
||
'game.noHintOptions': 'Нет вариантов с вашим набором.',
|
||
'game.thinking': 'думает…',
|
||
|
||
'move.pass': 'пас',
|
||
'move.exchange': 'обмен',
|
||
'move.resign': 'сдаюсь',
|
||
'move.timeout': 'тайм-аут',
|
||
|
||
'result.victory': 'Победа',
|
||
'result.defeat': 'Поражение',
|
||
'result.draw': 'Ничья',
|
||
'result.place2': 'II место',
|
||
'result.place3': 'III место',
|
||
'result.place4': 'IV место',
|
||
'result.yourMove': 'Ваш ход',
|
||
'result.oppMove': 'Ход соперника',
|
||
|
||
'chat.placeholder': 'Короткое сообщение…',
|
||
'chat.send': 'Отправить',
|
||
'chat.nudge': 'Жду Вашего хода 🤭',
|
||
'chat.nudgeBy': '{name}: Жду Вашего хода 🤭',
|
||
'chat.nudgeAction': 'Поторопить',
|
||
'chat.awaitingReply': 'Немного терпения',
|
||
'chat.empty': 'Сообщений пока нет.',
|
||
'chat.nudged': '{name} торопит вас',
|
||
'chat.sentThisTurn': 'Можно написать снова в следующем ходу.',
|
||
|
||
'profile.title': 'Профиль',
|
||
'profile.language': 'Язык',
|
||
'profile.timezone': 'Часовой пояс',
|
||
'profile.hintBalance': 'Баланс подсказок',
|
||
'profile.guest': 'Гостевой аккаунт',
|
||
'profile.edit': 'Редактировать профиль',
|
||
'profile.displayName': 'Отображаемое имя',
|
||
'profile.awayWindow': 'Окно отсутствия',
|
||
'profile.awayHint': 'В эти часы вам не засчитывают автопоражение.',
|
||
'profile.from': 'С',
|
||
'profile.to': 'До',
|
||
'profile.blockChat': 'Отключить чат',
|
||
'profile.blockFriendRequests': 'Отключить заявки в друзья',
|
||
'profile.notificationsInAppOnly': 'Уведомления только в приложении',
|
||
'profile.preferences': 'Предпочтения',
|
||
'profile.preferencesHint': 'Варианты игр, в которые вас могут подобрать. Выберите хотя бы один.',
|
||
'profile.email': 'Эл. почта',
|
||
'profile.bindEmail': 'Привязать почту',
|
||
'profile.emailCode': 'Код подтверждения',
|
||
'profile.emailSent': 'Мы отправили код на {email}.',
|
||
'profile.emailBound': 'Почта подтверждена.',
|
||
'profile.saved': 'Профиль сохранён.',
|
||
'profile.guestLocked': 'Войдите по почте, чтобы управлять профилем.',
|
||
'profile.linkAccount': 'Привязать аккаунт',
|
||
'profile.linkTelegram': 'Привязать Telegram',
|
||
'profile.linked': 'Аккаунт привязан.',
|
||
'profile.merged': 'Аккаунты объединены.',
|
||
'profile.mergeTitle': 'Объединить аккаунты?',
|
||
'profile.mergeBody': 'Эта личность уже принадлежит «{name}» (игр: {games}, друзей: {friends}).',
|
||
'profile.mergeIrreversible': 'Объединение сольёт оба аккаунта в этот и необратимо.',
|
||
'profile.mergeConfirm': 'Объединить',
|
||
|
||
'settings.title': 'Настройки',
|
||
'settings.theme': 'Тема',
|
||
'settings.themeAuto': 'Авто',
|
||
'settings.themeLight': 'Светлая',
|
||
'settings.themeDark': 'Тёмная',
|
||
'settings.language': 'Язык интерфейса',
|
||
'settings.boardStyle': 'Стиль доски',
|
||
'settings.boardLabels': 'Подписи бонусов',
|
||
'settings.labelsBeginner': 'Новичок',
|
||
'settings.labelsClassic': 'Классика',
|
||
'settings.labelsNone': 'Без текста',
|
||
'settings.reduceMotion': 'Меньше анимаций',
|
||
|
||
'about.title': 'О программе',
|
||
'about.tab': 'Инфо',
|
||
'about.description': 'Мультиплатформенная игра в скрабл.',
|
||
'about.version': 'Версия {v}',
|
||
|
||
'feedback.title': 'Обратная связь',
|
||
'feedback.placeholder': 'Опишите проблему или поделитесь предложением…',
|
||
'feedback.attach': 'Прикрепить файл',
|
||
'feedback.removeFile': 'Удалить',
|
||
'feedback.send': 'Отправить',
|
||
'feedback.fileRejected': 'Этот файл нельзя прикрепить.',
|
||
'feedback.sent': 'Ваше сообщение отправлено',
|
||
'feedback.waiting': 'Ожидаем рассмотрения вашего последнего обращения',
|
||
'feedback.banned': 'Отправка обратной связи недоступна',
|
||
'feedback.replyTitle': 'Ответ на ваше последнее сообщение',
|
||
|
||
'landing.tagline': 'Играй в Скрэббл с друзьями или умным роботом — в браузере или в Telegram.',
|
||
'landing.playTelegram': 'Играть в Telegram',
|
||
|
||
'lang.en': 'English',
|
||
'lang.ru': 'Русский',
|
||
|
||
'error.not_your_turn': 'Сейчас не ваш ход.',
|
||
'error.nudge_own_turn': 'Сейчас ваш ход — некого торопить.',
|
||
'error.illegal_play': 'Это недопустимый ход.',
|
||
'error.hint_unavailable': 'Подсказки недоступны.',
|
||
'error.no_hint_available': 'Нет вариантов с вашим набором.',
|
||
'error.chat_rejected': 'Сообщение отклонено (слишком длинное или содержит контакты).',
|
||
'error.nudge_too_soon': 'Не стоит торопить соперника так часто.',
|
||
'error.chat_not_your_turn': 'Писать в чат можно только в свой ход.',
|
||
'error.chat_already_sent': 'За один ход можно отправить только одно сообщение.',
|
||
'error.game_finished': 'Эта игра уже завершена.',
|
||
'error.not_a_player': 'Вы не участник этой игры.',
|
||
'error.already_queued': 'Вы уже в очереди.',
|
||
'error.email_taken': 'Эта почта принадлежит другому аккаунту.',
|
||
'error.code_invalid': 'Неверный или истёкший код.',
|
||
'error.invalid_email': 'Введите корректный адрес почты.',
|
||
'error.invalid_config': 'Неверные настройки игры.',
|
||
'error.not_found': 'Не найдено.',
|
||
'error.session_invalid': 'Сессия истекла. Войдите снова.',
|
||
'error.unauthenticated': 'Пожалуйста, войдите.',
|
||
'error.rate_limited': 'Слишком много запросов, помедленнее.',
|
||
'error.unavailable': 'Проблема соединения. Повторяем…',
|
||
'error.internal': 'Что-то пошло не так.',
|
||
'error.generic': 'Что-то пошло не так.',
|
||
|
||
'lobby.invitations': 'Приглашения',
|
||
'lobby.friends': 'Друзья',
|
||
|
||
'friends.title': 'Друзья',
|
||
'friends.yours': 'Ваши друзья',
|
||
'friends.none': 'Друзей пока нет.',
|
||
'friends.incoming': 'Заявки в друзья',
|
||
'friends.accept': 'Принять',
|
||
'friends.decline': 'Отклонить',
|
||
'friends.unfriend': 'Удалить',
|
||
'friends.block': 'Заблокировать',
|
||
'friends.actions': 'Действия',
|
||
'friends.blockConfirm': 'Заблокировать?',
|
||
'friends.unfriendConfirm': 'Удалить из друзей?',
|
||
'friends.add': 'Добавить друга',
|
||
'friends.addFromGame': 'В друзья',
|
||
'friends.blockFromGame': 'Заблокировать',
|
||
'friends.requestSent': 'Заявка в друзья отправлена.',
|
||
'friends.getCode': 'Показать мой код',
|
||
'friends.codeHint': 'Передайте этот код другу в течение 12 часов.',
|
||
'friends.codeExpires': 'Истекает в {time}',
|
||
'friends.enterCode': 'Есть код? Добавить друга',
|
||
'friends.codePlaceholder': 'Код из 6 цифр',
|
||
'friends.redeem': 'Добавить',
|
||
'friends.copy': 'Копировать',
|
||
'friends.codeCopied': 'Код скопирован.',
|
||
'friends.shareTelegram': 'Поделиться через Telegram',
|
||
'friends.inviteText': 'Давай играть в Эрудит!',
|
||
'friends.linkCopied': 'Ссылка скопирована.',
|
||
'friends.selfInvite': 'Надеюсь, что с собой Вы уже давно дружите ☺️',
|
||
'friends.staleInviteTitle': 'Ссылка устарела',
|
||
'friends.staleInvite': 'Вы открыли игру по устаревшей ссылке. Откройте бота {bot}, чтобы играть и получать уведомления.',
|
||
'friends.welcomeRedeemTitle': 'Добро пожаловать!',
|
||
'friends.welcomeRedeem': 'Добро пожаловать, {name}!\n\nВоспользуйтесь {bot}, чтобы заходить в игру наиболее удобным способом.',
|
||
'friends.added': 'Добавлен(а) {name}.',
|
||
'friends.blockedList': 'Заблокированные',
|
||
'friends.unblock': 'Разблокировать',
|
||
'friends.noneBlocked': 'Заблокированных нет.',
|
||
|
||
'invitations.none': 'Приглашений нет.',
|
||
'invitations.from': 'От {name}',
|
||
'invitations.with': 'С {names}',
|
||
'invitations.accept': 'Принять',
|
||
'invitations.decline': 'Отклонить',
|
||
'invitations.declineConfirm': 'Отклонить приглашение?',
|
||
'invitations.cancel': 'Отменить',
|
||
'invitations.waiting': 'Ожидаем ответы',
|
||
|
||
'new.auto': 'Быстрая игра',
|
||
'new.withFriends': 'Игра с друзьями',
|
||
'new.pickFriends': 'Кого пригласить',
|
||
'new.searchFriends': 'Поиск друзей',
|
||
'new.gameType': 'Вариант',
|
||
'new.invite': 'Отправить приглашение',
|
||
'new.moveTime': 'Время на ход',
|
||
'new.hintsPerPlayer': 'Подсказки',
|
||
'new.multipleWordsPerTurn': 'Несколько слов за ход',
|
||
'new.start': 'Начать игру',
|
||
'new.invited': 'Приглашение отправлено.',
|
||
'new.noFriends': 'Сначала добавьте друзей, чтобы пригласить их.',
|
||
'new.opponentAI': 'ИИ',
|
||
'new.opponentRandom': 'Случайный игрок',
|
||
'new.aiInactiveLimit': 'Поражение, если 7 дней нет активности',
|
||
|
||
'stats.title': 'Статистика',
|
||
'stats.wins': 'Победы',
|
||
'stats.losses': 'Поражения',
|
||
'stats.draws': 'Ничьи',
|
||
'stats.played': 'Игры',
|
||
'stats.moves': 'Ходы',
|
||
'stats.hintShare': 'Доля подсказок',
|
||
'stats.winRate': 'Доля побед',
|
||
'stats.maxGame': 'Лучшая игра',
|
||
'stats.maxWord': 'Лучший ход',
|
||
'stats.guestHint': 'Войдите, чтобы вести статистику.',
|
||
|
||
'game.exportGcg': 'Экспорт GCG',
|
||
'game.gcgActiveOnly': 'Доступно после завершения игры.',
|
||
'game.addFriendShort': 'В друзья?',
|
||
'game.blockShort': 'Блокируем?',
|
||
|
||
'time.minutes': '{n} мин',
|
||
'time.hours': '{n} ч',
|
||
|
||
'error.self_relation': 'Нельзя сделать это с самим собой.',
|
||
'error.request_exists': 'Заявка или дружба уже существует.',
|
||
'error.request_blocked': 'Игрок не принимает заявки.',
|
||
'error.request_not_found': 'Подходящей заявки нет.',
|
||
'error.no_shared_game': 'Можно добавить только того, с кем вы играли.',
|
||
'error.request_declined': 'Игрок отклонил вашу заявку.',
|
||
'error.friend_code_invalid': 'Код недействителен или истёк.',
|
||
'error.invalid_invitation': 'Неверное приглашение.',
|
||
'error.invitation_blocked': 'Нельзя пригласить этого игрока.',
|
||
'error.invitation_not_found': 'Приглашение не найдено.',
|
||
'error.invitation_not_pending': 'Приглашение больше не открыто.',
|
||
'error.invitation_expired': 'Приглашение истекло.',
|
||
'error.not_invited': 'Вас не приглашали.',
|
||
'error.already_responded': 'Вы уже ответили.',
|
||
'error.not_inviter': 'Только пригласивший может это сделать.',
|
||
'error.game_active': 'Доступно только после завершения игры.',
|
||
'error.invalid_profile': 'Некоторые поля профиля некорректны.',
|
||
'error.already_confirmed': 'Эта почта уже подтверждена.',
|
||
};
|