fix(ui): use the guest stubs' "sign in" wording, and link it in Stats
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m15s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m57s

The New Game hint said "Register" where the Stats and Profile guest stubs say
"Sign in". Move the shared word into common.signInLink so the two screens
cannot drift, and turn the Stats stub's opening word into the same link to the
profile screen.
This commit is contained in:
Ilia Denisov
2026-07-27 20:45:10 +02:00
parent b6d88da78c
commit c3d36dda2d
9 changed files with 39 additions and 19 deletions
+1 -1
View File
@@ -285,7 +285,7 @@ func (s *Store) AttachIdentity(ctx context.Context, accountID uuid.UUID, kind, e
// for an already-durable account.
//
// The promotion also widens the guest's narrow variant set (GuestVariantPreferences,
// Эрудит alone) to the registered default, so a player who registers from the New Game
// Эрудит alone) to the registered default, so a player who signs in from the New Game
// hint really does gain Russian Scrabble. A guest who changed the set themselves is left
// alone: only the untouched guest default is replaced.
func (s *Store) ClearGuest(ctx context.Context, accountID uuid.UUID) error {
+1 -1
View File
@@ -249,7 +249,7 @@ arrive from a platform rather than completing a mandatory registration).
**registered** account defaults to the two Russian-alphabet games — **Erudit + Russian
Scrabble** (a DB column default); a **guest** is deliberately narrower and starts on
**Erudit only** (`account.ProvisionGuest` writes the set explicitly), with New Game inviting
the guest to register for the rest. Registering promotes the set to the registered default
the guest to sign in for the rest. Signing in promotes the set to the registered default
(`account.ClearGuest`), but only while the guest still carries the untouched guest set —
a player who chose their own set is never overruled, and **existing accounts are not
backfilled** when the default moves. At least one variant must stay selected. The picker is
+2 -1
View File
@@ -455,7 +455,8 @@ variants — Erudite, Russian Scrabble and English Scrabble, shown **Erudite-fir
yourself to be matched into; a **newly registered account starts with Erudite + Russian
Scrabble**, and arriving through the **promo bot's deep link** also enables **English Scrabble**
when your Telegram language is not Russian. A **guest** starts with **Erudite only** and New Game
invites them to register for the rest; registering widens the account to the registered default.
invites them to **sign in** for the rest (the same wording and link as the Stats guest stub, both
routing to Settings → Profile); signing in widens the account to the registered default.
Changing the defaults never rewrites accounts that already exist. You must keep
**at least one** selected. This list is exactly what **New Game** offers when you start a game
(auto-match, an AI game, or a friend invitation you create) — a variant you have not enabled is
+2 -2
View File
@@ -462,8 +462,8 @@ Telegram. Внутри VK Mini App те же настройки — и язык
которые ты разрешаешь себя подбирать; **новый зарегистрированный аккаунт стартует с Эрудитом и
русским Scrabble**, а вход по **диплинку промо-бота** дополнительно включает **английский
Scrabble**, если язык Telegram не русский. **Гость** стартует **только с Эрудитом**, и «Новая игра»
предлагает ему зарегистрироваться ради остальных; регистрация расширяет аккаунт до набора
зарегистрированного. Смена набора по умолчанию никогда не переписывает уже существующие аккаунты.
предлагает ему **войти** ради остальных (та же формулировка и ссылка, что и у гостевой заглушки в
статистике, обе ведут в Settings → Profile); вход расширяет аккаунт до набора зарегистрированного. Смена набора по умолчанию никогда не переписывает уже существующие аккаунты.
Нужно оставить выбранным **хотя бы один**. Именно этот список предлагает **Новая игра**, когда ты
запускаешь партию (авто-подбор, игра с ИИ или приглашение друга, которое ты создаёшь), — не
включённый вариант не предлагается, и сервер его отклоняет. На партии, в которые тебя
+8 -5
View File
@@ -41,6 +41,10 @@ export const en = {
'common.you': 'You',
'common.guest': 'Guest',
'common.save': 'Save',
// The opening word of a guest's invitation to sign in, rendered as a link to the profile
// screen (New Game, Stats). Shared so the two screens cannot drift apart; each screen keeps
// its own tail, which carries its leading space — Svelte trims literal edge whitespace.
'common.signInLink': 'Sign in',
'login.title': 'Sign in',
'login.guest': 'Play as guest',
@@ -83,10 +87,8 @@ export const en = {
'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.',
// The guest's invitation to register, shown under the single variant a guest is offered. It is
// two keys because the first word is a link to the profile screen; the tail carries its own
// leading space, since Svelte trims literal whitespace at the edges of markup.
'new.guestVariantHintLink': 'Register',
// The tail of the guest's invitation to sign in (common.signInLink opens it), shown under the
// single variant a guest is offered.
'new.guestVariantHint': ' to play the Russian or English Scrabble.',
// First-run coachmark hints (components/Coachmark.svelte). The leading emoji in each comment
@@ -420,7 +422,8 @@ export const en = {
'stats.winRate': 'Win rate',
'stats.maxGame': 'Best game',
'stats.maxWord': 'Best move',
'stats.guestHint': 'Sign in to track your statistics.',
// The tail of the guest's invitation to sign in (common.signInLink opens it).
'stats.guestHint': ' to track your statistics.',
'game.exportGcg': 'Export game',
'game.exportChoice': 'Export the finished game as:',
+2 -2
View File
@@ -42,6 +42,7 @@ export const ru: Record<MessageKey, string> = {
'common.you': 'Вы',
'common.guest': 'Гость',
'common.save': 'Сохранить',
'common.signInLink': 'Войдите',
'login.title': 'Вход',
'login.guest': 'Играть как гость',
@@ -84,7 +85,6 @@ export const ru: Record<MessageKey, string> = {
'new.moveLimit': 'Время на ход: {n} ч. 00 мин.',
'new.searchHint':
'Иногда поиск соперника может занять некоторое время. Если не захотите ждать после начала игры, можете вернуться в приложение через несколько минут.',
'new.guestVariantHintLink': 'Зарегистрируйтесь',
'new.guestVariantHint': ', чтобы играть в русский или английский вариант Скрэббл.',
// Подсказки первого запуска (components/Coachmark.svelte).
@@ -417,7 +417,7 @@ export const ru: Record<MessageKey, string> = {
'stats.winRate': 'Доля побед',
'stats.maxGame': 'Лучшая игра',
'stats.maxWord': 'Лучший ход',
'stats.guestHint': 'Войдите, чтобы вести статистику.',
'stats.guestHint': ', чтобы вести статистику.',
'game.exportGcg': 'Экспорт партии',
'game.exportChoice': 'Экспортировать завершённую партию как:',
+3 -3
View File
@@ -90,11 +90,11 @@ export function availableVariants(preferences: Variant[] | undefined): VariantOp
return ALL_VARIANTS.filter((v) => effective.includes(v.id));
}
// showRegisterHint reports whether New Game invites the player to register in order to unlock the
// showRegisterHint reports whether New Game invites the player to sign in in order to unlock the
// other games, given whether they are a guest (isGuest), how many variants they are offered
// (offered) and whether the app is in offline mode (offline). It is the guest's case alone: a
// guest is created with Erudit only, while registering widens the account to both Russian-alphabet
// games. Offline it is suppressed — registration needs the network, so the link would be dead.
// guest is created with Erudit only, while signing in widens the account to both Russian-alphabet
// games. Offline it is suppressed — signing in needs the network, so the link would be dead.
export function showRegisterHint(isGuest: boolean, offered: number, offline: boolean): boolean {
return isGuest && !offline && offered === 1;
}
+4 -3
View File
@@ -396,11 +396,12 @@
</button>
{/each}
</div>
<!-- A guest is offered Erudit alone; registering widens the account to both Russian games
(and Settings then offers English), so point them at the profile screen. -->
<!-- A guest is offered Erudit alone; signing in widens the account to both Russian games
(and Settings then offers English), so point them at the profile screen — the same
wording and link the Stats guest stub uses. -->
{#if showRegisterHint(guest, variants.length, offlineMode.active)}
<p class="guesthint">
<button class="hintlink" onclick={() => navigate('/profile')}>{t('new.guestVariantHintLink')}</button>{t('new.guestVariantHint')}
<button class="hintlink" onclick={() => navigate('/profile')}>{t('common.signInLink')}</button>{t('new.guestVariantHint')}
</p>
{/if}
{#if variants.some((v) => supportsMultipleWordsToggle(v.id))}
+16 -1
View File
@@ -4,6 +4,7 @@
import WordTiles from '../components/WordTiles.svelte';
import { app, handleError } from '../lib/app.svelte';
import { gateway } from '../lib/gateway';
import { navigate } from '../lib/router.svelte';
import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte';
import { gamesPlayed, hintSharePercent, winRate } from '../lib/stats';
import { ALL_VARIANTS, variantNameKey } from '../lib/variants';
@@ -56,7 +57,11 @@
<Screen title={t('stats.title')} back="/">
<div class="page">
{#if app.profile?.isGuest}
<p class="muted">{t('stats.guestHint')}</p>
<!-- The opening word links to the profile screen, where a guest signs in (the same wording
and link New Game shows a guest under its lone variant). -->
<p class="muted">
<button class="hintlink" onclick={() => navigate('/profile')}>{t('common.signInLink')}</button>{t('stats.guestHint')}
</p>
{:else if stats}
<div class="grid">
{#each cards as c (c.key)}
@@ -89,6 +94,16 @@
.muted {
color: var(--text-muted);
}
/* The inline link-styled button opening the guest stub (mirrors New Game's .hintlink). */
.hintlink {
background: none;
border: none;
padding: 0;
color: var(--accent);
font: inherit;
text-decoration: underline;
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);