mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 00:06:53 +00:00
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:
@@ -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([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user