fix: translate the week view and label the calendar toolbar

The week view's "all-day" label and its "Untitled" fallback were
hard-coded; they now use the catalog's "All day" and "(No title)" like
the scrolling time grid. The calendar's menu, tasks, view switch, new
event and previous/next buttons get screen reader labels (the view
buttons also say which one is selected), and a calendar i18n test
covers the calendar views.
This commit is contained in:
Linus Rath
2026-09-24 21:44:46 +02:00
parent 13cba64854
commit b3a82f1241
3 changed files with 63 additions and 7 deletions
@@ -0,0 +1,27 @@
// Calendar screens and sheets whose every user-visible string goes through
// t(). A literal English label, alert or JSX text sneaking back in stays
// English in every locale, so this scans the source for them.
import { describe, it, expect } from 'vitest';
import { join } from 'node:path';
import { findUntranslatedLiterals } from './untranslated-literals';
const SRC = join(__dirname, '..', '..');
// File (relative to src/) → texts that stay as they are on purpose.
const TRANSLATED_FILES: Record<string, string[]> = {
'screens/CalendarScreen.tsx': [],
'components/calendar/AgendaView.tsx': [],
'components/calendar/EventCard.tsx': [],
'components/calendar/MonthScrollView.tsx': [],
'components/calendar/MonthView.tsx': [],
'components/calendar/TimeGridScrollView.tsx': [],
'components/calendar/WeekView.tsx': [],
};
describe('translated calendar screens', () => {
for (const [file, allow] of Object.entries(TRANSLATED_FILES)) {
it(`${file} has no hard-coded English text`, () => {
expect(findUntranslatedLiterals(join(SRC, file), allow)).toEqual([]);
});
}
});
+4 -4
View File
@@ -59,7 +59,7 @@ function WeekViewInner({
}: WeekViewProps) { }: WeekViewProps) {
const c = useColors(); const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const { locale } = useCalendarLocale(); const { locale, t } = useCalendarLocale();
const scrollRef = React.useRef<ScrollView>(null); const scrollRef = React.useRef<ScrollView>(null);
const index = React.useMemo( const index = React.useMemo(
@@ -173,7 +173,7 @@ function WeekViewInner({
{allDayRowCount > 0 && ( {allDayRowCount > 0 && (
<View style={[styles.allDayStrip, { height: allDayStripHeight }]}> <View style={[styles.allDayStrip, { height: allDayStripHeight }]}>
<View style={[styles.gutter, styles.allDayLabelWrap]}> <View style={[styles.gutter, styles.allDayLabelWrap]}>
<Text style={styles.allDayLabel}>all-day</Text> <Text style={styles.allDayLabel} numberOfLines={2}>{t('calendar.events.all_day', 'All day')}</Text>
</View> </View>
<View style={styles.allDayGrid}> <View style={styles.allDayGrid}>
{/* Empty per-day columns to draw vertical separators */} {/* Empty per-day columns to draw vertical separators */}
@@ -209,7 +209,7 @@ function WeekViewInner({
> >
<Text style={styles.allDayChipText} numberOfLines={1}> <Text style={styles.allDayChipText} numberOfLines={1}>
{segment.continuesBefore ? '… ' : ''} {segment.continuesBefore ? '… ' : ''}
{segment.event.title || 'Untitled'} {segment.event.title || t('calendar.events.no_title', '(No title)')}
</Text> </Text>
</Pressable> </Pressable>
); );
@@ -281,7 +281,7 @@ function WeekViewInner({
]} ]}
> >
<Text style={styles.eventBlockTitle} numberOfLines={1}> <Text style={styles.eventBlockTitle} numberOfLines={1}>
{event.title || 'Untitled'} {event.title || t('calendar.events.no_title', '(No title)')}
</Text> </Text>
{height > 32 && ( {height > 32 && (
<Text style={styles.eventBlockTime} numberOfLines={1}> <Text style={styles.eventBlockTime} numberOfLines={1}>
+32 -3
View File
@@ -931,6 +931,8 @@ export default function CalendarScreen() {
onPress={() => setSidebarVisible(true)} onPress={() => setSidebarVisible(true)}
hitSlop={8} hitSlop={8}
style={styles.headerBtn} style={styles.headerBtn}
accessibilityRole="button"
accessibilityLabel={t('calendar.nav_open_menu', 'Open menu')}
> >
<Menu size={20} color={c.text} /> <Menu size={20} color={c.text} />
</Pressable> </Pressable>
@@ -957,6 +959,8 @@ export default function CalendarScreen() {
style={styles.headerBtn} style={styles.headerBtn}
onPress={() => setTasksVisible(true)} onPress={() => setTasksVisible(true)}
hitSlop={6} hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('calendar.views.tasks', 'Tasks')}
> >
<ListChecks size={20} color={c.text} /> <ListChecks size={20} color={c.text} />
</Pressable> </Pressable>
@@ -969,31 +973,56 @@ export default function CalendarScreen() {
: mode === 'day' ? Calendar1 : mode === 'day' ? Calendar1
: ListIcon; : ListIcon;
const active = viewMode === mode; const active = viewMode === mode;
const label =
mode === 'month' ? t('calendar.views.month', 'Month')
: mode === 'week' ? t('calendar.views.week', 'Week')
: mode === 'day' ? t('calendar.views.day', 'Day')
: t('calendar.views.agenda', 'Agenda');
return ( return (
<Pressable <Pressable
key={mode} key={mode}
style={[styles.viewToggleBtn, active && styles.viewToggleBtnActive]} style={[styles.viewToggleBtn, active && styles.viewToggleBtnActive]}
onPress={() => changeViewMode(mode)} onPress={() => changeViewMode(mode)}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityState={{ selected: active }}
> >
<Icon size={16} color={active ? c.primary : c.textMuted} /> <Icon size={16} color={active ? c.primary : c.textMuted} />
</Pressable> </Pressable>
); );
})} })}
</View> </View>
<Pressable style={styles.fab} onPress={() => openCreate(selectedDate)}> <Pressable
style={styles.fab}
onPress={() => openCreate(selectedDate)}
accessibilityRole="button"
accessibilityLabel={t('calendar.events.new_event', 'New event')}
>
<Plus size={18} color={c.primaryForeground} /> <Plus size={18} color={c.primaryForeground} />
</Pressable> </Pressable>
</View> </View>
</View> </View>
<View style={styles.nav}> <View style={styles.nav}>
<Pressable onPress={goPrev} style={styles.navBtn} hitSlop={8}> <Pressable
onPress={goPrev}
style={styles.navBtn}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('calendar.nav_prev', 'Previous')}
>
<ChevronLeft size={20} color={c.text} /> <ChevronLeft size={20} color={c.text} />
</Pressable> </Pressable>
<Button variant="outline" size="sm" onPress={goToday}> <Button variant="outline" size="sm" onPress={goToday}>
{t('calendar.views.today', 'Today')} {t('calendar.views.today', 'Today')}
</Button> </Button>
<Pressable onPress={goNext} style={styles.navBtn} hitSlop={8}> <Pressable
onPress={goNext}
style={styles.navBtn}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('calendar.nav_next', 'Next')}
>
<ChevronRight size={20} color={c.text} /> <ChevronRight size={20} color={c.text} />
</Pressable> </Pressable>
</View> </View>