feat(web): translate topbar, navigation, context panel, footer, and reputation display to English

Translates the remaining German static copy in the layout/shared components
per the design doc glossary: top-bar (World Renown, Silver, loading text),
side-navigation (nav labels and aria-labels), context-panel (area info,
safety/hunt facts, encounters), game-footer (connection status, Ashen Fields),
and reputation-display (aria-label, highest-rank text). Also fixes stale
German rank-label fixtures in reputation-display.component.spec.ts flagged
by Task 2's reviewer, and updates app.spec.ts assertions that exercised the
now-translated side-navigation/top-bar output via the app shell.
This commit is contained in:
Bastian Wagner
2026-08-21 22:23:20 +02:00
parent 14d26d06a4
commit 846bb7428c
9 changed files with 57 additions and 55 deletions

View File

@@ -35,7 +35,7 @@ describe('App', () => {
}).compileComponents(); }).compileComponents();
}); });
it('renders the reusable game shell with Karte and Jagd available', async () => { it('renders the reusable game shell with Map and Hunt available', async () => {
const fixture = TestBed.createComponent(AppShellComponent); const fixture = TestBed.createComponent(AppShellComponent);
const router = TestBed.inject(Router); const router = TestBed.inject(Router);
await router.navigateByUrl('/world'); await router.navigateByUrl('/world');
@@ -52,20 +52,20 @@ describe('App', () => {
expect(mapButton).not.toBeNull(); expect(mapButton).not.toBeNull();
expect(mapButton?.disabled).toBe(false); expect(mapButton?.disabled).toBe(false);
expect(mapButton?.getAttribute('aria-current')).toBe('page'); expect(mapButton?.getAttribute('aria-current')).toBe('page');
expect(mapButton?.getAttribute('aria-label')).toBe('Karte'); expect(mapButton?.getAttribute('aria-label')).toBe('Map');
const huntButton = element.querySelector<HTMLButtonElement>('[data-navigation="hunt"]'); const huntButton = element.querySelector<HTMLButtonElement>('[data-navigation="hunt"]');
expect(huntButton).not.toBeNull(); expect(huntButton).not.toBeNull();
expect(huntButton?.disabled).toBe(false); expect(huntButton?.disabled).toBe(false);
expect(huntButton?.getAttribute('aria-current')).toBeNull(); expect(huntButton?.getAttribute('aria-current')).toBeNull();
expect(huntButton?.getAttribute('aria-label')).toBe('Jagd'); expect(huntButton?.getAttribute('aria-label')).toBe('Hunt');
const inventoryButton = element.querySelector<HTMLButtonElement>( const inventoryButton = element.querySelector<HTMLButtonElement>(
'[data-navigation="inventory"]', '[data-navigation="inventory"]',
); );
expect(inventoryButton).not.toBeNull(); expect(inventoryButton).not.toBeNull();
expect(inventoryButton?.disabled).toBe(false); expect(inventoryButton?.disabled).toBe(false);
expect(inventoryButton?.getAttribute('aria-label')).toBe('Inventar'); expect(inventoryButton?.getAttribute('aria-label')).toBe('Inventory');
for (const destination of ['quests', 'character']) { for (const destination of ['quests', 'character']) {
expect( expect(
@@ -76,7 +76,7 @@ describe('App', () => {
expect(element.textContent).not.toContain('Shop'); expect(element.textContent).not.toContain('Shop');
}); });
it('offers Ort as the first navigation entry, marked active on /location', async () => { it('offers Location as the first navigation entry, marked active on /location', async () => {
const fixture = TestBed.createComponent(AppShellComponent); const fixture = TestBed.createComponent(AppShellComponent);
const router = TestBed.inject(Router); const router = TestBed.inject(Router);
await router.navigateByUrl('/location'); await router.navigateByUrl('/location');
@@ -98,7 +98,7 @@ describe('App', () => {
'[data-navigation="location"]', '[data-navigation="location"]',
); );
expect(locationButton?.disabled).toBe(false); expect(locationButton?.disabled).toBe(false);
expect(locationButton?.getAttribute('aria-label')).toBe('Ort'); expect(locationButton?.getAttribute('aria-label')).toBe('Location');
expect(locationButton?.getAttribute('aria-current')).toBe('page'); expect(locationButton?.getAttribute('aria-current')).toBe('page');
expect( expect(
element.querySelector('[data-navigation="world"]')?.getAttribute('aria-current'), element.querySelector('[data-navigation="world"]')?.getAttribute('aria-current'),
@@ -135,7 +135,7 @@ describe('App', () => {
expect(fixture.nativeElement.querySelector('app-context-panel')).not.toBeNull(); expect(fixture.nativeElement.querySelector('app-context-panel')).not.toBeNull();
}); });
it('marks Jagd as the active navigation entry while on /hunt', async () => { it('marks Hunt as the active navigation entry while on /hunt', async () => {
const fixture = TestBed.createComponent(AppShellComponent); const fixture = TestBed.createComponent(AppShellComponent);
const router = TestBed.inject(Router); const router = TestBed.inject(Router);
await router.navigateByUrl('/hunt'); await router.navigateByUrl('/hunt');
@@ -183,7 +183,9 @@ describe('App', () => {
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain( expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain(
'Mara Ashfall', 'Mara Ashfall',
); );
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain('Ansehen 7'); expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain(
'World Renown 7',
);
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain('52 / 80'); expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain('52 / 80');
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain('150'); expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain('150');
}); });

View File

@@ -1,44 +1,44 @@
<aside class="context-panel" aria-label="Gebietsinformationen"> <aside class="context-panel" aria-label="Area Information">
@if (worldStore.currentLocation(); as location) { @if (worldStore.currentLocation(); as location) {
<span class="context-panel__eyebrow">GEBIETSINFO</span> <span class="context-panel__eyebrow">AREA INFO</span>
<h2>{{ location.name }}</h2> <h2>{{ location.name }}</h2>
@if (worldStore.selectedConnection(); as selected) { @if (worldStore.selectedConnection(); as selected) {
<p class="context-panel__selection">Ausgewähltes Ziel: {{ selected.targetLocation.name }}</p> <p class="context-panel__selection">Selected Destination: {{ selected.targetLocation.name }}</p>
} }
<picture class="context-panel__artwork"> <picture class="context-panel__artwork">
@if (runtimeArtworkPath(location.artworkPath); as runtimeArtwork) { @if (runtimeArtworkPath(location.artworkPath); as runtimeArtwork) {
<source [srcset]="runtimeArtwork" type="image/jpeg" /> <source [srcset]="runtimeArtwork" type="image/jpeg" />
} }
<img [src]="location.artworkPath" [alt]="'Ortsansicht: ' + location.name" /> <img [src]="location.artworkPath" [alt]="'Location view: ' + location.name" />
</picture> </picture>
<p class="context-panel__description">{{ location.description }}</p> <p class="context-panel__description">{{ location.description }}</p>
<dl class="context-panel__facts"> <dl class="context-panel__facts">
<div> <div>
<dt>Empfohlenes Ansehen</dt> <dt>Recommended Renown</dt>
<dd>{{ location.minRecommendedLevel }}{{ location.maxRecommendedLevel }}</dd> <dd>{{ location.minRecommendedLevel }}{{ location.maxRecommendedLevel }}</dd>
</div> </div>
<div> <div>
<dt>Sicherheit</dt> <dt>Safety</dt>
<dd [class.context-panel__safe]="location.isSafe"> <dd [class.context-panel__safe]="location.isSafe">
{{ location.isSafe ? 'Sicherer Ort' : 'Gefährliches Gebiet' }} {{ location.isSafe ? 'Safe Location' : 'Dangerous Area' }}
</dd> </dd>
</div> </div>
<div> <div>
<dt>Jagd</dt> <dt>Hunt</dt>
<dd>{{ location.huntingEnabled ? 'Jagd möglich' : 'Keine Jagd' }}</dd> <dd>{{ location.huntingEnabled ? 'Hunting Available' : 'No Hunting' }}</dd>
</div> </div>
@if (location.huntingEnabled && location.possibleMonsters.length) { @if (location.huntingEnabled && location.possibleMonsters.length) {
<div> <div>
<dt>Mögliche Begegnungen</dt> <dt>Possible Encounters</dt>
<dd>{{ location.possibleMonsters.join(', ') }}</dd> <dd>{{ location.possibleMonsters.join(', ') }}</dd>
</div> </div>
} }
</dl> </dl>
} @else { } @else {
<span class="context-panel__eyebrow">GEBIETSINFO</span> <span class="context-panel__eyebrow">AREA INFO</span>
<p class="context-panel__hint">Informationen erscheinen mit der Weltansicht.</p> <p class="context-panel__hint">Information appears with the world view.</p>
} }
</aside> </aside>

View File

@@ -57,7 +57,7 @@ describe('ContextPanelComponent', () => {
); );
}); });
it('hides "Mögliche Begegnungen" when the location has no hunting', async () => { it('hides "Possible Encounters" when the location has no hunting', async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ContextPanelComponent], imports: [ContextPanelComponent],
providers: [ providers: [
@@ -75,7 +75,7 @@ describe('ContextPanelComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).not.toContain('Mögliche Begegnungen'); expect(element.textContent).not.toContain('Possible Encounters');
}); });
it('lists possible monsters as plain text when hunting is enabled', async () => { it('lists possible monsters as plain text when hunting is enabled', async () => {
@@ -96,7 +96,7 @@ describe('ContextPanelComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Mögliche Begegnungen'); expect(element.textContent).toContain('Possible Encounters');
expect(element.textContent).toContain('Aschenratte, Straßenräuber'); expect(element.textContent).toContain('Aschenratte, Straßenräuber');
expect(element.textContent).not.toMatch(/\d+\s?%/); expect(element.textContent).not.toMatch(/\d+\s?%/);
}); });

View File

@@ -1,5 +1,5 @@
<footer class="game-footer"> <footer class="game-footer">
<span class="game-footer__status"><span aria-hidden="true"></span> Verbindung bereit</span> <span class="game-footer__status"><span aria-hidden="true"></span> Connection ready</span>
<span class="game-footer__ornament" aria-hidden="true"></span> <span class="game-footer__ornament" aria-hidden="true"></span>
<span>Aschenfelder</span> <span>Ashen Fields</span>
</footer> </footer>

View File

@@ -1,4 +1,4 @@
<nav class="side-navigation" aria-label="Spielnavigation"> <nav class="side-navigation" aria-label="Game navigation">
<button <button
class="side-navigation__item" class="side-navigation__item"
type="button" type="button"
@@ -7,12 +7,12 @@
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page" ariaCurrentWhenActive="page"
data-navigation="location" data-navigation="location"
aria-label="Ort" aria-label="Location"
> >
<!-- Drawn rather than loaded: no medallion for "Ort" has been painted yet. <!-- Drawn rather than loaded: no medallion for "Location" has been painted yet.
Framed to sit alongside the existing icons until one exists. --> Framed to sit alongside the existing icons until one exists. -->
<img src="/images/hud/runtime/LocationIcon-128.png" alt="" /> <img src="/images/hud/runtime/LocationIcon-128.png" alt="" />
<span>Ort</span> <span>Location</span>
</button> </button>
<button <button
@@ -23,10 +23,10 @@
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page" ariaCurrentWhenActive="page"
data-navigation="world" data-navigation="world"
aria-label="Karte" aria-label="Map"
> >
<img src="/images/hud/runtime/MapsIcon-128.png" alt="" /> <img src="/images/hud/runtime/MapsIcon-128.png" alt="" />
<span>Karte</span> <span>Map</span>
</button> </button>
<button <button
@@ -37,17 +37,17 @@
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page" ariaCurrentWhenActive="page"
data-navigation="hunt" data-navigation="hunt"
aria-label="Jagd" aria-label="Hunt"
> >
<img src="/images/hud/runtime/HuntIcon-128.png" alt="" /> <img src="/images/hud/runtime/HuntIcon-128.png" alt="" />
<span>Jagd</span> <span>Hunt</span>
</button> </button>
<button <button
class="side-navigation__item" class="side-navigation__item"
type="button" type="button"
data-navigation="quests" data-navigation="quests"
disabled disabled
aria-label="Quests sind noch nicht verfügbar" aria-label="Quests are not yet available"
> >
<img src="/images/hud/runtime/QuestsIcon-128.png" alt="" /> <img src="/images/hud/runtime/QuestsIcon-128.png" alt="" />
<span>Quests</span> <span>Quests</span>
@@ -60,19 +60,19 @@
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page" ariaCurrentWhenActive="page"
data-navigation="inventory" data-navigation="inventory"
aria-label="Inventar" aria-label="Inventory"
> >
<img src="/images/hud/runtime/InventoryIcon-128.png" alt="" /> <img src="/images/hud/runtime/InventoryIcon-128.png" alt="" />
<span>Inventar</span> <span>Inventory</span>
</button> </button>
<button <button
class="side-navigation__item" class="side-navigation__item"
type="button" type="button"
data-navigation="character" data-navigation="character"
disabled disabled
aria-label="Charakter ist noch nicht verfügbar" aria-label="Character is not yet available"
> >
<img src="/images/hud/runtime/CharacterIcon-128.png" alt="" /> <img src="/images/hud/runtime/CharacterIcon-128.png" alt="" />
<span>Charakter</span> <span>Character</span>
</button> </button>
</nav> </nav>

View File

@@ -4,9 +4,9 @@
@if (character(); as character) { @if (character(); as character) {
<div class="top-bar__identity"> <div class="top-bar__identity">
<span class="top-bar__name">{{ character.name }}</span> <span class="top-bar__name">{{ character.name }}</span>
<span class="top-bar__renown">Ansehen {{ character.renown }}</span> <span class="top-bar__renown">World Renown {{ character.renown }}</span>
</div> </div>
<div class="top-bar__health" aria-label="Lebenspunkte"> <div class="top-bar__health" aria-label="Health points">
<span>{{ character.currentHp }} / {{ character.maxHp }}</span> <span>{{ character.currentHp }} / {{ character.maxHp }}</span>
<span class="top-bar__health-track" aria-hidden="true"> <span class="top-bar__health-track" aria-hidden="true">
<span <span
@@ -17,12 +17,12 @@
</div> </div>
<dl class="top-bar__resources"> <dl class="top-bar__resources">
<div class="top-bar__resource" data-top-bar-silver> <div class="top-bar__resource" data-top-bar-silver>
<dt>Silber</dt> <dt>Silver</dt>
<dd>{{ character.silver }}</dd> <dd>{{ character.silver }}</dd>
</div> </div>
</dl> </dl>
} @else { } @else {
<span class="top-bar__loading">Charakterdaten werden geladen</span> <span class="top-bar__loading">Loading character data</span>
} }
</div> </div>

View File

@@ -39,16 +39,16 @@ describe('TopBarComponent', () => {
expect(element.querySelector('[data-top-bar-silver]')?.textContent).toContain('120'); expect(element.querySelector('[data-top-bar-silver]')?.textContent).toContain('120');
}); });
it('shows Ansehen instead of Level, and displays no XP value', async () => { it('shows World Renown instead of Level, and displays no XP value', async () => {
const element = await render(characterFixture({ renown: 3 })); const element = await render(characterFixture({ renown: 3 }));
expect(element.textContent).toContain('Ansehen 3'); expect(element.textContent).toContain('World Renown 3');
expect(element.querySelector('[data-top-bar-experience]')).toBeNull(); expect(element.querySelector('[data-top-bar-experience]')).toBeNull();
}); });
it('shows a loading message when no character is available yet', async () => { it('shows a loading message when no character is available yet', async () => {
const element = await render(null); const element = await render(null);
expect(element.textContent).toContain('Charakterdaten werden geladen'); expect(element.textContent).toContain('Loading character data');
}); });
}); });

View File

@@ -1,4 +1,4 @@
<div class="reputation-display" [attr.aria-label]="entry().factionName + ' Ruf'"> <div class="reputation-display" [attr.aria-label]="entry().factionName + ' Reputation'">
<div class="reputation-display__header"> <div class="reputation-display__header">
<span class="reputation-display__faction">{{ entry().factionName }}</span> <span class="reputation-display__faction">{{ entry().factionName }}</span>
<span class="reputation-display__rank">{{ entry().rankLabel }}</span> <span class="reputation-display__rank">{{ entry().rankLabel }}</span>
@@ -20,6 +20,6 @@
<span class="reputation-display__value">{{ entry().reputation }} / {{ entry().nextThreshold }}</span> <span class="reputation-display__value">{{ entry().reputation }} / {{ entry().nextThreshold }}</span>
</div> </div>
} @else { } @else {
<p class="reputation-display__max" data-reputation-max>{{ entry().reputation }} — Höchster Rang erreicht</p> <p class="reputation-display__max" data-reputation-max>{{ entry().reputation }} — Highest Rank Reached</p>
} }
</div> </div>

View File

@@ -17,13 +17,13 @@ describe('ReputationDisplayComponent', () => {
factionName: 'Grenzwacht', factionName: 'Grenzwacht',
reputation: 320, reputation: 320,
rank: 'KNOWN', rank: 'KNOWN',
rankLabel: 'Bekannt', rankLabel: 'Known',
nextThreshold: 500, nextThreshold: 500,
}); });
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Grenzwacht'); expect(element.textContent).toContain('Grenzwacht');
expect(element.textContent).toContain('Bekannt'); expect(element.textContent).toContain('Known');
}); });
it('shows current reputation against the next threshold', async () => { it('shows current reputation against the next threshold', async () => {
@@ -32,7 +32,7 @@ describe('ReputationDisplayComponent', () => {
factionName: 'Grenzwacht', factionName: 'Grenzwacht',
reputation: 320, reputation: 320,
rank: 'KNOWN', rank: 'KNOWN',
rankLabel: 'Bekannt', rankLabel: 'Known',
nextThreshold: 500, nextThreshold: 500,
}); });
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
@@ -47,7 +47,7 @@ describe('ReputationDisplayComponent', () => {
factionName: 'Grenzwacht', factionName: 'Grenzwacht',
reputation: 320, reputation: 320,
rank: 'KNOWN', rank: 'KNOWN',
rankLabel: 'Bekannt', rankLabel: 'Known',
nextThreshold: 500, nextThreshold: 500,
}); });
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
@@ -64,12 +64,12 @@ describe('ReputationDisplayComponent', () => {
factionName: 'Grenzwacht', factionName: 'Grenzwacht',
reputation: 1500, reputation: 1500,
rank: 'ESTEEMED', rank: 'ESTEEMED',
rankLabel: 'Geachtet', rankLabel: 'Esteemed',
nextThreshold: null, nextThreshold: null,
}); });
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Geachtet'); expect(element.textContent).toContain('Esteemed');
expect(element.querySelector('[data-reputation-max]')).toBeTruthy(); expect(element.querySelector('[data-reputation-max]')).toBeTruthy();
}); });
@@ -79,13 +79,13 @@ describe('ReputationDisplayComponent', () => {
factionName: 'Der Stille Orden', factionName: 'Der Stille Orden',
reputation: 0, reputation: 0,
rank: 'STRANGER', rank: 'STRANGER',
rankLabel: 'Fremder', rankLabel: 'Stranger',
nextThreshold: 100, nextThreshold: 100,
}); });
const element = fixture.nativeElement as HTMLElement; const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Der Stille Orden'); expect(element.textContent).toContain('Der Stille Orden');
expect(element.textContent).toContain('Fremder'); expect(element.textContent).toContain('Stranger');
expect(element.textContent).toContain('0'); expect(element.textContent).toContain('0');
const fill = element.querySelector<HTMLElement>('[data-reputation-progress-fill]'); const fill = element.querySelector<HTMLElement>('[data-reputation-progress-fill]');