feat(web): translate hunt screen and encounter card copy to English

Translates every static German string in hunt-page.component.html and
encounter-card.component.html/ts: headings, buttons, aria-labels,
loading/error text, the monster level label, and the encounter
action-label logic (Defeated/In Combat/Attack).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACkMEDYiwtcfchqKkiUJNX
This commit is contained in:
Bastian Wagner
2026-08-21 22:11:50 +02:00
parent 1c0a13475c
commit 14d26d06a4
5 changed files with 36 additions and 39 deletions

View File

@@ -18,7 +18,7 @@
<img <img
class="encounter-card__defeated-mark" class="encounter-card__defeated-mark"
[src]="defeatedMark" [src]="defeatedMark"
alt="Besiegt" alt="Defeated"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
/> />
@@ -28,7 +28,7 @@
<h3 class="encounter-card__name">{{ encounter.monster.name }}</h3> <h3 class="encounter-card__name">{{ encounter.monster.name }}</h3>
<div class="encounter-card__meta"> <div class="encounter-card__meta">
<span class="encounter-card__level">Stufe {{ encounter.monster.level }}</span> <span class="encounter-card__level">Level {{ encounter.monster.level }}</span>
<app-danger-badge class="encounter-card__danger" [rating]="encounter.dangerRating" /> <app-danger-badge class="encounter-card__danger" [rating]="encounter.dangerRating" />
</div> </div>

View File

@@ -46,7 +46,7 @@ describe('EncounterCardComponent', () => {
const artwork = element.querySelector('.encounter-card__artwork'); const artwork = element.querySelector('.encounter-card__artwork');
expect(element.textContent).toContain('Dämmerwolf'); expect(element.textContent).toContain('Dämmerwolf');
expect(element.textContent).toContain('Stufe 3'); expect(element.textContent).toContain('Level 3');
expect(element.textContent).toContain('Suitable'); expect(element.textContent).toContain('Suitable');
expect(artwork?.getAttribute('src')).toBe('/images/enemies/Dawnwolf.png'); expect(artwork?.getAttribute('src')).toBe('/images/enemies/Dawnwolf.png');
expect(artwork?.getAttribute('alt')).toBe('Dämmerwolf'); expect(artwork?.getAttribute('alt')).toBe('Dämmerwolf');
@@ -72,7 +72,7 @@ describe('EncounterCardComponent', () => {
expect(element.querySelector('.encounter-card__crest-icon')).toBeNull(); expect(element.querySelector('.encounter-card__crest-icon')).toBeNull();
}); });
it('emits the encounter id (never the monster key) when Angreifen is clicked', () => { it('emits the encounter id (never the monster key) when Attack is clicked', () => {
const fixture = TestBed.createComponent(EncounterCardComponent); const fixture = TestBed.createComponent(EncounterCardComponent);
fixture.componentRef.setInput('encounter', dawnwolfEncounter); fixture.componentRef.setInput('encounter', dawnwolfEncounter);
fixture.detectChanges(); fixture.detectChanges();
@@ -82,7 +82,7 @@ describe('EncounterCardComponent', () => {
const button = Array.from(fixture.nativeElement.querySelectorAll('button')).find( const button = Array.from(fixture.nativeElement.querySelectorAll('button')).find(
(candidate): candidate is HTMLButtonElement => (candidate): candidate is HTMLButtonElement =>
candidate instanceof HTMLButtonElement && candidate.textContent?.trim() === 'Angreifen', candidate instanceof HTMLButtonElement && candidate.textContent?.trim() === 'Attack',
); );
button?.click(); button?.click();
@@ -109,7 +109,7 @@ describe('EncounterCardComponent', () => {
const mark = element.querySelector('.encounter-card__defeated-mark'); const mark = element.querySelector('.encounter-card__defeated-mark');
expect(mark).not.toBeNull(); expect(mark).not.toBeNull();
expect(mark?.getAttribute('alt')).toBe('Besiegt'); expect(mark?.getAttribute('alt')).toBe('Defeated');
expect( expect(
element.querySelector<HTMLButtonElement>('.encounter-card__attack')?.disabled, element.querySelector<HTMLButtonElement>('.encounter-card__attack')?.disabled,
).toBe(true); ).toBe(true);

View File

@@ -29,10 +29,10 @@ export class EncounterCardComponent {
protected get actionLabel(): string { protected get actionLabel(): string {
if (this.defeated) { if (this.defeated) {
return 'Besiegt'; return 'Defeated';
} }
return this.encounter.status === 'IN_PROGRESS' ? 'Im Kampf' : 'Angreifen'; return this.encounter.status === 'IN_PROGRESS' ? 'In Combat' : 'Attack';
} }
protected onAttack(): void { protected onAttack(): void {

View File

@@ -1,17 +1,14 @@
<section class="hunt-page" aria-label="Jagd"> <section class="hunt-page" aria-label="Hunt">
@if (worldStore.currentLocation(); as location) { @if (worldStore.currentLocation(); as location) {
@if (!location.huntingEnabled) { @if (!location.huntingEnabled) {
<section class="hunt-page__unavailable"> <section class="hunt-page__unavailable">
<h2>Keine Jagd verfügbar</h2> <h2>No Hunt Available</h2>
<p> <p>No hunting ground is available here right now.</p>
Am Südtor von Graufurt gibt es keine regulären Jagdgebiete. Reise in ein gefährlicheres <button type="button" data-hunt-to-location (click)="goToLocation()">Back to Location</button>
Gebiet, um nach Gegnern zu suchen.
</p>
<button type="button" data-hunt-to-location (click)="goToLocation()">Zurück zum Ort</button>
</section> </section>
} @else if (huntingStore.currentHunt(); as hunt) { } @else if (huntingStore.currentHunt(); as hunt) {
<section class="hunt-page__results" [attr.aria-label]="'Begegnungen bei ' + location.name"> <section class="hunt-page__results" [attr.aria-label]="'Encounters at ' + location.name">
<p class="hunt-page__results-heading">{{ location.name }} — Begegnungen</p> <p class="hunt-page__results-heading">{{ location.name }} — Encounters</p>
<div class="hunt-page__encounters"> <div class="hunt-page__encounters">
@for (encounter of huntingStore.encounters(); track encounter.id) { @for (encounter of huntingStore.encounters(); track encounter.id) {
@@ -26,9 +23,9 @@
[disabled]="huntingStore.loading()" [disabled]="huntingStore.loading()"
(click)="refreshHunt()" (click)="refreshHunt()"
> >
Neu suchen Search Again
</button> </button>
<button type="button" data-hunt-to-location (click)="goToLocation()">Zurück zum Ort</button> <button type="button" data-hunt-to-location (click)="goToLocation()">Back to Location</button>
</div> </div>
</section> </section>
} @else { } @else {
@@ -36,7 +33,7 @@
<h2>{{ location.name }}</h2> <h2>{{ location.name }}</h2>
<p>{{ location.description }}</p> <p>{{ location.description }}</p>
<p class="hunt-page__hint"> <p class="hunt-page__hint">
Durchsuche die Umgebung nach Spuren und Gegnern, bevor du dich in den Kampf wagst. Search the surroundings for tracks and enemies before venturing into combat.
</p> </p>
<button <button
type="button" type="button"
@@ -44,31 +41,31 @@
[disabled]="huntingStore.loading()" [disabled]="huntingStore.loading()"
(click)="startHunt()" (click)="startHunt()"
> >
Jagd beginnen Begin Hunt
</button> </button>
</section> </section>
} }
@if (huntingStore.loading()) { @if (huntingStore.loading()) {
<p class="hunt-page__loading" role="status">Du suchst nach Spuren...</p> <p class="hunt-page__loading" role="status">You search for tracks...</p>
} }
} @else { } @else {
<section class="hunt-page__empty" aria-live="polite"> <section class="hunt-page__empty" aria-live="polite">
<p>Jagdgebiet wird vorbereitet.</p> <p>Preparing hunting ground.</p>
</section> </section>
} }
@if (huntingStore.error(); as error) { @if (huntingStore.error(); as error) {
<section class="hunt-page__error" role="alert"> <section class="hunt-page__error" role="alert">
<p>{{ error }}</p> <p>{{ error }}</p>
<button type="button" data-hunt-retry (click)="retry()">Erneut versuchen</button> <button type="button" data-hunt-retry (click)="retry()">Retry</button>
</section> </section>
} }
@if (combatStore.error(); as combatError) { @if (combatStore.error(); as combatError) {
<section class="hunt-page__error" role="alert"> <section class="hunt-page__error" role="alert">
<p>{{ combatError }}</p> <p>{{ combatError }}</p>
<button type="button" data-hunt-combat-dismiss (click)="dismissCombatError()">Schließen</button> <button type="button" data-hunt-combat-dismiss (click)="dismissCombatError()">Close</button>
</section> </section>
} }
</section> </section>

View File

@@ -130,26 +130,26 @@ describe('HuntPageComponent', () => {
return fixture; return fixture;
} }
it('shows the hunting-unavailable state at the Südtor, with no Jagd beginnen button, and a way back to the location', async () => { it('shows the hunting-unavailable state at the Südtor, with no Begin Hunt button, and a way back to the location', async () => {
const fixture = await setup(southGate); const fixture = await setup(southGate);
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Keine Jagd verfügbar'); expect(element.textContent).toContain('No Hunt Available');
expect(element.textContent).toContain('Am Südtor von Graufurt gibt es keine regulären Jagdgebiete.'); expect(element.textContent).toContain('No hunting ground is available here right now.');
expect( expect(
Array.from(element.querySelectorAll('button')).some( Array.from(element.querySelectorAll('button')).some(
(button) => button.textContent?.trim() === 'Jagd beginnen', (button) => button.textContent?.trim() === 'Begin Hunt',
), ),
).toBe(false); ).toBe(false);
const backButton = element.querySelector<HTMLButtonElement>('[data-hunt-to-location]'); const backButton = element.querySelector<HTMLButtonElement>('[data-hunt-to-location]');
expect(backButton?.textContent?.trim()).toBe('Zurück zum Ort'); expect(backButton?.textContent?.trim()).toBe('Back to Location');
backButton?.click(); backButton?.click();
expect(router.navigate).toHaveBeenCalledWith(['/location']); expect(router.navigate).toHaveBeenCalledWith(['/location']);
}); });
it('calls startHunt when Jagd beginnen is clicked at a hunting-enabled location', async () => { it('calls startHunt when Begin Hunt is clicked at a hunting-enabled location', async () => {
const fixture = await setup(burnedRoad); const fixture = await setup(burnedRoad);
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
@@ -175,11 +175,11 @@ describe('HuntPageComponent', () => {
'.encounter-card__artwork[src="/images/combat/sprites/road-bandit-620.png"]', '.encounter-card__artwork[src="/images/combat/sprites/road-bandit-620.png"]',
).length, ).length,
).toBe(1); ).toBe(1);
expect(element.textContent).toContain('Stufe 1'); expect(element.textContent).toContain('Level 1');
expect(element.textContent).toContain('Stufe 3'); expect(element.textContent).toContain('Level 3');
}); });
it('calls refreshHunt when Neu suchen is clicked', async () => { it('calls refreshHunt when Search Again is clicked', async () => {
const fixture = await setup(burnedRoad, threeEncounterHunt); const fixture = await setup(burnedRoad, threeEncounterHunt);
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
@@ -196,7 +196,7 @@ describe('HuntPageComponent', () => {
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
const attackButtons = Array.from(element.querySelectorAll('button')).filter( const attackButtons = Array.from(element.querySelectorAll('button')).filter(
(button) => button.textContent?.trim() === 'Angreifen', (button) => button.textContent?.trim() === 'Attack',
); );
expect(attackButtons.length).toBe(3); expect(attackButtons.length).toBe(3);
@@ -214,7 +214,7 @@ describe('HuntPageComponent', () => {
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
const attackButtons = Array.from(element.querySelectorAll('button')).filter( const attackButtons = Array.from(element.querySelectorAll('button')).filter(
(button) => button.textContent?.trim() === 'Angreifen', (button) => button.textContent?.trim() === 'Attack',
); );
attackButtons[0].click(); attackButtons[0].click();
await Promise.resolve(); await Promise.resolve();
@@ -234,7 +234,7 @@ describe('HuntPageComponent', () => {
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
const attackButtons = Array.from(element.querySelectorAll('button')).filter( const attackButtons = Array.from(element.querySelectorAll('button')).filter(
(button) => button.textContent?.trim() === 'Angreifen', (button) => button.textContent?.trim() === 'Attack',
); );
attackButtons[0].click(); attackButtons[0].click();
await Promise.resolve(); await Promise.resolve();
@@ -254,7 +254,7 @@ describe('HuntPageComponent', () => {
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
const attackButtons = Array.from(element.querySelectorAll('button')).filter( const attackButtons = Array.from(element.querySelectorAll('button')).filter(
(button) => button.textContent?.trim() === 'Angreifen', (button) => button.textContent?.trim() === 'Attack',
); );
attackButtons[0].click(); attackButtons[0].click();
await Promise.resolve(); await Promise.resolve();
@@ -374,7 +374,7 @@ describe('HuntPageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Du suchst nach Spuren...'); expect(element.textContent).toContain('You search for tracks...');
expect(element.querySelector<HTMLButtonElement>('[data-hunt-start]')?.disabled).toBe(true); expect(element.querySelector<HTMLButtonElement>('[data-hunt-start]')?.disabled).toBe(true);
}); });