-
Final leaderboard
+
{{ copy('player.final_leaderboard') }}
- {{ entry.nickname }}: {{ entry.score }}
@@ -84,12 +85,15 @@ function resolveLocalStorage(): Storage | undefined {
{{ submitError.message }}
-
-
+
+
`,
})
export class PlayerShellComponent implements OnInit, OnDestroy {
+ locale = resolvePreferredLocale();
+ readonly clientHasNoAudioOutput = clientHasNoAudioOutput;
+
sessionCode = '';
nickname = '';
playerId = 0;
@@ -198,17 +202,21 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
get loadingMessage(): string {
switch (this.loadingTransition) {
case 'join':
- return 'Joining session… restoring your player state.';
+ return this.copy('player.loading_join');
case 'submit-lie':
- return 'Submitting lie… waiting for guess phase.';
+ return this.copy('player.loading_submit_lie');
case 'submit-guess':
- return 'Submitting guess… waiting for reveal.';
+ return this.copy('player.loading_submit_guess');
case 'refresh':
default:
- return 'Loading latest session state…';
+ return this.copy('player.loading_refresh');
}
}
+ copy(key: string): string {
+ return t(key, this.locale);
+ }
+
private normalizeCode(value: string): string {
return value.trim().toUpperCase();
}
@@ -352,7 +360,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.syncRouteFromSession();
this.markOnline();
} catch (error) {
- this.error = `Session refresh failed: ${this.toMessage(error)}`;
+ this.error = `${this.copy('player.session_refresh_failed')}: ${this.toMessage(error)}`;
this.markConnectionIssue(error);
} finally {
this.loading = false;
@@ -382,7 +390,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.syncRouteFromSession();
this.markOnline();
} catch (error) {
- this.error = `Join failed: ${this.toMessage(error)}`;
+ this.error = `${this.copy('player.join_failed')}: ${this.toMessage(error)}`;
this.markConnectionIssue(error);
} finally {
this.loading = false;
@@ -410,7 +418,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
await this.refreshSession();
this.markOnline();
} catch (error) {
- this.submitError = { kind: 'lie', message: `Lie submit failed: ${this.toMessage(error)}` };
+ this.submitError = { kind: 'lie', message: `${this.copy('player.lie_submit_failed')}: ${this.toMessage(error)}` };
this.markConnectionIssue(error);
} finally {
this.loading = false;
@@ -438,7 +446,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
await this.refreshSession();
this.markOnline();
} catch (error) {
- this.submitError = { kind: 'guess', message: `Guess submit failed: ${this.toMessage(error)}` };
+ this.submitError = { kind: 'guess', message: `${this.copy('player.guess_submit_failed')}: ${this.toMessage(error)}` };
this.markConnectionIssue(error);
} finally {
this.loading = false;
diff --git a/frontend/angular/src/app/lobby-i18n.ts b/frontend/angular/src/app/lobby-i18n.ts
new file mode 100644
index 0000000..56b8d50
--- /dev/null
+++ b/frontend/angular/src/app/lobby-i18n.ts
@@ -0,0 +1,63 @@
+import lobbyCatalog from '../../../../shared/i18n/lobby.json';
+
+type SupportedLocale = (typeof lobbyCatalog.locales.supported)[number];
+
+const DEFAULT_LOCALE = lobbyCatalog.locales.default as SupportedLocale;
+const SUPPORTED_LOCALES = lobbyCatalog.locales.supported as readonly SupportedLocale[];
+
+export function normalizeLocale(rawLocale?: string | null): SupportedLocale {
+ const locale = (rawLocale ?? '').trim().toLowerCase();
+ if ((SUPPORTED_LOCALES as readonly string[]).includes(locale)) {
+ return locale as SupportedLocale;
+ }
+
+ const shortLocale = locale.split('-')[0] ?? '';
+ if ((SUPPORTED_LOCALES as readonly string[]).includes(shortLocale)) {
+ return shortLocale as SupportedLocale;
+ }
+
+ return DEFAULT_LOCALE;
+}
+
+export function resolvePreferredLocale(): SupportedLocale {
+ if (typeof window === 'undefined') {
+ return DEFAULT_LOCALE;
+ }
+
+ const queryLocale = new URLSearchParams(window.location?.search ?? '').get('lang');
+ const storedLocale = window.localStorage?.getItem?.('wpp.locale');
+ const browserLocale = typeof navigator !== 'undefined' ? navigator.language : '';
+
+ return normalizeLocale(queryLocale || storedLocale || browserLocale || DEFAULT_LOCALE);
+}
+
+export function setPreferredLocale(locale: string): SupportedLocale {
+ const normalized = normalizeLocale(locale);
+ if (typeof window !== 'undefined') {
+ window.localStorage?.setItem?.('wpp.locale', normalized);
+ }
+ return normalized;
+}
+
+export function t(key: string, locale: string): string {
+ const normalizedLocale = normalizeLocale(locale);
+ const fallbackLocale = DEFAULT_LOCALE;
+ const segments = key.split('.');
+
+ let cursor: unknown = lobbyCatalog.frontend.ui;
+ for (const segment of segments) {
+ if (!cursor || typeof cursor !== 'object' || !(segment in (cursor as Record
))) {
+ return key;
+ }
+ cursor = (cursor as Record)[segment];
+ }
+
+ if (!cursor || typeof cursor !== 'object') {
+ return key;
+ }
+
+ const translations = cursor as Record;
+ return translations[normalizedLocale] ?? translations[fallbackLocale] ?? key;
+}
+
+export const clientHasNoAudioOutput = Boolean(lobbyCatalog.frontend.capabilities.client_has_no_audio_output);
diff --git a/shared/i18n/lobby.json b/shared/i18n/lobby.json
index 1077e6d..c02c50c 100644
--- a/shared/i18n/lobby.json
+++ b/shared/i18n/lobby.json
@@ -1,7 +1,10 @@
{
"locales": {
"default": "en",
- "supported": ["en", "da"]
+ "supported": [
+ "en",
+ "da"
+ ]
},
"frontend": {
"errors": {
@@ -37,6 +40,227 @@
"en": "Action failed. Refresh status and try again.",
"da": "Handlingen fejlede. Opdater status og prøv igen."
}
+ },
+ "ui": {
+ "common": {
+ "refresh": {
+ "en": "Refresh",
+ "da": "Opdatér"
+ },
+ "retry": {
+ "en": "Retry",
+ "da": "Prøv igen"
+ },
+ "back_to_join": {
+ "en": "Back to join",
+ "da": "Tilbage til join"
+ },
+ "session_code": {
+ "en": "Session code",
+ "da": "Sessionskode"
+ },
+ "status": {
+ "en": "Status",
+ "da": "Status"
+ },
+ "prompt": {
+ "en": "Prompt",
+ "da": "Spørgsmål"
+ },
+ "round_question_id": {
+ "en": "Round question id",
+ "da": "Rundespørgsmål-id"
+ },
+ "round": {
+ "en": "round",
+ "da": "runde"
+ }
+ },
+ "app": {
+ "title": {
+ "en": "WPP Angular Shell",
+ "da": "WPP Angular Shell"
+ },
+ "host_nav": {
+ "en": "Host",
+ "da": "Vært"
+ },
+ "player_nav": {
+ "en": "Player",
+ "da": "Spiller"
+ },
+ "language_label": {
+ "en": "Language",
+ "da": "Sprog"
+ }
+ },
+ "host": {
+ "title": {
+ "en": "Host gameplay flow",
+ "da": "Vært gameplay-flow"
+ },
+ "category": {
+ "en": "Category",
+ "da": "Kategori"
+ },
+ "start_round": {
+ "en": "Start round",
+ "da": "Start runde"
+ },
+ "show_question": {
+ "en": "Show question",
+ "da": "Vis spørgsmål"
+ },
+ "mix_answers": {
+ "en": "Mix answers → guess",
+ "da": "Bland svar → gæt"
+ },
+ "calculate_scores": {
+ "en": "Calculate scores → reveal",
+ "da": "Udregn score → afslør"
+ },
+ "load_scoreboard": {
+ "en": "Load scoreboard",
+ "da": "Hent scoreboard"
+ },
+ "start_next_round": {
+ "en": "Start next round",
+ "da": "Start næste runde"
+ },
+ "finish_game": {
+ "en": "Finish game",
+ "da": "Afslut spil"
+ },
+ "retry_scoreboard": {
+ "en": "Retry scoreboard",
+ "da": "Prøv scoreboard igen"
+ },
+ "retry_next_round": {
+ "en": "Retry next round",
+ "da": "Prøv næste runde igen"
+ },
+ "retry_finish": {
+ "en": "Retry finish game",
+ "da": "Prøv afslutning igen"
+ },
+ "session_refresh_failed": {
+ "en": "Session refresh failed",
+ "da": "Kunne ikke opdatere session"
+ },
+ "scoreboard_failed": {
+ "en": "Scoreboard failed",
+ "da": "Scoreboard fejlede"
+ },
+ "next_round_failed": {
+ "en": "Next round failed",
+ "da": "Næste runde fejlede"
+ },
+ "finish_game_failed": {
+ "en": "Finish game failed",
+ "da": "Afslutning fejlede"
+ },
+ "session_code_required": {
+ "en": "Session code is required",
+ "da": "Sessionskode er påkrævet"
+ },
+ "final_leaderboard": {
+ "en": "Final leaderboard",
+ "da": "Finale leaderboard"
+ },
+ "winner": {
+ "en": "Winner",
+ "da": "Vinder"
+ },
+ "audio_locale_hint": {
+ "en": "Host locale for audio references",
+ "da": "Værtens locale for lydreferencer"
+ }
+ },
+ "player": {
+ "title": {
+ "en": "Player gameplay flow",
+ "da": "Spiller gameplay-flow"
+ },
+ "nickname": {
+ "en": "Nickname",
+ "da": "Kaldenavn"
+ },
+ "join": {
+ "en": "Join",
+ "da": "Join"
+ },
+ "lie_label": {
+ "en": "Lie",
+ "da": "Løgn"
+ },
+ "submit_lie": {
+ "en": "Submit lie",
+ "da": "Send løgn"
+ },
+ "retry_lie_submit": {
+ "en": "Retry lie submit",
+ "da": "Prøv løgn igen"
+ },
+ "submit_guess": {
+ "en": "Submit guess",
+ "da": "Send gæt"
+ },
+ "retry_guess_submit": {
+ "en": "Retry guess submit",
+ "da": "Prøv gæt igen"
+ },
+ "final_leaderboard": {
+ "en": "Final leaderboard",
+ "da": "Finale leaderboard"
+ },
+ "reconnecting_text": {
+ "en": "Reconnecting… trying to refresh session state.",
+ "da": "Forbinder igen… prøver at opdatere session."
+ },
+ "offline_text": {
+ "en": "You are offline. Reconnect to continue gameplay.",
+ "da": "Du er offline. Forbind igen for at fortsætte."
+ },
+ "retry_now": {
+ "en": "Retry now",
+ "da": "Prøv nu"
+ },
+ "loading_refresh": {
+ "en": "Loading latest session state…",
+ "da": "Indlæser seneste session-status…"
+ },
+ "loading_join": {
+ "en": "Joining session… restoring your player state.",
+ "da": "Joiner session… gendanner spillerstatus."
+ },
+ "loading_submit_lie": {
+ "en": "Submitting lie… waiting for guess phase.",
+ "da": "Sender løgn… venter på gættefase."
+ },
+ "loading_submit_guess": {
+ "en": "Submitting guess… waiting for reveal.",
+ "da": "Sender gæt… venter på afsløring."
+ },
+ "session_refresh_failed": {
+ "en": "Session refresh failed",
+ "da": "Kunne ikke opdatere session"
+ },
+ "join_failed": {
+ "en": "Join failed",
+ "da": "Join fejlede"
+ },
+ "lie_submit_failed": {
+ "en": "Lie submit failed",
+ "da": "Løgn-fejl"
+ },
+ "guess_submit_failed": {
+ "en": "Guess submit failed",
+ "da": "Gætte-fejl"
+ }
+ }
+ },
+ "capabilities": {
+ "client_has_no_audio_output": true
}
},
"backend": {
@@ -90,4 +314,4 @@
}
}
}
-}
+}
\ No newline at end of file