From 5586e6c6721a5a02775782ab354ab6e2e5454456 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Fri, 21 Aug 2026 21:29:10 +0200 Subject: [PATCH] feat(web): translate error messages surfaced from API error codes to English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translates the five client-side error-message maps (world, hunting, combat, inventory, local-location stores) that re-translate already-English API error codes back to display text, per design doc §4. Also fixes four component specs that hard-coded the same German strings when asserting rendered error text (combat-page, hunt-page, location-interaction-panel, location-page). --- .../combat-page/combat-page.component.spec.ts | 4 ++-- .../app/features/combat/combat.store.spec.ts | 6 +++--- .../src/app/features/combat/combat.store.ts | 20 +++++++++---------- .../hunt-page/hunt-page.component.spec.ts | 12 +++++------ .../features/hunting/hunting.store.spec.ts | 10 +++++----- .../src/app/features/hunting/hunting.store.ts | 10 +++++----- .../inventory/inventory.store.spec.ts | 2 +- .../app/features/inventory/inventory.store.ts | 12 +++++------ .../world/local-location.store.spec.ts | 4 ++-- .../features/world/local-location.store.ts | 6 +++--- ...cation-interaction-panel.component.spec.ts | 4 ++-- .../location-page.component.spec.ts | 2 +- .../app/features/world/world.store.spec.ts | 4 ++-- .../web/src/app/features/world/world.store.ts | 10 +++++----- 14 files changed, 53 insertions(+), 53 deletions(-) diff --git a/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts b/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts index 1ebf01f..3752029 100644 --- a/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts +++ b/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts @@ -510,12 +510,12 @@ describe('CombatPageComponent', () => { it('shows an error and retries loading the combat', async () => { const fixture = await setup(null); - combatStore.error.set('Dieser Kampf wurde nicht gefunden.'); + combatStore.error.set('This combat could not be found.'); fixture.detectChanges(); const element = fixture.nativeElement as HTMLElement; expect(element.querySelector('[role="alert"]')?.textContent).toContain( - 'Dieser Kampf wurde nicht gefunden.', + 'This combat could not be found.', ); element.querySelector('[data-combat-retry]')?.click(); diff --git a/apps/web/src/app/features/combat/combat.store.spec.ts b/apps/web/src/app/features/combat/combat.store.spec.ts index 69a2336..0823d73 100644 --- a/apps/web/src/app/features/combat/combat.store.spec.ts +++ b/apps/web/src/app/features/combat/combat.store.spec.ts @@ -80,7 +80,7 @@ describe('CombatStore', () => { await store.startCombat('encounter-1'); expect(store.combat()).toBeNull(); - expect(store.error()).toBe('Du befindest dich bereits in einem Kampf.'); + expect(store.error()).toBe("You're already in a combat."); expect(store.errorCode()).toBe('COMBAT_ALREADY_ACTIVE'); }); @@ -97,7 +97,7 @@ describe('CombatStore', () => { await store.startCombat('encounter-1'); - expect(store.error()).toBe('Du bist zu schwer verwundet, um zu kämpfen. Warte, bis du dich erholt hast.'); + expect(store.error()).toBe("You're too badly wounded to fight. Wait until you've recovered."); }); it('loads the running combat and clears the error that sent us looking for it', async () => { @@ -150,7 +150,7 @@ describe('CombatStore', () => { await store.loadCombat('unknown'); - expect(store.error()).toBe('Dieser Kampf wurde nicht gefunden.'); + expect(store.error()).toBe('This combat could not be found.'); }); it('sends only the ATTACK action and replaces combat with the server response', async () => { diff --git a/apps/web/src/app/features/combat/combat.store.ts b/apps/web/src/app/features/combat/combat.store.ts index a9e45b5..d645a9b 100644 --- a/apps/web/src/app/features/combat/combat.store.ts +++ b/apps/web/src/app/features/combat/combat.store.ts @@ -4,20 +4,20 @@ import { firstValueFrom } from 'rxjs'; import { Combat, CombatAction } from '../../core/api/game-api.models'; import { GameApiService } from '../../core/api/game-api.service'; -const GENERIC_ERROR_MESSAGE = 'Der Kampf konnte nicht geladen werden.'; +const GENERIC_ERROR_MESSAGE = 'Could not load the combat.'; // Mirrors the combat error codes returned by the combat endpoints. // Unknown/missing codes fall back to `GENERIC_ERROR_MESSAGE`. const COMBAT_ERROR_MESSAGES: Readonly> = { - HUNT_ENCOUNTER_NOT_FOUND: 'Diese Begegnung wurde nicht gefunden.', - HUNT_ENCOUNTER_ALREADY_CONSUMED: 'Diese Begegnung wurde bereits genutzt.', - INVALID_HUNT_ENCOUNTER: 'Diese Begegnung ist nicht mehr gültig.', - CHARACTER_TRAVELLING: 'Du kannst nicht kämpfen, während du unterwegs bist.', - CHARACTER_TOO_WOUNDED: 'Du bist zu schwer verwundet, um zu kämpfen. Warte, bis du dich erholt hast.', - COMBAT_ALREADY_ACTIVE: 'Du befindest dich bereits in einem Kampf.', - COMBAT_NOT_FOUND: 'Dieser Kampf wurde nicht gefunden.', - COMBAT_ALREADY_FINISHED: 'Dieser Kampf ist bereits beendet.', - COMBAT_NO_POTIONS_REMAINING: 'Du hast keine Tränke mehr.', + HUNT_ENCOUNTER_NOT_FOUND: 'This encounter could not be found.', + HUNT_ENCOUNTER_ALREADY_CONSUMED: 'This encounter has already been used.', + INVALID_HUNT_ENCOUNTER: 'This encounter is no longer valid.', + CHARACTER_TRAVELLING: "You can't fight while travelling.", + CHARACTER_TOO_WOUNDED: "You're too badly wounded to fight. Wait until you've recovered.", + COMBAT_ALREADY_ACTIVE: "You're already in a combat.", + COMBAT_NOT_FOUND: 'This combat could not be found.', + COMBAT_ALREADY_FINISHED: 'This combat has already ended.', + COMBAT_NO_POTIONS_REMAINING: 'You have no potions left.', }; @Injectable({ providedIn: 'root' }) diff --git a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts index d3c76c8..c99d887 100644 --- a/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts +++ b/apps/web/src/app/features/hunting/hunt-page/hunt-page.component.spec.ts @@ -228,7 +228,7 @@ describe('HuntPageComponent', () => { const fixture = await setup(burnedRoad, threeEncounterHunt); combatStore.startCombat.mockImplementation(async () => { combatStore.errorCode.set('COMBAT_ALREADY_ACTIVE'); - combatStore.error.set('Du befindest dich bereits in einem Kampf.'); + combatStore.error.set("You're already in a combat."); }); combatStore.loadActiveCombat.mockResolvedValue({ ...startedCombat, id: 'combat-running' }); const element = fixture.nativeElement as HTMLElement; @@ -249,7 +249,7 @@ describe('HuntPageComponent', () => { const fixture = await setup(burnedRoad, threeEncounterHunt); combatStore.startCombat.mockImplementation(async () => { combatStore.errorCode.set('HUNT_ENCOUNTER_ALREADY_CONSUMED'); - combatStore.error.set('Diese Begegnung wurde bereits genutzt.'); + combatStore.error.set('This encounter has already been used.'); }); const element = fixture.nativeElement as HTMLElement; @@ -267,12 +267,12 @@ describe('HuntPageComponent', () => { it('shows a combat-start error and dismisses it', async () => { const fixture = await setup(burnedRoad, threeEncounterHunt); - combatStore.error.set('Du befindest dich bereits in einem Kampf.'); + combatStore.error.set("You're already in a combat."); fixture.detectChanges(); const element = fixture.nativeElement as HTMLElement; const alerts = Array.from(element.querySelectorAll('[role="alert"]')); - expect(alerts.some((alert) => alert.textContent?.includes('Du befindest dich bereits in einem Kampf.'))).toBe( + expect(alerts.some((alert) => alert.textContent?.includes("You're already in a combat."))).toBe( true, ); @@ -380,12 +380,12 @@ describe('HuntPageComponent', () => { it('displays a hunting error and retries via startHunt when there is no current hunt', async () => { const fixture = await setup(burnedRoad); - huntingStore.error.set('An diesem Ort gibt es keine Jagdgebiete.'); + huntingStore.error.set("There's no hunting ground at this location."); fixture.detectChanges(); const element = fixture.nativeElement as HTMLElement; expect(element.querySelector('[role="alert"]')?.textContent).toContain( - 'An diesem Ort gibt es keine Jagdgebiete.', + "There's no hunting ground at this location.", ); element.querySelector('[data-hunt-retry]')?.click(); diff --git a/apps/web/src/app/features/hunting/hunting.store.spec.ts b/apps/web/src/app/features/hunting/hunting.store.spec.ts index de5bc06..1a3941a 100644 --- a/apps/web/src/app/features/hunting/hunting.store.spec.ts +++ b/apps/web/src/app/features/hunting/hunting.store.spec.ts @@ -133,7 +133,7 @@ describe('HuntingStore', () => { await store.startHunt(); - expect(store.error()).toBe('Dein Charakter konnte nicht gefunden werden.'); + expect(store.error()).toBe('Your character could not be found.'); }); it('maps CHARACTER_TRAVELLING to its German message', async () => { @@ -149,7 +149,7 @@ describe('HuntingStore', () => { await store.startHunt(); - expect(store.error()).toBe('Du kannst nicht jagen, während du unterwegs bist.'); + expect(store.error()).toBe("You can't hunt while travelling."); }); it('maps HUNTING_NOT_AVAILABLE to its German message', async () => { @@ -165,7 +165,7 @@ describe('HuntingStore', () => { await store.startHunt(); - expect(store.error()).toBe('An diesem Ort gibt es keine Jagdgebiete.'); + expect(store.error()).toBe("There's no hunting ground at this location."); }); it('maps NO_HUNT_ENCOUNTERS_AVAILABLE to its German message', async () => { @@ -185,7 +185,7 @@ describe('HuntingStore', () => { await store.startHunt(); - expect(store.error()).toBe('Aktuell sind hier keine Gegner zu finden.'); + expect(store.error()).toBe('No enemies can currently be found here.'); }); it('falls back to the generic message for an HttpErrorResponse with no known code', async () => { @@ -201,7 +201,7 @@ describe('HuntingStore', () => { await store.startHunt(); - expect(store.error()).toBe('Weltzustand konnte nicht geladen werden.'); + expect(store.error()).toBe('Could not load world state.'); }); it('uses the message of a genuine non-HTTP Error', async () => { diff --git a/apps/web/src/app/features/hunting/hunting.store.ts b/apps/web/src/app/features/hunting/hunting.store.ts index a264883..3b1cdbd 100644 --- a/apps/web/src/app/features/hunting/hunting.store.ts +++ b/apps/web/src/app/features/hunting/hunting.store.ts @@ -4,15 +4,15 @@ import { firstValueFrom } from 'rxjs'; import { HuntResult } from '../../core/api/game-api.models'; import { GameApiService } from '../../core/api/game-api.service'; -const GENERIC_ERROR_MESSAGE = 'Weltzustand konnte nicht geladen werden.'; +const GENERIC_ERROR_MESSAGE = 'Could not load world state.'; // Mirrors the hunt error codes returned by `POST /api/hunts`. // Unknown/missing codes fall back to `GENERIC_ERROR_MESSAGE`. const HUNT_ERROR_MESSAGES: Readonly> = { - CHARACTER_NOT_FOUND: 'Dein Charakter konnte nicht gefunden werden.', - CHARACTER_TRAVELLING: 'Du kannst nicht jagen, während du unterwegs bist.', - HUNTING_NOT_AVAILABLE: 'An diesem Ort gibt es keine Jagdgebiete.', - NO_HUNT_ENCOUNTERS_AVAILABLE: 'Aktuell sind hier keine Gegner zu finden.', + CHARACTER_NOT_FOUND: 'Your character could not be found.', + CHARACTER_TRAVELLING: "You can't hunt while travelling.", + HUNTING_NOT_AVAILABLE: "There's no hunting ground at this location.", + NO_HUNT_ENCOUNTERS_AVAILABLE: 'No enemies can currently be found here.', }; @Injectable({ providedIn: 'root' }) diff --git a/apps/web/src/app/features/inventory/inventory.store.spec.ts b/apps/web/src/app/features/inventory/inventory.store.spec.ts index 1e543d1..887a075 100644 --- a/apps/web/src/app/features/inventory/inventory.store.spec.ts +++ b/apps/web/src/app/features/inventory/inventory.store.spec.ts @@ -140,6 +140,6 @@ describe('InventoryStore', () => { await store.equip('item-blade'); - expect(store.error()).toBe('Dieser Gegenstand kann nicht ausgerüstet werden.'); + expect(store.error()).toBe("This item can't be equipped."); }); }); diff --git a/apps/web/src/app/features/inventory/inventory.store.ts b/apps/web/src/app/features/inventory/inventory.store.ts index dd7a381..1f71f54 100644 --- a/apps/web/src/app/features/inventory/inventory.store.ts +++ b/apps/web/src/app/features/inventory/inventory.store.ts @@ -5,15 +5,15 @@ import { EquipmentResponse, InventoryItem, InventoryResponse } from '../../core/ import { GameApiService } from '../../core/api/game-api.service'; import { WorldStore } from '../world/world.store'; -const GENERIC_ERROR_MESSAGE = 'Inventar konnte nicht geladen werden.'; +const GENERIC_ERROR_MESSAGE = 'Could not load the inventory.'; // Mirrors `EquipmentErrorCode` in `apps/api/src/equipment/equipment.errors.ts`. const EQUIPMENT_ERROR_MESSAGES: Readonly> = { - CHARACTER_ITEM_NOT_FOUND: 'Dieser Gegenstand konnte nicht gefunden werden.', - ITEM_NOT_OWNED: 'Dieser Gegenstand gehört dir nicht.', - ITEM_NOT_EQUIPPABLE: 'Dieser Gegenstand kann nicht ausgerüstet werden.', - INVALID_EQUIPMENT_SLOT: 'Dieser Ausrüstungsplatz ist ungültig.', - CHARACTER_IN_COMBAT: 'Ausrüstung kann während eines Kampfes nicht geändert werden.', + CHARACTER_ITEM_NOT_FOUND: 'This item could not be found.', + ITEM_NOT_OWNED: "This item doesn't belong to you.", + ITEM_NOT_EQUIPPABLE: "This item can't be equipped.", + INVALID_EQUIPMENT_SLOT: 'This equipment slot is invalid.', + CHARACTER_IN_COMBAT: "Equipment can't be changed during combat.", }; @Injectable({ providedIn: 'root' }) diff --git a/apps/web/src/app/features/world/local-location.store.spec.ts b/apps/web/src/app/features/world/local-location.store.spec.ts index 7432a01..6be9c61 100644 --- a/apps/web/src/app/features/world/local-location.store.spec.ts +++ b/apps/web/src/app/features/world/local-location.store.spec.ts @@ -93,7 +93,7 @@ describe('LocalLocationStore', () => { await store.runInteraction('inspect-tracks'); - expect(store.interactionError()).toBe('Hier gibt es dazu nichts zu entdecken.'); + expect(store.interactionError()).toBe("There's nothing to discover here."); expect(store.interactionResult()).toBeNull(); expect(store.interactionOpen()).toBe(true); }); @@ -107,7 +107,7 @@ describe('LocalLocationStore', () => { await store.runInteraction('inspect-tracks'); - expect(store.interactionError()).toBe('Diese Handlung ist gerade nicht möglich.'); + expect(store.interactionError()).toBe("This action isn't possible right now."); }); it('closes the panel without clearing the location', async () => { diff --git a/apps/web/src/app/features/world/local-location.store.ts b/apps/web/src/app/features/world/local-location.store.ts index 10428f2..a47e999 100644 --- a/apps/web/src/app/features/world/local-location.store.ts +++ b/apps/web/src/app/features/world/local-location.store.ts @@ -9,12 +9,12 @@ import { import { GameApiService } from '../../core/api/game-api.service'; import { WorldStore } from './world.store'; -const GENERIC_INTERACTION_ERROR = 'Diese Handlung ist gerade nicht möglich.'; +const GENERIC_INTERACTION_ERROR = "This action isn't possible right now."; // Mirrors the codes in `apps/api/src/world/world.errors.ts`. const INTERACTION_ERROR_MESSAGES: Readonly> = { - LOCATION_INTERACTION_UNAVAILABLE: 'Hier gibt es dazu nichts zu entdecken.', - CHARACTER_NOT_FOUND: 'Dein Charakter konnte nicht gefunden werden.', + LOCATION_INTERACTION_UNAVAILABLE: "There's nothing to discover here.", + CHARACTER_NOT_FOUND: 'Your character could not be found.', }; /** diff --git a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts index 5af9cc4..4a0b040 100644 --- a/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts +++ b/apps/web/src/app/features/world/location-interaction-panel/location-interaction-panel.component.spec.ts @@ -61,10 +61,10 @@ describe('LocationInteractionPanelComponent', () => { }); it('reports a rejected interaction in place instead of navigating away', async () => { - const { element } = await setup({ error: 'Hier gibt es dazu nichts zu entdecken.' }); + const { element } = await setup({ error: "There's nothing to discover here." }); expect(element.querySelector('[role="alert"]')?.textContent).toContain( - 'Hier gibt es dazu nichts zu entdecken.', + "There's nothing to discover here.", ); }); diff --git a/apps/web/src/app/features/world/location-page/location-page.component.spec.ts b/apps/web/src/app/features/world/location-page/location-page.component.spec.ts index 555f3c4..8335f40 100644 --- a/apps/web/src/app/features/world/location-page/location-page.component.spec.ts +++ b/apps/web/src/app/features/world/location-page/location-page.component.spec.ts @@ -220,7 +220,7 @@ describe('LocationPageComponent', () => { it('offers a retry when the location could not be loaded', async () => { const { fixture, element } = await setup(null); - error.set('Weltzustand konnte nicht geladen werden.'); + error.set('Could not load world state.'); fixture.detectChanges(); expect(element.querySelector('[role="alert"]')?.textContent).toContain( diff --git a/apps/web/src/app/features/world/world.store.spec.ts b/apps/web/src/app/features/world/world.store.spec.ts index 8a30353..19e2856 100644 --- a/apps/web/src/app/features/world/world.store.spec.ts +++ b/apps/web/src/app/features/world/world.store.spec.ts @@ -284,7 +284,7 @@ describe('WorldStore', () => { await store.startTravel(); - expect(store.error()).toBe('Du befindest dich bereits auf Reisen.'); + expect(store.error()).toBe("You're already travelling."); pendingResync.next({ status: 'IDLE' }); pendingResync.complete(); @@ -307,7 +307,7 @@ describe('WorldStore', () => { await store.startTravel(); - expect(store.error()).toBe('Weltzustand konnte nicht geladen werden.'); + expect(store.error()).toBe('Could not load world state.'); pendingResync.next({ status: 'IDLE' }); pendingResync.complete(); diff --git a/apps/web/src/app/features/world/world.store.ts b/apps/web/src/app/features/world/world.store.ts index 5bb0588..7ef224a 100644 --- a/apps/web/src/app/features/world/world.store.ts +++ b/apps/web/src/app/features/world/world.store.ts @@ -10,15 +10,15 @@ import { } from '../../core/api/game-api.models'; import { GameApiService } from '../../core/api/game-api.service'; -const GENERIC_ERROR_MESSAGE = 'Weltzustand konnte nicht geladen werden.'; +const GENERIC_ERROR_MESSAGE = 'Could not load world state.'; // Mirrors the `TravelErrorCode` union in `apps/api/src/travel/travel.errors.ts`. // Unknown/missing codes fall back to `GENERIC_ERROR_MESSAGE`. const TRAVEL_ERROR_MESSAGES: Readonly> = { - TRAVEL_ALREADY_ACTIVE: 'Du befindest dich bereits auf Reisen.', - INVALID_TRAVEL_TARGET: 'Dieses Ziel ist von hier aus nicht erreichbar.', - CHARACTER_NOT_FOUND: 'Dein Charakter konnte nicht gefunden werden.', - TRAVEL_STATE_INVALID: 'Der Reisezustand ist ungültig. Bitte lade die Seite neu.', + TRAVEL_ALREADY_ACTIVE: "You're already travelling.", + INVALID_TRAVEL_TARGET: "This destination isn't reachable from here.", + CHARACTER_NOT_FOUND: 'Your character could not be found.', + TRAVEL_STATE_INVALID: 'Travel state is invalid. Please reload the page.', }; @Injectable({ providedIn: 'root' })