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) {
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const { locale } = useCalendarLocale();
const { locale, t } = useCalendarLocale();
const scrollRef = React.useRef<ScrollView>(null);
const index = React.useMemo(
@@ -173,7 +173,7 @@ function WeekViewInner({
{allDayRowCount > 0 && (
<View style={[styles.allDayStrip, { height: allDayStripHeight }]}>
<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 style={styles.allDayGrid}>
{/* Empty per-day columns to draw vertical separators */}
@@ -209,7 +209,7 @@ function WeekViewInner({
>
<Text style={styles.allDayChipText} numberOfLines={1}>
{segment.continuesBefore ? '… ' : ''}
{segment.event.title || 'Untitled'}
{segment.event.title || t('calendar.events.no_title', '(No title)')}
</Text>
</Pressable>
);
@@ -281,7 +281,7 @@ function WeekViewInner({
]}
>
<Text style={styles.eventBlockTitle} numberOfLines={1}>
{event.title || 'Untitled'}
{event.title || t('calendar.events.no_title', '(No title)')}
</Text>
{height > 32 && (
<Text style={styles.eventBlockTime} numberOfLines={1}>
+32 -3
View File
@@ -931,6 +931,8 @@ export default function CalendarScreen() {
onPress={() => setSidebarVisible(true)}
hitSlop={8}
style={styles.headerBtn}
accessibilityRole="button"
accessibilityLabel={t('calendar.nav_open_menu', 'Open menu')}
>
<Menu size={20} color={c.text} />
</Pressable>
@@ -957,6 +959,8 @@ export default function CalendarScreen() {
style={styles.headerBtn}
onPress={() => setTasksVisible(true)}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('calendar.views.tasks', 'Tasks')}
>
<ListChecks size={20} color={c.text} />
</Pressable>
@@ -969,31 +973,56 @@ export default function CalendarScreen() {
: mode === 'day' ? Calendar1
: ListIcon;
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 (
<Pressable
key={mode}
style={[styles.viewToggleBtn, active && styles.viewToggleBtnActive]}
onPress={() => changeViewMode(mode)}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityState={{ selected: active }}
>
<Icon size={16} color={active ? c.primary : c.textMuted} />
</Pressable>
);
})}
</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} />
</Pressable>
</View>
</View>
<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} />
</Pressable>
<Button variant="outline" size="sm" onPress={goToday}>
{t('calendar.views.today', 'Today')}
</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} />
</Pressable>
</View>