Translate auth flows and PM UI to English

This commit is contained in:
Remi
2025-09-18 23:46:06 +02:00
parent db4e30292c
commit a2ad53c51d
4 changed files with 163 additions and 135 deletions

View File

@@ -10,55 +10,80 @@
>
<section class="cookie-consent-panel">
<header class="cookie-consent-header">
<h2 id="cookie-consent-title">Cookies &amp; Analysen</h2>
<h2 id="cookie-consent-title">{{ isGerman ? 'Cookies &amp; Analysen' : 'Cookies &amp; analytics' }}</h2>
<p id="cookie-consent-description">
Wir verwenden notwendige Cookies, damit die Website zuverlässig funktioniert. Darüber hinaus würden wir gern optionale
Analyse-Cookies von Hotjar einsetzen, um das Nutzungserlebnis zu verbessern. Sie entscheiden, ob Sie diese zulassen
alle Details finden Sie in unserer
<a href="/datenschutz" target="_blank" rel="noopener">Datenschutzerklärung</a>.
<template v-if="isGerman">
Wir verwenden notwendige Cookies, damit die Website zuverlässig funktioniert. Darüber hinaus würden wir gern optionale
Analyse-Cookies von Hotjar einsetzen, um das Nutzungserlebnis zu verbessern. Sie entscheiden, ob Sie diese zulassen alle
Details finden Sie in unserer
</template>
<template v-else>
We use essential cookies to keep the site reliable. We would also like to use optional Hotjar analytics cookies to improve your
experience. You decide whether to allow themsee our
</template>
<a href="/datenschutz" target="_blank" rel="noopener">{{ isGerman ? 'Datenschutzerklärung' : 'privacy policy' }}</a>.
</p>
</header>
<div class="cookie-consent-options" role="list">
<article class="cookie-option is-required" role="listitem">
<div class="cookie-option-text">
<h3>Notwendige Cookies</h3>
<p>Speichern Ihre Auswahl und sorgen für sichere Anmeldung sowie eine stabile Grundfunktionalität.</p>
<h3>{{ isGerman ? 'Notwendige Cookies' : 'Essential cookies' }}</h3>
<p>
{{
isGerman
? 'Speichern Ihre Auswahl und sorgen für sichere Anmeldung sowie eine stabile Grundfunktionalität.'
: 'Store your selection and enable secure sign-in plus a stable core experience.'
}}
</p>
</div>
<span class="cookie-option-badge" aria-hidden="true">Immer aktiv</span>
<span class="cookie-option-badge" aria-hidden="true">{{ isGerman ? 'Immer aktiv' : 'Always on' }}</span>
</article>
<article class="cookie-option" role="listitem">
<div class="cookie-option-text">
<h3>Analyse (Hotjar)</h3>
<p>Hilft uns, Nutzungsverhalten anonym auszuwerten und unsere Inhalte gezielt zu verbessern.</p>
<h3>{{ isGerman ? 'Analyse (Hotjar)' : 'Analytics (Hotjar)' }}</h3>
<p>
{{
isGerman
? 'Hilft uns, Nutzungsverhalten anonym auszuwerten und unsere Inhalte gezielt zu verbessern.'
: 'Helps us analyse anonymised usage to improve content and product decisions.'
}}
</p>
</div>
<button
type="button"
class="cookie-toggle"
role="switch"
:aria-checked="analyticsSelection"
@click="toggleAnalytics"
type="button"
class="cookie-toggle"
role="switch"
:aria-checked="analyticsSelection"
@click="toggleAnalytics"
>
<span class="cookie-toggle-track" :class="{ 'is-active': analyticsSelection }">
<span class="cookie-toggle-thumb" />
</span>
<span class="sr-only">
Analyse-Cookies {{ analyticsSelection ? 'deaktivieren' : 'aktivieren' }}
{{
isGerman
? `Analyse-Cookies ${analyticsSelection ? 'deaktivieren' : 'aktivieren'}`
: `${analyticsSelection ? 'Disable' : 'Enable'} analytics cookies`
}}
</span>
</button>
</article>
</div>
<p class="cookie-consent-note">
Optionales Tracking wird erst nach Ihrer Zustimmung geladen. Sie können Ihre Entscheidung jederzeit über Cookie-Einstellungen
unten links widerrufen.
{{
isGerman
? 'Optionales Tracking wird erst nach Ihrer Zustimmung geladen. Sie können Ihre Entscheidung jederzeit über „Cookie-Einstellungen“ unten links widerrufen.'
: 'Optional tracking only loads after you consent. You can revisit your choice at any time via “Cookie settings” in the lower left.'
}}
</p>
<div class="cookie-consent-actions">
<button type="button" class="cookie-button ghost" @click="handleRejectAll">Nur notwendige</button>
<button type="button" class="cookie-button secondary" @click="handleSave">Auswahl speichern</button>
<button type="button" class="cookie-button primary" @click="handleAcceptAll">Alle akzeptieren</button>
<button type="button" class="cookie-button ghost" @click="handleRejectAll">{{ isGerman ? 'Nur notwendige' : 'Only necessary' }}</button>
<button type="button" class="cookie-button secondary" @click="handleSave">{{ isGerman ? 'Auswahl speichern' : 'Save selection' }}</button>
<button type="button" class="cookie-button primary" @click="handleAcceptAll">{{ isGerman ? 'Alle akzeptieren' : 'Accept all' }}</button>
</div>
</section>
</div>
@@ -66,19 +91,22 @@
<transition name="cookie-manage">
<button v-if="showManageButton" type="button" class="cookie-manage-button" @click="openManager">
Cookie-Einstellungen
{{ isGerman ? 'Cookie-Einstellungen' : 'Cookie settings' }}
</button>
</transition>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useState } from '#imports';
const { hasConsent, analyticsEnabled, acceptAll, rejectAll, savePreferences } = useCookieConsent();
const isDialogVisible = ref(!hasConsent.value);
const isManuallyOpened = ref(false);
const analyticsSelection = ref(analyticsEnabled.value);
const locale = useState<'de' | 'en'>('landing-locale', () => 'de');
const isGerman = computed(() => locale.value === 'de');
watch(
() => hasConsent.value,

View File

@@ -2,36 +2,36 @@
<div class="min-h-screen bg-[#0b1020] text-white">
<div class="mx-auto max-w-5xl px-6 py-12 lg:py-16 space-y-8">
<NuxtLink to="/login" class="inline-flex items-center gap-2 text-sm text-white/70 hover:text-cyan-300">
<v-icon icon="mdi-arrow-left" size="18" /> Zurück zum Login
<v-icon icon="mdi-arrow-left" size="18" /> Back to login
</NuxtLink>
<div class="grid gap-10 lg:grid-cols-[1.1fr_1fr] items-start">
<div class="space-y-6">
<div class="space-y-4">
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">Passworthilfe</p>
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">Password support</p>
<h1 class="text-3xl md:text-4xl font-semibold">
{{ isResetMode ? 'Neues Passwort festlegen' : 'Passwort zurücksetzen' }}
{{ isResetMode ? 'Set a new password' : 'Reset your password' }}
</h1>
<p class="text-white/70">
{{
isResetMode
? 'Lege jetzt ein neues Passwort für deinen Account fest.'
: 'Trage deine registrierte E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen.'
? 'Set a new password for your account now.'
: 'Enter the email you registered with and we will send you a reset link.'
}}
</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 space-y-4">
<h2 class="text-lg font-semibold">So funktioniert es</h2>
<h2 class="text-lg font-semibold">How it works</h2>
<ol v-if="!isResetMode" class="space-y-3 text-white/70 text-sm">
<li class="flex gap-3"><span class="chip">1</span><span>E-Mail-Adresse eingeben und Link anfordern.</span></li>
<li class="flex gap-3"><span class="chip">2</span><span>E-Mail öffnen und auf den Link klicken.</span></li>
<li class="flex gap-3"><span class="chip">3</span><span>Neues Passwort vergeben und einloggen.</span></li>
<li class="flex gap-3"><span class="chip">1</span><span>Enter your email address and request the link.</span></li>
<li class="flex gap-3"><span class="chip">2</span><span>Open the email and click the link.</span></li>
<li class="flex gap-3"><span class="chip">3</span><span>Choose a new password and sign in.</span></li>
</ol>
<ol v-else class="space-y-3 text-white/70 text-sm">
<li class="flex gap-3"><span class="chip">1</span><span>Neues, sicheres Passwort auswählen.</span></li>
<li class="flex gap-3"><span class="chip">2</span><span>Zur Sicherheit erneut bestätigen.</span></li>
<li class="flex gap-3"><span class="chip">3</span><span>Speichern und mit dem neuen Passwort anmelden.</span></li>
<li class="flex gap-3"><span class="chip">1</span><span>Choose a new secure password.</span></li>
<li class="flex gap-3"><span class="chip">2</span><span>Confirm it for safety.</span></li>
<li class="flex gap-3"><span class="chip">3</span><span>Save it and sign in with the new password.</span></li>
</ol>
</div>
</div>
@@ -39,7 +39,7 @@
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 space-y-6">
<form v-if="!isResetMode" class="space-y-4" @submit.prevent="submitRequest">
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">E-Mail</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Email</label>
<input
v-model.trim="requestForm.email"
type="email"
@@ -52,9 +52,9 @@
<button type="submit" class="btn btn-primary w-full" :disabled="requestLoading">
<span v-if="requestLoading" class="flex items-center justify-center gap-2">
<v-progress-circular indeterminate size="16" width="2" color="white" />
Link wird gesendet
Sending link
</span>
<span v-else>Link anfordern</span>
<span v-else>Request link</span>
</button>
<p v-if="requestError" class="text-sm text-red-300">{{ requestError }}</p>
@@ -62,22 +62,22 @@
v-else-if="requestSuccess"
class="rounded-xl border border-emerald-400/30 bg-emerald-400/10 p-4 text-sm text-emerald-100"
>
Wenn ein Konto zu dieser Adresse existiert, findest du gleich eine E-Mail mit weiteren Schritten in deinem Postfach.
If an account exists for this address, you'll soon find an email with the next steps in your inbox.
</div>
</form>
<div v-else class="space-y-4">
<div v-if="resetCompleted" class="space-y-4">
<div class="rounded-xl border border-emerald-400/30 bg-emerald-400/10 p-4 text-sm text-emerald-100">
Dein Passwort wurde erfolgreich geändert. Du kannst dich jetzt mit dem neuen Passwort anmelden.
Your password was changed successfully. You can now sign in with the new password.
</div>
<NuxtLink to="/login" class="btn btn-primary w-full">Zum Login</NuxtLink>
<NuxtLink to="/login" class="btn btn-primary w-full">Back to login</NuxtLink>
</div>
<form v-else class="space-y-4" @submit.prevent="submitReset">
<div class="space-y-3">
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Neues Passwort</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">New password</label>
<input
v-model="resetForm.password"
type="password"
@@ -86,10 +86,10 @@
autocomplete="new-password"
class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
/>
<p class="mt-1 text-xs text-white/50">Mindestens 8 Zeichen, gerne inkl. Zahl &amp; Sonderzeichen.</p>
<p class="mt-1 text-xs text-white/50">At least 8 characters, ideally include a number and special character.</p>
</div>
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Passwort bestätigen</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Confirm password</label>
<input
v-model="resetForm.confirm"
type="password"
@@ -100,20 +100,20 @@
</div>
</div>
<p v-if="passwordMismatch" class="text-xs text-red-300">Die Passwörter stimmen nicht überein.</p>
<p v-if="passwordMismatch" class="text-xs text-red-300">The passwords do not match.</p>
<button type="submit" class="btn btn-primary w-full" :disabled="resetLoading || !canSubmitReset">
<span v-if="resetLoading" class="flex items-center justify-center gap-2">
<v-progress-circular indeterminate size="16" width="2" color="white" />
Speichere neues Passwort
Saving new password…
</span>
<span v-else>Passwort speichern</span>
<span v-else>Save password</span>
</button>
<p v-if="resetError" class="text-sm text-red-300">{{ resetError }}</p>
<p class="text-xs text-white/60">
Link funktioniert nicht?
<NuxtLink to="/forgot-password" class="text-cyan-300 underline">Neuen Link anfordern</NuxtLink>
Link not working?
<NuxtLink to="/forgot-password" class="text-cyan-300 underline">Request a new link</NuxtLink>
</p>
</form>
</div>
@@ -196,7 +196,7 @@ async function submitRequest() {
requestSuccess.value = true
requestForm.email = ''
} catch (err: any) {
const message = err?.data?.statusMessage || err?.message || 'Senden fehlgeschlagen'
const message = err?.data?.statusMessage || err?.message || 'Sending failed'
requestError.value = message
} finally {
requestLoading.value = false
@@ -218,7 +218,7 @@ async function submitReset() {
resetForm.password = ''
resetForm.confirm = ''
} catch (err: any) {
const message = err?.data?.statusMessage || err?.message || 'Zurücksetzen fehlgeschlagen'
const message = err?.data?.statusMessage || err?.message || 'Reset failed'
resetError.value = message
} finally {
resetLoading.value = false
@@ -226,7 +226,7 @@ async function submitReset() {
}
useHead({
title: 'OpenSquawk Passwort zurücksetzen',
title: 'OpenSquawk Reset password',
meta: [
{ name: 'robots', content: 'noindex,nofollow' },
],

View File

@@ -4,26 +4,26 @@
<div class="grid gap-10 lg:grid-cols-[1.1fr_1fr] items-start">
<div class="space-y-6">
<NuxtLink to="/" class="inline-flex items-center gap-2 text-sm text-white/70 hover:text-cyan-300">
<v-icon icon="mdi-arrow-left" size="18" /> Zurück zur Landingpage
<v-icon icon="mdi-arrow-left" size="18" /> Back to landing page
</NuxtLink>
<div class="space-y-4">
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">OpenSquawk Alpha Access</p>
<h1 class="text-3xl md:text-4xl font-semibold">Melde dich an oder sichere dir deinen Invite</h1>
<h1 class="text-3xl md:text-4xl font-semibold">Sign in or claim your invite</h1>
<p class="text-white/70">
Registrierungen sind nur mit Einladungscode möglich. Codes werden nach Eintrag auf die Warteliste vergeben oder von bestehenden Mitgliedern nach zwei Wochen Nutzungsdauer ausgestellt.
Registration requires an invitation code. Codes are issued after joining the waitlist or shared by active members after two weeks of use.
</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 space-y-4">
<h2 class="text-lg font-semibold">So bekommst du Zugang</h2>
<h2 class="text-lg font-semibold">How to get access</h2>
<ol class="space-y-3 text-white/70 text-sm">
<li class="flex gap-3"><span class="chip">1</span><span>Auf der <NuxtLink to="/#cta" class="text-cyan-300 underline">Warteliste</NuxtLink> eintragen.</span></li>
<li class="flex gap-3"><span class="chip">2</span><span>Invite per E-Mail erhalten oder von einem aktiven Nutzer einen Code bekommen.</span></li>
<li class="flex gap-3"><span class="chip">3</span><span>Hier registrieren, AGB & Datenschutz bestätigen und direkt loslegen.</span></li>
<li class="flex gap-3"><span class="chip">1</span><span>Join the <NuxtLink to="/#cta" class="text-cyan-300 underline">waitlist</NuxtLink>.</span></li>
<li class="flex gap-3"><span class="chip">2</span><span>Receive your invite by email or get a code from an active member.</span></li>
<li class="flex gap-3"><span class="chip">3</span><span>Register here, accept the terms & privacy policy and start flying.</span></li>
</ol>
</div>
<div class="rounded-2xl border border-cyan-400/20 bg-cyan-400/10 p-5">
<p class="text-sm text-cyan-100">
Tipp: Nach 14 Tagen aktiver Nutzung kannst du selbst bis zu zwei Einladungscodes generieren und mit Freunden teilen.
Tip: After 14 days of active use you can generate up to two invitation codes yourself and share them with friends.
</p>
</div>
</div>
@@ -42,14 +42,14 @@
:class="mode === 'register' ? 'border-cyan-400 bg-cyan-400/20 text-white' : 'border-white/10 text-white/70 hover:text-white'"
@click="mode = 'register'"
>
Registrieren
Register
</button>
</div>
<form v-if="mode === 'login'" class="space-y-4" @submit.prevent="submitLogin">
<div class="space-y-3">
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">E-Mail</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Email</label>
<input
v-model.trim="loginForm.email"
type="email"
@@ -59,7 +59,7 @@
/>
</div>
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Passwort</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Password</label>
<input
v-model="loginForm.password"
type="password"
@@ -70,7 +70,7 @@
</div>
<div class="text-right">
<NuxtLink to="/forgot-password" class="text-xs text-cyan-300 underline hover:text-cyan-200">
Passwort vergessen?
Forgot your password?
</NuxtLink>
</div>
</div>
@@ -78,7 +78,7 @@
<button type="submit" class="btn btn-primary w-full" :disabled="loginLoading">
<span v-if="loginLoading" class="flex items-center justify-center gap-2">
<v-progress-circular indeterminate size="16" width="2" color="white" />
Anmeldung
Signing you in
</span>
<span v-else>Login</span>
</button>
@@ -94,12 +94,12 @@
v-model.trim="registerForm.name"
type="text"
autocomplete="name"
placeholder="Vorname Nachname"
placeholder="First Last"
class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
/>
</div>
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">E-Mail</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Email</label>
<input
v-model.trim="registerForm.email"
type="email"
@@ -109,7 +109,7 @@
/>
</div>
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Passwort</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Password</label>
<input
v-model="registerForm.password"
type="password"
@@ -118,13 +118,13 @@
autocomplete="new-password"
class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
/>
<p class="mt-1 text-xs text-white/50">Mindestens 8 Zeichen, gerne inkl. Zahl & Sonderzeichen.</p>
<p class="mt-1 text-xs text-white/50">At least 8 characters, ideally include a number and special character.</p>
</div>
<div>
<div class="flex items-center justify-between">
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Einladungscode</label>
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Invitation code</label>
<button type="button" class="text-[11px] text-cyan-300 underline" @click="checkInvitationCode" :disabled="!registerForm.invitationCode">
Code prüfen
Check code
</button>
</div>
<input
@@ -132,31 +132,31 @@
type="text"
required
class="mt-1 w-full uppercase tracking-widest rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
placeholder="z.B. ABCD1234"
placeholder="e.g. ABCD1234"
/>
<p v-if="invitationStatus === 'valid'" class="mt-1 text-xs text-green-300">Code ist gültig.</p>
<p v-else-if="invitationStatus === 'invalid'" class="mt-1 text-xs text-red-300">Dieser Einladungscode ist ungültig oder bereits verwendet.</p>
<p v-else-if="invitationStatus === 'checking'" class="mt-1 text-xs text-white/60">Prüfe Einladungscode</p>
<p v-if="invitationStatus === 'valid'" class="mt-1 text-xs text-green-300">Code is valid.</p>
<p v-else-if="invitationStatus === 'invalid'" class="mt-1 text-xs text-red-300">This invitation code is invalid or already used.</p>
<p v-else-if="invitationStatus === 'checking'" class="mt-1 text-xs text-white/60">Checking invitation code</p>
</div>
</div>
<div class="space-y-2 text-xs text-white/60">
<label class="flex items-start gap-3">
<input type="checkbox" v-model="registerForm.acceptTerms" class="mt-1" required />
<span>Ich akzeptiere die <NuxtLink to="/agb" class="text-cyan-300 underline">AGB</NuxtLink>.</span>
<span>I accept the <NuxtLink to="/agb" class="text-cyan-300 underline">Terms of Service</NuxtLink>.</span>
</label>
<label class="flex items-start gap-3">
<input type="checkbox" v-model="registerForm.acceptPrivacy" class="mt-1" required />
<span>Ich habe die <NuxtLink to="/datenschutz" class="text-cyan-300 underline">Datenschutzerklärung</NuxtLink> gelesen und stimme der Datenverarbeitung zu.</span>
<span>I have read the <NuxtLink to="/datenschutz" class="text-cyan-300 underline">privacy policy</NuxtLink> and consent to data processing.</span>
</label>
</div>
<button type="submit" class="btn btn-primary w-full" :disabled="registerLoading || !canRegister">
<span v-if="registerLoading" class="flex items-center justify-center gap-2">
<v-progress-circular indeterminate size="16" width="2" color="white" />
Registriere
Registering
</span>
<span v-else>Account erstellen</span>
<span v-else>Create account</span>
</button>
<p v-if="registerError" class="text-sm text-red-300">{{ registerError }}</p>
@@ -221,7 +221,7 @@ async function submitLogin() {
await auth.fetchUser()
router.push('/pm')
} catch (err: any) {
const message = err?.data?.statusMessage || err?.message || 'Anmeldung fehlgeschlagen'
const message = err?.data?.statusMessage || err?.message || 'Login failed'
loginError.value = message
} finally {
loginLoading.value = false
@@ -271,7 +271,7 @@ async function submitRegister() {
await auth.fetchUser()
router.push('/pm')
} catch (err: any) {
const message = err?.data?.statusMessage || err?.message || 'Registrierung fehlgeschlagen'
const message = err?.data?.statusMessage || err?.message || 'Registration failed'
registerError.value = message
} finally {
registerLoading.value = false
@@ -279,7 +279,7 @@ async function submitRegister() {
}
useHead({
title: 'OpenSquawk Login & Einladung',
title: 'OpenSquawk Login & invite',
meta: [
{ name: 'robots', content: 'noindex,nofollow' },
],

View File

@@ -21,8 +21,8 @@
<v-card class="bg-white/5 backdrop-blur border border-white/10">
<v-card-text class="space-y-4">
<div>
<h2 class="text-lg font-semibold">VATSIM Integration</h2>
<p class="text-sm text-white/70">Gib deine CID ein um deine gefilterten Flugpläne zu laden</p>
<h2 class="text-lg font-semibold">VATSIM integration</h2>
<p class="text-sm text-white/70">Enter your CID to load your filtered flight plans.</p>
</div>
<v-text-field
v-model="vatsimId"
@@ -41,7 +41,7 @@
:loading="loading"
@click="loadFlightPlans"
>
Flugpläne abrufen
Load flight plans
</v-btn>
<v-alert
v-if="error"
@@ -59,8 +59,8 @@
<v-card class="bg-white/5 backdrop-blur border border-white/10">
<v-card-text class="space-y-4">
<div>
<h2 class="text-lg font-semibold">Demo Mode</h2>
<p class="text-sm text-white/70">Starte mit einem Beispiel-Flugplan zum Testen</p>
<h2 class="text-lg font-semibold">Demo mode</h2>
<p class="text-sm text-white/70">Start with a sample flight plan to test the workflow.</p>
</div>
<v-btn
block
@@ -68,7 +68,7 @@
variant="outlined"
@click="startDemoFlight"
>
Demo starten (DLH39A EDDFEDDM)
Start demo (DLH39A EDDFEDDM)
</v-btn>
</v-card-text>
</v-card>
@@ -80,20 +80,20 @@
<v-btn icon @click="currentScreen = 'login'" class="text-cyan-300">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<h2 class="text-lg font-semibold">Verfügbare Flugpläne</h2>
<h2 class="text-lg font-semibold">Available flight plans</h2>
<v-chip color="cyan" variant="outlined" size="small">{{ vatsimId }}</v-chip>
</div>
<div v-if="loading" class="text-center py-8">
<v-progress-circular indeterminate color="cyan" class="mb-4" />
<p class="text-sm text-white/70">Lade Flugpläne von VATSIM...</p>
<p class="text-sm text-white/70">Loading flight plans from VATSIM</p>
</div>
<div v-else-if="flightPlans.length === 0" class="text-center py-8">
<v-icon size="48" class="text-white/30 mb-4">mdi-airplane-off</v-icon>
<p class="text-white/70">Keine Flugpläne gefunden</p>
<p class="text-white/70">No flight plans found</p>
<v-btn color="cyan" variant="outlined" class="mt-4" @click="currentScreen = 'login'">
Zurück
Back
</v-btn>
</div>
@@ -135,7 +135,7 @@
<v-card-text class="space-y-4">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/50">Aktiver Flug</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/50">Active flight</p>
<h2 class="text-2xl font-semibold">{{ flightContext.callsign || 'N/A' }}</h2>
<p class="text-sm text-white/70">{{ flightContext.dep }} {{ flightContext.dest }}</p>
</div>
@@ -203,7 +203,7 @@
@click="backToSetup"
prepend-icon="mdi-airplane-off"
>
Neuen Flug wählen
Choose a different flight
</v-btn>
</v-card-text>
</v-card>
@@ -247,7 +247,7 @@
@click="swapFrequencies"
:class="{ 'swap-animation': swapAnimation }"
>
Frequenzen tauschen
Swap frequencies
</v-btn>
<!-- Signal Quality -->
@@ -270,7 +270,7 @@
<v-card v-if="currentStep" class="bg-white/5 border border-white/10">
<v-card-text class="space-y-3">
<div class="flex items-center justify-between">
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Erwartete Kommunikation</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Expected communication</p>
<v-chip color="orange" variant="tonal" size="small">{{ currentStep.frequencyName }}</v-chip>
</div>
@@ -285,7 +285,7 @@
<div v-if="currentStep.pilot" class="space-y-2 rounded-2xl bg-blue-500/10 border border-blue-500/20 p-3 text-sm">
<div class="flex items-center gap-2 text-blue-300">
<v-icon size="16">mdi-account-pilot</v-icon>
<span class="text-xs uppercase font-semibold">Pilot (Du)</span>
<span class="text-xs uppercase font-semibold">Pilot (you)</span>
</div>
<p class="font-mono text-white">{{ normalizeExpectedText(currentStep.pilot) }}</p>
</div>
@@ -314,9 +314,9 @@
class="bg-cyan-500/10 text-cyan-100 mb-4"
density="compact"
>
Mikrofonberechtigung erforderlich r Push-To-Talk.
Microphone permission required for push-to-talk.
<template #append>
<v-btn color="cyan" size="small" variant="flat" @click="requestMicAccess">Erlauben</v-btn>
<v-btn color="cyan" size="small" variant="flat" @click="requestMicAccess">Allow</v-btn>
</template>
</v-alert>
@@ -344,7 +344,7 @@
{{ radioMode === 'atc' ? 'ATC' : 'INTERCOM' }}
</p>
<p class="mt-2 text-sm text-white/60">
{{ radioMode === 'atc' ? `Sendet auf ${frequencies.active}` : 'Crew Intercom • Checklist' }}
{{ radioMode === 'atc' ? `Transmitting on ${frequencies.active}` : 'Crew Intercom • Checklist' }}
</p>
</div>
</div>
@@ -376,7 +376,7 @@
prepend-icon="mdi-information-variant"
density="comfortable"
>
ATIS abhören
Listen to ATIS
</v-btn>
<v-btn
color="cyan"
@@ -386,7 +386,7 @@
prepend-icon="mdi-progress-check"
density="comfortable"
>
Voller Flug (Simulation)
Full flight (simulation)
</v-btn>
</div>
</v-card-text>
@@ -396,7 +396,7 @@
<v-card class="bg-white/5 border border-white/10">
<v-card-text class="space-y-3">
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-lg font-semibold">Frequenzübersicht</h3>
<h3 class="text-lg font-semibold">Frequency directory</h3>
<div class="flex items-center gap-2">
<v-progress-circular
v-if="airportFrequencyLoading"
@@ -421,7 +421,7 @@
<v-expansion-panels variant="accordion" class="bg-transparent">
<v-expansion-panel>
<v-expansion-panel-title class="text-sm text-white/70">
Frequenzen r {{ flightContext.dep || 'Abflug' }}
Frequencies for {{ flightContext.dep || 'Departure' }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<div
@@ -429,13 +429,13 @@
class="flex items-center gap-2 text-sm text-white/60"
>
<v-progress-circular indeterminate size="20" color="cyan" />
<span>Lade Frequenzen vom Netzwerk</span>
<span>Loading frequencies from the network</span>
</div>
<div
v-else-if="airportFrequencies.length === 0"
class="text-xs text-white/50"
>
Keine Frequenzen verfügbar. Bitte später erneut versuchen.
No frequencies available. Please try again later.
</div>
<div v-else class="space-y-3">
<div
@@ -527,7 +527,7 @@
@click:append-inner="sendPilotText"
/>
<p class="text-xs text-white/50">
Für Notfälle wenn PTT nicht funktioniert oder r Tests
For emergencies when PTT fails or for testing.
</p>
</v-card-text>
</v-card>
@@ -546,7 +546,7 @@
</v-chip>
</div>
<v-chip size="small" :color="simulationRunning ? 'orange' : 'grey'" variant="tonal">
{{ simulationRunning ? 'Läuft' : 'Bereit' }}
{{ simulationRunning ? 'Running' : 'Ready' }}
</v-chip>
</div>
@@ -564,7 +564,7 @@
v-if="simulationRunning && simulationTrace.length === 0"
class="text-sm text-white/60"
>
Simulation initialisiert...
Simulation initializing
</div>
<div
@@ -602,7 +602,7 @@
</div>
<div class="space-y-2 rounded-2xl border border-white/10 bg-black/30 p-3">
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Aktueller Knoten</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Current node</p>
<p class="font-mono text-sm text-white">{{ debugState?.id || '—' }}</p>
<p class="text-[11px] text-white/50">
{{ debugState ? `${debugState.role}${debugState.phase}` : 'N/A' }}
@@ -612,12 +612,12 @@
Auto: <span class="font-mono text-white">{{ debugState.sayPlain }}</span>
</p>
<p v-if="debugState?.sayNormalized" class="text-[11px] text-white/40">
Funk: {{ debugState.sayNormalized }}
Radio: {{ debugState.sayNormalized }}
</p>
</div>
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/40 mb-2">Nächste Entscheidungen</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/40 mb-2">Next decisions</p>
<div v-if="debugNextStates.length" class="space-y-2">
<div
v-for="state in debugNextStates"
@@ -646,11 +646,11 @@
ATC: <span class="font-mono text-white">{{ state.sayPlain }}</span>
</p>
<p v-if="state.sayNormalized" class="text-[11px] text-white/40">
Funk: {{ state.sayNormalized }}
Radio: {{ state.sayNormalized }}
</p>
</div>
</div>
<p v-else class="text-xs text-white/50">Keine weiteren Entscheidungen verfügbar.</p>
<p v-else class="text-xs text-white/50">No further decisions available.</p>
</div>
</v-card-text>
</v-card>
@@ -671,7 +671,7 @@
:disabled="log.length === 0"
@click="clearLog"
>
Leeren
Clear
</v-btn>
</div>
@@ -702,7 +702,7 @@
</div>
<p v-if="log.length === 0" class="text-xs text-white/50 text-center py-4">
Noch keine Kommunikation aufgezeichnet.
No transmissions logged yet.
</p>
</div>
</v-card-text>
@@ -730,7 +730,7 @@
class="text-xs uppercase tracking-[0.3em]"
:class="lastTransmissionFaulty ? 'text-red-300' : 'text-cyan-300'"
>
Letzte Übertragung
Last transmission
</span>
</div>
<div class="flex flex-wrap items-center gap-1">
@@ -740,7 +740,7 @@
color="red"
variant="flat"
>
Fehlerhaft
Faulty
</v-chip>
<v-btn
variant="text"
@@ -752,7 +752,7 @@
<template #prepend>
<v-icon size="16">mdi-alert-circle-outline</v-icon>
</template>
{{ lastTransmissionFaulty ? 'Fehler bearbeiten' : 'Fehler markieren' }}
{{ lastTransmissionFaulty ? 'Review issue' : 'Mark issue' }}
</v-btn>
<v-btn
v-if="lastTransmissionFaulty"
@@ -765,7 +765,7 @@
<template #prepend>
<v-icon size="16">mdi-restore</v-icon>
</template>
Zurücksetzen
Reset
</v-btn>
<v-btn
variant="text"
@@ -777,7 +777,7 @@
<template #prepend>
<v-icon size="16">mdi-close</v-icon>
</template>
Löschen
Delete
</v-btn>
</div>
</div>
@@ -786,12 +786,12 @@
v-if="lastTransmissionFaulty"
class="space-y-2 rounded-xl border border-red-400/30 bg-red-500/10 px-3 py-2"
>
<p class="text-xs uppercase tracking-[0.25em] text-red-200/80">Fehlerbeschreibung</p>
<p class="text-xs uppercase tracking-[0.25em] text-red-200/80">Issue description</p>
<p v-if="lastTransmissionFaultNote" class="text-sm text-red-100">
{{ lastTransmissionFaultNote }}
</p>
<p v-else class="text-xs text-red-200/70">
Als fehlerhaft markiert.
Marked as faulty.
</p>
</div>
</v-card-text>
@@ -801,16 +801,16 @@
<v-card class="bg-[#0b101d] border border-white/10 text-white">
<v-card-title class="flex items-center gap-2 text-base font-semibold">
<v-icon icon="mdi-alert-circle-outline" color="#f87171" size="20" />
Übertragung als fehlerhaft markieren
Flag transmission as faulty
</v-card-title>
<v-card-text class="space-y-4 text-sm text-white/70">
<p>
Hinterlasse optional eine kurze Beschreibung, damit das Dev Team die
Übertragung nachvollziehen kann.
Leave an optional short description so the dev team can trace the
transmission.
</p>
<v-textarea
v-model="transmissionIssueNote"
label="Fehlerbeschreibung (optional)"
label="Issue description (optional)"
variant="outlined"
color="red"
rows="3"
@@ -819,7 +819,7 @@
</v-card-text>
<v-card-actions class="justify-end gap-2">
<v-btn variant="text" color="grey" @click="cancelTransmissionIssue">
Abbrechen
Cancel
</v-btn>
<v-btn
v-if="lastTransmissionFaulty"
@@ -827,10 +827,10 @@
color="cyan"
@click="removeTransmissionIssue"
>
Markierung entfernen
Remove flag
</v-btn>
<v-btn color="red" variant="flat" @click="confirmTransmissionIssue">
Speichern
Save
</v-btn>
</v-card-actions>
</v-card>
@@ -844,7 +844,7 @@
<div class="space-y-4">
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40 block mb-2">
Signal Stärke
Signal strength
</label>
<v-slider
v-model="signalStrength"
@@ -863,14 +863,14 @@
v-model="radioEffectsEnabled"
color="cyan"
inset
label="Radio-Effekte"
label="Radio effects"
hide-details
/>
<v-switch
v-model="readbackEnabled"
color="cyan"
inset
label="Readback Stimme"
label="Readback voice"
hide-details
/>
<v-switch
@@ -1579,11 +1579,11 @@ const loadFlightPlans = async () => {
flightPlans.value = response.slice(0, 10)
currentScreen.value = 'flightselect'
} else {
error.value = 'Keine Flugpläne für diese VATSIM ID gefunden'
error.value = 'No flight plans found for this VATSIM ID'
}
} catch (err) {
console.error('Error loading flight plans:', err)
error.value = 'Fehler beim Laden der Flugpläne. Bitte VATSIM ID prüfen.'
error.value = 'Error loading flight plans. Please check the VATSIM ID.'
} finally {
loading.value = false
}
@@ -1947,7 +1947,7 @@ const playAtisBroadcast = async () => {
}
if (!content) {
setLastTransmission('ATIS: Keine aktuellen Informationen verfügbar')
setLastTransmission('ATIS: No current information available')
return
}