feat(web): translate location, travel, and world-map screen copy to English

This commit is contained in:
Bastian Wagner
2026-08-21 22:02:26 +02:00
parent ca3e072807
commit 1c0a13475c
10 changed files with 53 additions and 53 deletions

View File

@@ -3,14 +3,14 @@
class="interaction-panel"
role="dialog"
aria-modal="false"
[attr.aria-label]="result ? result.title : 'Handlung nicht möglich'"
[attr.aria-label]="result ? result.title : 'Action not possible'"
data-interaction-panel
>
@if (result) {
<h2 class="interaction-panel__title">{{ result.title }}</h2>
<p class="interaction-panel__text">{{ result.text }}</p>
} @else {
<h2 class="interaction-panel__title">Nichts zu entdecken</h2>
<h2 class="interaction-panel__title">Nothing to discover</h2>
<p class="interaction-panel__text interaction-panel__text--error" role="alert">
{{ error }}
</p>
@@ -22,7 +22,7 @@
data-interaction-close
(click)="closePanel.emit()"
>
Schließen
Close
</button>
</div>
}

View File

@@ -12,6 +12,6 @@
<span class="location-node__marker" aria-hidden="true"></span>
<span class="location-node__name">{{ location.name }}</span>
<span class="location-node__state">{{
current ? 'Aktueller Ort' : selected ? 'Ausgewähltes Ziel' : 'Erreichbar'
current ? 'Current Location' : selected ? 'Selected Destination' : 'Reachable'
}}</span>
</button>

View File

@@ -1,4 +1,4 @@
<section class="location-page" aria-label="Ortsansicht">
<section class="location-page" aria-label="Location View">
@if (store.location(); as location) {
<div class="location-page__main">
<header class="location-page__header">
@@ -16,7 +16,7 @@
@if (runtimeArtwork(location.localArtworkPath); as runtime) {
<source [srcset]="runtime" type="image/jpeg" />
}
<img [src]="location.localArtworkPath" [alt]="'Ortsansicht: ' + location.name" />
<img [src]="location.localArtworkPath" [alt]="'Location view: ' + location.name" />
</picture>
<div class="location-page__hotspots">
@@ -36,7 +36,7 @@
/>
</div>
<nav class="location-page__actions" aria-label="Ortsaktionen">
<nav class="location-page__actions" aria-label="Location Actions">
@for (action of location.primaryActions; track action.key) {
<button
class="location-action"
@@ -49,7 +49,7 @@
<span class="location-action__label">{{ action.label }}</span>
@if (action.description) {
<span class="location-action__hint">
{{ isPending(action.poiKey ?? action.key) ? 'Einen Moment…' : action.description }}
{{ isPending(action.poiKey ?? action.key) ? 'One moment…' : action.description }}
</span>
}
</button>
@@ -60,13 +60,13 @@
<app-location-sidebar class="location-page__sidebar" [location]="location" />
} @else if (store.error(); as error) {
<section class="location-page__notice" role="alert">
<p>Ort konnte nicht geladen werden.</p>
<p>Could not load location.</p>
<p class="location-page__notice-detail">{{ error }}</p>
<button type="button" data-location-retry (click)="retry()">Erneut versuchen</button>
<button type="button" data-location-retry (click)="retry()">Retry</button>
</section>
} @else {
<section class="location-page__notice" aria-live="polite">
<p role="status">Der Ort wird geladen…</p>
<p role="status">Loading location…</p>
</section>
}
</section>

View File

@@ -75,7 +75,7 @@ describe('LocationPageComponent', () => {
const image = element.querySelector('img') as HTMLImageElement;
expect(image.getAttribute('src')).toBe('/images/backgrounds/Aschestrasse.png');
expect(image.getAttribute('alt')).toBe('Ortsansicht: Verbrannte Straße');
expect(image.getAttribute('alt')).toBe('Location view: Verbrannte Straße');
});
it('places every hotspot on the artwork', async () => {
@@ -205,14 +205,14 @@ describe('LocationPageComponent', () => {
const actions = element.querySelectorAll<HTMLButtonElement>('[data-action]');
expect([...actions].every((action) => action.disabled)).toBe(true);
expect(element.textContent).toContain('Einen Moment…');
expect(element.textContent).toContain('One moment…');
});
it('renders a restrained loading state that shows no misplaced hotspots', async () => {
const { element } = await setup(null);
expect(element.querySelector('[role="status"]')?.textContent).toContain(
'Der Ort wird geladen',
'Loading location',
);
expect(element.querySelectorAll('app-location-poi')).toHaveLength(0);
});
@@ -224,7 +224,7 @@ describe('LocationPageComponent', () => {
fixture.detectChanges();
expect(element.querySelector('[role="alert"]')?.textContent).toContain(
'Ort konnte nicht geladen werden.',
'Could not load location.',
);
element.querySelector<HTMLButtonElement>('[data-location-retry]')?.click();

View File

@@ -1,28 +1,28 @@
<aside class="location-sidebar" aria-label="Ortsinformationen">
<aside class="location-sidebar" aria-label="Location Information">
<dl class="location-sidebar__identity">
<div>
<dt>Gebiet</dt>
<dt>Region</dt>
<dd>{{ location.regionName }}</dd>
</div>
<div>
<dt>Ort</dt>
<dt>Location</dt>
<dd class="location-sidebar__name">{{ location.name }}</dd>
</div>
<div>
<dt>Ortstyp</dt>
<dt>Location Type</dt>
<dd>{{ locationTypeLabel }}</dd>
</div>
<div>
<dt>Empfohlenes Ansehen</dt>
<dt>Recommended Renown</dt>
<dd class="location-sidebar__recommendation">{{ location.recommendationLabel }}</dd>
</div>
<div>
<dt>Gefahr</dt>
<dt>Danger</dt>
<dd>
@if (location.dangerRating; as rating) {
<app-danger-badge [rating]="rating" />
} @else {
<span class="location-sidebar__safe">Sicher</span>
<span class="location-sidebar__safe">Safe</span>
}
</dd>
</div>
@@ -30,7 +30,7 @@
@if (location.encounterPreview.length) {
<section class="location-sidebar__block" data-sidebar="encounters">
<h3>Mögliche Begegnungen</h3>
<h3>Possible Encounters</h3>
<ul class="location-sidebar__encounters">
@for (encounter of location.encounterPreview; track encounter.key) {
<li>
@@ -39,13 +39,13 @@
</li>
}
</ul>
<p class="location-sidebar__note">Vorschau — die Jagd würfelt eigenständig.</p>
<p class="location-sidebar__note">Preview — the hunt rolls independently.</p>
</section>
}
@if (interactions.length) {
<section class="location-sidebar__block" data-sidebar="interactions">
<h3>Verfügbare Interaktionen</h3>
<h3>Available Interactions</h3>
<ul class="location-sidebar__interactions">
@for (interaction of interactions; track interaction.label) {
<li>
@@ -59,7 +59,7 @@
@if (location.rewardPreview.length) {
<section class="location-sidebar__block" data-sidebar="rewards">
<h3>Mögliche Belohnungen</h3>
<h3>Possible Rewards</h3>
<ul class="location-sidebar__rewards">
@for (reward of location.rewardPreview; track reward.key) {
<li [attr.title]="reward.label">

View File

@@ -36,7 +36,7 @@ describe('LocationSidebarComponent', () => {
const element = await render(southGateFixture());
expect(element.querySelector('app-danger-badge')).toBeNull();
expect(element.textContent).toContain('Sicher');
expect(element.textContent).toContain('Safe');
});
it('previews the encounters and marks them as preview only', async () => {
@@ -47,7 +47,7 @@ describe('LocationSidebarComponent', () => {
'Aschenratte',
'Straßenräuber',
]);
expect(element.textContent).toContain('die Jagd würfelt eigenständig');
expect(element.textContent).toContain('the hunt rolls independently');
});
it('lists each interaction kind once, taken from the enabled hotspots', async () => {

View File

@@ -1,46 +1,46 @@
<section class="travel-panel" aria-labelledby="travel-panel-title">
@if (travellingTravel; as travel) {
<span class="travel-panel__eyebrow" role="status">REISE LÄUFT</span>
<h2 id="travel-panel-title">Reiseziel: {{ travel.targetLocation.name }}</h2>
<span class="travel-panel__eyebrow" role="status">TRAVELING</span>
<h2 id="travel-panel-title">Destination: {{ travel.targetLocation.name }}</h2>
<dl class="travel-panel__details">
<div>
<dt>Ankunft in</dt>
<dt>Arrival in</dt>
<dd>
<time role="timer">{{ formatDuration(remainingSeconds ?? 0) }}</time>
</dd>
</div>
<div>
<dt>Ziel</dt>
<dt>Destination</dt>
<dd>{{ travel.targetLocation.name }}</dd>
</div>
</dl>
<button type="button" data-travel-start disabled>Reise läuft</button>
<button type="button" data-travel-start disabled>Travel in progress</button>
} @else if (selectedConnection; as connection) {
<span class="travel-panel__eyebrow">REISEN</span>
<h2 id="travel-panel-title">Zur {{ connection.targetLocation.name }} reisen?</h2>
<span class="travel-panel__eyebrow">TRAVEL</span>
<h2 id="travel-panel-title">Travel to {{ connection.targetLocation.name }}?</h2>
<dl class="travel-panel__details">
<div>
<dt>Ziel</dt>
<dt>Destination</dt>
<dd>{{ connection.targetLocation.name }}</dd>
</div>
<div>
<dt>Reisezeit</dt>
<dt>Travel Time</dt>
<dd>{{ formatDuration(connection.travelDurationSeconds) }}</dd>
</div>
<div>
<dt>Gefahr</dt>
<dt>Danger</dt>
<dd [class.travel-panel__danger--low]="connection.danger === 'LOW'">
{{ dangerLabel(connection.danger) }}
</dd>
</div>
</dl>
<button type="button" data-travel-start [disabled]="busy" (click)="travelStart.emit()">
Reise beginnen
Begin Travel
</button>
} @else {
<span class="travel-panel__eyebrow">REISEN</span>
<span class="travel-panel__eyebrow">TRAVEL</span>
<p id="travel-panel-title" class="travel-panel__instruction">
Wähle einen erreichbaren Ort auf der Karte.
Select a reachable location on the map.
</p>
}
</section>

View File

@@ -27,6 +27,6 @@ export class TravelPanelComponent {
}
protected dangerLabel(danger: CurrentLocationConnection['danger']): string {
return danger === 'LOW' ? 'Niedrig' : 'Hoch';
return danger === 'LOW' ? 'Low' : 'High';
}
}

View File

@@ -1,6 +1,6 @@
<section class="world-page" aria-label="Weltansicht">
<section class="world-page" aria-label="World View">
@if (worldStore.currentLocation(); as location) {
<section class="world-page__scene" [attr.aria-label]="'Weltkarte bei ' + location.name">
<section class="world-page__scene" [attr.aria-label]="'World map at ' + location.name">
<div class="world-page__atmosphere" aria-hidden="true"></div>
<p class="world-page__location-title">{{ location.name }}</p>
@@ -43,18 +43,18 @@
/>
} @else {
<section class="world-page__empty" aria-live="polite">
<p>Weltkarte wird vorbereitet.</p>
<p>Preparing world map.</p>
</section>
}
@if (worldStore.loading()) {
<p class="world-page__loading" role="status">Weltzustand wird geladen</p>
<p class="world-page__loading" role="status">Loading world state</p>
}
@if (worldStore.error(); as error) {
<section class="world-page__error" role="alert">
<p>{{ error }}</p>
<button type="button" data-world-retry (click)="retry()">Erneut versuchen</button>
<button type="button" data-world-retry (click)="retry()">Retry</button>
</section>
}
</section>

View File

@@ -83,10 +83,10 @@ describe('WorldPageComponent', () => {
fixture.detectChanges();
expect(store.selectConnection).toHaveBeenCalledWith(burnedRoadConnection);
expect(element.textContent).toContain('Ziel');
expect(element.textContent).toContain('Destination');
expect(element.textContent).toContain('Verbrannte Straße');
expect(element.textContent).toContain('00:00:10');
expect(element.textContent).toContain('Niedrig');
expect(element.textContent).toContain('Low');
});
it('keeps each location at its geographic position when the authoritative current location swaps', () => {
@@ -140,8 +140,8 @@ describe('WorldPageComponent', () => {
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.textContent).toContain('Reiseziel: Verbrannte Straße');
expect(element.textContent).toContain('Ankunft in');
expect(element.textContent).toContain('Destination: Verbrannte Straße');
expect(element.textContent).toContain('Arrival in');
expect(element.textContent).toContain('00:00:06');
expect(element.querySelector<HTMLButtonElement>('[data-travel-start]')?.disabled).toBe(true);
expect(element.querySelector('app-travel-panel section')?.hasAttribute('aria-live')).toBe(
@@ -170,13 +170,13 @@ describe('WorldPageComponent', () => {
});
it('retries a displayed world error through the store', () => {
store.error.set('Weltzustand nicht verfügbar.');
store.error.set('World state unavailable.');
const fixture = TestBed.createComponent(WorldPageComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('[role="alert"]')?.textContent).toContain(
'Weltzustand nicht verfügbar.',
'World state unavailable.',
);
element.querySelector<HTMLButtonElement>('[data-world-retry]')?.click();