feat(i18n): share lobby message catalog across frontend/backend
This commit is contained in:
31
frontend/src/spa/lobby-i18n.ts
Normal file
31
frontend/src/spa/lobby-i18n.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import lobbyCatalog from '../../../shared/i18n/lobby.json';
|
||||
|
||||
type FrontendErrorKey = keyof typeof lobbyCatalog.frontend.errors;
|
||||
|
||||
const frontendErrors = lobbyCatalog.frontend.errors;
|
||||
|
||||
const apiErrorMap: Record<string, FrontendErrorKey> = {
|
||||
session_code_required: 'session_code_required',
|
||||
session_not_found: 'session_not_found',
|
||||
nickname_invalid: 'nickname_invalid',
|
||||
nickname_taken: 'nickname_taken'
|
||||
};
|
||||
|
||||
export function lobbyMessage(key: FrontendErrorKey): string {
|
||||
return frontendErrors[key] ?? frontendErrors.unknown;
|
||||
}
|
||||
|
||||
export function lobbyMessageFromApiPayload(payload: unknown, fallbackKey: FrontendErrorKey): string {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return lobbyMessage(fallbackKey);
|
||||
}
|
||||
|
||||
const record = payload as Record<string, unknown>;
|
||||
const code = typeof record.error_code === 'string' ? record.error_code : '';
|
||||
const mappedKey = apiErrorMap[code];
|
||||
if (!mappedKey) {
|
||||
return lobbyMessage(fallbackKey);
|
||||
}
|
||||
|
||||
return lobbyMessage(mappedKey);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type SessionContextStore as PersistedSessionContextStore
|
||||
} from './session-context-store';
|
||||
import { deriveGameplayPhase, type GameplayPhase } from './gameplay-phase-machine';
|
||||
import { lobbyMessage, lobbyMessageFromApiPayload } from './lobby-i18n';
|
||||
|
||||
export type AsyncState = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
@@ -57,7 +58,7 @@ export function createVerticalSliceController(
|
||||
|
||||
if (!state.sessionCode) {
|
||||
state.loadingSession = false;
|
||||
state.errorMessage = 'Session-kode mangler.';
|
||||
state.errorMessage = lobbyMessage('session_code_required');
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
@@ -65,7 +66,7 @@ export function createVerticalSliceController(
|
||||
state.loadingSession = false;
|
||||
|
||||
if (!result.ok) {
|
||||
state.errorMessage = 'Kunne ikke hente lobby-status.';
|
||||
state.errorMessage = lobbyMessageFromApiPayload(result.error.payload, 'session_fetch_failed');
|
||||
state.gameplayPhase = null;
|
||||
return { ...state };
|
||||
}
|
||||
@@ -92,7 +93,7 @@ export function createVerticalSliceController(
|
||||
const join = await api.joinSession({ code: requestCode, nickname });
|
||||
if (!join.ok) {
|
||||
state.joinState = 'error';
|
||||
state.errorMessage = 'Join fejlede. Tjek kode eller nickname og prøv igen.';
|
||||
state.errorMessage = lobbyMessageFromApiPayload(join.error.payload, 'join_failed');
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
@@ -119,14 +120,14 @@ export function createVerticalSliceController(
|
||||
|
||||
if (!codeToUse) {
|
||||
state.startRoundState = 'error';
|
||||
state.errorMessage = 'Session-kode mangler.';
|
||||
state.errorMessage = lobbyMessage('session_code_required');
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
const start = await api.startRound(codeToUse, { category_slug: categorySlug });
|
||||
if (!start.ok) {
|
||||
state.startRoundState = 'error';
|
||||
state.errorMessage = 'Kunne ikke starte runden. Opdatér lobbyen og prøv igen.';
|
||||
state.errorMessage = lobbyMessageFromApiPayload(start.error.payload, 'start_round_failed');
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('vertical slice controller: lobby -> join -> start round', () => {
|
||||
joinSession: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
error: { kind: 'http', status: 404, message: 'HTTP 404', payload: { error: 'Session not found' } }
|
||||
error: { kind: 'http', status: 404, message: 'HTTP 404', payload: { error: 'Session not found', error_code: 'session_not_found' } }
|
||||
})
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('vertical slice controller: lobby -> join -> start round', () => {
|
||||
|
||||
const state = controller.getState();
|
||||
expect(state.joinState).toBe('error');
|
||||
expect(state.errorMessage).toContain('Join fejlede');
|
||||
expect(state.errorMessage).toBe('Session code is invalid or the session no longer exists.');
|
||||
});
|
||||
|
||||
it('surfaces a friendly error when round start fails', async () => {
|
||||
@@ -178,7 +178,7 @@ describe('vertical slice controller: lobby -> join -> start round', () => {
|
||||
startRound: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
error: { kind: 'http', status: 400, message: 'HTTP 400', payload: { error: 'Round can only be started from lobby' } }
|
||||
error: { kind: 'http', status: 400, message: 'HTTP 400', payload: { error: 'Round can only be started from lobby', error_code: 'round_start_invalid_phase' } }
|
||||
})
|
||||
});
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('vertical slice controller: lobby -> join -> start round', () => {
|
||||
|
||||
const state = controller.getState();
|
||||
expect(state.startRoundState).toBe('error');
|
||||
expect(state.errorMessage).toContain('Kunne ikke starte runden');
|
||||
expect(state.errorMessage).toBe('Could not start round. Refresh the lobby and try again.');
|
||||
});
|
||||
|
||||
it('shows local validation error and avoids API call when hydrating without any session code', async () => {
|
||||
@@ -197,7 +197,7 @@ describe('vertical slice controller: lobby -> join -> start round', () => {
|
||||
await controller.hydrateLobby(' ');
|
||||
|
||||
const state = controller.getState();
|
||||
expect(state.errorMessage).toBe('Session-kode mangler.');
|
||||
expect(state.errorMessage).toBe('Session code is required.');
|
||||
expect(state.loadingSession).toBe(false);
|
||||
expect(api.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -210,7 +210,7 @@ describe('vertical slice controller: lobby -> join -> start round', () => {
|
||||
|
||||
const state = controller.getState();
|
||||
expect(state.startRoundState).toBe('error');
|
||||
expect(state.errorMessage).toBe('Session-kode mangler.');
|
||||
expect(state.errorMessage).toBe('Session code is required.');
|
||||
expect(api.startRound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
"include": ["src", "tests", "../shared/i18n/*.json"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user