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();
});
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 router = TestBed.inject(Router);
await router.navigateByUrl('/world');
@@ -52,20 +52,20 @@ describe('App', () => {
expect(mapButton).not.toBeNull();
expect(mapButton?.disabled).toBe(false);
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"]');
expect(huntButton).not.toBeNull();
expect(huntButton?.disabled).toBe(false);
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>(
'[data-navigation="inventory"]',
);
expect(inventoryButton).not.toBeNull();
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']) {
expect(
@@ -76,7 +76,7 @@ describe('App', () => {
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 router = TestBed.inject(Router);
await router.navigateByUrl('/location');
@@ -98,7 +98,7 @@ describe('App', () => {
'[data-navigation="location"]',
);
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(
element.querySelector('[data-navigation="world"]')?.getAttribute('aria-current'),
@@ -135,7 +135,7 @@ describe('App', () => {
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 router = TestBed.inject(Router);
await router.navigateByUrl('/hunt');
@@ -183,7 +183,9 @@ describe('App', () => {
expect(fixture.nativeElement.querySelector('app-top-bar')?.textContent).toContain(
'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('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) {
<span class="context-panel__eyebrow">GEBIETSINFO</span>
<span class="context-panel__eyebrow">AREA INFO</span>
<h2>{{ location.name }}</h2>
@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">
@if (runtimeArtworkPath(location.artworkPath); as runtimeArtwork) {
<source [srcset]="runtimeArtwork" type="image/jpeg" />
}
<img [src]="location.artworkPath" [alt]="'Ortsansicht: ' + location.name" />
<img [src]="location.artworkPath" [alt]="'Location view: ' + location.name" />
</picture>
<p class="context-panel__description">{{ location.description }}</p>
<dl class="context-panel__facts">
<div>
<dt>Empfohlenes Ansehen</dt>
<dt>Recommended Renown</dt>
<dd>{{ location.minRecommendedLevel }}{{ location.maxRecommendedLevel }}</dd>
</div>
<div>
<dt>Sicherheit</dt>
<dt>Safety</dt>
<dd [class.context-panel__safe]="location.isSafe">
{{ location.isSafe ? 'Sicherer Ort' : 'Gefährliches Gebiet' }}
{{ location.isSafe ? 'Safe Location' : 'Dangerous Area' }}
</dd>
</div>
<div>
<dt>Jagd</dt>
<dd>{{ location.huntingEnabled ? 'Jagd möglich' : 'Keine Jagd' }}</dd>
<dt>Hunt</dt>
<dd>{{ location.huntingEnabled ? 'Hunting Available' : 'No Hunting' }}</dd>
</div>
@if (location.huntingEnabled && location.possibleMonsters.length) {
<div>
<dt>Mögliche Begegnungen</dt>
<dt>Possible Encounters</dt>
<dd>{{ location.possibleMonsters.join(', ') }}</dd>
</div>
}
</dl>
} @else {
<span class="context-panel__eyebrow">GEBIETSINFO</span>
<p class="context-panel__hint">Informationen erscheinen mit der Weltansicht.</p>
<span class="context-panel__eyebrow">AREA INFO</span>
<p class="context-panel__hint">Information appears with the world view.</p>
}
</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({
imports: [ContextPanelComponent],
providers: [
@@ -75,7 +75,7 @@ describe('ContextPanelComponent', () => {
fixture.detectChanges();
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 () => {
@@ -96,7 +96,7 @@ describe('ContextPanelComponent', () => {
fixture.detectChanges();
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).not.toMatch(/\d+\s?%/);
});

View File

@@ -1,5 +1,5 @@
<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>Aschenfelder</span>
<span>Ashen Fields</span>
</footer>

View File

@@ -1,4 +1,4 @@
<nav class="side-navigation" aria-label="Spielnavigation">
<nav class="side-navigation" aria-label="Game navigation">
<button
class="side-navigation__item"
type="button"
@@ -7,12 +7,12 @@
[routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page"
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. -->
<img src="/images/hud/runtime/LocationIcon-128.png" alt="" />
<span>Ort</span>
<span>Location</span>
</button>
<button
@@ -23,10 +23,10 @@
[routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page"
data-navigation="world"
aria-label="Karte"
aria-label="Map"
>
<img src="/images/hud/runtime/MapsIcon-128.png" alt="" />
<span>Karte</span>
<span>Map</span>
</button>
<button
@@ -37,17 +37,17 @@
[routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page"
data-navigation="hunt"
aria-label="Jagd"
aria-label="Hunt"
>
<img src="/images/hud/runtime/HuntIcon-128.png" alt="" />
<span>Jagd</span>
<span>Hunt</span>
</button>
<button
class="side-navigation__item"
type="button"
data-navigation="quests"
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="" />
<span>Quests</span>
@@ -60,19 +60,19 @@
[routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page"
data-navigation="inventory"
aria-label="Inventar"
aria-label="Inventory"
>
<img src="/images/hud/runtime/InventoryIcon-128.png" alt="" />
<span>Inventar</span>
<span>Inventory</span>
</button>
<button
class="side-navigation__item"
type="button"
data-navigation="character"
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="" />
<span>Charakter</span>
<span>Character</span>
</button>
</nav>

View File

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

View File

@@ -39,16 +39,16 @@ describe('TopBarComponent', () => {
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 }));
expect(element.textContent).toContain('Ansehen 3');
expect(element.textContent).toContain('World Renown 3');
expect(element.querySelector('[data-top-bar-experience]')).toBeNull();
});
it('shows a loading message when no character is available yet', async () => {
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">
<span class="reputation-display__faction">{{ entry().factionName }}</span>
<span class="reputation-display__rank">{{ entry().rankLabel }}</span>
@@ -20,6 +20,6 @@
<span class="reputation-display__value">{{ entry().reputation }} / {{ entry().nextThreshold }}</span>
</div>
} @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>

View File

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