feat(web): translate combat log, action buttons, and victory/defeat screen to English

Translates formatEvent() and monsterIntentLabel() combat-log templates,
default participant-name fallbacks (Du/Der Gegner -> You/The enemy), and
every static string in the combat page template: round header, action
buttons, outcome headings, rewards panel, post-combat buttons, log panel,
and loading/error/aria-label text.

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 21:40:04 +02:00
parent 5586e6c672
commit 0923fc0a23
3 changed files with 73 additions and 73 deletions

View File

@@ -28,7 +28,7 @@ const activeCombat: Combat = {
rewards: null,
};
const monsterHitLine = 'Aschenratte trifft Aric Duskwalker für 5 Schaden.';
const monsterHitLine = 'Aschenratte hits Aric Duskwalker for 5 damage.';
function countOccurrences(haystack: string | null, needle: string): number {
return haystack ? haystack.split(needle).length - 1 : 0;
@@ -91,7 +91,7 @@ describe('CombatPageComponent', () => {
expect(combatStore.loadCombat).toHaveBeenCalledWith('combat-1');
});
it('shows the player, monster, HP bars, round, and the Angriff action', async () => {
it('shows the player, monster, HP bars, round, and the Attack action', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
@@ -99,22 +99,22 @@ describe('CombatPageComponent', () => {
expect(element.textContent).toContain('95 / 100');
expect(element.textContent).toContain('Aschenratte');
expect(element.textContent).toContain('31 / 45');
expect(element.querySelector('[data-combat-round]')?.textContent).toContain('Runde 2');
expect(element.querySelector('[data-combat-round]')?.textContent).toContain('Round 2');
expect(element.querySelector('[data-combat-attack]')).toBeTruthy();
});
it('shows all five combat actions with their German labels', async () => {
it('shows all five combat actions with their English labels', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[data-combat-attack]')?.textContent).toContain('Angriff');
expect(element.querySelector('[data-combat-heavy-strike]')?.textContent).toContain('Schwerer Hieb');
expect(element.querySelector('[data-combat-shield-bash]')?.textContent).toContain('Schildstoß');
expect(element.querySelector('[data-combat-defend]')?.textContent).toContain('Verteidigen');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Trank 2/2');
expect(element.querySelector('[data-combat-attack]')?.textContent).toContain('Attack');
expect(element.querySelector('[data-combat-heavy-strike]')?.textContent).toContain('Heavy Strike');
expect(element.querySelector('[data-combat-shield-bash]')?.textContent).toContain('Shield Bash');
expect(element.querySelector('[data-combat-defend]')?.textContent).toContain('Defend');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Potion 2/2');
});
it('sends HEAVY_STRIKE when Schwerer Hieb is clicked', async () => {
it('sends HEAVY_STRIKE when Heavy Strike is clicked', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
@@ -123,7 +123,7 @@ describe('CombatPageComponent', () => {
expect(combatStore.performAction).toHaveBeenCalledWith('HEAVY_STRIKE');
});
it('sends SHIELD_BASH when Schildstoß is clicked', async () => {
it('sends SHIELD_BASH when Shield Bash is clicked', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
@@ -132,7 +132,7 @@ describe('CombatPageComponent', () => {
expect(combatStore.performAction).toHaveBeenCalledWith('SHIELD_BASH');
});
it('sends DEFEND when Verteidigen is clicked', async () => {
it('sends DEFEND when Defend is clicked', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
@@ -141,7 +141,7 @@ describe('CombatPageComponent', () => {
expect(combatStore.performAction).toHaveBeenCalledWith('DEFEND');
});
it('sends POTION when Trank is clicked', async () => {
it('sends POTION when Potion is clicked', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
@@ -169,7 +169,7 @@ describe('CombatPageComponent', () => {
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[data-combat-telegraph]')?.textContent).toContain(
'Aschenratte bereitet Schweren Hieb vor.',
'Aschenratte is winding up a Heavy Strike.',
);
});
@@ -192,21 +192,21 @@ describe('CombatPageComponent', () => {
});
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Aric Duskwalker trinkt einen Trank und heilt 35 Lebenspunkte.');
expect(element.textContent).toContain('Aric Duskwalker geht in die Verteidigung.');
expect(element.textContent).toContain('Aschenratte bereitet Schweren Hieb vor.');
expect(element.textContent).toContain('Aric Duskwalker unterbricht den vorbereiteten Angriff von Aschenratte.');
expect(element.textContent).toContain('Aric Duskwalker drinks a potion and heals 35 HP.');
expect(element.textContent).toContain('Aric Duskwalker braces to defend.');
expect(element.textContent).toContain('Aschenratte is winding up a Heavy Strike.');
expect(element.textContent).toContain("Aric Duskwalker interrupts Aschenratte's attack.");
});
it('renders the structured events as readable German combat-log entries', async () => {
it('renders the structured events as readable English combat-log entries', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Aric Duskwalker trifft Aschenratte für 14 Schaden.');
expect(element.textContent).toContain('Aschenratte trifft Aric Duskwalker für 5 Schaden.');
expect(element.textContent).toContain('Aric Duskwalker hits Aschenratte for 14 damage.');
expect(element.textContent).toContain('Aschenratte hits Aric Duskwalker for 5 damage.');
});
it('calls combatStore.performAction("ATTACK") when Angriff is clicked', async () => {
it('calls combatStore.performAction("ATTACK") when Attack is clicked', async () => {
const fixture = await setup(activeCombat);
const element = fixture.nativeElement as HTMLElement;
@@ -303,7 +303,7 @@ describe('CombatPageComponent', () => {
await vi.advanceTimersByTimeAsync(1260);
fixture.detectChanges();
expect(element.querySelector('[data-combat-telegraph]')?.textContent).toContain('bereitet Schweren Hieb vor');
expect(element.querySelector('[data-combat-telegraph]')?.textContent).toContain('is winding up a Heavy Strike');
expect(monster?.classList.contains('sprite--lunge')).toBe(false);
});
@@ -334,7 +334,7 @@ describe('CombatPageComponent', () => {
await vi.advanceTimersByTimeAsync(540);
fixture.detectChanges();
expect(element.textContent).toContain('Aric Duskwalker unterbricht den vorbereiteten Angriff von Aschenratte.');
expect(element.textContent).toContain("Aric Duskwalker interrupts Aschenratte's attack.");
expect(element.querySelector('[data-combat-telegraph]')).toBeNull();
expect(monster?.classList.contains('sprite--lunge')).toBe(false);
});
@@ -365,7 +365,7 @@ describe('CombatPageComponent', () => {
// Before the checkpoint: the pre-heal HP and potion count still show.
expect(element.textContent).toContain('70 / 100');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Trank 2/2');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Potion 2/2');
// First checkpoint: the heal already landed from the player's own action,
// so the HP bar and potion count update here -- well before the monster's
@@ -373,8 +373,8 @@ describe('CombatPageComponent', () => {
await vi.advanceTimersByTimeAsync(540);
fixture.detectChanges();
expect(element.textContent).toContain('85 / 100');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Trank 1/2');
expect(element.textContent).toContain('Aric Duskwalker trinkt einen Trank und heilt 15 Lebenspunkte.');
expect(element.querySelector('[data-combat-potion]')?.textContent).toContain('Potion 1/2');
expect(element.textContent).toContain('Aric Duskwalker drinks a potion and heals 15 HP.');
// The monster's reply is still held back at this point.
expect(countOccurrences(element.textContent, monsterHitLine)).toBe(1);
@@ -444,7 +444,7 @@ describe('CombatPageComponent', () => {
expect(worldStore.refreshCharacter).toHaveBeenCalledOnce();
});
it('disables Angriff while an action is pending', async () => {
it('disables Attack while an action is pending', async () => {
const fixture = await setup(activeCombat);
combatStore.actionPending.set(true);
fixture.detectChanges();
@@ -453,21 +453,21 @@ describe('CombatPageComponent', () => {
expect(element.querySelector<HTMLButtonElement>('[data-combat-attack]')?.disabled).toBe(true);
});
it('shows the victory state and hides Angriff when the combat is WON', async () => {
it('shows the victory state and hides Attack when the combat is WON', async () => {
const fixture = await setup({ ...activeCombat, status: 'WON' });
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[data-combat-result="WON"]')).toBeTruthy();
expect(element.textContent).toContain('Sieg');
expect(element.textContent).toContain('Victory');
expect(element.querySelector('[data-combat-attack]')).toBeNull();
});
it('shows the defeat state and hides Angriff when the combat is LOST', async () => {
it('shows the defeat state and hides Attack when the combat is LOST', async () => {
const fixture = await setup({ ...activeCombat, status: 'LOST' });
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[data-combat-result="LOST"]')).toBeTruthy();
expect(element.textContent).toContain('Niederlage');
expect(element.textContent).toContain('Defeat');
expect(element.querySelector('[data-combat-attack]')).toBeNull();
});
@@ -542,7 +542,7 @@ describe('CombatPageComponent', () => {
it('hides the silver tile entirely when the kill granted none', async () => {
// Every monster seeded in this slice rolls silverMin/silverMax = 0 (design
// R7), so without this guard the victory screen advertises "Silber +0"
// R7), so without this guard the victory screen advertises "Silver +0"
// after every single fight in the shipped content.
const fixture = await setup({
...wonWithRewards,
@@ -589,7 +589,7 @@ describe('CombatPageComponent', () => {
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[data-reward-empty]')?.textContent).toContain(
'Keine besondere Beute gefunden.',
'No notable loot found.',
);
expect(element.querySelector('[role="alert"]')).toBeNull();
});