Translate UI text to English

Translate mobile app UI text to English
This commit is contained in:
2026-05-18 21:50:46 +02:00
parent 2ac1777570
commit b240d638ef
10 changed files with 159 additions and 167 deletions
+23 -23
View File
@@ -166,7 +166,7 @@ describe('EventListScreen', () => {
);
await waitFor(() => {
expect(getByText('Keine kommenden Termine')).toBeTruthy();
expect(getByText('No upcoming events')).toBeTruthy();
});
});
@@ -197,9 +197,9 @@ describe('EventListScreen', () => {
expect(getByText('Test Destination')).toBeTruthy();
});
await waitFor(() => {
expect(getByText('Losgehen in')).toBeTruthy();
expect(getByText('Leave in')).toBeTruthy();
expect(getByText('S1 -> Wien')).toBeTruthy();
expect(getByText('Inkl. Fußweg zur Station: 10 min')).toBeTruthy();
expect(getByText('Incl. walk to station: 10 min')).toBeTruthy();
});
});
@@ -241,12 +241,12 @@ describe('AddEventScreen', () => {
<AddEventScreen navigation={createMockNavigationProp<RootStack, 'AddEvent'>()} route={mockRouteAddEvent} />
);
expect(getByPlaceholderText('z.B. Team Meeting')).toBeTruthy();
expect(getByPlaceholderText('z.B. Technikum Wien')).toBeTruthy();
expect(getByPlaceholderText('JJJJ-MM-TT')).toBeTruthy();
expect(getByPlaceholderText('SS:MM')).toBeTruthy();
expect(getByText('Speichern')).toBeTruthy();
expect(getByText('Abbrechen')).toBeTruthy();
expect(getByPlaceholderText('e.g. Team Meeting')).toBeTruthy();
expect(getByPlaceholderText('e.g. Technikum Wien')).toBeTruthy();
expect(getByPlaceholderText('YYYY-MM-DD')).toBeTruthy();
expect(getByPlaceholderText('HH:MM')).toBeTruthy();
expect(getByText('Save')).toBeTruthy();
expect(getByText('Cancel')).toBeTruthy();
});
it('should show validation errors', () => {
@@ -255,11 +255,11 @@ describe('AddEventScreen', () => {
);
// Try to save without filling form
const saveButton = getByText('Speichern');
const saveButton = getByText('Save');
fireEvent.press(saveButton);
// Should show error text
expect(getByText('Titel erforderlich')).toBeTruthy();
expect(getByText('Title required')).toBeTruthy();
});
it('should validate date format', () => {
@@ -268,15 +268,15 @@ describe('AddEventScreen', () => {
);
// Fill in all required fields except date format is invalid
fireEvent.changeText(getByPlaceholderText('z.B. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('z.B. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('JJJJ-MM-TT'), 'invalid-date');
fireEvent.changeText(getByPlaceholderText('SS:MM'), '12:00');
fireEvent.changeText(getByPlaceholderText('e.g. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('e.g. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('YYYY-MM-DD'), 'invalid-date');
fireEvent.changeText(getByPlaceholderText('HH:MM'), '12:00');
const saveButton = getByText('Speichern');
const saveButton = getByText('Save');
fireEvent.press(saveButton);
expect(getByText('Ungültiges Datum')).toBeTruthy();
expect(getByText('Invalid date')).toBeTruthy();
});
it('should validate future date', () => {
@@ -285,14 +285,14 @@ describe('AddEventScreen', () => {
);
// Fill in all required fields with a past date
fireEvent.changeText(getByPlaceholderText('z.B. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('z.B. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('JJJJ-MM-TT'), '2020-01-01');
fireEvent.changeText(getByPlaceholderText('SS:MM'), '12:00');
fireEvent.changeText(getByPlaceholderText('e.g. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('e.g. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('YYYY-MM-DD'), '2020-01-01');
fireEvent.changeText(getByPlaceholderText('HH:MM'), '12:00');
const saveButton = getByText('Speichern');
const saveButton = getByText('Save');
fireEvent.press(saveButton);
expect(getByText('Datum muss in der Zukunft liegen')).toBeTruthy();
expect(getByText('Date must be in the future')).toBeTruthy();
});
});
+4 -4
View File
@@ -19,12 +19,12 @@ interface Props {
export function BikeSection({ bikeRoute, loading, origin, colors }: Props) {
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Radroute</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Bike route</Text>
{loading ? (
<View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} />
<Text style={[styles.hint, { color: colors.subtext }]}>Radroute wird geladen</Text>
<Text style={[styles.hint, { color: colors.subtext }]}>Loading bike route</Text>
</View>
) : bikeRoute ? (
<View style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }]}>
@@ -37,12 +37,12 @@ export function BikeSection({ bikeRoute, loading, origin, colors }: Props) {
<Text style={[styles.value, { color: colors.accent }]}>{formatDistance(bikeRoute.distance)}</Text>
</View>
<View style={[styles.mapPlaceholder, { backgroundColor: colors.background, borderColor: colors.border }]}>
<Text style={[styles.mapText, { color: colors.subtext }]}>🗺 Karte (post-MVP)</Text>
<Text style={[styles.mapText, { color: colors.subtext }]}>🗺 Map (post-MVP)</Text>
</View>
</View>
) : (
<Text style={[styles.empty, { color: colors.subtext }]}>
{origin ? 'Keine Radroute verfügbar' : 'Ursprungstation festlegen'}
{origin ? 'No bike route available' : 'Set origin station'}
</Text>
)}
</View>
+17 -17
View File
@@ -45,25 +45,25 @@ export function JourneyList({
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Zugverbindungen</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Train connections</Text>
{destStationLoading && (
<View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} />
<Text style={[styles.hint, { color: colors.subtext }]}>Ziel-Station wird aufgelöst</Text>
<Text style={[styles.hint, { color: colors.subtext }]}>Resolving destination station</Text>
</View>
)}
{journeys.length === 0 && !destStationLoading ? (
<Text style={[styles.empty, { color: colors.subtext }]}>
{origin ? 'Keine Verbindungen gefunden' : 'Ursprungstation festlegen'}
{origin ? 'No connections found' : 'Set origin station'}
</Text>
) : (
<TouchableOpacity
activeOpacity={rankedJourneys.length > 1 ? 0.75 : 1}
onPress={() => rankedJourneys.length > 1 && setExpanded((current) => !current)}
accessibilityRole={rankedJourneys.length > 1 ? 'button' : undefined}
accessibilityLabel={expanded ? 'Weniger Zugverbindungen anzeigen' : 'Alle Zugverbindungen anzeigen'}
accessibilityLabel={expanded ? 'Show fewer train connections' : 'Show all train connections'}
>
{visibleJourneys.map(({ journey: j }, index) => {
const finalArrival = new Date(j.rA.getTime() + walkDurationMs);
@@ -90,24 +90,24 @@ export function JourneyList({
<Text style={[styles.delayBadge, { backgroundColor: colors.error }]}>+{j.delay} min</Text>
)}
{j.cancelled && (
<Text style={[styles.cancelBadge, { backgroundColor: colors.text }]}>Storniert</Text>
<Text style={[styles.cancelBadge, { backgroundColor: colors.text }]}>Cancelled</Text>
)}
</View>
<Text style={[styles.detail, { color: colors.text }]}>
Abfahrt: {new Date(j.sD).toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit' })}
{' '}(Plattform {j.platform || '—'})
Departure: {new Date(j.sD).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
{' '}(Platform {j.platform || '—'})
</Text>
<Text style={[styles.detail, { color: colors.subtext }]}>
Ankunft: {new Date(j.sA).toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit' })}
{' '}({j.changes === 0 ? 'Direkt' : `${j.changes} Umst.`})
Arrival: {new Date(j.sA).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
{' '}({j.changes === 0 ? 'Direct' : `${j.changes} tr.`})
</Text>
<Text style={[styles.detail, { color: colors.subtext }]}>
Dauer: {durationMinutes} min
Duration: {durationMinutes} min
</Text>
{walkDurationMs > 0 && (
<Text style={[styles.detail, { color: arrivesTooLate ? colors.error : colors.subtext }]}>
Ziel: {finalArrival.toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit' })}
{arrivesTooLate ? ' (zu spät)' : ''}
Arrive: {finalArrival.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
{arrivesTooLate ? ' (too late)' : ''}
</Text>
)}
</View>
@@ -115,7 +115,7 @@ export function JourneyList({
})}
{rankedJourneys.length > 1 && (
<Text style={[styles.expandHint, { color: colors.accent }]}>
{expanded ? 'Weniger Verbindungen anzeigen' : `${rankedJourneys.length - 1} weitere Verbindungen anzeigen`}
{expanded ? 'Show fewer connections' : `Show ${rankedJourneys.length - 1} more connections`}
</Text>
)}
</TouchableOpacity>
@@ -123,13 +123,13 @@ export function JourneyList({
{showWalkingOption && walkRoute && (
<View style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border, marginTop: 10 }]}>
<Text style={[styles.walkTitle, { color: colors.text }]}>🚶 Finaler Fußweg</Text>
<Text style={[styles.walkTitle, { color: colors.text }]}>🚶 Final walk</Text>
<View style={styles.row}>
<Text style={[styles.walkLabel, { color: colors.text }]}> Dauer</Text>
<Text style={[styles.walkLabel, { color: colors.text }]}> Duration</Text>
<Text style={[styles.walkValue, { color: colors.accent }]}>{formatDuration(walkRoute.duration)}</Text>
</View>
<View style={styles.row}>
<Text style={[styles.walkLabel, { color: colors.text }]}>📏 Distanz</Text>
<Text style={[styles.walkLabel, { color: colors.text }]}>📏 Distance</Text>
<Text style={[styles.walkValue, { color: colors.accent }]}>{formatDistance(walkRoute.distance)}</Text>
</View>
</View>
@@ -138,7 +138,7 @@ export function JourneyList({
{showWalkingOption && loadingWalk && (
<View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} />
<Text style={[styles.hint, { color: colors.subtext }]}>Fußweg wird geladen</Text>
<Text style={[styles.hint, { color: colors.subtext }]}>Loading walk route</Text>
</View>
)}
</View>
+3 -3
View File
@@ -22,12 +22,12 @@ export function NearbyStops({ stops, departures, loading, error, colors }: Props
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>🚏 ÖPNV in der Nähe des Ziels</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>🚏 Public transit near destination</Text>
{loading ? (
<View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} />
<Text style={[styles.hint, { color: colors.subtext }]}>Haltestellen werden geladen</Text>
<Text style={[styles.hint, { color: colors.subtext }]}>Loading stops</Text>
</View>
) : departures.length > 0 ? (
departures.slice(0, 8).map((dep, i) => (
@@ -43,7 +43,7 @@ export function NearbyStops({ stops, departures, loading, error, colors }: Props
{dep.direction}
</Text>
<Text style={[styles.minutes, { color: dep.minutes <= 2 ? colors.error : colors.accent }]}>
{dep.minutes === 0 ? 'jetzt' : `${dep.minutes} min`}
{dep.minutes === 0 ? 'now' : `${dep.minutes} min`}
</Text>
</View>
</View>
+1 -1
View File
@@ -93,7 +93,7 @@ export function useWienerLinien(
await fetchMonitor(ids);
} catch (err) {
if (cancelledRef.current) return;
setError(err instanceof Error ? err.message : 'Haltestellen konnten nicht geladen werden');
setError(err instanceof Error ? err.message : 'Stops could not be loaded');
setLoading(false);
}
}, DEBOUNCE_MS);
+17 -17
View File
@@ -52,12 +52,12 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
}, [route.params?.editEventId]);
const validate = (): boolean => {
if (!title.trim()) { setError('Titel erforderlich'); return false; }
if (!destination.trim()) { setError('Ziel erforderlich'); return false; }
if (!dateStr || !timeStr) { setError('Datum und Zeit erforderlich'); return false; }
if (!title.trim()) { setError('Title required'); return false; }
if (!destination.trim()) { setError('Destination required'); return false; }
if (!dateStr || !timeStr) { setError('Date and time required'); return false; }
const eventTime = new Date(`${dateStr}T${timeStr}`);
if (isNaN(eventTime.getTime())) { setError('Ungültiges Datum'); return false; }
if (eventTime <= new Date()) { setError('Datum muss in der Zukunft liegen'); return false; }
if (isNaN(eventTime.getTime())) { setError('Invalid date'); return false; }
if (eventTime <= new Date()) { setError('Date must be in the future'); return false; }
setError('');
return true;
};
@@ -96,40 +96,40 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
return (
<View style={[styles.container, { backgroundColor: colors.background, position: 'relative' }]}>
<View style={styles.form}>
<Text style={[styles.label, { color: colors.text }]}>Titel</Text>
<Text style={[styles.label, { color: colors.text }]}>Title</Text>
<TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="z.B. Team Meeting"
placeholder="e.g. Team Meeting"
placeholderTextColor={colors.subtext}
value={title}
onChangeText={setTitle}
autoCapitalize="words"
/>
<Text style={[styles.label, { color: colors.text }]}>Ziel</Text>
<Text style={[styles.label, { color: colors.text }]}>Destination</Text>
<TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="z.B. Technikum Wien"
placeholder="e.g. Technikum Wien"
placeholderTextColor={colors.subtext}
value={destination}
onChangeText={setDestination}
autoCapitalize="words"
/>
<Text style={[styles.label, { color: colors.text }]}>Datum</Text>
<Text style={[styles.label, { color: colors.text }]}>Date</Text>
<TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="JJJJ-MM-TT"
placeholder="YYYY-MM-DD"
placeholderTextColor={colors.subtext}
value={dateStr}
onChangeText={setDateStr}
keyboardType="numbers-and-punctuation"
/>
<Text style={[styles.label, { color: colors.text }]}>Zeit</Text>
<Text style={[styles.label, { color: colors.text }]}>Time</Text>
<TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="SS:MM"
placeholder="HH:MM"
placeholderTextColor={colors.subtext}
value={timeStr}
onChangeText={setTimeStr}
@@ -139,14 +139,14 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
{error ? <Text style={[styles.errorText, { color: colors.error }]}>{error}</Text> : null}
<TouchableOpacity style={styles.saveBtn} onPress={handleSave}>
<Text style={styles.saveBtnText}>{route.params?.editEventId ? 'Aktualisieren' : 'Speichern'}</Text>
<Text style={styles.saveBtnText}>{route.params?.editEventId ? 'Update' : 'Save'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.saveBtn, styles.cancelBtn, { backgroundColor: colors.border }]}
onPress={() => navigation.goBack()}
>
<Text style={[styles.cancelText, { color: colors.text }]}>Abbrechen</Text>
<Text style={[styles.cancelText, { color: colors.text }]}>Cancel</Text>
</TouchableOpacity>
</View>
@@ -156,9 +156,9 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
<View style={styles.successCircle}>
<Text style={styles.checkmark}></Text>
</View>
<Text style={[styles.successTitle, { color: colors.text }]}>Erfolg!</Text>
<Text style={[styles.successTitle, { color: colors.text }]}>Success!</Text>
<Text style={[styles.successSubtitle, { color: colors.subtext }]}>
{route.params?.editEventId ? 'Termin aktualisiert' : 'Termin hinzugefügt'}
{route.params?.editEventId ? 'Event updated' : 'Event added'}
</Text>
</View>
</View>
@@ -158,7 +158,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
const handleImport = async () => {
if (!url.trim()) {
setError('Bitte ICS-URL eingeben');
setError('Please enter ICS URL');
return;
}
@@ -188,7 +188,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
}
setCount(added);
} catch (err) {
setError(err instanceof Error ? err.message : 'Import fehlgeschlagen');
setError(err instanceof Error ? err.message : 'Import failed');
} finally {
setLoading(false);
}
@@ -228,7 +228,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
setCount(added);
} catch (err) {
setError(err instanceof Error ? err.message : 'Sync fehlgeschlagen');
setError(err instanceof Error ? err.message : 'Sync failed');
} finally {
setLoading(false);
}
@@ -240,14 +240,14 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
return (
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.content}>
<Text style={[styles.heading, { color: colors.text }]}>Kalender-Import</Text>
<Text style={[styles.heading, { color: colors.text }]}>Calendar Import</Text>
<Text style={[styles.description, { color: colors.subtext }]}>
Importiere Termine über eine ICS-URL oder sync mit dem Geräte-Kalender.
Import events via an ICS URL or sync with the device calendar.
</Text>
{/* ── ICS URL Import ── */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>ICS-URL Import</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>ICS URL Import</Text>
<TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
@@ -267,7 +267,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.importBtnText}>ICS Importieren</Text>
<Text style={styles.importBtnText}>Import ICS</Text>
)}
</TouchableOpacity>
</View>
@@ -275,33 +275,33 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{/* ── Calendar Selection ── */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Kalender auswählen</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Select calendars</Text>
<View style={styles.selectionActions}>
<TouchableOpacity onPress={selectAll} accessibilityLabel="Alle auswählen">
<Text style={[styles.linkText, { color: colors.accent }]}>Alle</Text>
<TouchableOpacity onPress={selectAll} accessibilityLabel="Select all">
<Text style={[styles.linkText, { color: colors.accent }]}>All</Text>
</TouchableOpacity>
<Text style={[styles.dividerText, { color: colors.subtext }]}> · </Text>
<TouchableOpacity onPress={deselectAll} accessibilityLabel="Alle abwählen">
<Text style={[styles.linkText, { color: colors.accent }]}>Keine</Text>
<TouchableOpacity onPress={deselectAll} accessibilityLabel="Deselect all">
<Text style={[styles.linkText, { color: colors.accent }]}>None</Text>
</TouchableOpacity>
</View>
</View>
<Text style={[styles.sectionDesc, { color: colors.subtext }]}>
Wähle die Kalender aus, die gesynct werden sollen. Ohne Auswahl werden alle Kalender verwendet.
Select the calendars to sync. If no selection is made, all calendars will be used.
{'\n'}
CalDAV-Quellen (DAVx5, Apple Calendar, etc.) werden automatisch erkannt.
CalDAV sources (DAVx5, Apple Calendar, etc.) are detected automatically.
</Text>
{!calendarsLoaded ? (
<View style={styles.loadingContainer}>
<ActivityIndicator color={colors.accent} />
<Text style={[styles.loadingText, { color: colors.subtext }]}>Kalender werden geladen</Text>
<Text style={[styles.loadingText, { color: colors.subtext }]}>Loading calendars</Text>
</View>
) : availableCalendars.length === 0 ? (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.subtext }]}>
Keine Kalender auf diesem Gerät gefunden.
No calendars found on this device.
</Text>
</View>
) : (
@@ -332,7 +332,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{hasSelection && (
<View style={[styles.selectionInfo, { backgroundColor: colors.highlight }]}>
<Text style={[styles.selectionInfoText, { color: colors.text }]}>
{selectedCalendarIds.size} von {availableCalendars.length} Kalendern ausgewählt
{selectedCalendarIds.size} of {availableCalendars.length} calendars selected
</Text>
</View>
)}
@@ -342,9 +342,9 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{/* ── Device Calendar Sync ── */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Geräte-Kalender Sync</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Device Calendar Sync</Text>
<Text style={[styles.sectionDesc, { color: colors.subtext }]}>
Hole Termine der nächsten 30 Tage aus den Kalendern auf deinem Gerät.
Fetch events for the next 30 days from the calendars on your device.
</Text>
<TouchableOpacity
@@ -352,7 +352,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
onPress={handleSyncNative}
disabled={loading}
>
<Text style={styles.importBtnText}>📅 Kalender Sync</Text>
<Text style={styles.importBtnText}>📅 Calendar Sync</Text>
</TouchableOpacity>
</View>
@@ -365,7 +365,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{count !== null && (
<View style={[styles.successBanner, { backgroundColor: colors.success }]}>
<Text style={styles.successText}>
{count} Termin(e) erfolgreich importiert!
{count} event(s) successfully imported!
</Text>
</View>
)}
@@ -374,7 +374,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
style={styles.backBtn}
onPress={() => navigation.goBack()}
>
<Text style={[styles.backBtnText, { color: colors.accent }]}> Zurück</Text>
<Text style={[styles.backBtnText, { color: colors.accent }]}> Back</Text>
</TouchableOpacity>
</View>
</ScrollView>
+15 -15
View File
@@ -89,7 +89,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
setShowWalkingOption(settings.showWalkingOption);
const found = events.find((e) => e.id === eventId);
if (!found) { setError('Termin nicht gefunden'); return; }
if (!found) { setError('Event not found'); return; }
setEvent(found);
if (originStation) {
@@ -112,7 +112,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
const results = await api.searchJourneys(originStation.extId, destExtId, target, { arriveBy: true });
setJourneys(results);
} else if (destStation.error) {
setError(`Ziel-Station nicht auflösbar: ${destStation.error}`);
setError(`Destination station not resolvable: ${destStation.error}`);
}
try {
@@ -131,7 +131,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
}
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden');
setError(err instanceof Error ? err.message : 'Error loading');
} finally {
setLoading(false);
}
@@ -170,7 +170,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
<View style={[styles.center, { backgroundColor: colors.background }]}>
<ActivityIndicator size="large" color={colors.accent} />
<Text style={[styles.loadingText, { color: colors.subtext }]}>
{destStation.loading && !event ? 'Ziel-Station wird aufgelöst…' : 'Termine werden geladen…'}
{destStation.loading && !event ? 'Resolving destination station…' : 'Loading events…'}
</Text>
</View>
);
@@ -200,9 +200,9 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
{!origin && !error && (
<View style={[styles.warningBanner, { backgroundColor: colors.warning }]}>
<Text style={styles.bannerText}>
Keine Ursprungstation festgelegt.{' '}
No origin station set.{' '}
<Text style={styles.bannerLink} onPress={() => navigation.navigate('Settings')}>
Einstellungen öffnen
Open settings
</Text>
</Text>
</View>
@@ -218,15 +218,15 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
onPress={() => setActiveMode('train')}
>
<View style={styles.modeHeader}>
<Text style={[styles.modeLabel, { color: colors.text }]}>🚆 Zug</Text>
<Text style={[styles.modeLabel, { color: colors.text }]}>🚆 Train</Text>
{effectiveMode === 'train' && (
<View style={[styles.activeBadge, { backgroundColor: colors.accent }]}>
<Text style={styles.activeBadgeText}>Aktiv</Text>
<Text style={styles.activeBadgeText}>Active</Text>
</View>
)}
</View>
<Text style={[styles.modeMeta, { color: colors.subtext }]}>
{showWalkingOption ? 'Bahn + finaler Fußweg' : 'Nur Bahn'}
{showWalkingOption ? 'Train + final walk' : 'Train only'}
</Text>
</TouchableOpacity>
@@ -240,19 +240,19 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
disabled={bikeDisabled}
>
<View style={styles.modeHeader}>
<Text style={[styles.modeLabel, { color: colors.text }]}>🚲 Rad</Text>
<Text style={[styles.modeLabel, { color: colors.text }]}>🚲 Bike</Text>
{effectiveMode === 'bike' && (
<View style={[styles.activeBadge, { backgroundColor: colors.accent }]}>
<Text style={styles.activeBadgeText}>Aktiv</Text>
<Text style={styles.activeBadgeText}>Active</Text>
</View>
)}
</View>
<Text style={[styles.modeMeta, { color: colors.subtext }]}>
{bikeDisabled
? 'In Einstellungen deaktiviert'
? 'Disabled in settings'
: loadingBike
? 'Route wird berechnet...'
: 'Direktweg'}
? 'Calculating route...'
: 'Direct route'}
</Text>
</TouchableOpacity>
</View>
@@ -293,7 +293,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
style={[styles.refreshBtn, { backgroundColor: colors.border }]}
onPress={handleRefresh}
>
<Text style={[styles.refreshBtnText, { color: colors.text }]}>🔄 Neu laden</Text>
<Text style={[styles.refreshBtnText, { color: colors.text }]}>🔄 Refresh</Text>
</TouchableOpacity>
<View style={{ height: 40 }} />
+24 -32
View File
@@ -145,7 +145,7 @@ export function EventListScreen({ navigation }: ScreenProps) {
setJourneys(results);
} catch (err) {
if (!isMounted) return;
setJourneysError(err instanceof Error ? err.message : 'Verbindungen konnten nicht geladen werden');
setJourneysError(err instanceof Error ? err.message : 'Connections could not be loaded');
} finally {
if (isMounted) setJourneysLoading(false);
}
@@ -175,16 +175,16 @@ export function EventListScreen({ navigation }: ScreenProps) {
const renderItem = ({ item }: { item: CalendarEvent }) => {
const leaveBy = departureInfo.departureTime;
const leaveCountdown = leaveBy ? calculateCountdown(leaveBy) : null;
const leaveCountdownLabel = leaveCountdown?.label === 'Now' ? 'Jetzt' : leaveCountdown?.label;
const leaveCountdownLabel = leaveCountdown?.label;
const leaveByLabel = leaveBy ? formatTime(leaveBy) : null;
const trainLabel = selectedJourney?.trains.length ? selectedJourney.trains.join(', ') : 'Zugverbindung wird gesucht';
const trainLabel = selectedJourney?.trains.length ? selectedJourney.trains.join(', ') : 'Searching for train connection';
const status = leaveCountdown
? leaveCountdown.label === 'Now'
? 'Jetzt losgehen'
: `Losgehen in ${leaveCountdown.label}`
? 'Leave now'
: `Leave in ${leaveCountdown.label}`
: journeysLoading || destStation.loading
? 'Losgehzeit wird berechnet'
: 'Keine Verbindung';
? 'Calculating departure time'
: 'No connection';
return (
<View style={styles.cardWrapper}>
@@ -197,12 +197,13 @@ export function EventListScreen({ navigation }: ScreenProps) {
<View style={styles.dotRow}>
<View style={[styles.dot, { backgroundColor: leaveCountdown?.urgent ? colors.delete : '#34C759' }]} />
<Text style={[styles.title, { color: colors.text }]}>{item.title}</Text>
<Text style={[styles.badge, { color: leaveByLabel ? colors.accent : colors.subtext }]}>
{leaveByLabel ?? '--:--'}
</Text>
</View>
<Text style={[styles.subtitle, { color: colors.subtext }]}>{item.destination}</Text>
<Text style={[styles.leaveLabel, { color: colors.subtext }]}>Losgehen in</Text>
<Text style={[styles.leaveLabel, { color: colors.subtext }]}>Time To Leave</Text>
<Text style={[styles.leaveTime, { color: leaveByLabel ? colors.accent : colors.subtext }]}>
{leaveByLabel ?? '--:--'}
</Text>
<Text style={[styles.leaveLabel, { color: colors.subtext }]}>Leave in</Text>
<Text
style={[
styles.leaveTime,
@@ -213,41 +214,32 @@ export function EventListScreen({ navigation }: ScreenProps) {
? leaveCountdownLabel
: journeysLoading || destStation.loading
? '--'
: 'Keine Verbindung'}
: 'No connection'}
</Text>
<View style={[styles.trainBox, { borderColor: colors.border }]}>
<Text style={[styles.trainTitle, { color: colors.text }]} numberOfLines={2}>
{journeysError ? 'Zugverbindung nicht erreichbar' : trainLabel}
{journeysError ? 'Train connection unavailable' : trainLabel}
</Text>
{selectedJourney ? (
<Text style={[styles.trainMeta, { color: colors.subtext }]}>
Abfahrt {formatTime(selectedJourney.rD)}
{selectedJourney.platform ? ` · Gleis ${selectedJourney.platform}` : ''}
Departure {formatTime(selectedJourney.rD)}
{selectedJourney.platform ? ` · Platform ${selectedJourney.platform}` : ''}
{' · '}
Ankunft {formatTime(selectedJourney.rA)}
Arrival {formatTime(selectedJourney.rA)}
{' · '}
{selectedJourney.changes === 0 ? 'Direkt' : `${selectedJourney.changes} Umstiege`}
{selectedJourney.changes === 0 ? 'Direct' : `${selectedJourney.changes} transfers`}
</Text>
) : (
<Text style={[styles.trainMeta, { color: journeysError ? colors.delete : colors.subtext }]}>
{journeysError ?? (origin ? 'Beste Verbindung für den nächsten Termin' : 'Ursprungstation festlegen')}
{journeysError ?? (origin ? 'Best connection for the next event' : 'Set origin station')}
</Text>
)}
{originWalk.walkRoute && originWalk.walkRoute.duration > 30 && (
<Text style={[styles.trainMeta, { color: colors.subtext }]}>
Inkl. Fußweg zur Station: {Math.ceil(originWalk.walkRoute.duration / 60)} min
Incl. walk to station: {Math.ceil(originWalk.walkRoute.duration / 60)} min
</Text>
)}
</View>
<Text style={[styles.time, { color: colors.subtext }]}>
Termin: {item.eventTime.toLocaleString('de-AT', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
})}
</Text>
<Text style={[styles.status, { color: leaveCountdown?.urgent ? colors.delete : '#34C759' }]}>{status}</Text>
</View>
</TouchableOpacity>
@@ -256,12 +248,12 @@ export function EventListScreen({ navigation }: ScreenProps) {
onPress={() => navigation.navigate('AddEvent', { editEventId: item.id })}
style={styles.editBtn}
>
<Text style={[styles.editText, { color: colors.accent }]}>Bearbeiten</Text>
<Text style={[styles.editText, { color: colors.accent }]}>edit</Text>
</TouchableOpacity>
{/* Delete button */}
<TouchableOpacity onPress={() => removeEvent(item.id, reload)} style={styles.deleteBtn}>
<Text style={[styles.deleteText, { color: colors.delete }]}>Entfernen</Text>
<Text style={[styles.deleteText, { color: colors.delete }]}>delete</Text>
</TouchableOpacity>
</View>
);
@@ -270,12 +262,12 @@ export function EventListScreen({ navigation }: ScreenProps) {
if (!upcomingEvent) {
return (
<View style={[styles.center, { backgroundColor: colors.background }]}>
<Text style={[styles.empty, { color: colors.subtext }]}>Keine kommenden Termine</Text>
<Text style={[styles.empty, { color: colors.subtext }]}>No upcoming events</Text>
<TouchableOpacity
style={styles.addBtn}
onPress={() => navigation.navigate('AddEvent')}
>
<Text style={styles.addBtnText}>+ Termin hinzufügen</Text>
<Text style={styles.addBtnText}>+ Add event</Text>
</TouchableOpacity>
</View>
);
+33 -33
View File
@@ -73,10 +73,10 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
try {
const stations = await api.searchStation(q.trim());
setResults(stations);
if (stations.length === 0) setSearchError('Keine Stationen gefunden.');
if (stations.length === 0) setSearchError('No stations found.');
} catch {
setResults([]);
setSearchError('API nicht erreichbar. Läuft der Server auf deinem Gerät? Überprüfe EXPO_PUBLIC_API_BASE_URL in .env.');
setSearchError('API not reachable. Is the server running on your device? Check EXPO_PUBLIC_API_BASE_URL in .env.');
} finally {
setSearching(false);
}
@@ -97,9 +97,9 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
try {
await saveOriginStation(station);
await rescheduleAllNotifications();
Alert.alert('Station gespeichert', station.name);
Alert.alert('Station saved', station.name);
} catch {
Alert.alert('Fehler', 'Station konnte nicht gespeichert werden.');
Alert.alert('Error', 'Station could not be saved.');
}
};
@@ -109,7 +109,7 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
setLocPermission(status === 'granted' ? 'granted' : 'denied');
if (status !== 'granted') {
Alert.alert('Berechtigung erforderlich', 'Standortzugriff ist nötig für die automatische Stationssuche.');
Alert.alert('Permission required', 'Location access is required for automatic station search.');
return;
}
@@ -149,7 +149,7 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
try {
const nearestStation = await api.findNearestStationByCoords(userLat, userLng);
if (!nearestStation) {
Alert.alert('Keine Station gefunden', 'Kein ÖPNV-Halt in der Nähe gefunden.');
Alert.alert('No station found', 'No public transport stop found nearby.');
return;
}
await selectStation({
@@ -164,14 +164,14 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
if (!apiReachable) {
Alert.alert(
'API nicht erreichbar',
'Der Server konnte nicht erreicht werden. Stelle sicher, dass EXPO_PUBLIC_API_BASE_URL in der .env-Datei auf die LAN-IP deines Entwicklungsrechners zeigt (z. B. http://192.168.1.x:3000) und nicht auf localhost.'
'API not reachable',
'The server could not be reached. Make sure EXPO_PUBLIC_API_BASE_URL in your .env file points to the LAN IP of your development machine (e.g. http://192.168.1.x:3000) and not localhost.'
);
} else {
Alert.alert('Keine Station gefunden', 'Kein ÖPNV-Halt in der Nähe gefunden.');
Alert.alert('No station found', 'No public transport stop found nearby.');
}
} catch (_err) {
Alert.alert('Fehler', 'Standortermittlung fehlgeschlagen. Bitte überprüfe die Berechtigungen in den Systemeinstellungen.');
Alert.alert('Error', 'Location detection failed. Please check the permissions in your system settings.');
}
};
@@ -228,36 +228,36 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
>
{/* Appearance */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Erscheinungsbild</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Appearance</Text>
<View style={styles.settingRow}>
<Text style={[styles.settingLabel, { color: colors.text }]}>Dunkelmodus</Text>
<Text style={[styles.settingLabel, { color: colors.text }]}>Dark mode</Text>
<Switch
value={dark}
onValueChange={toggleTheme}
trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Dunkelmodus umschalten"
accessibilityLabel="Toggle dark mode"
/>
</View>
</View>
{/* Origin Station */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Ursprungstation</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Origin station</Text>
<TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="Station suchen …"
placeholder="Search station …"
placeholderTextColor={colors.subtext}
value={query}
onChangeText={onQueryChange}
autoCapitalize="words"
accessibilityLabel="Station suchen"
accessibilityLabel="Search station"
/>
{searching && <ActivityIndicator style={{ marginVertical: 8 }} color={colors.accent} />}
{searchError && (
<Text style={[styles.errorText, { color: '#ff453a' }]}>{searchError}</Text>
)}
{origin && (
<Text style={[styles.currentStation, { color: colors.success }]}>Aktuell: {origin.name}</Text>
<Text style={[styles.currentStation, { color: colors.success }]}>Current: {origin.name}</Text>
)}
{results.map((s) => (
@@ -267,72 +267,72 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
))}
<TouchableOpacity style={[styles.locBtn, { backgroundColor: colors.highlight }]} onPress={useCurrentLocation}>
<Text style={[styles.locBtnText, { color: colors.accent }]}>📍 Aktuelle Position verwenden</Text>
<Text style={[styles.locBtnText, { color: colors.accent }]}>📍 Use current location</Text>
</TouchableOpacity>
<Text style={[styles.locStatus, { color: colors.subtext }]}>
Standort: {locPermission === 'granted' ? 'Erlaubt ✓' : locPermission === 'denied' ? 'Verweigert ✗' : 'Noch nicht angefragt'}
Location: {locPermission === 'granted' ? 'Granted ✓' : locPermission === 'denied' ? 'Denied ✗' : 'Not yet requested'}
</Text>
</View>
{/* Notification Settings */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Benachrichtigungen</Text>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Notifications</Text>
<View style={styles.settingRow}>
<Text style={[styles.settingLabel, { color: colors.text }]}>Benachrichtigungen aktivieren</Text>
<Text style={[styles.settingLabel, { color: colors.text }]}>Enable notifications</Text>
<Switch
value={notifSettings.enabled}
onValueChange={toggleNotifications}
trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Benachrichtigungen umschalten"
accessibilityLabel="Toggle notifications"
/>
</View>
<Text style={[styles.settingLabel, { color: colors.text }]}>Pufferzeit (Minuten)</Text>
<Text style={[styles.settingLabel, { color: colors.text }]}>Buffer time (minutes)</Text>
<TextInput
style={[styles.input, styles.numberInput, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
value={String(notifSettings.bufferMinutes)}
onChangeText={updateBufferMinutes}
keyboardType="numeric"
accessibilityLabel="Pufferzeit in Minuten"
accessibilityLabel="Buffer time in minutes"
/>
<Text style={[styles.hint, { color: colors.subtext }]}>
Du wirst {notifSettings.bufferMinutes} Minuten vor der geplanten Abfahrt erinnert.
You will be reminded {notifSettings.bufferMinutes} minutes before the scheduled departure.
</Text>
<TouchableOpacity style={[styles.advancedToggle, styles.locBtn, { backgroundColor: colors.highlight }]} onPress={toggleAdvanced}>
<Text style={[styles.locBtnText, { color: colors.accent }]}>
{showAdvanced ? '↑ Weniger Optionen zeigen' : '↓ Mehr Optionen zeigen'}
{showAdvanced ? '↑ Show fewer options' : '↓ Show more options'}
</Text>
</TouchableOpacity>
{showAdvanced && (
<View style={[styles.advancedSection, { borderTopColor: colors.border }]}>
<Text style={[styles.settingLabel, { color: colors.text }]}>Ankunfts-Puffer (Minuten)</Text>
<Text style={[styles.settingLabel, { color: colors.text }]}>Arrival buffer (minutes)</Text>
<TextInput
style={[styles.input, styles.numberInput, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
value={String(notifSettings.arrivalBufferMinutes)}
onChangeText={updateArrivalBuffer}
keyboardType="numeric"
accessibilityLabel="Ankunfts-Puffer in Minuten"
accessibilityLabel="Arrival buffer in minutes"
/>
<Text style={[styles.hint, { color: colors.subtext }]}>Wie viele Minuten vor der Event-Zeit du am Ziel ankommen möchtest</Text>
<Text style={[styles.hint, { color: colors.subtext }]}>How many minutes before the event time you want to arrive at the destination</Text>
<View style={styles.settingRow}>
<Text style={[styles.settingLabel, { color: colors.text }]}>Zu Fuß-Option anzeigen</Text>
<Text style={[styles.settingLabel, { color: colors.text }]}>Show walking option</Text>
<Switch
value={notifSettings.showWalkingOption}
onValueChange={toggleWalking}
trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Zu Fuß-Option umschalten"
accessibilityLabel="Toggle walking option"
/>
</View>
<View style={styles.settingRow}>
<Text style={[styles.settingLabel, { color: colors.text }]}>Fahrrad-Option anzeigen</Text>
<Text style={[styles.settingLabel, { color: colors.text }]}>Show bike option</Text>
<Switch
value={notifSettings.showBikeOption}
onValueChange={toggleBike}
trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Fahrrad-Option umschalten"
accessibilityLabel="Toggle bike option"
/>
</View>
</View>