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(() => { 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(); expect(getByText('Test Destination')).toBeTruthy();
}); });
await waitFor(() => { await waitFor(() => {
expect(getByText('Losgehen in')).toBeTruthy(); expect(getByText('Leave in')).toBeTruthy();
expect(getByText('S1 -> Wien')).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} /> <AddEventScreen navigation={createMockNavigationProp<RootStack, 'AddEvent'>()} route={mockRouteAddEvent} />
); );
expect(getByPlaceholderText('z.B. Team Meeting')).toBeTruthy(); expect(getByPlaceholderText('e.g. Team Meeting')).toBeTruthy();
expect(getByPlaceholderText('z.B. Technikum Wien')).toBeTruthy(); expect(getByPlaceholderText('e.g. Technikum Wien')).toBeTruthy();
expect(getByPlaceholderText('JJJJ-MM-TT')).toBeTruthy(); expect(getByPlaceholderText('YYYY-MM-DD')).toBeTruthy();
expect(getByPlaceholderText('SS:MM')).toBeTruthy(); expect(getByPlaceholderText('HH:MM')).toBeTruthy();
expect(getByText('Speichern')).toBeTruthy(); expect(getByText('Save')).toBeTruthy();
expect(getByText('Abbrechen')).toBeTruthy(); expect(getByText('Cancel')).toBeTruthy();
}); });
it('should show validation errors', () => { it('should show validation errors', () => {
@@ -255,11 +255,11 @@ describe('AddEventScreen', () => {
); );
// Try to save without filling form // Try to save without filling form
const saveButton = getByText('Speichern'); const saveButton = getByText('Save');
fireEvent.press(saveButton); fireEvent.press(saveButton);
// Should show error text // Should show error text
expect(getByText('Titel erforderlich')).toBeTruthy(); expect(getByText('Title required')).toBeTruthy();
}); });
it('should validate date format', () => { it('should validate date format', () => {
@@ -268,15 +268,15 @@ describe('AddEventScreen', () => {
); );
// Fill in all required fields except date format is invalid // Fill in all required fields except date format is invalid
fireEvent.changeText(getByPlaceholderText('z.B. Team Meeting'), 'Meeting'); fireEvent.changeText(getByPlaceholderText('e.g. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('z.B. Technikum Wien'), 'Wien'); fireEvent.changeText(getByPlaceholderText('e.g. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('JJJJ-MM-TT'), 'invalid-date'); fireEvent.changeText(getByPlaceholderText('YYYY-MM-DD'), 'invalid-date');
fireEvent.changeText(getByPlaceholderText('SS:MM'), '12:00'); fireEvent.changeText(getByPlaceholderText('HH:MM'), '12:00');
const saveButton = getByText('Speichern'); const saveButton = getByText('Save');
fireEvent.press(saveButton); fireEvent.press(saveButton);
expect(getByText('Ungültiges Datum')).toBeTruthy(); expect(getByText('Invalid date')).toBeTruthy();
}); });
it('should validate future date', () => { it('should validate future date', () => {
@@ -285,14 +285,14 @@ describe('AddEventScreen', () => {
); );
// Fill in all required fields with a past date // Fill in all required fields with a past date
fireEvent.changeText(getByPlaceholderText('z.B. Team Meeting'), 'Meeting'); fireEvent.changeText(getByPlaceholderText('e.g. Team Meeting'), 'Meeting');
fireEvent.changeText(getByPlaceholderText('z.B. Technikum Wien'), 'Wien'); fireEvent.changeText(getByPlaceholderText('e.g. Technikum Wien'), 'Wien');
fireEvent.changeText(getByPlaceholderText('JJJJ-MM-TT'), '2020-01-01'); fireEvent.changeText(getByPlaceholderText('YYYY-MM-DD'), '2020-01-01');
fireEvent.changeText(getByPlaceholderText('SS:MM'), '12:00'); fireEvent.changeText(getByPlaceholderText('HH:MM'), '12:00');
const saveButton = getByText('Speichern'); const saveButton = getByText('Save');
fireEvent.press(saveButton); 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) { export function BikeSection({ bikeRoute, loading, origin, colors }: Props) {
return ( return (
<View style={[styles.container, { backgroundColor: colors.background }]}> <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 ? ( {loading ? (
<View style={styles.centered}> <View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} /> <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> </View>
) : bikeRoute ? ( ) : bikeRoute ? (
<View style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }]}> <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> <Text style={[styles.value, { color: colors.accent }]}>{formatDistance(bikeRoute.distance)}</Text>
</View> </View>
<View style={[styles.mapPlaceholder, { backgroundColor: colors.background, borderColor: colors.border }]}> <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>
</View> </View>
) : ( ) : (
<Text style={[styles.empty, { color: colors.subtext }]}> <Text style={[styles.empty, { color: colors.subtext }]}>
{origin ? 'Keine Radroute verfügbar' : 'Ursprungstation festlegen'} {origin ? 'No bike route available' : 'Set origin station'}
</Text> </Text>
)} )}
</View> </View>
+17 -17
View File
@@ -45,25 +45,25 @@ export function JourneyList({
return ( return (
<View style={[styles.container, { backgroundColor: colors.background }]}> <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 && ( {destStationLoading && (
<View style={styles.centered}> <View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} /> <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> </View>
)} )}
{journeys.length === 0 && !destStationLoading ? ( {journeys.length === 0 && !destStationLoading ? (
<Text style={[styles.empty, { color: colors.subtext }]}> <Text style={[styles.empty, { color: colors.subtext }]}>
{origin ? 'Keine Verbindungen gefunden' : 'Ursprungstation festlegen'} {origin ? 'No connections found' : 'Set origin station'}
</Text> </Text>
) : ( ) : (
<TouchableOpacity <TouchableOpacity
activeOpacity={rankedJourneys.length > 1 ? 0.75 : 1} activeOpacity={rankedJourneys.length > 1 ? 0.75 : 1}
onPress={() => rankedJourneys.length > 1 && setExpanded((current) => !current)} onPress={() => rankedJourneys.length > 1 && setExpanded((current) => !current)}
accessibilityRole={rankedJourneys.length > 1 ? 'button' : undefined} 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) => { {visibleJourneys.map(({ journey: j }, index) => {
const finalArrival = new Date(j.rA.getTime() + walkDurationMs); 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> <Text style={[styles.delayBadge, { backgroundColor: colors.error }]}>+{j.delay} min</Text>
)} )}
{j.cancelled && ( {j.cancelled && (
<Text style={[styles.cancelBadge, { backgroundColor: colors.text }]}>Storniert</Text> <Text style={[styles.cancelBadge, { backgroundColor: colors.text }]}>Cancelled</Text>
)} )}
</View> </View>
<Text style={[styles.detail, { color: colors.text }]}> <Text style={[styles.detail, { color: colors.text }]}>
Abfahrt: {new Date(j.sD).toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit' })} Departure: {new Date(j.sD).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
{' '}(Plattform {j.platform || '—'}) {' '}(Platform {j.platform || '—'})
</Text> </Text>
<Text style={[styles.detail, { color: colors.subtext }]}> <Text style={[styles.detail, { color: colors.subtext }]}>
Ankunft: {new Date(j.sA).toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit' })} Arrival: {new Date(j.sA).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
{' '}({j.changes === 0 ? 'Direkt' : `${j.changes} Umst.`}) {' '}({j.changes === 0 ? 'Direct' : `${j.changes} tr.`})
</Text> </Text>
<Text style={[styles.detail, { color: colors.subtext }]}> <Text style={[styles.detail, { color: colors.subtext }]}>
Dauer: {durationMinutes} min Duration: {durationMinutes} min
</Text> </Text>
{walkDurationMs > 0 && ( {walkDurationMs > 0 && (
<Text style={[styles.detail, { color: arrivesTooLate ? colors.error : colors.subtext }]}> <Text style={[styles.detail, { color: arrivesTooLate ? colors.error : colors.subtext }]}>
Ziel: {finalArrival.toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit' })} Arrive: {finalArrival.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
{arrivesTooLate ? ' (zu spät)' : ''} {arrivesTooLate ? ' (too late)' : ''}
</Text> </Text>
)} )}
</View> </View>
@@ -115,7 +115,7 @@ export function JourneyList({
})} })}
{rankedJourneys.length > 1 && ( {rankedJourneys.length > 1 && (
<Text style={[styles.expandHint, { color: colors.accent }]}> <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> </Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
@@ -123,13 +123,13 @@ export function JourneyList({
{showWalkingOption && walkRoute && ( {showWalkingOption && walkRoute && (
<View style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border, marginTop: 10 }]}> <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}> <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> <Text style={[styles.walkValue, { color: colors.accent }]}>{formatDuration(walkRoute.duration)}</Text>
</View> </View>
<View style={styles.row}> <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> <Text style={[styles.walkValue, { color: colors.accent }]}>{formatDistance(walkRoute.distance)}</Text>
</View> </View>
</View> </View>
@@ -138,7 +138,7 @@ export function JourneyList({
{showWalkingOption && loadingWalk && ( {showWalkingOption && loadingWalk && (
<View style={styles.centered}> <View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} /> <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>
)} )}
</View> </View>
+3 -3
View File
@@ -22,12 +22,12 @@ export function NearbyStops({ stops, departures, loading, error, colors }: Props
return ( return (
<View style={[styles.container, { backgroundColor: colors.background }]}> <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 ? ( {loading ? (
<View style={styles.centered}> <View style={styles.centered}>
<ActivityIndicator size="small" color={colors.accent} /> <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> </View>
) : departures.length > 0 ? ( ) : departures.length > 0 ? (
departures.slice(0, 8).map((dep, i) => ( departures.slice(0, 8).map((dep, i) => (
@@ -43,7 +43,7 @@ export function NearbyStops({ stops, departures, loading, error, colors }: Props
{dep.direction} {dep.direction}
</Text> </Text>
<Text style={[styles.minutes, { color: dep.minutes <= 2 ? colors.error : colors.accent }]}> <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> </Text>
</View> </View>
</View> </View>
+1 -1
View File
@@ -93,7 +93,7 @@ export function useWienerLinien(
await fetchMonitor(ids); await fetchMonitor(ids);
} catch (err) { } catch (err) {
if (cancelledRef.current) return; 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); setLoading(false);
} }
}, DEBOUNCE_MS); }, DEBOUNCE_MS);
+17 -17
View File
@@ -52,12 +52,12 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
}, [route.params?.editEventId]); }, [route.params?.editEventId]);
const validate = (): boolean => { const validate = (): boolean => {
if (!title.trim()) { setError('Titel erforderlich'); return false; } if (!title.trim()) { setError('Title required'); return false; }
if (!destination.trim()) { setError('Ziel erforderlich'); return false; } if (!destination.trim()) { setError('Destination required'); return false; }
if (!dateStr || !timeStr) { setError('Datum und Zeit erforderlich'); return false; } if (!dateStr || !timeStr) { setError('Date and time required'); return false; }
const eventTime = new Date(`${dateStr}T${timeStr}`); const eventTime = new Date(`${dateStr}T${timeStr}`);
if (isNaN(eventTime.getTime())) { setError('Ungültiges Datum'); return false; } if (isNaN(eventTime.getTime())) { setError('Invalid date'); return false; }
if (eventTime <= new Date()) { setError('Datum muss in der Zukunft liegen'); return false; } if (eventTime <= new Date()) { setError('Date must be in the future'); return false; }
setError(''); setError('');
return true; return true;
}; };
@@ -96,40 +96,40 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
return ( return (
<View style={[styles.container, { backgroundColor: colors.background, position: 'relative' }]}> <View style={[styles.container, { backgroundColor: colors.background, position: 'relative' }]}>
<View style={styles.form}> <View style={styles.form}>
<Text style={[styles.label, { color: colors.text }]}>Titel</Text> <Text style={[styles.label, { color: colors.text }]}>Title</Text>
<TextInput <TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} 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} placeholderTextColor={colors.subtext}
value={title} value={title}
onChangeText={setTitle} onChangeText={setTitle}
autoCapitalize="words" autoCapitalize="words"
/> />
<Text style={[styles.label, { color: colors.text }]}>Ziel</Text> <Text style={[styles.label, { color: colors.text }]}>Destination</Text>
<TextInput <TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} 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} placeholderTextColor={colors.subtext}
value={destination} value={destination}
onChangeText={setDestination} onChangeText={setDestination}
autoCapitalize="words" autoCapitalize="words"
/> />
<Text style={[styles.label, { color: colors.text }]}>Datum</Text> <Text style={[styles.label, { color: colors.text }]}>Date</Text>
<TextInput <TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="JJJJ-MM-TT" placeholder="YYYY-MM-DD"
placeholderTextColor={colors.subtext} placeholderTextColor={colors.subtext}
value={dateStr} value={dateStr}
onChangeText={setDateStr} onChangeText={setDateStr}
keyboardType="numbers-and-punctuation" keyboardType="numbers-and-punctuation"
/> />
<Text style={[styles.label, { color: colors.text }]}>Zeit</Text> <Text style={[styles.label, { color: colors.text }]}>Time</Text>
<TextInput <TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="SS:MM" placeholder="HH:MM"
placeholderTextColor={colors.subtext} placeholderTextColor={colors.subtext}
value={timeStr} value={timeStr}
onChangeText={setTimeStr} onChangeText={setTimeStr}
@@ -139,14 +139,14 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
{error ? <Text style={[styles.errorText, { color: colors.error }]}>{error}</Text> : null} {error ? <Text style={[styles.errorText, { color: colors.error }]}>{error}</Text> : null}
<TouchableOpacity style={styles.saveBtn} onPress={handleSave}> <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>
<TouchableOpacity <TouchableOpacity
style={[styles.saveBtn, styles.cancelBtn, { backgroundColor: colors.border }]} style={[styles.saveBtn, styles.cancelBtn, { backgroundColor: colors.border }]}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<Text style={[styles.cancelText, { color: colors.text }]}>Abbrechen</Text> <Text style={[styles.cancelText, { color: colors.text }]}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -156,9 +156,9 @@ export function AddEventScreen({ navigation, route }: ScreenProps) {
<View style={styles.successCircle}> <View style={styles.successCircle}>
<Text style={styles.checkmark}></Text> <Text style={styles.checkmark}></Text>
</View> </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 }]}> <Text style={[styles.successSubtitle, { color: colors.subtext }]}>
{route.params?.editEventId ? 'Termin aktualisiert' : 'Termin hinzugefügt'} {route.params?.editEventId ? 'Event updated' : 'Event added'}
</Text> </Text>
</View> </View>
</View> </View>
@@ -158,7 +158,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
const handleImport = async () => { const handleImport = async () => {
if (!url.trim()) { if (!url.trim()) {
setError('Bitte ICS-URL eingeben'); setError('Please enter ICS URL');
return; return;
} }
@@ -188,7 +188,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
} }
setCount(added); setCount(added);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Import fehlgeschlagen'); setError(err instanceof Error ? err.message : 'Import failed');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -228,7 +228,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
setCount(added); setCount(added);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Sync fehlgeschlagen'); setError(err instanceof Error ? err.message : 'Sync failed');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -240,14 +240,14 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
return ( return (
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}> <ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.content}> <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 }]}> <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> </Text>
{/* ── ICS URL Import ── */} {/* ── ICS URL Import ── */}
<View style={styles.section}> <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 <TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
@@ -267,7 +267,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{loading ? ( {loading ? (
<ActivityIndicator color="#fff" /> <ActivityIndicator color="#fff" />
) : ( ) : (
<Text style={styles.importBtnText}>ICS Importieren</Text> <Text style={styles.importBtnText}>Import ICS</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -275,33 +275,33 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{/* ── Calendar Selection ── */} {/* ── Calendar Selection ── */}
<View style={styles.section}> <View style={styles.section}>
<View style={styles.sectionHeader}> <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}> <View style={styles.selectionActions}>
<TouchableOpacity onPress={selectAll} accessibilityLabel="Alle auswählen"> <TouchableOpacity onPress={selectAll} accessibilityLabel="Select all">
<Text style={[styles.linkText, { color: colors.accent }]}>Alle</Text> <Text style={[styles.linkText, { color: colors.accent }]}>All</Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={[styles.dividerText, { color: colors.subtext }]}> · </Text> <Text style={[styles.dividerText, { color: colors.subtext }]}> · </Text>
<TouchableOpacity onPress={deselectAll} accessibilityLabel="Alle abwählen"> <TouchableOpacity onPress={deselectAll} accessibilityLabel="Deselect all">
<Text style={[styles.linkText, { color: colors.accent }]}>Keine</Text> <Text style={[styles.linkText, { color: colors.accent }]}>None</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<Text style={[styles.sectionDesc, { color: colors.subtext }]}> <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'} {'\n'}
CalDAV-Quellen (DAVx5, Apple Calendar, etc.) werden automatisch erkannt. CalDAV sources (DAVx5, Apple Calendar, etc.) are detected automatically.
</Text> </Text>
{!calendarsLoaded ? ( {!calendarsLoaded ? (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator color={colors.accent} /> <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> </View>
) : availableCalendars.length === 0 ? ( ) : availableCalendars.length === 0 ? (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.subtext }]}> <Text style={[styles.emptyText, { color: colors.subtext }]}>
Keine Kalender auf diesem Gerät gefunden. No calendars found on this device.
</Text> </Text>
</View> </View>
) : ( ) : (
@@ -332,7 +332,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{hasSelection && ( {hasSelection && (
<View style={[styles.selectionInfo, { backgroundColor: colors.highlight }]}> <View style={[styles.selectionInfo, { backgroundColor: colors.highlight }]}>
<Text style={[styles.selectionInfoText, { color: colors.text }]}> <Text style={[styles.selectionInfoText, { color: colors.text }]}>
{selectedCalendarIds.size} von {availableCalendars.length} Kalendern ausgewählt {selectedCalendarIds.size} of {availableCalendars.length} calendars selected
</Text> </Text>
</View> </View>
)} )}
@@ -342,9 +342,9 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{/* ── Device Calendar Sync ── */} {/* ── Device Calendar Sync ── */}
<View style={styles.section}> <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 }]}> <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> </Text>
<TouchableOpacity <TouchableOpacity
@@ -352,7 +352,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
onPress={handleSyncNative} onPress={handleSyncNative}
disabled={loading} disabled={loading}
> >
<Text style={styles.importBtnText}>📅 Kalender Sync</Text> <Text style={styles.importBtnText}>📅 Calendar Sync</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -365,7 +365,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
{count !== null && ( {count !== null && (
<View style={[styles.successBanner, { backgroundColor: colors.success }]}> <View style={[styles.successBanner, { backgroundColor: colors.success }]}>
<Text style={styles.successText}> <Text style={styles.successText}>
{count} Termin(e) erfolgreich importiert! {count} event(s) successfully imported!
</Text> </Text>
</View> </View>
)} )}
@@ -374,7 +374,7 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
style={styles.backBtn} style={styles.backBtn}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<Text style={[styles.backBtnText, { color: colors.accent }]}> Zurück</Text> <Text style={[styles.backBtnText, { color: colors.accent }]}> Back</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</ScrollView> </ScrollView>
+15 -15
View File
@@ -89,7 +89,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
setShowWalkingOption(settings.showWalkingOption); setShowWalkingOption(settings.showWalkingOption);
const found = events.find((e) => e.id === eventId); const found = events.find((e) => e.id === eventId);
if (!found) { setError('Termin nicht gefunden'); return; } if (!found) { setError('Event not found'); return; }
setEvent(found); setEvent(found);
if (originStation) { if (originStation) {
@@ -112,7 +112,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
const results = await api.searchJourneys(originStation.extId, destExtId, target, { arriveBy: true }); const results = await api.searchJourneys(originStation.extId, destExtId, target, { arriveBy: true });
setJourneys(results); setJourneys(results);
} else if (destStation.error) { } else if (destStation.error) {
setError(`Ziel-Station nicht auflösbar: ${destStation.error}`); setError(`Destination station not resolvable: ${destStation.error}`);
} }
try { try {
@@ -131,7 +131,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
} }
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden'); setError(err instanceof Error ? err.message : 'Error loading');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -170,7 +170,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
<View style={[styles.center, { backgroundColor: colors.background }]}> <View style={[styles.center, { backgroundColor: colors.background }]}>
<ActivityIndicator size="large" color={colors.accent} /> <ActivityIndicator size="large" color={colors.accent} />
<Text style={[styles.loadingText, { color: colors.subtext }]}> <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> </Text>
</View> </View>
); );
@@ -200,9 +200,9 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
{!origin && !error && ( {!origin && !error && (
<View style={[styles.warningBanner, { backgroundColor: colors.warning }]}> <View style={[styles.warningBanner, { backgroundColor: colors.warning }]}>
<Text style={styles.bannerText}> <Text style={styles.bannerText}>
Keine Ursprungstation festgelegt.{' '} No origin station set.{' '}
<Text style={styles.bannerLink} onPress={() => navigation.navigate('Settings')}> <Text style={styles.bannerLink} onPress={() => navigation.navigate('Settings')}>
Einstellungen öffnen Open settings
</Text> </Text>
</Text> </Text>
</View> </View>
@@ -218,15 +218,15 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
onPress={() => setActiveMode('train')} onPress={() => setActiveMode('train')}
> >
<View style={styles.modeHeader}> <View style={styles.modeHeader}>
<Text style={[styles.modeLabel, { color: colors.text }]}>🚆 Zug</Text> <Text style={[styles.modeLabel, { color: colors.text }]}>🚆 Train</Text>
{effectiveMode === 'train' && ( {effectiveMode === 'train' && (
<View style={[styles.activeBadge, { backgroundColor: colors.accent }]}> <View style={[styles.activeBadge, { backgroundColor: colors.accent }]}>
<Text style={styles.activeBadgeText}>Aktiv</Text> <Text style={styles.activeBadgeText}>Active</Text>
</View> </View>
)} )}
</View> </View>
<Text style={[styles.modeMeta, { color: colors.subtext }]}> <Text style={[styles.modeMeta, { color: colors.subtext }]}>
{showWalkingOption ? 'Bahn + finaler Fußweg' : 'Nur Bahn'} {showWalkingOption ? 'Train + final walk' : 'Train only'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@@ -240,19 +240,19 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
disabled={bikeDisabled} disabled={bikeDisabled}
> >
<View style={styles.modeHeader}> <View style={styles.modeHeader}>
<Text style={[styles.modeLabel, { color: colors.text }]}>🚲 Rad</Text> <Text style={[styles.modeLabel, { color: colors.text }]}>🚲 Bike</Text>
{effectiveMode === 'bike' && ( {effectiveMode === 'bike' && (
<View style={[styles.activeBadge, { backgroundColor: colors.accent }]}> <View style={[styles.activeBadge, { backgroundColor: colors.accent }]}>
<Text style={styles.activeBadgeText}>Aktiv</Text> <Text style={styles.activeBadgeText}>Active</Text>
</View> </View>
)} )}
</View> </View>
<Text style={[styles.modeMeta, { color: colors.subtext }]}> <Text style={[styles.modeMeta, { color: colors.subtext }]}>
{bikeDisabled {bikeDisabled
? 'In Einstellungen deaktiviert' ? 'Disabled in settings'
: loadingBike : loadingBike
? 'Route wird berechnet...' ? 'Calculating route...'
: 'Direktweg'} : 'Direct route'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -293,7 +293,7 @@ export function EventDetailScreen({ navigation, route }: ScreenProps) {
style={[styles.refreshBtn, { backgroundColor: colors.border }]} style={[styles.refreshBtn, { backgroundColor: colors.border }]}
onPress={handleRefresh} onPress={handleRefresh}
> >
<Text style={[styles.refreshBtnText, { color: colors.text }]}>🔄 Neu laden</Text> <Text style={[styles.refreshBtnText, { color: colors.text }]}>🔄 Refresh</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={{ height: 40 }} /> <View style={{ height: 40 }} />
+24 -32
View File
@@ -145,7 +145,7 @@ export function EventListScreen({ navigation }: ScreenProps) {
setJourneys(results); setJourneys(results);
} catch (err) { } catch (err) {
if (!isMounted) return; 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 { } finally {
if (isMounted) setJourneysLoading(false); if (isMounted) setJourneysLoading(false);
} }
@@ -175,16 +175,16 @@ export function EventListScreen({ navigation }: ScreenProps) {
const renderItem = ({ item }: { item: CalendarEvent }) => { const renderItem = ({ item }: { item: CalendarEvent }) => {
const leaveBy = departureInfo.departureTime; const leaveBy = departureInfo.departureTime;
const leaveCountdown = leaveBy ? calculateCountdown(leaveBy) : null; 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 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 const status = leaveCountdown
? leaveCountdown.label === 'Now' ? leaveCountdown.label === 'Now'
? 'Jetzt losgehen' ? 'Leave now'
: `Losgehen in ${leaveCountdown.label}` : `Leave in ${leaveCountdown.label}`
: journeysLoading || destStation.loading : journeysLoading || destStation.loading
? 'Losgehzeit wird berechnet' ? 'Calculating departure time'
: 'Keine Verbindung'; : 'No connection';
return ( return (
<View style={styles.cardWrapper}> <View style={styles.cardWrapper}>
@@ -197,12 +197,13 @@ export function EventListScreen({ navigation }: ScreenProps) {
<View style={styles.dotRow}> <View style={styles.dotRow}>
<View style={[styles.dot, { backgroundColor: leaveCountdown?.urgent ? colors.delete : '#34C759' }]} /> <View style={[styles.dot, { backgroundColor: leaveCountdown?.urgent ? colors.delete : '#34C759' }]} />
<Text style={[styles.title, { color: colors.text }]}>{item.title}</Text> <Text style={[styles.title, { color: colors.text }]}>{item.title}</Text>
<Text style={[styles.badge, { color: leaveByLabel ? colors.accent : colors.subtext }]}>
{leaveByLabel ?? '--:--'}
</Text>
</View> </View>
<Text style={[styles.subtitle, { color: colors.subtext }]}>{item.destination}</Text> <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 <Text
style={[ style={[
styles.leaveTime, styles.leaveTime,
@@ -213,41 +214,32 @@ export function EventListScreen({ navigation }: ScreenProps) {
? leaveCountdownLabel ? leaveCountdownLabel
: journeysLoading || destStation.loading : journeysLoading || destStation.loading
? '--' ? '--'
: 'Keine Verbindung'} : 'No connection'}
</Text> </Text>
<View style={[styles.trainBox, { borderColor: colors.border }]}> <View style={[styles.trainBox, { borderColor: colors.border }]}>
<Text style={[styles.trainTitle, { color: colors.text }]} numberOfLines={2}> <Text style={[styles.trainTitle, { color: colors.text }]} numberOfLines={2}>
{journeysError ? 'Zugverbindung nicht erreichbar' : trainLabel} {journeysError ? 'Train connection unavailable' : trainLabel}
</Text> </Text>
{selectedJourney ? ( {selectedJourney ? (
<Text style={[styles.trainMeta, { color: colors.subtext }]}> <Text style={[styles.trainMeta, { color: colors.subtext }]}>
Abfahrt {formatTime(selectedJourney.rD)} Departure {formatTime(selectedJourney.rD)}
{selectedJourney.platform ? ` · Gleis ${selectedJourney.platform}` : ''} {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>
) : ( ) : (
<Text style={[styles.trainMeta, { color: journeysError ? colors.delete : colors.subtext }]}> <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> </Text>
)} )}
{originWalk.walkRoute && originWalk.walkRoute.duration > 30 && ( {originWalk.walkRoute && originWalk.walkRoute.duration > 30 && (
<Text style={[styles.trainMeta, { color: colors.subtext }]}> <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> </Text>
)} )}
</View> </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> </View>
</TouchableOpacity> </TouchableOpacity>
@@ -256,12 +248,12 @@ export function EventListScreen({ navigation }: ScreenProps) {
onPress={() => navigation.navigate('AddEvent', { editEventId: item.id })} onPress={() => navigation.navigate('AddEvent', { editEventId: item.id })}
style={styles.editBtn} style={styles.editBtn}
> >
<Text style={[styles.editText, { color: colors.accent }]}>Bearbeiten</Text> <Text style={[styles.editText, { color: colors.accent }]}>edit</Text>
</TouchableOpacity> </TouchableOpacity>
{/* Delete button */} {/* Delete button */}
<TouchableOpacity onPress={() => removeEvent(item.id, reload)} style={styles.deleteBtn}> <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> </TouchableOpacity>
</View> </View>
); );
@@ -270,12 +262,12 @@ export function EventListScreen({ navigation }: ScreenProps) {
if (!upcomingEvent) { if (!upcomingEvent) {
return ( return (
<View style={[styles.center, { backgroundColor: colors.background }]}> <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 <TouchableOpacity
style={styles.addBtn} style={styles.addBtn}
onPress={() => navigation.navigate('AddEvent')} onPress={() => navigation.navigate('AddEvent')}
> >
<Text style={styles.addBtnText}>+ Termin hinzufügen</Text> <Text style={styles.addBtnText}>+ Add event</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
+33 -33
View File
@@ -73,10 +73,10 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
try { try {
const stations = await api.searchStation(q.trim()); const stations = await api.searchStation(q.trim());
setResults(stations); setResults(stations);
if (stations.length === 0) setSearchError('Keine Stationen gefunden.'); if (stations.length === 0) setSearchError('No stations found.');
} catch { } catch {
setResults([]); 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 { } finally {
setSearching(false); setSearching(false);
} }
@@ -97,9 +97,9 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
try { try {
await saveOriginStation(station); await saveOriginStation(station);
await rescheduleAllNotifications(); await rescheduleAllNotifications();
Alert.alert('Station gespeichert', station.name); Alert.alert('Station saved', station.name);
} catch { } 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'); setLocPermission(status === 'granted' ? 'granted' : 'denied');
if (status !== 'granted') { 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; return;
} }
@@ -149,7 +149,7 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
try { try {
const nearestStation = await api.findNearestStationByCoords(userLat, userLng); const nearestStation = await api.findNearestStationByCoords(userLat, userLng);
if (!nearestStation) { 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; return;
} }
await selectStation({ await selectStation({
@@ -164,14 +164,14 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
if (!apiReachable) { if (!apiReachable) {
Alert.alert( Alert.alert(
'API nicht erreichbar', 'API not reachable',
'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.' '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 { } 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) { } 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 */} {/* Appearance */}
<View style={styles.section}> <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}> <View style={styles.settingRow}>
<Text style={[styles.settingLabel, { color: colors.text }]}>Dunkelmodus</Text> <Text style={[styles.settingLabel, { color: colors.text }]}>Dark mode</Text>
<Switch <Switch
value={dark} value={dark}
onValueChange={toggleTheme} onValueChange={toggleTheme}
trackColor={{ true: colors.accent, false: colors.border }} trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Dunkelmodus umschalten" accessibilityLabel="Toggle dark mode"
/> />
</View> </View>
</View> </View>
{/* Origin Station */} {/* Origin Station */}
<View style={styles.section}> <View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Ursprungstation</Text> <Text style={[styles.sectionTitle, { color: colors.text }]}>Origin station</Text>
<TextInput <TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="Station suchen …" placeholder="Search station …"
placeholderTextColor={colors.subtext} placeholderTextColor={colors.subtext}
value={query} value={query}
onChangeText={onQueryChange} onChangeText={onQueryChange}
autoCapitalize="words" autoCapitalize="words"
accessibilityLabel="Station suchen" accessibilityLabel="Search station"
/> />
{searching && <ActivityIndicator style={{ marginVertical: 8 }} color={colors.accent} />} {searching && <ActivityIndicator style={{ marginVertical: 8 }} color={colors.accent} />}
{searchError && ( {searchError && (
<Text style={[styles.errorText, { color: '#ff453a' }]}>{searchError}</Text> <Text style={[styles.errorText, { color: '#ff453a' }]}>{searchError}</Text>
)} )}
{origin && ( {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) => ( {results.map((s) => (
@@ -267,72 +267,72 @@ export function SettingsScreen({ navigation: _navigation }: ScreenProps) {
))} ))}
<TouchableOpacity style={[styles.locBtn, { backgroundColor: colors.highlight }]} onPress={useCurrentLocation}> <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> </TouchableOpacity>
<Text style={[styles.locStatus, { color: colors.subtext }]}> <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> </Text>
</View> </View>
{/* Notification Settings */} {/* Notification Settings */}
<View style={styles.section}> <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}> <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 <Switch
value={notifSettings.enabled} value={notifSettings.enabled}
onValueChange={toggleNotifications} onValueChange={toggleNotifications}
trackColor={{ true: colors.accent, false: colors.border }} trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Benachrichtigungen umschalten" accessibilityLabel="Toggle notifications"
/> />
</View> </View>
<Text style={[styles.settingLabel, { color: colors.text }]}>Pufferzeit (Minuten)</Text> <Text style={[styles.settingLabel, { color: colors.text }]}>Buffer time (minutes)</Text>
<TextInput <TextInput
style={[styles.input, styles.numberInput, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} style={[styles.input, styles.numberInput, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
value={String(notifSettings.bufferMinutes)} value={String(notifSettings.bufferMinutes)}
onChangeText={updateBufferMinutes} onChangeText={updateBufferMinutes}
keyboardType="numeric" keyboardType="numeric"
accessibilityLabel="Pufferzeit in Minuten" accessibilityLabel="Buffer time in minutes"
/> />
<Text style={[styles.hint, { color: colors.subtext }]}> <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> </Text>
<TouchableOpacity style={[styles.advancedToggle, styles.locBtn, { backgroundColor: colors.highlight }]} onPress={toggleAdvanced}> <TouchableOpacity style={[styles.advancedToggle, styles.locBtn, { backgroundColor: colors.highlight }]} onPress={toggleAdvanced}>
<Text style={[styles.locBtnText, { color: colors.accent }]}> <Text style={[styles.locBtnText, { color: colors.accent }]}>
{showAdvanced ? '↑ Weniger Optionen zeigen' : '↓ Mehr Optionen zeigen'} {showAdvanced ? '↑ Show fewer options' : '↓ Show more options'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
{showAdvanced && ( {showAdvanced && (
<View style={[styles.advancedSection, { borderTopColor: colors.border }]}> <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 <TextInput
style={[styles.input, styles.numberInput, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]} style={[styles.input, styles.numberInput, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
value={String(notifSettings.arrivalBufferMinutes)} value={String(notifSettings.arrivalBufferMinutes)}
onChangeText={updateArrivalBuffer} onChangeText={updateArrivalBuffer}
keyboardType="numeric" 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}> <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 <Switch
value={notifSettings.showWalkingOption} value={notifSettings.showWalkingOption}
onValueChange={toggleWalking} onValueChange={toggleWalking}
trackColor={{ true: colors.accent, false: colors.border }} trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Zu Fuß-Option umschalten" accessibilityLabel="Toggle walking option"
/> />
</View> </View>
<View style={styles.settingRow}> <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 <Switch
value={notifSettings.showBikeOption} value={notifSettings.showBikeOption}
onValueChange={toggleBike} onValueChange={toggleBike}
trackColor={{ true: colors.accent, false: colors.border }} trackColor={{ true: colors.accent, false: colors.border }}
accessibilityLabel="Fahrrad-Option umschalten" accessibilityLabel="Toggle bike option"
/> />
</View> </View>
</View> </View>