feat(i18n): bind angular host/player copy to shared locale catalog
All checks were successful
CI / test-and-quality (push) Successful in 2m26s
CI / test-and-quality (pull_request) Successful in 2m31s

This commit is contained in:
2026-03-01 19:27:15 +00:00
committed by Asger Geel Weirsoee
parent 377722eb9a
commit dd3b48067a
7 changed files with 389 additions and 64 deletions

View File

@@ -5,6 +5,7 @@ import { FormsModule } from '@angular/forms';
import { createApiClient } from '../../../../../src/api/client';
import type { FinishGameResponse, ScoreboardResponse } from '../../../../../src/api/types';
import { createVerticalSliceController } from '../../../../../src/spa/vertical-slice';
import { clientHasNoAudioOutput, resolvePreferredLocale, t } from '../../lobby-i18n';
interface SessionDetail {
session: { code: string; status: string; current_round: number };
@@ -20,40 +21,41 @@ type LeaderboardResponse = FinishGameResponse;
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<h2>Host SPA gameplay flow</h2>
<h2>{{ copy('host.title') }}</h2>
<div class="panel">
<label>Session code <input [(ngModel)]="sessionCode" /></label>
<label>Category <input [(ngModel)]="categorySlug" /></label>
<button (click)="refreshSession()" [disabled]="loading">Refresh</button>
<button (click)="startRound()" [disabled]="loading">Start round</button>
<button (click)="showQuestion()" [disabled]="loading || !roundQuestionId">Show question</button>
<button (click)="mixAnswers()" [disabled]="loading || !roundQuestionId">Mix answers → guess</button>
<button (click)="calculateScores()" [disabled]="loading || !roundQuestionId">Calculate scores → reveal</button>
<button (click)="loadScoreboard()" [disabled]="loading">Load scoreboard</button>
<button (click)="startNextRound()" [disabled]="loading">Start next round</button>
<button (click)="finishGame()" [disabled]="loading">Finish game</button>
<button *ngIf="scoreboardError" (click)="loadScoreboard()" [disabled]="loading">Retry scoreboard</button>
<button *ngIf="nextRoundError" (click)="startNextRound()" [disabled]="loading">Retry next round</button>
<button *ngIf="finishError" (click)="finishGame()" [disabled]="loading">Retry finish game</button>
<div class="panel" [attr.data-client-has-no-audio-output]="clientHasNoAudioOutput">
<label>{{ copy('common.session_code') }} <input [(ngModel)]="sessionCode" /></label>
<label>{{ copy('host.category') }} <input [(ngModel)]="categorySlug" /></label>
<button (click)="refreshSession()" [disabled]="loading">{{ copy('common.refresh') }}</button>
<button (click)="startRound()" [disabled]="loading">{{ copy('host.start_round') }}</button>
<button (click)="showQuestion()" [disabled]="loading || !roundQuestionId">{{ copy('host.show_question') }}</button>
<button (click)="mixAnswers()" [disabled]="loading || !roundQuestionId">{{ copy('host.mix_answers') }}</button>
<button (click)="calculateScores()" [disabled]="loading || !roundQuestionId">{{ copy('host.calculate_scores') }}</button>
<button (click)="loadScoreboard()" [disabled]="loading">{{ copy('host.load_scoreboard') }}</button>
<button (click)="startNextRound()" [disabled]="loading">{{ copy('host.start_next_round') }}</button>
<button (click)="finishGame()" [disabled]="loading">{{ copy('host.finish_game') }}</button>
<button *ngIf="scoreboardError" (click)="loadScoreboard()" [disabled]="loading">{{ copy('host.retry_scoreboard') }}</button>
<button *ngIf="nextRoundError" (click)="startNextRound()" [disabled]="loading">{{ copy('host.retry_next_round') }}</button>
<button *ngIf="finishError" (click)="finishGame()" [disabled]="loading">{{ copy('host.retry_finish') }}</button>
</div>
<p *ngIf="session" class="hint">{{ copy('host.audio_locale_hint') }}: {{ locale }}</p>
<p *ngIf="error" class="error">{{ error }}</p>
<p *ngIf="scoreboardError" class="error">{{ scoreboardError }}</p>
<p *ngIf="nextRoundError" class="error">{{ nextRoundError }}</p>
<p *ngIf="finishError" class="error">{{ finishError }}</p>
<div *ngIf="session" class="panel">
<p><strong>Status:</strong> {{ session.session.status }} · round {{ session.session.current_round }}</p>
<p><strong>Round question id:</strong> {{ roundQuestionId || '-' }}</p>
<p *ngIf="session.round_question"><strong>Prompt:</strong> {{ session.round_question.prompt }}</p>
<p><strong>{{ copy('common.status') }}:</strong> {{ session.session.status }} · {{ copy('common.round') }} {{ session.session.current_round }}</p>
<p><strong>{{ copy('common.round_question_id') }}:</strong> {{ roundQuestionId || '-' }}</p>
<p *ngIf="session.round_question"><strong>{{ copy('common.prompt') }}:</strong> {{ session.round_question.prompt }}</p>
<ul>
<li *ngFor="let p of session.players">{{ p.nickname }}: {{ p.score }}</li>
</ul>
<pre *ngIf="scoreboardPayload">{{ scoreboardPayload }}</pre>
<div *ngIf="finalLeaderboard.length">
<h3>Final leaderboard</h3>
<p *ngIf="finalWinner"><strong>Winner:</strong> {{ finalWinner.nickname }} ({{ finalWinner.score }} pts)</p>
<h3>{{ copy('host.final_leaderboard') }}</h3>
<p *ngIf="finalWinner"><strong>{{ copy('host.winner') }}:</strong> {{ finalWinner.nickname }} ({{ finalWinner.score }} pts)</p>
<ol>
<li *ngFor="let entry of finalLeaderboard">{{ entry.nickname }}: {{ entry.score }}</li>
</ol>
@@ -63,6 +65,9 @@ type LeaderboardResponse = FinishGameResponse;
`,
})
export class HostShellComponent implements OnInit {
locale = resolvePreferredLocale();
readonly clientHasNoAudioOutput = clientHasNoAudioOutput;
sessionCode = '';
categorySlug = 'general';
roundQuestionId = '';
@@ -100,6 +105,10 @@ export class HostShellComponent implements OnInit {
void this.refreshSession();
}
copy(key: string): string {
return t(key, this.locale);
}
private normalizeCode(value: string): string {
return value.trim().toUpperCase();
}
@@ -149,7 +158,7 @@ export class HostShellComponent implements OnInit {
}
this.syncRouteFromSession();
} catch (error) {
this.error = `Session refresh failed: ${(error as Error).message}`;
this.error = `${this.copy('host.session_refresh_failed')}: ${(error as Error).message}`;
} finally {
this.loading = false;
}
@@ -207,7 +216,7 @@ export class HostShellComponent implements OnInit {
this.scoreboardPayload = JSON.stringify(payload, null, 2);
await this.refreshSession();
} catch (error) {
this.scoreboardError = `Scoreboard failed: ${(error as Error).message}`;
this.scoreboardError = `${this.copy('host.scoreboard_failed')}: ${(error as Error).message}`;
} finally {
this.loading = false;
}
@@ -220,14 +229,14 @@ export class HostShellComponent implements OnInit {
try {
const code = this.normalizeCode(this.sessionCode);
if (!code) {
throw new Error('Session code is required');
throw new Error(this.copy('host.session_code_required'));
}
await this.request(`/lobby/sessions/${encodeURIComponent(code)}/rounds/next`, 'POST', {});
this.scoreboardPayload = '';
this.resetFinalLeaderboard();
await this.refreshSession();
} catch (error) {
this.nextRoundError = `Next round failed: ${(error as Error).message}`;
this.nextRoundError = `${this.copy('host.next_round_failed')}: ${(error as Error).message}`;
} finally {
this.loading = false;
}
@@ -240,7 +249,7 @@ export class HostShellComponent implements OnInit {
try {
const code = this.normalizeCode(this.sessionCode);
if (!code) {
throw new Error('Session code is required');
throw new Error(this.copy('host.session_code_required'));
}
const payload = await this.request<LeaderboardResponse>(`/lobby/sessions/${encodeURIComponent(code)}/finish`, 'POST', {});
this.finalLeaderboardPayload = JSON.stringify(payload, null, 2);
@@ -253,7 +262,7 @@ export class HostShellComponent implements OnInit {
this.finalWinner = payload.winner ?? this.finalLeaderboard[0] ?? null;
await this.refreshSession();
} catch (error) {
this.finishError = `Finish game failed: ${(error as Error).message}`;
this.finishError = `${this.copy('host.finish_game_failed')}: ${(error as Error).message}`;
} finally {
this.loading = false;
}