mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: translate the calendar import sheet
This commit is contained in:
+9
-1
@@ -29,7 +29,15 @@
|
|||||||
"time": "Time"
|
"time": "Time"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"refused_count": "{count, plural, one {# event could not be imported:} other {# events could not be imported:}}"
|
"file_too_large_max": "That file is too large (max {size} MB).",
|
||||||
|
"imported_count": "{count, plural, one {Imported # event.} other {Imported # events.}}",
|
||||||
|
"no_calendar": "Create a calendar before importing events.",
|
||||||
|
"no_calendar_title": "No calendar",
|
||||||
|
"picker_error": "Could not open the file picker",
|
||||||
|
"picker_unavailable": "The file picker is not installed in this build. Rebuild the app to enable importing.",
|
||||||
|
"picker_unavailable_title": "Import unavailable",
|
||||||
|
"refused_count": "{count, plural, one {# event could not be imported:} other {# events could not be imported:}}",
|
||||||
|
"selected_count": "{count} selected"
|
||||||
},
|
},
|
||||||
"invitation": {
|
"invitation": {
|
||||||
"accept": "Yes",
|
"accept": "Yes",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { getCalendarColor } from '../../lib/calendar-utils';
|
|||||||
import { isWritableCalendar } from '../../lib/calendar-editability';
|
import { isWritableCalendar } from '../../lib/calendar-editability';
|
||||||
import { ImportRefusedError, type ImportResult } from '../../stores/calendar-store';
|
import { ImportRefusedError, type ImportResult } from '../../stores/calendar-store';
|
||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
|
import { useCalendarLocale } from '../../lib/calendar-locale';
|
||||||
|
import type { Locale } from 'date-fns';
|
||||||
|
|
||||||
// expo-document-picker is loaded lazily on first use; its native module is not
|
// expo-document-picker is loaded lazily on first use; its native module is not
|
||||||
// linked into every build (matches the FilesScreen / ContactImportSheet flow).
|
// linked into every build (matches the FilesScreen / ContactImportSheet flow).
|
||||||
@@ -42,11 +44,13 @@ interface Props {
|
|||||||
onImported?: (count: number) => void;
|
onImported?: (count: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventDateLabel(event: Partial<CalendarEvent>): string | null {
|
function eventDateLabel(event: Partial<CalendarEvent>, locale: Locale): string | null {
|
||||||
if (!event.start) return null;
|
if (!event.start) return null;
|
||||||
const d = parseISO(event.start);
|
const d = parseISO(event.start);
|
||||||
if (isNaN(d.getTime())) return null;
|
if (isNaN(d.getTime())) return null;
|
||||||
return event.showWithoutTime ? format(d, 'EEE, MMM d, yyyy') : format(d, 'EEE, MMM d · HH:mm');
|
return event.showWithoutTime
|
||||||
|
? format(d, 'EEE, MMM d, yyyy', { locale })
|
||||||
|
: format(d, 'EEE, MMM d · HH:mm', { locale });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ICalImportSheet({ visible, onClose, calendars, onImport, onImported }: Props) {
|
export function ICalImportSheet({ visible, onClose, calendars, onImport, onImported }: Props) {
|
||||||
@@ -54,6 +58,7 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
|
const { locale } = useCalendarLocale();
|
||||||
|
|
||||||
const writable = React.useMemo(
|
const writable = React.useMemo(
|
||||||
() => calendars.filter((cal) => isWritableCalendar(cal)),
|
() => calendars.filter((cal) => isWritableCalendar(cal)),
|
||||||
@@ -101,8 +106,11 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
const picker = loadDocumentPicker();
|
const picker = loadDocumentPicker();
|
||||||
if (!picker) {
|
if (!picker) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Import unavailable',
|
t('calendar.import.picker_unavailable_title', 'Import unavailable'),
|
||||||
'The file picker is not installed in this build. Rebuild the app (expo run:android) to enable importing.',
|
t(
|
||||||
|
'calendar.import.picker_unavailable',
|
||||||
|
'The file picker is not installed in this build. Rebuild the app to enable importing.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -111,13 +119,13 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
try {
|
try {
|
||||||
res = await picker.getDocumentAsync({ copyToCacheDirectory: true, multiple: false });
|
res = await picker.getDocumentAsync({ copyToCacheDirectory: true, multiple: false });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Could not open the file picker');
|
setError(e instanceof Error ? e.message : t('calendar.import.picker_error', 'Could not open the file picker'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res.canceled || res.assets.length === 0) return;
|
if (res.canceled || res.assets.length === 0) return;
|
||||||
const asset = res.assets[0];
|
const asset = res.assets[0];
|
||||||
if (asset.size && asset.size > 5 * 1024 * 1024) {
|
if (asset.size && asset.size > 5 * 1024 * 1024) {
|
||||||
setError('That file is too large (max 5 MB).');
|
setError(t('calendar.import.file_too_large_max', 'That file is too large (max {size} MB).', { size: 5 }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,13 +136,13 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
const events = await parseCalendarBlob(blobId);
|
const events = await parseCalendarBlob(blobId);
|
||||||
const usable = events.filter((e) => !!e.start || e['@type'] === 'Task');
|
const usable = events.filter((e) => !!e.start || e['@type'] === 'Task');
|
||||||
if (usable.length === 0) {
|
if (usable.length === 0) {
|
||||||
setError('No events were found in that file.');
|
setError(t('calendar.import.no_events', 'No events found in file'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setParsed(usable);
|
setParsed(usable);
|
||||||
setSelected(new Set(usable.map((_, i) => i)));
|
setSelected(new Set(usable.map((_, i) => i)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Could not read or parse that file.');
|
setError(e instanceof Error ? e.message : t('calendar.import.invalid_format', 'Invalid calendar file format'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -153,7 +161,10 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
|
|
||||||
const doImport = async () => {
|
const doImport = async () => {
|
||||||
if (!calendarId) {
|
if (!calendarId) {
|
||||||
Alert.alert('No calendar', 'Create a calendar before importing events.');
|
Alert.alert(
|
||||||
|
t('calendar.import.no_calendar_title', 'No calendar'),
|
||||||
|
t('calendar.import.no_calendar', 'Create a calendar before importing events.'),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toImport = parsed.filter((_, i) => selected.has(i));
|
const toImport = parsed.filter((_, i) => selected.has(i));
|
||||||
@@ -166,7 +177,7 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Everything was refused: list what and why, like a partial import.
|
// Everything was refused: list what and why, like a partial import.
|
||||||
if (e instanceof ImportRefusedError) setResult({ imported: 0, refused: e.refused });
|
if (e instanceof ImportRefusedError) setResult({ imported: 0, refused: e.refused });
|
||||||
else setError(e instanceof Error ? e.message : 'Import failed.');
|
else setError(e instanceof Error ? e.message : t('calendar.import.error', 'Failed to import calendar'));
|
||||||
} finally {
|
} finally {
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
}
|
}
|
||||||
@@ -180,7 +191,7 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||||
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
|
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>Import calendar</Text>
|
<Text style={styles.title}>{t('calendar.import.title', 'Import Calendar')}</Text>
|
||||||
<Pressable onPress={onClose} hitSlop={8} style={styles.close}>
|
<Pressable onPress={onClose} hitSlop={8} style={styles.close}>
|
||||||
<X size={20} color={c.text} />
|
<X size={20} color={c.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -201,7 +212,11 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
<View style={{ flex: 1, minWidth: 0 }}>
|
<View style={{ flex: 1, minWidth: 0 }}>
|
||||||
{result.imported > 0 && (
|
{result.imported > 0 && (
|
||||||
<Text style={styles.successText}>
|
<Text style={styles.successText}>
|
||||||
Imported {result.imported} event{result.imported === 1 ? '' : 's'}.
|
{t(
|
||||||
|
'calendar.import.imported_count',
|
||||||
|
'{count, plural, one {Imported # event.} other {Imported # events.}}',
|
||||||
|
{ count: result.imported },
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Text style={styles.refusedTitle}>
|
<Text style={styles.refusedTitle}>
|
||||||
@@ -215,12 +230,14 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
</View>
|
</View>
|
||||||
<ScrollView style={{ flex: 1 }}>
|
<ScrollView style={{ flex: 1 }}>
|
||||||
{result.refused.map(({ event, reason }, idx) => {
|
{result.refused.map(({ event, reason }, idx) => {
|
||||||
const date = eventDateLabel(event);
|
const date = eventDateLabel(event, locale);
|
||||||
return (
|
return (
|
||||||
<View key={idx} style={styles.row}>
|
<View key={idx} style={styles.row}>
|
||||||
<AlertTriangle size={16} color={c.error} />
|
<AlertTriangle size={16} color={c.error} />
|
||||||
<View style={{ flex: 1, minWidth: 0 }}>
|
<View style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text style={styles.rowName} numberOfLines={1}>{event.title || 'Untitled event'}</Text>
|
<Text style={styles.rowName} numberOfLines={1}>
|
||||||
|
{event.title || t('calendar.events.no_title', '(No title)')}
|
||||||
|
</Text>
|
||||||
{!!date && <Text style={styles.rowDate} numberOfLines={1}>{date}</Text>}
|
{!!date && <Text style={styles.rowDate} numberOfLines={1}>{date}</Text>}
|
||||||
<Text style={styles.refusedReason} numberOfLines={3}>{reason}</Text>
|
<Text style={styles.refusedReason} numberOfLines={3}>{reason}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -229,7 +246,7 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
})}
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.md) }]}>
|
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.md) }]}>
|
||||||
<Button variant="outline" onPress={onClose}>Done</Button>
|
<Button variant="outline" onPress={onClose}>{t('common.done', 'Done')}</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : result !== null ? (
|
) : result !== null ? (
|
||||||
@@ -238,9 +255,13 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
<Check size={26} color={c.primaryForeground} />
|
<Check size={26} color={c.primaryForeground} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.successText}>
|
<Text style={styles.successText}>
|
||||||
Imported {result.imported} event{result.imported === 1 ? '' : 's'}.
|
{t(
|
||||||
|
'calendar.import.imported_count',
|
||||||
|
'{count, plural, one {Imported # event.} other {Imported # events.}}',
|
||||||
|
{ count: result.imported },
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Button variant="outline" size="sm" onPress={onClose}>Done</Button>
|
<Button variant="outline" size="sm" onPress={onClose}>{t('common.done', 'Done')}</Button>
|
||||||
</View>
|
</View>
|
||||||
) : parsed.length === 0 ? (
|
) : parsed.length === 0 ? (
|
||||||
<View style={styles.center}>
|
<View style={styles.center}>
|
||||||
@@ -250,8 +271,14 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
) : (
|
) : (
|
||||||
<Upload size={32} color={c.textMuted} />
|
<Upload size={32} color={c.textMuted} />
|
||||||
)}
|
)}
|
||||||
<Text style={styles.dropTitle}>{loading ? 'Parsing…' : 'Choose an .ics file'}</Text>
|
<Text style={styles.dropTitle}>
|
||||||
<Text style={styles.dropHint}>iCalendar — single or multiple events</Text>
|
{loading
|
||||||
|
? t('calendar.import.parsing', 'Parsing calendar file...')
|
||||||
|
: t('calendar.import.select_file', 'Select .ics file')}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.dropHint}>
|
||||||
|
{t('calendar.import.supported_formats', 'Supports iCalendar (.ics) files')}
|
||||||
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{!!error && (
|
{!!error && (
|
||||||
<View style={styles.errorBox}>
|
<View style={styles.errorBox}>
|
||||||
@@ -279,18 +306,24 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
|
|
||||||
<View style={styles.toolbar}>
|
<View style={styles.toolbar}>
|
||||||
<Text style={styles.toolbarCount}>
|
<Text style={styles.toolbarCount}>
|
||||||
{parsed.length} found · {selected.size} selected
|
{t('calendar.import.parsed_events', '{count} events found', { count: parsed.length })}
|
||||||
|
{' · '}
|
||||||
|
{t('calendar.import.selected_count', '{count} selected', { count: selected.size })}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: spacing.md }}>
|
<View style={{ flexDirection: 'row', gap: spacing.md }}>
|
||||||
<Pressable onPress={selectAll} hitSlop={6}><Text style={styles.link}>All</Text></Pressable>
|
<Pressable onPress={selectAll} hitSlop={6}>
|
||||||
<Pressable onPress={selectNone} hitSlop={6}><Text style={styles.link}>None</Text></Pressable>
|
<Text style={styles.link}>{t('calendar.import.select_all', 'Select all')}</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable onPress={selectNone} hitSlop={6}>
|
||||||
|
<Text style={styles.link}>{t('calendar.import.deselect_all', 'Deselect all')}</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={{ flex: 1 }}>
|
<ScrollView style={{ flex: 1 }}>
|
||||||
{parsed.map((event, idx) => {
|
{parsed.map((event, idx) => {
|
||||||
const isSelected = selected.has(idx);
|
const isSelected = selected.has(idx);
|
||||||
const date = eventDateLabel(event);
|
const date = eventDateLabel(event, locale);
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -302,7 +335,9 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
</View>
|
</View>
|
||||||
<Clock size={16} color={c.textMuted} />
|
<Clock size={16} color={c.textMuted} />
|
||||||
<View style={{ flex: 1, minWidth: 0 }}>
|
<View style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text style={styles.rowName} numberOfLines={1}>{event.title || 'Untitled event'}</Text>
|
<Text style={styles.rowName} numberOfLines={1}>
|
||||||
|
{event.title || t('calendar.events.no_title', '(No title)')}
|
||||||
|
</Text>
|
||||||
{!!date && <Text style={styles.rowDate} numberOfLines={1}>{date}</Text>}
|
{!!date && <Text style={styles.rowDate} numberOfLines={1}>{date}</Text>}
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -318,13 +353,15 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.md) }]}>
|
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.md) }]}>
|
||||||
<Button variant="outline" onPress={onClose} disabled={importing}>Cancel</Button>
|
<Button variant="outline" onPress={onClose} disabled={importing}>{t('common.cancel', 'Cancel')}</Button>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => { void doImport(); }}
|
onPress={() => { void doImport(); }}
|
||||||
disabled={importing || selected.size === 0}
|
disabled={importing || selected.size === 0}
|
||||||
icon={importing ? <ActivityIndicator size="small" color={c.primaryForeground} /> : <CalendarPlus size={16} color={c.primaryForeground} />}
|
icon={importing ? <ActivityIndicator size="small" color={c.primaryForeground} /> : <CalendarPlus size={16} color={c.primaryForeground} />}
|
||||||
>
|
>
|
||||||
{importing ? 'Importing…' : `Import ${selected.size}`}
|
{importing
|
||||||
|
? t('calendar.import.importing', 'Importing events...')
|
||||||
|
: t('calendar.import.import_button', 'Import selected')}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user