From 62e4806353a47fba7479d9c65058f4c1b47b918a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:57:03 +0100 Subject: [PATCH 01/13] fix: save a settings sheet on the first tap while the keyboard is open --- .../calendar/CalendarSidebarDrawer.tsx | 2 +- src/components/calendar/EventDetailSheet.tsx | 1 + src/components/calendar/ICalImportSheet.tsx | 6 +- .../calendar/ICalSubscriptionSheet.tsx | 2 +- src/components/calendar/MonthDropdown.tsx | 2 + src/components/calendar/TasksSheet.tsx | 6 +- .../settings/ContentSendersSettings.tsx | 1 + src/components/settings/DelegateDialog.tsx | 2 +- src/components/settings/TemplateSettings.tsx | 4 +- .../__tests__/keyboard-persist-taps.test.ts | 69 +++++++++++++++++++ .../DeviceSyncCollectionsSheet.tsx | 2 +- src/components/settings/settings-section.tsx | 2 +- src/screens/FilesScreen.tsx | 2 +- src/screens/SettingsScreen.tsx | 2 +- 14 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 src/components/settings/__tests__/keyboard-persist-taps.test.ts diff --git a/src/components/calendar/CalendarSidebarDrawer.tsx b/src/components/calendar/CalendarSidebarDrawer.tsx index 5900e2f1..2ead2a93 100644 --- a/src/components/calendar/CalendarSidebarDrawer.tsx +++ b/src/components/calendar/CalendarSidebarDrawer.tsx @@ -186,7 +186,7 @@ export function CalendarSidebarDrawer({ {t('calendar.title', 'Calendar')} - + {views && views.length > 0 && ( {views.map(({ key, label, Icon }) => { diff --git a/src/components/calendar/EventDetailSheet.tsx b/src/components/calendar/EventDetailSheet.tsx index 975b5f48..5dac0fa0 100644 --- a/src/components/calendar/EventDetailSheet.tsx +++ b/src/components/calendar/EventDetailSheet.tsx @@ -321,6 +321,7 @@ export function EventDetailSheet({ - + {result.refused.map(({ event, reason }, idx) => { const date = eventDateLabel(event, locale); return ( @@ -302,7 +302,7 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor ) : ( <> {writable.length > 1 && ( - + {writable.map((cal) => ( - + {parsed.map((event, idx) => { const isSelected = selected.has(idx); const date = eventDateLabel(event, locale); diff --git a/src/components/calendar/ICalSubscriptionSheet.tsx b/src/components/calendar/ICalSubscriptionSheet.tsx index f05f87f9..db175177 100644 --- a/src/components/calendar/ICalSubscriptionSheet.tsx +++ b/src/components/calendar/ICalSubscriptionSheet.tsx @@ -162,7 +162,7 @@ export function ICalSubscriptionSheet({ visible, onClose }: Props) { - + {editingId ? t('calendar.subscription.edit_title', 'Edit Subscription') diff --git a/src/components/calendar/MonthDropdown.tsx b/src/components/calendar/MonthDropdown.tsx index 4caa1283..a8f71ac9 100644 --- a/src/components/calendar/MonthDropdown.tsx +++ b/src/components/calendar/MonthDropdown.tsx @@ -79,6 +79,7 @@ export function MonthDropdown({ const renderItem = React.useCallback( ({ item }: ListRenderItemInfo) => ( {writableCalendars.length > 1 && ( - + {writableCalendars.map((cal) => ( )} - + {FILTERS.map((f) => { const active = filter === f.value; return ( @@ -405,7 +405,7 @@ export function TasksSheet({ })} - + {filtered.length === 0 ? ( diff --git a/src/components/settings/ContentSendersSettings.tsx b/src/components/settings/ContentSendersSettings.tsx index e1f1a0aa..c8eb177b 100644 --- a/src/components/settings/ContentSendersSettings.tsx +++ b/src/components/settings/ContentSendersSettings.tsx @@ -258,6 +258,7 @@ export function ContentSendersSettings() { ) : ( item.email} style={styles.list} diff --git a/src/components/settings/DelegateDialog.tsx b/src/components/settings/DelegateDialog.tsx index 76cf9553..c3cdb3a3 100644 --- a/src/components/settings/DelegateDialog.tsx +++ b/src/components/settings/DelegateDialog.tsx @@ -177,7 +177,7 @@ export function DelegateDialog({ {failures ? ( - + {t('delegates.partial_title', "These couldn't be changed. The rest is done.")} diff --git a/src/components/settings/TemplateSettings.tsx b/src/components/settings/TemplateSettings.tsx index e94008fe..3c832508 100644 --- a/src/components/settings/TemplateSettings.tsx +++ b/src/components/settings/TemplateSettings.tsx @@ -225,7 +225,7 @@ export function TemplateSettings() { - + {tr('settings.templates.name', 'Template Name')} - + JSON also swallows the first tap +// on the sheet's Save button (Settings > Folders > New folder on the tablet, +// where the page sits in the split pane's ScrollView). No renderer in the test +// env, so this checks every scroll container in the settings, calendar sheets, +// files and sharing code. + +const SRC = path.resolve(__dirname, '..', '..', '..'); + +function walk(dir: string): string[] { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return entry.name === '__tests__' ? [] : walk(full); + return entry.name.endsWith('.tsx') ? [full] : []; + }); +} + +// The calendar grids hold no inputs and open their sheets as siblings. +const GRIDS = new Set(['MonthScrollView.tsx', 'TimeGridScrollView.tsx', 'WeekView.tsx', 'AgendaView.tsx']); + +const files = [ + path.join(SRC, 'screens', 'SettingsScreen.tsx'), + path.join(SRC, 'screens', 'FilesScreen.tsx'), + ...walk(path.join(SRC, 'components', 'settings')), + ...walk(path.join(SRC, 'components', 'calendar')).filter((f) => !GRIDS.has(path.basename(f))), + ...walk(path.join(SRC, 'components', 'files')), + ...walk(path.join(SRC, 'components', 'sharing')), +]; + +/** The opening JSX tags of every scroll container in `src`. */ +function scrollTags(src: string): string[] { + const tags: string[] = []; + // Not a type argument such as useRef. + const re = /(?' && depth === 0) break; + } + tags.push(src.slice(m.index, i + 1)); + } + return tags; +} + +describe('keyboardShouldPersistTaps on scroll containers', () => { + it('finds the opening tag across lines and arrow functions', () => { + const tags = scrollTags(' }\n keyboardShouldPersistTaps="handled"\n/>'); + expect(tags).toHaveLength(1); + expect(tags[0]).toContain('keyboardShouldPersistTaps'); + expect(scrollTags('const ref = React.useRef(null);')).toHaveLength(0); + }); + + it.each(files.map((f) => [path.relative(SRC, f), f]))('%s', (_name, file) => { + const src = fs.readFileSync(file, 'utf8'); + for (const tag of scrollTags(src)) { + expect(tag, `${path.basename(file)}: ${tag.split('\n')[0]}`).toMatch(/keyboardShouldPersistTaps="(handled|always)"/); + } + }); +}); diff --git a/src/components/settings/device-sync/DeviceSyncCollectionsSheet.tsx b/src/components/settings/device-sync/DeviceSyncCollectionsSheet.tsx index fa04d816..357386ef 100644 --- a/src/components/settings/device-sync/DeviceSyncCollectionsSheet.tsx +++ b/src/components/settings/device-sync/DeviceSyncCollectionsSheet.tsx @@ -147,7 +147,7 @@ export function DeviceSyncCollectionsSheet({ visible, onClose, registryId, autho - + {state.kind === 'error' && ( {errorText} diff --git a/src/components/settings/settings-section.tsx b/src/components/settings/settings-section.tsx index e22a4353..9d82d402 100644 --- a/src/components/settings/settings-section.tsx +++ b/src/components/settings/settings-section.tsx @@ -303,7 +303,7 @@ export function Select({ value, onChange, options, style }: SelectProps) { > setOpen(false)}> - + {options.map((opt) => { const selected = opt.value === value; return ( diff --git a/src/screens/FilesScreen.tsx b/src/screens/FilesScreen.tsx index 8aafeb73..3b19ef67 100644 --- a/src/screens/FilesScreen.tsx +++ b/src/screens/FilesScreen.tsx @@ -2092,7 +2092,7 @@ function FolderPickerSheet({ target, allNodes, rootLabel, t, onClose, onPick }: {t('files.move_to', 'Move to…')} · {target.displayName} - + {options.map((opt) => { const isCurrent = opt.id === currentParent; return ( diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index dc679a7d..309761f7 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -456,7 +456,7 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin {/* Keyed per page: React would otherwise reuse the list's ScrollView and open the page at the list's scroll offset. */} - + {scopeBanner} From d88d3dfbafba7c976f26b7efe34e50b26e3528ba Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:03:12 +0100 Subject: [PATCH 02/13] fix: scroll the new tag form above the keyboard in settings --- src/components/settings/KeywordSettings.tsx | 7 +- .../__tests__/keyboard-reveal.test.ts | 48 ++++++++++++++ src/components/settings/keyboard-reveal.ts | 55 ++++++++++++++++ src/screens/SettingsScreen.tsx | 64 +++++++++++++++++-- 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/components/settings/__tests__/keyboard-reveal.test.ts create mode 100644 src/components/settings/keyboard-reveal.ts diff --git a/src/components/settings/KeywordSettings.tsx b/src/components/settings/KeywordSettings.tsx index 7ac2dceb..072eec9a 100644 --- a/src/components/settings/KeywordSettings.tsx +++ b/src/components/settings/KeywordSettings.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, Pressable, TextInput, Alert, ActivityIndicator } from 'react-native'; import { Plus, Pencil, Trash2, Check, X, RotateCcw, ScanSearch } from '../icons'; import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section'; +import { SettingsKeyboardContext } from './keyboard-reveal'; import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; import { useColors } from '../../theme/colors'; import { useKeywordsStore, currentTagsLogin, type KeywordDef, type KeywordVisibility } from '../../stores/keywords-store'; @@ -343,6 +344,9 @@ function KeywordForm({ initial, existingIds, keywords, nested, onSave, onCancel ...parentKeywordOptions(keywords).map((o) => ({ value: o.id, label: o.label })), ]; const initialParent = initial ? getParentKeywordId(initial.id) : null; + // The whole form goes above the keyboard, the parent picker included. + const revealOnKeyboard = React.useContext(SettingsKeyboardContext); + const formRef = React.useRef(null); const handleSave = () => { if (!isValid) return; @@ -350,7 +354,7 @@ function KeywordForm({ initial, existingIds, keywords, nested, onSave, onCancel }; return ( - + {nested && !initial && keywords.length > 0 && ( {t('settings.keywords.parent_field', 'Parent tag')} @@ -368,6 +372,7 @@ function KeywordForm({ initial, existingIds, keywords, nested, onSave, onCancel revealOnKeyboard?.(formRef.current)} placeholder={t('settings.keywords.label_placeholder', "e.g. Work, Personal, Urgent")} placeholderTextColor={c.mutedForeground} style={styles.input} diff --git a/src/components/settings/__tests__/keyboard-reveal.test.ts b/src/components/settings/__tests__/keyboard-reveal.test.ts new file mode 100644 index 00000000..7ab1e0aa --- /dev/null +++ b/src/components/settings/__tests__/keyboard-reveal.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { revealScrollY, settingsPagePaddingBottom, visibleHeightAboveKeyboard } from '../keyboard-reveal'; + +// Settings > Tags > Add tag: the form at the end of the page stayed under the +// keyboard (the window does not shrink for it on Android edge to edge), so +// taps meant for "No parent" landed on the keys. + +const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', '..', rel), 'utf8'); + +describe('settings keyboard reveal', () => { + // A phone: page from y=80 to y=800, keyboard top at y=500. + const visible = visibleHeightAboveKeyboard(80, 720, 500); + + it('measures the page height left above the keyboard', () => { + expect(visible).toBe(420); + expect(visibleHeightAboveKeyboard(80, 720, 900)).toBe(720); + expect(visibleHeightAboveKeyboard(80, 720, 60)).toBe(0); + }); + + it('scrolls a form under the keyboard up until its end clears it', () => { + // Form at page y 600..800 with the page at the top: its end is hidden. + expect(revealScrollY(0, 600, 800, visible, 12)).toBe(392); + }); + + it('keeps the top of a form taller than the space above the keyboard', () => { + expect(revealScrollY(0, 600, 1200, visible, 12)).toBe(588); + }); + + it('leaves a form that already fits alone', () => { + expect(revealScrollY(0, 100, 300, visible, 12)).toBeNull(); + }); + + it('pads the page end by the keyboard so it can scroll that far', () => { + expect(settingsPagePaddingBottom(40, 0)).toBe(40); + expect(settingsPagePaddingBottom(40, 300)).toBe(340); + }); + + it('pads the settings page and hands the tag form to it on focus', () => { + const screen = read('screens/SettingsScreen.tsx'); + expect(screen).toMatch(/paddingBottom: settingsPagePaddingBottom\(DETAIL_PADDING_BOTTOM, keyboard\)/); + expect(screen).toMatch(//); + const tags = read('components/settings/KeywordSettings.tsx'); + expect(tags).toMatch(//); + expect(tags).toMatch(/onFocus=\{\(\) => revealOnKeyboard\?\.\(formRef\.current\)\}/); + }); +}); diff --git a/src/components/settings/keyboard-reveal.ts b/src/components/settings/keyboard-reveal.ts new file mode 100644 index 00000000..f6076e0a --- /dev/null +++ b/src/components/settings/keyboard-reveal.ts @@ -0,0 +1,55 @@ +import { createContext } from 'react'; +import type { View } from 'react-native'; + +/** + * Keeps a settings form in view while the keyboard is up. Android draws the + * app edge to edge, so the window does not shrink for the keyboard: a form at + * the bottom of a settings page (Settings > Tags > Add tag) stayed under the + * keyboard, and taps meant for its fields landed on the keys. The page pads + * its bottom by the keyboard and scrolls the form the user is typing in above + * it. + */ + +/** + * Hand a form's outer View to the settings page when one of its fields gets + * focus; the page scrolls the whole form above the keyboard. Without a form, + * the page scrolls the focused field into view. + */ +export const SettingsKeyboardContext = createContext<((form: View | null) => void) | null>(null); + +/** Space kept between the form and the keyboard or the page top. */ +export const REVEAL_MARGIN = 12; + +/** + * The height of the scroll area still visible above the keyboard. + * `viewportTop` and `keyboardTop` are window coordinates. + */ +export function visibleHeightAboveKeyboard(viewportTop: number, viewportHeight: number, keyboardTop: number): number { + return Math.max(0, Math.min(viewportHeight, keyboardTop - viewportTop)); +} + +/** + * Where to scroll so the form (content coordinates `formTop`..`formBottom`) + * fits in the `visibleHeight` above the keyboard, or null when it already + * does. A form taller than that keeps its top in view. + */ +export function revealScrollY( + scrollY: number, + formTop: number, + formBottom: number, + visibleHeight: number, + margin: number = REVEAL_MARGIN, +): number | null { + if (visibleHeight <= 0) return null; + const keepTop = formTop - margin; + let target = scrollY; + if (formBottom + margin > scrollY + visibleHeight) target = Math.min(formBottom + margin - visibleHeight, keepTop); + else if (keepTop < scrollY) target = keepTop; + target = Math.max(0, Math.round(target)); + return target === Math.round(scrollY) ? null : target; +} + +/** Bottom padding of a settings page: room to scroll its end above the keyboard. */ +export function settingsPagePaddingBottom(base: number, keyboard: number): number { + return base + Math.max(0, Math.round(keyboard)); +} diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 309761f7..aa9c55b2 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, Keyboard } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, Keyboard, Dimensions } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useKeyboardObstruction } from '../lib/use-keyboard-obstruction'; import { useLayout } from '../lib/use-layout'; import { confirmSignOut } from '../lib/sign-out-confirm'; import { @@ -71,11 +72,16 @@ import { type SettingsTabId, type SubResult, } from '../lib/settings-search'; import { SearchHighlightContext, type SearchHighlight } from '../components/settings/search-highlight'; +import { + SettingsKeyboardContext, revealScrollY, settingsPagePaddingBottom, visibleHeightAboveKeyboard, +} from '../components/settings/keyboard-reveal'; import { BUILTIN_THEMES } from '../theme/builtin-themes'; import { getDictionary } from '../i18n'; type Tab = SettingsTabId; +const DETAIL_PADDING_BOTTOM = 40; + type TabGroup = 'general' | 'appearance' | 'mail' | 'privacy' | 'apps' | 'advanced'; interface TabDef { @@ -391,6 +397,46 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin } setPendingHighlight(null); }, []); + // The window does not shrink for the keyboard (Android edge to edge, iOS): + // the page pads its end by the keyboard and scrolls the form being typed + // in above it (Settings > Tags > Add tag sat under the keyboard). + const insets = useSafeAreaInsets(); + const keyboard = useKeyboardObstruction(insets.bottom); + const keyboardRef = React.useRef(keyboard); + keyboardRef.current = keyboard; + const paneScrollY = React.useRef(0); + const revealForm = React.useRef(null); + const revealFocused = React.useCallback(() => { + if (keyboardRef.current <= 0) return; + const scroll = paneScrollRef.current; + const viewport = scroll?.getNativeScrollRef(); + const target = revealForm.current ?? (TextInput.State.currentlyFocusedInput() as unknown as View | null); + if (!scroll || !viewport || !target) return; + viewport.measureInWindow((_vx, viewportTop, _vw, viewportHeight) => { + target.measureInWindow((_x, top, _w, height) => { + const keyboardTop = Dimensions.get('screen').height - keyboardRef.current; + const visible = visibleHeightAboveKeyboard(viewportTop, viewportHeight, keyboardTop); + // Window coordinates turned into the page's own. + const formTop = top - viewportTop + paneScrollY.current; + const y = revealScrollY(paneScrollY.current, formTop, formTop + height, visible); + if (y !== null) scroll.scrollTo({ y, animated: true }); + }); + }); + }, []); + const revealOnKeyboard = React.useCallback((form: View | null) => { + revealForm.current = form; + revealFocused(); + }, [revealFocused]); + useEffect(() => { + if (keyboard <= 0) { + revealForm.current = null; + return undefined; + } + // After the keyboard padding is laid out, so the page can scroll that far. + const frame = requestAnimationFrame(revealFocused); + return () => cancelAnimationFrame(frame); + }, [keyboard, revealFocused]); + const searchHighlight = React.useMemo( () => (pendingHighlight && pendingHighlight.tab === selectedTab ? { label: pendingHighlight.label, reveal: revealHighlight } @@ -456,8 +502,17 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin {/* Keyed per page: React would otherwise reuse the list's ScrollView and open the page at the list's scroll offset. */} - + { paneScrollY.current = e.nativeEvent.contentOffset.y; }} + scrollEventThrottle={64} + > + {scopeBanner} {selectedTab === 'filters' ? ( @@ -470,6 +525,7 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin openPage('debug')} /> ) : Component ? : null} + @@ -662,7 +718,7 @@ function makeStyles(c: ThemePalette) { scrollContent: { paddingTop: spacing.md, paddingBottom: 40 }, // A page keeps form width on a wide pane: labels and their controls // 700 dp apart are hard to read across. - detailContent: { padding: spacing.lg, paddingBottom: 40, width: '100%', maxWidth: 720, alignSelf: 'center' }, + detailContent: { padding: spacing.lg, paddingBottom: DETAIL_PADDING_BOTTOM, width: '100%', maxWidth: 720, alignSelf: 'center' }, accountCard: { flexDirection: 'row', From 818c45c280a39a6087d60a1195ca69aae022c7d6 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:12:12 +0100 Subject: [PATCH 03/13] fix: say cancelled when the organizer cancels an event --- .../live-stalwart-event-notifications.test.ts | 5 ++++- src/api/calendar-event-notifications.ts | 16 ++++++++++++++-- .../calendar-event-notifications.test.ts | 14 +++++++++++++- src/lib/calendar-event-notifications.ts | 5 ++++- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/api/__tests__/live-stalwart-event-notifications.test.ts b/src/api/__tests__/live-stalwart-event-notifications.test.ts index 6f35a0df..fdc8366c 100644 --- a/src/api/__tests__/live-stalwart-event-notifications.test.ts +++ b/src/api/__tests__/live-stalwart-event-notifications.test.ts @@ -159,8 +159,11 @@ liveTwo('live Stalwart calendar event notifications', () => { const cancel = notes.find((n) => n.calendarEventId === attendeeEventId && !ours.has(n.id)); expect(cancel, JSON.stringify(notes)).toBeTruthy(); ours.add(cancel!.id); + // Both servers send it as an update that sets the status to cancelled + // (the attendee's copy stays, cancelled); it must read as a cancellation. const toast = eventNotificationToast(cancel!, t)!; - if (cancel!.type === 'destroyed') expect(toast.type).toBe('warning'); + expect(toast.type, JSON.stringify(cancel)).toBe('warning'); + expect(toast.title).toContain(`cancelled "${RUN} review"`); await destroyCalendarEventNotifications([...ours]); const after = (await getCalendarEventNotifications()).map((n) => n.id); diff --git a/src/api/calendar-event-notifications.ts b/src/api/calendar-event-notifications.ts index deaf3ae3..6ba2bf35 100644 --- a/src/api/calendar-event-notifications.ts +++ b/src/api/calendar-event-notifications.ts @@ -20,8 +20,20 @@ export interface CalendarEventNotification { type: 'created' | 'updated' | 'destroyed'; calendarEventId: string; isDraft?: boolean; - /** The event as it is now; 1.0 returns it, 0.16 leaves it out. */ + /** The event before the change on 1.0 (as created for an invitation); 0.16 leaves it out. */ event?: { title?: string | null } | null; + /** + * What changed, for an `updated` one: a JSCalendar patch on 1.0, the whole + * event as it is now on 0.16. An organizer's cancellation reaches the + * attendee as an update that sets `status` to `cancelled` (iTIP CANCEL + * keeps the attendee's copy), never as `destroyed`. + */ + eventPatch?: { status?: string | null; title?: string | null } | null; +} + +/** An organizer cancelled the event: the change sets its status to cancelled. */ +export function isCancellation(n: Pick): boolean { + return n.type === 'destroyed' || (n.type === 'updated' && n.eventPatch?.status === 'cancelled'); } const USING = [CAPABILITIES.CORE, CAPABILITIES.CALENDARS]; @@ -42,7 +54,7 @@ export async function getCalendarEventNotifications(): Promise ({ vi.mock('react-native', () => ({ AppState: { addEventListener: vi.fn() } })); vi.mock('../../api/jmap-client', () => ({ jmapClient: h.client })); -vi.mock('../../api/calendar-event-notifications', () => ({ +vi.mock('../../api/calendar-event-notifications', async (importOriginal) => ({ + ...(await importOriginal()), getCalendarEventNotifications: h.list, destroyCalendarEventNotifications: h.destroy, })); @@ -71,6 +72,17 @@ describe('the toast for a calendar event notification', () => { .toEqual({ type: 'warning', title: 'Land cancelled "Planning"', message: 'Sorry, ill' }); }); + it('says cancelled, as a warning, when the organizer cancels (an update setting status cancelled)', () => { + // What Stalwart 1.0.1 sends the attendee after CalendarEvent/set destroy + // with sendSchedulingMessages; 0.16.27 sends the whole event as the patch. + expect(eventNotificationToast(note('4', { eventPatch: { sequence: null, status: 'cancelled' } as never }), t)) + .toEqual({ type: 'warning', title: 'Land cancelled "Planning"', message: undefined }); + expect(eventNotificationToast(note('5', { event: null, eventPatch: { title: 'Planning', status: 'cancelled' } }), t)!.type) + .toBe('warning'); + // Any other change stays an update. + expect(eventNotificationToast(note('6', { eventPatch: { title: 'Planning v2' } }), t)!.type).toBe('info'); + }); + it('falls back to the address, then "Someone", and to an untitled event', () => { expect(eventNotificationToast(note('1', { changedBy: { email: 'x@example.org' }, event: null }), t)!.title) .toBe('x@example.org updated "Untitled event"'); diff --git a/src/lib/calendar-event-notifications.ts b/src/lib/calendar-event-notifications.ts index b25e7d2b..8f21ca1b 100644 --- a/src/lib/calendar-event-notifications.ts +++ b/src/lib/calendar-event-notifications.ts @@ -3,6 +3,7 @@ import { jmapClient } from '../api/jmap-client'; import { destroyCalendarEventNotifications, getCalendarEventNotifications, + isCancellation, type CalendarEventNotification, } from '../api/calendar-event-notifications'; import { useAuthStore } from '../stores/auth-store'; @@ -33,7 +34,9 @@ export interface EventNotificationToast { /** The toast for one notification; null for a draft (the user's own unsent change). */ export function eventNotificationToast(n: CalendarEventNotification, t: Translate): EventNotificationToast | null { if (n.isDraft) return null; - const kind = n.type === 'created' ? 'invited' : n.type === 'destroyed' ? 'cancelled' : 'updated'; + // Stalwart sends an organizer's cancellation as an update setting the + // status to cancelled (the webmail reads only `type` and says "updated"). + const kind = n.type === 'created' ? 'invited' : isCancellation(n) ? 'cancelled' : 'updated'; const params = { name: n.changedBy?.name || n.changedBy?.email || t('calendar_event_notifications.someone', 'Someone'), title: n.event?.title || t('calendar_event_notifications.untitled', 'Untitled event'), From 305b836001be6f3f2fa911c35889de43416f86b7 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:14:38 +0100 Subject: [PATCH 04/13] fix: close an untouched all-day event without asking to discard changes --- src/components/calendar/EventModal.tsx | 4 +- .../__tests__/calendar-editor-discard.test.ts | 48 ++++++++++++++++++- src/lib/calendar-editor-discard.ts | 17 +++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/components/calendar/EventModal.tsx b/src/components/calendar/EventModal.tsx index 9dd5175f..802f83f1 100644 --- a/src/components/calendar/EventModal.tsx +++ b/src/components/calendar/EventModal.tsx @@ -82,7 +82,7 @@ import { SafeAreaModal } from '../SafeAreaModal'; import { useKeyboardObstruction } from '../../lib/use-keyboard-obstruction'; import { editorKeyboardInset } from '../../lib/calendar-editor-keyboard'; import ToggleSwitch from '../ToggleSwitch'; -import { askBeforeDiscard, editorFormChanged, editorFormSnapshot } from '../../lib/calendar-editor-discard'; +import { askBeforeDiscard, editorFormChanged, editorFormSnapshot, editorTimesSnapshot } from '../../lib/calendar-editor-discard'; import { saveOfferingWithoutInvitations } from '../../lib/calendar-scheduling-denied'; type RecurrenceOption = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom'; @@ -269,7 +269,7 @@ export function EventModal({ // What the form holds against what it opened with: Close and Back ask // before they drop changes (webmail 13f9f27c4). const formSnapshot = React.useMemo(() => editorFormSnapshot({ - title, description, allDay, start, end, calendarId, attendees, recurrence, customRule, + title, description, times: editorTimesSnapshot({ allDay, start, end }), calendarId, attendees, recurrence, customRule, reminders, useDefaults, preserved, sendInvitations, location, videoUrl, }), [title, description, allDay, start, end, calendarId, attendees, recurrence, customRule, reminders, useDefaults, preserved, sendInvitations, location, videoUrl]); diff --git a/src/lib/__tests__/calendar-editor-discard.test.ts b/src/lib/__tests__/calendar-editor-discard.test.ts index 8c2e5292..85e5fa51 100644 --- a/src/lib/__tests__/calendar-editor-discard.test.ts +++ b/src/lib/__tests__/calendar-editor-discard.test.ts @@ -3,8 +3,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const alert = vi.fn(); vi.mock('react-native', () => ({ Alert: { alert: (...args: unknown[]) => alert(...args) } })); -import { askBeforeDiscard, editorFormChanged, editorFormSnapshot } from '../calendar-editor-discard'; +import { askBeforeDiscard, editorFormChanged, editorFormSnapshot, editorTimesSnapshot } from '../calendar-editor-discard'; import { emptyTaskEditor, taskEditorChanged, taskEditorFromTask } from '../task-editor'; +import { editorTimesFromEvent } from '../event-editor-times'; +import type { CalendarEvent } from '../../api/types'; +import fs from 'node:fs'; +import path from 'node:path'; const t = (_key: string, fallback?: string) => fallback ?? ''; @@ -13,6 +17,48 @@ const buttons = (): Button[] => alert.mock.calls.at(-1)![2] as Button[]; beforeEach(() => alert.mockReset()); +describe('the event editor times in the dirty check', () => { + // What the editor does with an all-day event it opens: seed the times, + // then its all-day effect sets start and end to midnight. + const opened = (event: Partial) => { + const seeded = editorTimesFromEvent(event as CalendarEvent); + const s = new Date(seeded.start); s.setHours(0, 0, 0, 0); + const e = new Date(seeded.end); e.setHours(0, 0, 0, 0); + if (e < s) e.setTime(s.getTime()); + return { seeded, normalized: { allDay: seeded.allDay, start: s, end: e } }; + }; + const snap = (times: { allDay: boolean; start: Date; end: Date }) => + editorFormSnapshot({ title: 'Company holiday', times: editorTimesSnapshot(times) }); + + it('finds no change in an all-day event opened and left alone', () => { + // "Company holiday" as Stalwart 0.16 returns it: a day, P1D. + const { seeded, normalized } = opened({ + '@type': 'Event', showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P1D', + } as Partial); + expect(seeded.end.getHours()).toBe(23); // the inclusive last day, 23:59:59.999 + expect(editorFormChanged(snap(seeded), snap(normalized))).toBe(false); + // Two days, and one with a time zone attached. + const two = opened({ showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P2D' }); + expect(editorFormChanged(snap(two.seeded), snap(two.normalized))).toBe(false); + const zoned = opened({ showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P1D', timeZone: 'Europe/Berlin' }); + expect(editorFormChanged(snap(zoned.seeded), snap(zoned.normalized))).toBe(false); + }); + + it('still sees another day, and a moved timed event', () => { + const { normalized } = opened({ showWithoutTime: true, start: '2026-10-03T00:00:00', duration: 'P1D' }); + const nextDay = { ...normalized, end: new Date(normalized.end.getTime() + 86_400_000) }; + expect(editorFormChanged(snap(normalized), snap(nextDay))).toBe(true); + const timed = { allDay: false, start: new Date(2026, 9, 3, 9), end: new Date(2026, 9, 3, 10) }; + expect(editorFormChanged(snap(timed), snap({ ...timed, end: new Date(2026, 9, 3, 10, 30) }))).toBe(true); + expect(editorFormChanged(snap(timed), snap({ ...timed, allDay: true }))).toBe(true); + }); + + it('is what the event editor compares', () => { + const src = fs.readFileSync(path.resolve(__dirname, '../../components/calendar/EventModal.tsx'), 'utf8'); + expect(src).toMatch(/times: editorTimesSnapshot\(\{ allDay, start, end \}\)/); + }); +}); + describe('askBeforeDiscard', () => { it('closes at once when nothing was changed', () => { const close = vi.fn(); diff --git a/src/lib/calendar-editor-discard.ts b/src/lib/calendar-editor-discard.ts index f824a81b..eb5f7f99 100644 --- a/src/lib/calendar-editor-discard.ts +++ b/src/lib/calendar-editor-discard.ts @@ -11,6 +11,23 @@ export function editorFormSnapshot(fields: Record): string { return JSON.stringify(fields); } +/** + * The editor's times as the dirty check compares them. An all-day event + * compares by its first and last day: the editor seeds its last day as + * 23:59:59.999 (the exclusive end minus 1 ms) and then sets both to + * midnight, so comparing instants asked "Discard changes?" for an all-day + * event opened and closed untouched. A timed one compares by the instant. + */ +export function editorTimesSnapshot(times: { allDay: boolean; start: Date; end: Date }): Record { + if (!times.allDay) return { allDay: false, start: times.start, end: times.end }; + return { allDay: true, start: dayKey(times.start), end: dayKey(times.end) }; +} + +function dayKey(date: Date): string { + const pad = (n: number) => String(n).padStart(2, '0'); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; +} + /** Whether the form holds changes against `baseline`, the form it opened with (null: not seeded yet). */ export function editorFormChanged(baseline: string | null, current: string): boolean { return baseline !== null && baseline !== current; From db1fac408572354a72aeea91b790e9f2a786c67a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:17:11 +0100 Subject: [PATCH 05/13] fix: read the year on the calendar's month button also in the current year --- src/lib/__tests__/calendar-a11y.test.ts | 25 ++++++++++++++++++++++++- src/lib/calendar-a11y.ts | 9 +++++---- src/screens/CalendarScreen.tsx | 2 +- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/lib/__tests__/calendar-a11y.test.ts b/src/lib/__tests__/calendar-a11y.test.ts index 23f25d68..27e6e71d 100644 --- a/src/lib/__tests__/calendar-a11y.test.ts +++ b/src/lib/__tests__/calendar-a11y.test.ts @@ -14,6 +14,8 @@ import { participantRemoveAccessibilityLabel, } from '../calendar-a11y'; import { useSettingsStore } from '../../stores/settings-store'; +import { gregorianCalendar, jalaliCalendar } from '../calendar-system'; +import { displayNow } from '../calendar-timezone'; const t = (_key: string, fallback?: string, params?: Record) => formatMessage(fallback ?? '', params, 'en'); @@ -54,7 +56,28 @@ describe('eventAccessibilityLabel', () => { describe('header and chip buttons', () => { it('names the month button by the month it shows, not "Click to change month"', () => { - expect(monthButtonAccessibilityLabel('October 2026', t)).toBe('October 2026, change month'); + expect(monthButtonAccessibilityLabel(new Date(2026, 9, 15), gregorianCalendar, enUS, t)).toBe('October 2026, change month'); + }); + + it('reads the year also in the current year, where the header leaves it out', () => { + const now = displayNow(); + const month = new Intl.DateTimeFormat('en-US', { month: 'long' }).format(now); + expect(monthButtonAccessibilityLabel(now, gregorianCalendar, enUS, t)).toBe(`${month} ${now.getFullYear()}, change month`); + expect(monthButtonAccessibilityLabel(now, jalaliCalendar, enUS, t)).toMatch(/ \d{4}, change month$/); + }); + + it('has "change month" translated in every app overlay', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + const dir = path.resolve(__dirname, '../../../locales/rn'); + const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')); + expect(files.length).toBeGreaterThanOrEqual(29); + for (const file of files) { + const overlay = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')); + const value = overlay.calendar?.change_month_named as string | undefined; + expect(value, file).toContain('{month}'); + if (file !== 'en.json') expect(value, file).not.toContain('change month'); + } }); it('names the cross on a participant chip by the person it removes', () => { diff --git a/src/lib/calendar-a11y.ts b/src/lib/calendar-a11y.ts index 97fcbd6c..c5a6d37e 100644 --- a/src/lib/calendar-a11y.ts +++ b/src/lib/calendar-a11y.ts @@ -3,7 +3,7 @@ import type { Locale } from 'date-fns'; import type { Calendar, CalendarEvent } from '../api/types'; import { getEventDisplayEndDate, getEventStartDate, getPrimaryCalendarId, timePattern } from './calendar-utils'; import { isDeclinedByUser } from './calendar-participants'; -import { formatCalendarDay } from './calendar-system'; +import { formatCalendarDay, monthTitle, type CalendarSystem } from './calendar-system'; type Translate = (key: string, fallback?: string, params?: Record) => string; @@ -14,10 +14,11 @@ type Translate = (key: string, fallback?: string, params?: Record From 421fa6592a58865de7eed0f011a379e40e8c83ed Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:23:02 +0100 Subject: [PATCH 06/13] fix: draw the app text in the chosen reading font in release builds --- src/theme/__tests__/reading-fonts.test.ts | 55 ++++++++++++++++++----- src/theme/reading-font-install.ts | 7 +-- src/theme/reading-font-text.ts | 49 ++++++++++++++------ 3 files changed, 82 insertions(+), 29 deletions(-) diff --git a/src/theme/__tests__/reading-fonts.test.ts b/src/theme/__tests__/reading-fonts.test.ts index 289479de..8817e689 100644 --- a/src/theme/__tests__/reading-fonts.test.ts +++ b/src/theme/__tests__/reading-fonts.test.ts @@ -26,7 +26,7 @@ import { getUiFontFamily, setUiFontFamily, withFontFamily, - wrapModuleDefaults, + wrapNamedExports, wrapWithReadingFont, } from '../reading-font-text'; import { APP_FONTS, mergeWithDefaults } from '../../stores/settings-store'; @@ -191,15 +191,48 @@ describe('the reading font on the app text', () => { setUiFontFamily(null); }); - it('wraps each module default once and leaves non-functions alone', () => { - const text = { default: () => null }; - const memo = { default: { $$typeof: Symbol.for('react.memo') } }; - const original = text.default; - expect(wrapModuleDefaults([text, memo])).toBe(1); - expect(text.default).not.toBe(original); - const wrapped = text.default; - expect(wrapModuleDefaults([text])).toBe(0); - expect(text.default).toBe(wrapped); - expect(memo.default).toEqual({ $$typeof: Symbol.for('react.memo') }); + // The shapes in a release bundle (expo export:embed --dev false): the Text + // module's default export is a getter without a setter, and react-native's + // index exposes Text through a configurable lazy getter that reads it. + function releaseModules() { + const seen: unknown[] = []; + const TextImpl = (props: Record) => { seen.push(props); return null; }; + const textModule = {}; + Object.defineProperty(textModule, '__esModule', { value: true }); + Object.defineProperty(textModule, 'default', { enumerable: true, get: () => TextImpl }); + const reactNative = { + get Text() { return (textModule as { default: typeof TextImpl }).default; }, + get TextInput() { return { $$typeof: Symbol.for('react.forward_ref') }; }, + }; + return { seen, TextImpl, textModule, reactNative }; + } + + it('cannot replace the Text module default of a release bundle', () => { + const { TextImpl, textModule } = releaseModules(); + // What the app used to do: the assignment fails and the plain Text stays. + expect(() => { (textModule as { default: unknown }).default = () => null; }).toThrow(TypeError); + expect((textModule as { default: unknown }).default).toBe(TextImpl); + }); + + it('wraps Text on react-native’s exports, which app code reads at render time', () => { + const { seen, TextImpl, reactNative } = releaseModules(); + expect(wrapNamedExports(reactNative, ['Text', 'TextInput'])).toBe(1); + const Text = reactNative.Text as unknown as (props: Record) => unknown; + expect(Text).not.toBe(TextImpl); + setUiFontFamily('OpenDyslexic'); + Text({ style: { color: 'red' }, children: 'Inbox' }); + expect(seen.pop()).toEqual({ style: [{ fontFamily: 'OpenDyslexic' }, { color: 'red' }], children: 'Inbox' }); + setUiFontFamily(null); + // Once only; a forwardRef object is left alone. + expect(wrapNamedExports(reactNative, ['Text', 'TextInput'])).toBe(0); + expect(reactNative.Text).toBe(Text); + expect(reactNative.TextInput).toEqual({ $$typeof: Symbol.for('react.forward_ref') }); + }); + + it('is what the startup install wraps, before the app loads', () => { + const install = readFileSync(join(__dirname, '..', 'reading-font-install.ts'), 'utf8'); + expect(install).toMatch(/wrapNamedExports\(require\('react-native'\), \['Text', 'TextInput'\]\)/); + const entry = readFileSync(join(__dirname, '..', '..', '..', 'index.ts'), 'utf8'); + expect(entry.match(/^import .*$/m)?.[0]).toBe("import './src/theme/reading-font-install';"); }); }); diff --git a/src/theme/reading-font-install.ts b/src/theme/reading-font-install.ts index 5b0ed8cc..4b930bec 100644 --- a/src/theme/reading-font-install.ts +++ b/src/theme/reading-font-install.ts @@ -6,14 +6,11 @@ /* eslint-disable @typescript-eslint/no-require-imports */ import { Platform } from 'react-native'; -import { setUiFontFamily, wrapModuleDefaults } from './reading-font-text'; +import { setUiFontFamily, wrapNamedExports } from './reading-font-text'; import { uiFontFamily, uiFontsBundled } from './reading-fonts'; if (uiFontsBundled(Platform.OS)) { - wrapModuleDefaults([ - require('react-native/Libraries/Text/Text'), - require('react-native/Libraries/Components/TextInput/TextInput'), - ]); + wrapNamedExports(require('react-native'), ['Text', 'TextInput']); const { useSettingsStore } = require('../stores/settings-store') as typeof import('../stores/settings-store'); setUiFontFamily(uiFontFamily(useSettingsStore.getState().appFont, Platform.OS)); useSettingsStore.subscribe((state, prev) => { diff --git a/src/theme/reading-font-text.ts b/src/theme/reading-font-text.ts index adbbd1ee..7b0650d2 100644 --- a/src/theme/reading-font-text.ts +++ b/src/theme/reading-font-text.ts @@ -8,10 +8,18 @@ // chosen family under the caller's own style (an explicit fontFamily still // wins) and re-renders when the setting changes. // +// The wrapped components replace the `Text` and `TextInput` getters on +// react-native's own exports object, which every `import { Text } from +// 'react-native'` reads at render time. Replacing the `default` export of +// react-native/Libraries/Text/Text instead worked in a dev bundle only: the +// release bundle defines that export as a getter without a setter +// (`Object.defineProperty(exports, "default", { get })`), so the assignment +// failed, was swallowed, and the app text stayed in the theme font while the +// plain-text mail body (which takes the font another way) changed. +// // Wrapping happens before any screen module loads (index.ts imports -// ./reading-font-install first): with Metro's import support every module -// copies `Text` out of react-native when it is evaluated, so a module loaded -// earlier keeps the plain component. The wrapper stays installed for the +// ./reading-font-install first), so a module that copies the exports (an +// `import * as`) copies the wrapped ones. The wrapper stays installed for the // whole run, also while the theme font is chosen; swapping the component // later would remount every text field. @@ -66,23 +74,38 @@ export function wrapWithReadingFont(Original: FunctionComponent): FunctionCompon } /** - * Wraps the default export of each module (react-native's Text and - * TextInput) in place. Only plain function components are wrapped; anything - * else (a future memo or forwardRef object) is left alone. Returns how many - * were wrapped; a second call wraps nothing. + * Replaces the named components on `exports` (react-native's exports object: + * Text, TextInput) with wrapped ones. A getter is redefined as a getter that + * returns the wrapped component, as react-native's own lazy getters are + * configurable; a property that can be neither redefined nor assigned is + * left alone. Only plain function components are wrapped; anything else (a + * future memo or forwardRef object) is left alone. Returns how many were + * wrapped; a second call wraps nothing. */ -export function wrapModuleDefaults(modules: Array<{ default?: unknown }>): number { +export function wrapNamedExports(exports: object, names: readonly string[]): number { + const target = exports as Record; let wrapped = 0; - for (const mod of modules) { - const component = mod?.default; + for (const name of names) { + let component: unknown; + try { + component = target[name]; + } catch { + continue; + } if (typeof component !== 'function') continue; if ((component as unknown as Record)[WRAPPED]) continue; + const next = wrapWithReadingFont(component as FunctionComponent); + const own = Object.getOwnPropertyDescriptor(target, name); try { - mod.default = wrapWithReadingFont(component as FunctionComponent); - wrapped++; + if (!own || own.configurable) { + Object.defineProperty(target, name, { configurable: true, enumerable: own?.enumerable ?? true, get: () => next }); + } else if (own.writable) { + target[name] = next; + } } catch { - // A read-only export: leave the component as it is. + // Neither redefinable nor writable: leave the component as it is. } + if (target[name] === next) wrapped++; } return wrapped; } From 1286e6cb822b11a8047c431cdb61545629af9d67 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:27:02 +0100 Subject: [PATCH 07/13] fix: list the address books after an account switch and mark only the own default --- src/components/settings/ContactsSettings.tsx | 5 ++++- .../__tests__/address-book-rows.test.ts | 19 ++++++++++++++++++ src/components/settings/address-book-rows.ts | 10 ++++++++++ src/stores/__tests__/contacts-store.test.ts | 20 +++++++++++++++++++ src/stores/contacts-store.ts | 4 ++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/components/settings/__tests__/address-book-rows.test.ts create mode 100644 src/components/settings/address-book-rows.ts diff --git a/src/components/settings/ContactsSettings.tsx b/src/components/settings/ContactsSettings.tsx index c746c304..8db4d379 100644 --- a/src/components/settings/ContactsSettings.tsx +++ b/src/components/settings/ContactsSettings.tsx @@ -23,6 +23,7 @@ import { spacing, radius, typography, componentSizes, type ThemePalette } from ' import { useColors } from '../../theme/colors'; import type { AddressBook } from '../../api/types'; import { addressBookLabel } from '../../lib/address-book-label'; +import { showsDefaultBadge } from './address-book-rows'; export function ContactsSettings() { const c = useColors(); @@ -65,6 +66,8 @@ export function ContactsSettings() { const [shareTarget, setShareTarget] = React.useState(null); // Android with the native module only (#34). const deviceSync = React.useMemo(() => deviceSyncAvailable(), []); + // The books as they are now, also after an account switch. + React.useEffect(() => { void fetchAddressBooks(); }, [fetchAddressBooks]); const exportable = React.useMemo(() => contacts.filter((cc) => !isGroup(cc)), [contacts]); const exportLabel = @@ -266,7 +269,7 @@ export function ContactsSettings() { {t('settings.contacts.book_count', '{count, plural, one {# contact} other {# contacts}}', { count: book.count })} - {book.isDefault ? ` · ${t('contacts.address_books.default', 'Default')}` : ''} + {showsDefaultBadge(book) ? ` · ${t('contacts.address_books.default', 'Default')}` : ''} {/* Own books the user may share (webmail address-book settings). */} diff --git a/src/components/settings/__tests__/address-book-rows.test.ts b/src/components/settings/__tests__/address-book-rows.test.ts new file mode 100644 index 00000000..978d507b --- /dev/null +++ b/src/components/settings/__tests__/address-book-rows.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { showsDefaultBadge } from '../address-book-rows'; + +describe('Settings > Contacts address book rows', () => { + it('marks only the user’s own default book as Default, not a group’s', () => { + expect(showsDefaultBadge({ isDefault: true, isShared: false })).toBe(true); + expect(showsDefaultBadge({ isDefault: true, isShared: true })).toBe(false); + expect(showsDefaultBadge({ isDefault: false, isShared: false })).toBe(false); + expect(showsDefaultBadge({})).toBe(false); + }); + + it('is what the rows use, and the page loads the books when it opens', () => { + const src = fs.readFileSync(path.resolve(__dirname, '..', 'ContactsSettings.tsx'), 'utf8'); + expect(src).toMatch(/\{showsDefaultBadge\(book\) \? ` · \$\{t\('contacts\.address_books\.default', 'Default'\)\}` : ''\}/); + expect(src).toMatch(/React\.useEffect\(\(\) => \{ void fetchAddressBooks\(\); \}, \[fetchAddressBooks\]\);/); + }); +}); diff --git a/src/components/settings/address-book-rows.ts b/src/components/settings/address-book-rows.ts new file mode 100644 index 00000000..cf3f7aa3 --- /dev/null +++ b/src/components/settings/address-book-rows.ts @@ -0,0 +1,10 @@ +import type { AddressBook } from '../../api/types'; + +/** + * Whether Settings > Contacts marks a book "Default": the user's own default + * book only. A group's book is the default of the group's account, which made + * the own and the team book both read "· Default". + */ +export function showsDefaultBadge(book: Pick): boolean { + return !!book.isDefault && !book.isShared; +} diff --git a/src/stores/__tests__/contacts-store.test.ts b/src/stores/__tests__/contacts-store.test.ts index 2794445c..19cb50d8 100644 --- a/src/stores/__tests__/contacts-store.test.ts +++ b/src/stores/__tests__/contacts-store.test.ts @@ -150,6 +150,24 @@ describe('contacts-store', () => { expect(useContactsStore.getState().contacts).toHaveLength(2); }); + it('loads the address books with the cards when it has none, as after an account switch', async () => { + const order: string[] = []; + useContactsStore.setState({ addressBooks: [{ id: 'old', name: 'Other account' } as AddressBook] }); + useContactsStore.getState().reset(); + mockGetAllAddressBooks.mockImplementation(async () => { order.push('books'); return [{ id: 'ab-1', name: 'Personal' }]; }); + mockGetAllContacts.mockImplementation(async () => { order.push('contacts'); return [card('c1')]; }); + + await useContactsStore.getState().fetchContacts(); + + expect(useContactsStore.getState().addressBooks).toEqual([{ id: 'ab-1', name: 'Personal' }]); + // Books first: AddressBook/get makes Stalwart mint the default book. + expect(order).toEqual(['books', 'contacts']); + + // With books loaded, a reload of the cards leaves them alone. + await useContactsStore.getState().fetchContacts(); + expect(mockGetAllAddressBooks).toHaveBeenCalledTimes(1); + }); + it('waits for an in-flight address book load before querying', async () => { const order: string[] = []; let resolveBooks: (v: unknown) => void = () => {}; @@ -787,6 +805,8 @@ describe('an account switch while contacts load or save', () => { it('drops the previous account\'s cards that arrive after the switch', async () => { const old = deferred(); mockGetAllContacts.mockReturnValueOnce(old.promise); + // The old account has its books, so its card load starts at once. + useContactsStore.setState({ addressBooks: [{ id: 'ab-1', name: 'Personal' } as AddressBook] }); const oldLoad = useContactsStore.getState().fetchContacts(); useContactsStore.getState().reset(); diff --git a/src/stores/contacts-store.ts b/src/stores/contacts-store.ts index a568451c..37a8a3c8 100644 --- a/src/stores/contacts-store.ts +++ b/src/stores/contacts-store.ts @@ -491,6 +491,10 @@ export const useContactsStore = create()( if (contactsPromise) return contactsPromise; const { stale } = generationGuard(); const promise: Promise = (async () => { + // No books yet (a reset for another account, a first start): load + // them with the cards. An account switch reloads only the cards, + // so Settings > Contacts listed none until Refresh. + if (!addressBooksPromise && get().addressBooks.length === 0) void get().fetchAddressBooks(); // Books first: AddressBook/get is the call that makes Stalwart mint // the default book, so ContactCard/query must never run alongside it. if (addressBooksPromise) await addressBooksPromise; From 3165b40127a6abaca09dd8ee749a1be3e14bc797 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:29:26 +0100 Subject: [PATCH 08/13] fix: toggle a settings switch by tapping anywhere on its row --- .../settings/__tests__/switch-row.test.ts | 40 +++++++++++++++++++ src/components/settings/settings-section.tsx | 36 +++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/components/settings/__tests__/switch-row.test.ts diff --git a/src/components/settings/__tests__/switch-row.test.ts b/src/components/settings/__tests__/switch-row.test.ts new file mode 100644 index 00000000..10d7c34c --- /dev/null +++ b/src/components/settings/__tests__/switch-row.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +vi.mock('../../ToggleSwitch', () => ({ default: () => null })); +vi.mock('../../icons', () => ({})); +vi.mock('../../../stores/locale-store', () => ({ useLocaleStore: () => undefined })); +vi.mock('../../../theme/colors', () => ({ useColors: () => ({}) })); +vi.mock('../search-highlight', () => ({ useSearchHighlight: () => ({}) })); +vi.mock('../../../lib/modal-orientations', () => ({ MODAL_ORIENTATIONS: [] })); +vi.mock('react-native', () => ({ StyleSheet: { create: (s: unknown) => s } })); + +import { switchRowPress } from '../settings-section'; + +// Tapping the "Colour-blind-safe colours" label did nothing: only the switch +// itself flipped the setting. + +describe('a settings row with a switch', () => { + it('flips the switch when the row is tapped', () => { + const onChange = vi.fn(); + switchRowPress(false, onChange)!(); + expect(onChange).toHaveBeenLastCalledWith(true); + switchRowPress(true, onChange)!(); + expect(onChange).toHaveBeenLastCalledWith(false); + }); + + it('leaves a disabled switch alone', () => { + expect(switchRowPress(false, vi.fn(), true)).toBeNull(); + }); + + it('hands the switch to its row, which presses it unless the setting is locked', () => { + const src = fs.readFileSync(path.resolve(__dirname, '..', 'settings-section.tsx'), 'utf8'); + expect(src).toMatch(/rowToggle\.current = switchRowPress\(checked, onChange, disabled\);/); + expect(src).toMatch(/onPress=\{\(\) => \{ if \(!locked\) rowToggle\.current\?\.\(\); \}\}/); + expect(src).toMatch(//); + // The colour-blind row is such a row. + const a11y = fs.readFileSync(path.resolve(__dirname, '..', 'AccessibilitySettings.tsx'), 'utf8'); + expect(a11y).toMatch(/label=\{t\('settings\.accessibility\.cvd_palette\.label'[\s\S]{0,300}(undefined); +// What a tap on the row does when its control is a switch: flip it, so the +// label and the description are as good a target as the 44 x 24 switch +// (tapping "Colour-blind-safe colours" did nothing). +const SettingToggleContext = React.createContext void) | null> | null>(null); + +/** The row tap for a switch: flips it; null while it is disabled. */ +export function switchRowPress(checked: boolean, onChange: (v: boolean) => void, disabled?: boolean): (() => void) | null { + return disabled ? null : () => onChange(!checked); +} + interface SettingItemProps { label: string; description?: string; @@ -100,6 +110,8 @@ export function SettingItem({ const styles = React.useMemo(() => makeStyles(c), [c]); const t = useLocaleStore((s) => s.t); const highlight = useSearchHighlight(label); + // Set by a ToggleSwitch in the row (switchRowPress). + const rowToggle = React.useRef<(() => void) | null>(null); // The control sits on the right. When that leaves the label column narrower // than STACK_BELOW (a long translation, two buttons), the control moves // under the label, as the webmail stacks it on a phone. A new row width @@ -130,8 +142,10 @@ export function SettingItem({ {children && ( - {/* A locked setting shows its value but can't be changed. */} - {children} + + {/* A locked setting shows its value but can't be changed. */} + {children} + )} {onPress && (link === 'external' @@ -152,10 +166,18 @@ export function SettingItem({ ); } + // A tap anywhere on the row flips its switch, if it has one. Not a + // control of its own for a screen reader: the switch carries the label. return ( - + { if (!locked) rowToggle.current?.(); }} + accessible={false} + style={rowStyle} + > {body} - + ); } @@ -219,6 +241,12 @@ interface ToggleSwitchProps { export function ToggleSwitch({ checked, onChange, disabled, accessibilityLabel }: ToggleSwitchProps) { const settingLabel = useContext(SettingLabelContext); + const rowToggle = useContext(SettingToggleContext); + React.useEffect(() => { + if (!rowToggle) return undefined; + rowToggle.current = switchRowPress(checked, onChange, disabled); + return () => { rowToggle.current = null; }; + }, [rowToggle, checked, onChange, disabled]); return ( Date: Sat, 31 Oct 2026 20:31:23 +0100 Subject: [PATCH 09/13] fix: make the remove cross on a participant chip easier to tap --- src/components/calendar/ParticipantInput.tsx | 9 ++++++--- src/lib/__tests__/calendar-a11y.test.ts | 13 +++++++++++++ src/lib/calendar-a11y.ts | 8 ++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/calendar/ParticipantInput.tsx b/src/components/calendar/ParticipantInput.tsx index 55d56359..6576cb17 100644 --- a/src/components/calendar/ParticipantInput.tsx +++ b/src/components/calendar/ParticipantInput.tsx @@ -14,7 +14,10 @@ import { useContactsStore, type RecipientSuggestion } from '../../stores/contact import { useLocaleStore } from '../../stores/locale-store'; import type { Attendee } from '../../lib/calendar-participants'; import { participantSuggestions, participantsFromSuggestion } from '../../lib/calendar-participant-suggestions'; -import { participantRemoveAccessibilityLabel } from '../../lib/calendar-a11y'; +import { participantRemoveAccessibilityLabel, touchSlop } from '../../lib/calendar-a11y'; + +/** The cross on a chip: 12 dp drawn, 44 dp to tap. */ +const REMOVE_ICON = 12; interface ParticipantInputProps { attendees: Attendee[]; @@ -91,11 +94,11 @@ export function ParticipantInput({ attendees, onAdd, onRemove }: ParticipantInpu onRemove(a.email)} - hitSlop={6} + hitSlop={touchSlop(REMOVE_ICON)} accessibilityRole="button" accessibilityLabel={participantRemoveAccessibilityLabel(a, t)} > - + ))} diff --git a/src/lib/__tests__/calendar-a11y.test.ts b/src/lib/__tests__/calendar-a11y.test.ts index 27e6e71d..9194dfa7 100644 --- a/src/lib/__tests__/calendar-a11y.test.ts +++ b/src/lib/__tests__/calendar-a11y.test.ts @@ -12,6 +12,7 @@ import { eventAccessibilityLabel, monthButtonAccessibilityLabel, participantRemoveAccessibilityLabel, + touchSlop, } from '../calendar-a11y'; import { useSettingsStore } from '../../stores/settings-store'; import { gregorianCalendar, jalaliCalendar } from '../calendar-system'; @@ -85,6 +86,18 @@ describe('header and chip buttons', () => { expect(participantRemoveAccessibilityLabel({ name: '', email: 'ana@example.com' }, t)).toBe('Remove ana@example.com'); }); + it('grows the 12 dp cross on a participant chip to a 44 dp touch target', async () => { + expect(touchSlop(12)).toBe(16); + expect(12 + 2 * touchSlop(12)).toBeGreaterThanOrEqual(44); + expect(touchSlop(13)).toBe(16); + expect(touchSlop(48)).toBe(0); + const fs = await import('node:fs'); + const path = await import('node:path'); + const chips = fs.readFileSync(path.resolve(__dirname, '../../components/calendar/ParticipantInput.tsx'), 'utf8'); + expect(chips).toMatch(/const REMOVE_ICON = 12;/); + expect(chips).toMatch(/hitSlop=\{touchSlop\(REMOVE_ICON\)\}[\s\S]{0,200} { const fs = await import('node:fs'); const path = await import('node:path'); diff --git a/src/lib/calendar-a11y.ts b/src/lib/calendar-a11y.ts index c5a6d37e..8e331f21 100644 --- a/src/lib/calendar-a11y.ts +++ b/src/lib/calendar-a11y.ts @@ -21,6 +21,14 @@ export function monthButtonAccessibilityLabel(date: Date, system: CalendarSystem return t('calendar.change_month_named', '{month}, change month', { month: monthTitle(date, system, locale, true) }); } +/** The smallest touch target, in dp (WCAG 2.5.5, Android's 48 dp guideline rounded to Apple's 44 pt). */ +export const MIN_TOUCH_TARGET = 44; + +/** The hitSlop that grows an icon of `size` dp to a `target` dp touch area on every side. */ +export function touchSlop(size: number, target: number = MIN_TOUCH_TARGET): number { + return Math.max(0, Math.ceil((target - size) / 2)); +} + /** "Remove Ana" for the cross on an invited participant's chip (webmail recipient_remove_named). */ export function participantRemoveAccessibilityLabel(attendee: { name?: string; email: string }, t: Translate): string { return t('calendar.participants.remove_named', 'Remove {name}', { name: attendee.name?.trim() || attendee.email }); From a832f6ca3bdb41fdd5796f266b67091b1752eea1 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:38:02 +0100 Subject: [PATCH 10/13] fix: say this device instead of this phone, also on a tablet --- locales/rn/ar.json | 18 ++--- locales/rn/ca.json | 18 ++--- locales/rn/cs.json | 18 ++--- locales/rn/da.json | 18 ++--- locales/rn/de.json | 18 ++--- locales/rn/en.json | 18 ++--- locales/rn/es.json | 18 ++--- locales/rn/et.json | 18 ++--- locales/rn/fa.json | 18 ++--- locales/rn/fr.json | 18 ++--- locales/rn/he.json | 18 ++--- locales/rn/hu.json | 18 ++--- locales/rn/it.json | 18 ++--- locales/rn/ja.json | 18 ++--- locales/rn/ko.json | 18 ++--- locales/rn/lv.json | 18 ++--- locales/rn/mn.json | 18 ++--- locales/rn/nb.json | 18 ++--- locales/rn/nl.json | 18 ++--- locales/rn/pl.json | 18 ++--- locales/rn/pt.json | 18 ++--- locales/rn/ro.json | 18 ++--- locales/rn/ru.json | 18 ++--- locales/rn/sk.json | 18 ++--- locales/rn/th.json | 18 ++--- locales/rn/tr.json | 18 ++--- locales/rn/uk.json | 18 ++--- locales/rn/zh-TW.json | 18 ++--- locales/rn/zh.json | 18 ++--- src/components/settings/AboutSettings.tsx | 2 +- .../settings/AccessibilitySettings.tsx | 4 +- src/components/settings/CalendarSettings.tsx | 2 +- src/components/settings/PrivacySettings.tsx | 2 +- src/components/settings/StorageSettings.tsx | 4 +- src/i18n/__tests__/device-wording.test.ts | 66 +++++++++++++++++++ src/lib/__tests__/settings-search.test.ts | 2 +- 36 files changed, 335 insertions(+), 269 deletions(-) create mode 100644 src/i18n/__tests__/device-wording.test.ts diff --git a/locales/rn/ar.json b/locales/rn/ar.json index 2caef126..6d511c2a 100644 --- a/locales/rn/ar.json +++ b/locales/rn/ar.json @@ -124,7 +124,7 @@ "time_zone": "المنطقة الزمنية", "time_zone_auto": "المنطقة الزمنية للجهاز", "time_zone_auto_zone": "المنطقة الزمنية للجهاز ({zone})", - "time_zone_desc": "المنطقة المستخدمة للأحداث الجديدة ولتفسير التقويم. خيار \"المنطقة الزمنية للجهاز\" يتبع إعداد الهاتف." + "time_zone_desc": "المنطقة المستخدمة للأحداث الجديدة ولتفسير التقويم. خيار \"المنطقة الزمنية للجهاز\" يتبع إعداد هذا الجهاز." }, "share": { "add_people": "إضافة أشخاص", @@ -670,7 +670,7 @@ "err_pair_connect_detail": "اعرض رمزًا جديدًا على حاسوبك وامسحه مجددًا.", "err_pair_connect_title": "تم تسجيل الدخول، لكن تعذّر الاتصال بـ {host}", "err_pair_insecure_detail": "يريد {host} استخدام اتصال غير مشفّر.", - "err_pair_network_detail": "تحقق من اتصالك ومن أن هاتفك يستطيع الوصول إلى بريد الويب، ثم حاول مرة أخرى.", + "err_pair_network_detail": "تحقق من اتصالك ومن أن هذا الجهاز يستطيع الوصول إلى بريد الويب، ثم حاول مرة أخرى.", "err_pair_rate_detail": "انتظر دقيقة، ثم حاول مرة أخرى.", "err_pair_server_detail": "حاول مرة أخرى باستخدام رمز جديد. إذا استمر الفشل، فاتصل بالمسؤول.", "err_pair_server_title": "تعذّر على بريد الويب إكمال تسجيل الدخول", @@ -758,7 +758,7 @@ "server_cert_active": "يتم تسجيل الدخول بشهادة العميل \"{alias}\". اضغط لاختيار شهادة أخرى.", "server_cert_clear": "تسجيل الدخول بدون الشهادة", "server_cert_failed": "تعذّر اختيار شهادة", - "server_cert_hint": "هل يطلب خادمك شهادة عميل؟ اختر شهادة مثبّتة على هذا الهاتف." + "server_cert_hint": "هل يطلب خادمك شهادة عميل؟ اختر شهادة مثبّتة على هذا الجهاز." } }, "offline": { @@ -1233,7 +1233,7 @@ "window_24h": "آخر 24 ساعة", "window_days": "{count, plural, zero {آخر # يوم} one {آخر يوم} two {آخر يومين} few {آخر # أيام} many {آخر # يومًا} other {آخر # يوم}}", "window_desc": "المدة التي يُخزَّن بريدها مؤقتًا، بحسب تاريخ استلام الرسالة.", - "description_short": "احتفظ بالرسائل الأخيرة على هذا الهاتف لتُفتح دون اتصال.", + "description_short": "احتفظ بالرسائل الأخيرة على هذا الجهاز لتُفتح دون اتصال.", "keep_24h": "24 ساعة", "keep_days": "{count} أيام", "keep_from": "الاحتفاظ بالبريد من آخر", @@ -1433,7 +1433,7 @@ "licences_desc": "AGPL-3.0 للتطبيق، والمكتبات التي يستخدمها", "never_checked": "لم يتم التحقق بعد", "privacy": "الخصوصية", - "privacy_desc": "ما يبقى على هذا الهاتف وما يذهب إلى خادمك", + "privacy_desc": "ما يبقى على هذا الجهاز وما يذهب إلى خادمك", "report_problem": "الإبلاغ عن مشكلة", "report_problem_desc": "يفتح بلاغًا على GitHub مع تعبئة الإصدار", "server": "الخادم", @@ -1472,17 +1472,17 @@ } }, "reduce_motion": { - "description_mobile": "يوقف اللوحات المنزلقة والانتقالات. يفعل التطبيق ذلك أيضًا عند تفعيل تقليل الحركة في إعدادات هاتفك." + "description_mobile": "يوقف اللوحات المنزلقة والانتقالات. يفعل التطبيق ذلك أيضًا عند تفعيل تقليل الحركة في إعدادات جهازك." }, "text_size": { - "description_mobile": "يغيّر حجم نص التطبيق فوق حجم النص في هاتفك. 100% هو حجم هاتفك." + "description_mobile": "يغيّر حجم نص التطبيق فوق حجم النص في جهازك. 100% هو حجم جهازك." }, "toast_duration": { "automatic": "تلقائي (أطول مع قارئ الشاشة)" } }, "backup": { - "description": "انقل إعداداتك إلى هاتف آخر أو إلى بريد الويب.", + "description": "انقل إعداداتك إلى جهاز آخر أو إلى بريد الويب.", "export_import": "تصدير أو استيراد", "export_import_desc": "ملف JSON، بنفس صيغة بريد الويب", "reset": "إعادة تعيين", @@ -1501,7 +1501,7 @@ "other_title": "اتصالات أخرى", "phone_backup": "لا يدخل أي منها في النسخ الاحتياطي السحابي لنظام Android.", "phone_secure": "تُحفظ بيانات تسجيل دخولك في التخزين الآمن للنظام. وتُحفظ الإعدادات وذاكرة البريد المؤقتة دون اتصال والتغييرات التي تنتظر المزامنة في تخزين التطبيق نفسه.", - "phone_title": "على هذا الهاتف", + "phone_title": "على هذا الجهاز", "push": "عند تفعيل الإشعارات، يُبلغ خادمك خادم ترحيل الإشعارات ({relay}) بأن شيئًا ما تغيّر، فيوقظ خادم الترحيل التطبيق عبر {service}. يرى خادم الترحيل رمز جهاز وعلامة تغيير، ولا يرى بريدك أبدًا. يمكنك استخدام خادم ترحيل خاص بك من الإشعارات.", "push_title": "الإشعارات الفورية", "server": "يُحمَّل البريد والتقويمات وجهات الاتصال والملفات من خادمك وتُحفظ عليه. وباستثناء الاتصالات أدناه، لا يتواصل التطبيق إلا مع خادمك.", diff --git a/locales/rn/ca.json b/locales/rn/ca.json index a4300c13..ab98f05a 100644 --- a/locales/rn/ca.json +++ b/locales/rn/ca.json @@ -124,7 +124,7 @@ "time_zone": "Zona horària", "time_zone_auto": "Zona horària del dispositiu", "time_zone_auto_zone": "Zona horària del dispositiu ({zone})", - "time_zone_desc": "Zona que s'utilitza per als esdeveniments nous i per interpretar el calendari. «Dispositiu» segueix la del telèfon." + "time_zone_desc": "Zona que s'utilitza per als esdeveniments nous i per interpretar el calendari. «Dispositiu» segueix la d'aquest dispositiu." }, "share": { "add_people": "Afegeix persones", @@ -675,7 +675,7 @@ "err_pair_connect_detail": "Mostreu un codi nou a l'ordinador i torneu-lo a escanejar.", "err_pair_connect_title": "Sessió iniciada, però no s'ha pogut connectar a {host}", "err_pair_insecure_detail": "{host} vol utilitzar una connexió sense xifrar.", - "err_pair_network_detail": "Comproveu la connexió i que el telèfon pugui accedir al webmail, i torneu-ho a provar.", + "err_pair_network_detail": "Comproveu la connexió i que aquest dispositiu pugui accedir al webmail, i torneu-ho a provar.", "err_pair_rate_detail": "Espereu un minut i torneu-ho a provar.", "err_pair_server_detail": "Torneu-ho a provar amb un codi nou. Si continua fallant, consulteu l'administrador.", "err_pair_server_title": "El webmail no ha pogut completar l'inici de sessió", @@ -763,7 +763,7 @@ "server_cert_active": "S'inicia la sessió amb el certificat de client «{alias}». Toqueu per triar-ne un altre.", "server_cert_clear": "Inicia la sessió sense el certificat", "server_cert_failed": "No s'ha pogut triar cap certificat", - "server_cert_hint": "El vostre servidor demana un certificat de client? Trieu-ne un d'instal·lat en aquest telèfon." + "server_cert_hint": "El vostre servidor demana un certificat de client? Trieu-ne un d'instal·lat en aquest dispositiu." } }, "offline": { @@ -1238,7 +1238,7 @@ "window_24h": "Últimes 24 hores", "window_days": "Últims {count} dies", "window_desc": "Fins a quina data desar a la memòria cau, segons la data de recepció del missatge.", - "description_short": "Conserva els missatges recents en aquest telèfon perquè s'obrin sense connexió.", + "description_short": "Conserva els missatges recents en aquest dispositiu perquè s'obrin sense connexió.", "keep_24h": "24 hores", "keep_days": "{count} dies", "keep_from": "Conserva el correu dels últims", @@ -1438,7 +1438,7 @@ "licences_desc": "AGPL-3.0 per a l'aplicació, i les biblioteques que fa servir", "never_checked": "Encara no s'ha comprovat", "privacy": "Privadesa", - "privacy_desc": "Què es queda en aquest telèfon i què va al vostre servidor", + "privacy_desc": "Què es queda en aquest dispositiu i què va al vostre servidor", "report_problem": "Informa d'un problema", "report_problem_desc": "Obre una incidència a GitHub amb la versió ja emplenada", "server": "Servidor", @@ -1477,17 +1477,17 @@ } }, "reduce_motion": { - "description_mobile": "Desactiva els panells lliscants i les transicions. L'aplicació també ho fa quan teniu activat el moviment reduït a la configuració del telèfon." + "description_mobile": "Desactiva els panells lliscants i les transicions. L'aplicació també ho fa quan teniu activat el moviment reduït a la configuració del dispositiu." }, "text_size": { - "description_mobile": "Escala el text de l'aplicació per sobre de la mida de text del telèfon. El 100% és la mida del telèfon." + "description_mobile": "Escala el text de l'aplicació per sobre de la mida de text del dispositiu. El 100% és la mida del dispositiu." }, "toast_duration": { "automatic": "Automàtica (més llarga amb un lector de pantalla)" } }, "backup": { - "description": "Traslladeu la configuració a un altre telèfon o al webmail.", + "description": "Traslladeu la configuració a un altre dispositiu o al webmail.", "export_import": "Exporta o importa", "export_import_desc": "Un fitxer JSON, amb el mateix format que el webmail", "reset": "Restableix", @@ -1506,7 +1506,7 @@ "other_title": "Altres connexions", "phone_backup": "Res d'això va a la còpia de seguretat al núvol d'Android.", "phone_secure": "Les vostres dades d'inici de sessió es guarden a l'emmagatzematge segur del sistema. La configuració, la memòria cau del correu sense connexió i els canvis pendents de sincronitzar es guarden a l'emmagatzematge propi de l'aplicació.", - "phone_title": "En aquest telèfon", + "phone_title": "En aquest dispositiu", "push": "Quan les notificacions estan activades, el vostre servidor avisa el repetidor push ({relay}) que alguna cosa ha canviat, i el repetidor desperta l'aplicació mitjançant {service}. El repetidor veu un testimoni del dispositiu i un marcador de canvi, mai el vostre correu. Podeu fer servir el vostre propi repetidor a Notificacions.", "push_title": "Notificacions push", "server": "El correu, els calendaris, els contactes i els fitxers es carreguen del vostre servidor i s'hi tornen a desar. A part de les connexions següents, l'aplicació només es comunica amb el vostre servidor.", diff --git a/locales/rn/cs.json b/locales/rn/cs.json index fd9ecb0f..7fcb02f1 100644 --- a/locales/rn/cs.json +++ b/locales/rn/cs.json @@ -124,7 +124,7 @@ "time_zone": "Časové pásmo", "time_zone_auto": "Časové pásmo zařízení", "time_zone_auto_zone": "Časové pásmo zařízení ({zone})", - "time_zone_desc": "Pásmo pro nové události a pro zobrazení kalendáře. „Časové pásmo zařízení“ se řídí telefonem." + "time_zone_desc": "Pásmo pro nové události a pro zobrazení kalendáře. „Časové pásmo zařízení“ se řídí tímto zařízením." }, "share": { "add_people": "Přidat osoby", @@ -675,7 +675,7 @@ "err_pair_connect_detail": "Zobrazte na počítači nový kód a naskenujte ho znovu.", "err_pair_connect_title": "Přihlášeno, ale nelze se připojit k {host}", "err_pair_insecure_detail": "{host} chce použít nešifrované spojení.", - "err_pair_network_detail": "Zkontrolujte připojení a zda se telefon dostane k webmailu, a zkuste to znovu.", + "err_pair_network_detail": "Zkontrolujte připojení a zda se toto zařízení dostane k webmailu, a zkuste to znovu.", "err_pair_rate_detail": "Počkejte minutu a zkuste to znovu.", "err_pair_server_detail": "Zkuste to znovu s novým kódem. Pokud to stále selhává, obraťte se na správce.", "err_pair_server_title": "Webmail nemohl dokončit přihlášení", @@ -763,7 +763,7 @@ "server_cert_active": "Přihlášení pomocí klientského certifikátu „{alias}“. Klepnutím zvolíte jiný.", "server_cert_clear": "Přihlásit se bez certifikátu", "server_cert_failed": "Certifikát se nepodařilo vybrat", - "server_cert_hint": "Vyžaduje váš server klientský certifikát? Vyberte jeden z nainstalovaných v tomto telefonu." + "server_cert_hint": "Vyžaduje váš server klientský certifikát? Vyberte jeden z nainstalovaných v tomto zařízení." } }, "offline": { @@ -1238,7 +1238,7 @@ "window_24h": "Posledních 24 hodin", "window_days": "{count, plural, one {Poslední # den} few {Poslední # dny} many {Posledních # dne} other {Posledních # dní}}", "window_desc": "Jak daleko do minulosti ukládat, podle data přijetí zprávy.", - "description_short": "Uchovávat nedávné zprávy v tomto telefonu, aby se otevřely i bez připojení.", + "description_short": "Uchovávat nedávné zprávy v tomto zařízení, aby se otevřely i bez připojení.", "keep_24h": "24 hodin", "keep_days": "{count} dní", "keep_from": "Uchovávat poštu za posledních", @@ -1438,7 +1438,7 @@ "licences_desc": "AGPL-3.0 pro aplikaci a knihovny, které používá", "never_checked": "Zatím nezkontrolováno", "privacy": "Soukromí", - "privacy_desc": "Co zůstává v tomto telefonu a co jde na váš server", + "privacy_desc": "Co zůstává v tomto zařízení a co jde na váš server", "report_problem": "Nahlásit problém", "report_problem_desc": "Otevře hlášení na GitHubu s vyplněnou verzí", "server": "Server", @@ -1477,17 +1477,17 @@ } }, "reduce_motion": { - "description_mobile": "Vypne vysouvací panely a přechody. Aplikace to udělá i tehdy, když je v nastavení telefonu zapnuté omezení pohybu." + "description_mobile": "Vypne vysouvací panely a přechody. Aplikace to udělá i tehdy, když je v nastavení zařízení zapnuté omezení pohybu." }, "text_size": { - "description_mobile": "Mění velikost textu aplikace nad rámec velikosti textu v telefonu. 100 % odpovídá velikosti v telefonu." + "description_mobile": "Mění velikost textu aplikace nad rámec velikosti textu v zařízení. 100 % odpovídá velikosti v zařízení." }, "toast_duration": { "automatic": "Automaticky (déle se čtečkou obrazovky)" } }, "backup": { - "description": "Přeneste nastavení do jiného telefonu nebo do webmailu.", + "description": "Přeneste nastavení do jiného zařízení nebo do webmailu.", "export_import": "Export nebo import", "export_import_desc": "Soubor JSON ve stejném formátu jako ve webmailu", "reset": "Resetovat", @@ -1506,7 +1506,7 @@ "other_title": "Další připojení", "phone_backup": "Nic z toho se neukládá do cloudové zálohy Androidu.", "phone_secure": "Vaše přihlašovací údaje jsou uloženy v zabezpečeném úložišti systému. Nastavení, mezipaměť offline pošty a změny čekající na synchronizaci jsou uloženy ve vlastním úložišti aplikace.", - "phone_title": "V tomto telefonu", + "phone_title": "V tomto zařízení", "push": "Když jsou oznámení zapnutá, váš server sdělí push relay ({relay}), že se něco změnilo, a relay probudí aplikaci přes {service}. Relay vidí token zařízení a značku změny, nikdy vaši poštu. V nastavení Oznámení můžete použít vlastní relay.", "push_title": "Push oznámení", "server": "Pošta, kalendáře, kontakty a soubory se načítají z vašeho serveru a ukládají zpět na něj. Kromě níže uvedených připojení komunikuje aplikace jen s vaším serverem.", diff --git a/locales/rn/da.json b/locales/rn/da.json index 4d72dd6d..efdb45f0 100644 --- a/locales/rn/da.json +++ b/locales/rn/da.json @@ -124,7 +124,7 @@ "time_zone": "Tidszone", "time_zone_auto": "Enhedens tidszone", "time_zone_auto_zone": "Enhedens tidszone ({zone})", - "time_zone_desc": "Tidszone til nye begivenheder og til visning af kalenderen. \"Enhedens tidszone\" følger telefonen." + "time_zone_desc": "Tidszone til nye begivenheder og til visning af kalenderen. \"Enhedens tidszone\" følger denne enhed." }, "share": { "add_people": "Tilføj personer", @@ -672,7 +672,7 @@ "err_pair_connect_detail": "Vis en ny kode på din computer, og scan igen.", "err_pair_connect_title": "Logget ind, men kunne ikke oprette forbindelse til {host}", "err_pair_insecure_detail": "{host} vil bruge en ukrypteret forbindelse.", - "err_pair_network_detail": "Tjek din forbindelse, og at din telefon kan nå webmailen, og prøv så igen.", + "err_pair_network_detail": "Tjek din forbindelse, og at denne enhed kan nå webmailen, og prøv så igen.", "err_pair_rate_detail": "Vent et minut, og prøv så igen.", "err_pair_server_detail": "Prøv igen med en ny kode. Hvis det bliver ved med at mislykkes, så kontakt din administrator.", "err_pair_server_title": "Webmailen kunne ikke gennemføre login", @@ -760,7 +760,7 @@ "server_cert_active": "Logger ind med klientcertifikatet \"{alias}\". Tryk for at vælge et andet.", "server_cert_clear": "Log ind uden certifikatet", "server_cert_failed": "Kunne ikke vælge et certifikat", - "server_cert_hint": "Beder din server om et klientcertifikat? Vælg et, der er installeret på denne telefon." + "server_cert_hint": "Beder din server om et klientcertifikat? Vælg et, der er installeret på denne enhed." } }, "offline": { @@ -1235,7 +1235,7 @@ "window_24h": "Seneste 24 timer", "window_days": "Seneste {count} dage", "window_desc": "Hvor langt tilbage der gemmes i cachen, målt efter beskedens modtagelsesdato.", - "description_short": "Gem de seneste beskeder på denne telefon, så de kan åbnes uden forbindelse.", + "description_short": "Gem de seneste beskeder på denne enhed, så de kan åbnes uden forbindelse.", "keep_24h": "24 timer", "keep_days": "{count} dage", "keep_from": "Gem mail fra de seneste", @@ -1435,7 +1435,7 @@ "licences_desc": "AGPL-3.0 for appen og de biblioteker, den bruger", "never_checked": "Ikke tjekket endnu", "privacy": "Privatliv", - "privacy_desc": "Hvad der bliver på denne telefon, og hvad der sendes til din server", + "privacy_desc": "Hvad der bliver på denne enhed, og hvad der sendes til din server", "report_problem": "Rapportér et problem", "report_problem_desc": "Opretter en sag på GitHub med versionen udfyldt", "server": "Server", @@ -1474,17 +1474,17 @@ } }, "reduce_motion": { - "description_mobile": "Slår glidende paneler og overgange fra. Appen gør det også, når reduceret bevægelse er slået til i telefonens indstillinger." + "description_mobile": "Slår glidende paneler og overgange fra. Appen gør det også, når reduceret bevægelse er slået til i enhedens indstillinger." }, "text_size": { - "description_mobile": "Skalerer appens tekst oven i telefonens tekststørrelse. 100 % er telefonens størrelse." + "description_mobile": "Skalerer appens tekst oven i enhedens tekststørrelse. 100 % er enhedens størrelse." }, "toast_duration": { "automatic": "Automatisk (længere med skærmlæser)" } }, "backup": { - "description": "Flyt dine indstillinger til en anden telefon eller til webmailen.", + "description": "Flyt dine indstillinger til en anden enhed eller til webmailen.", "export_import": "Eksportér eller importér", "export_import_desc": "En JSON-fil i samme format som webmailen", "reset": "Nulstil", @@ -1503,7 +1503,7 @@ "other_title": "Andre forbindelser", "phone_backup": "Intet af det kommer med i Androids sikkerhedskopi i skyen.", "phone_secure": "Dine loginoplysninger opbevares i systemets sikre lager. Indstillinger, offlinemail-cachen og ændringer, der venter på synkronisering, opbevares i appens eget lager.", - "phone_title": "På denne telefon", + "phone_title": "På denne enhed", "push": "Når notifikationer er slået til, fortæller din server push-relæet ({relay}), at noget er ændret, og relæet vækker appen via {service}. Relæet ser et enhedstoken og en ændringsmarkør, aldrig din mail. Du kan bruge dit eget relæ under Notifikationer.", "push_title": "Push-notifikationer", "server": "Mail, kalendere, kontakter og filer indlæses fra din server og gemmes tilbage på den. Bortset fra forbindelserne nedenfor taler appen kun med din server.", diff --git a/locales/rn/de.json b/locales/rn/de.json index cae7fe7b..0984c211 100644 --- a/locales/rn/de.json +++ b/locales/rn/de.json @@ -124,7 +124,7 @@ "time_zone": "Zeitzone", "time_zone_auto": "Gerätezeitzone", "time_zone_auto_zone": "Gerätezeitzone ({zone})", - "time_zone_desc": "Zeitzone für neue Termine und für die Darstellung des Kalenders. \"Gerätezeitzone\" folgt dem Telefon." + "time_zone_desc": "Zeitzone für neue Termine und für die Darstellung des Kalenders. \"Gerätezeitzone\" folgt diesem Gerät." }, "share": { "add_people": "Personen hinzufügen", @@ -682,7 +682,7 @@ "err_pair_connect_detail": "Zeigen Sie auf Ihrem Computer einen neuen Code an und scannen Sie erneut.", "err_pair_connect_title": "Angemeldet, aber keine Verbindung zu {host}", "err_pair_insecure_detail": "{host} möchte eine unverschlüsselte Verbindung verwenden.", - "err_pair_network_detail": "Prüfen Sie Ihre Verbindung und ob Ihr Telefon das Webmail erreichen kann, und versuchen Sie es dann erneut.", + "err_pair_network_detail": "Prüfen Sie Ihre Verbindung und ob dieses Gerät das Webmail erreichen kann, und versuchen Sie es dann erneut.", "err_pair_rate_detail": "Warten Sie eine Minute und versuchen Sie es dann erneut.", "err_pair_server_detail": "Versuchen Sie es mit einem neuen Code erneut. Wenn es weiterhin fehlschlägt, wenden Sie sich an Ihren Administrator.", "err_pair_server_title": "Das Webmail konnte die Anmeldung nicht abschließen", @@ -770,7 +770,7 @@ "server_cert_active": "Anmeldung mit dem Client-Zertifikat „{alias}“. Tippen, um ein anderes zu wählen.", "server_cert_clear": "Ohne Zertifikat anmelden", "server_cert_failed": "Zertifikat konnte nicht ausgewählt werden", - "server_cert_hint": "Verlangt Ihr Server ein Client-Zertifikat? Wählen Sie eines, das auf diesem Telefon installiert ist." + "server_cert_hint": "Verlangt Ihr Server ein Client-Zertifikat? Wählen Sie eines, das auf diesem Gerät installiert ist." } }, "notifications": { @@ -1248,7 +1248,7 @@ "window_24h": "Letzte 24 Stunden", "window_days": "Letzte {count} Tage", "window_desc": "Wie weit zurück zwischengespeichert wird, gemessen am Empfangsdatum der Nachricht.", - "description_short": "Neue Nachrichten auf dem Telefon behalten, damit sie auch ohne Verbindung aufgehen.", + "description_short": "Neue Nachrichten auf diesem Gerät behalten, damit sie auch ohne Verbindung aufgehen.", "keep_24h": "24 Stunden", "keep_days": "{count} Tage", "keep_from": "Mails der letzten", @@ -1430,7 +1430,7 @@ "licences_desc": "AGPL-3.0 für die App und die Bibliotheken, die sie nutzt", "never_checked": "Noch nicht geprüft", "privacy": "Datenschutz", - "privacy_desc": "Was auf diesem Telefon bleibt und was an Ihren Server geht", + "privacy_desc": "Was auf diesem Gerät bleibt und was an Ihren Server geht", "report_problem": "Problem melden", "report_problem_desc": "Öffnet ein Issue auf GitHub mit eingetragener Version", "server": "Server", @@ -1442,7 +1442,7 @@ "updates": "Updates" }, "backup": { - "description": "Übertragen Sie Ihre Einstellungen auf ein anderes Telefon oder ins Webmail.", + "description": "Übertragen Sie Ihre Einstellungen auf ein anderes Gerät oder ins Webmail.", "export_import": "Exportieren oder importieren", "export_import_desc": "Eine JSON-Datei im selben Format wie im Webmail", "reset": "Zurücksetzen", @@ -1461,7 +1461,7 @@ "other_title": "Weitere Verbindungen", "phone_backup": "Nichts davon landet in der Cloud-Sicherung von Android.", "phone_secure": "Ihre Anmeldedaten liegen im sicheren Speicher des Systems. Einstellungen, der Offline-Cache und noch nicht synchronisierte Änderungen liegen im Speicher der App.", - "phone_title": "Auf diesem Telefon", + "phone_title": "Auf diesem Gerät", "push": "Wenn Benachrichtigungen an sind, meldet Ihr Server dem Push-Relay ({relay}), dass sich etwas geändert hat, und das Relay weckt die App über {service}. Das Relay sieht ein Gerätetoken und eine Änderungsmarke, nie Ihre Mails. Unter Benachrichtigungen können Sie ein eigenes Relay eintragen.", "push_title": "Push-Benachrichtigungen", "server": "Mails, Kalender, Kontakte und Dateien werden von Ihrem Server geladen und dort gespeichert. Außer den Verbindungen unten spricht die App nur mit Ihrem Server.", @@ -1550,10 +1550,10 @@ "description_mobile": "Einfachere Formulierungen in Einstellungen und Fehlermeldungen. Auf Deutsch und Englisch verfügbar." }, "reduce_motion": { - "description_mobile": "Schaltet gleitende Bereiche und Übergänge aus. Die App tut das auch, wenn in den Einstellungen Ihres Telefons „Bewegung reduzieren“ aktiviert ist." + "description_mobile": "Schaltet gleitende Bereiche und Übergänge aus. Die App tut das auch, wenn in den Einstellungen Ihres Geräts „Bewegung reduzieren“ aktiviert ist." }, "text_size": { - "description_mobile": "Skaliert den Text der App zusätzlich zur Schriftgröße Ihres Telefons. 100 % entspricht der Größe Ihres Telefons." + "description_mobile": "Skaliert den Text der App zusätzlich zur Schriftgröße Ihres Geräts. 100 % entspricht der Größe Ihres Geräts." }, "toast_duration": { "automatic": "Automatisch (länger mit Screenreader)" diff --git a/locales/rn/en.json b/locales/rn/en.json index 381918ba..3f93b8cf 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -149,7 +149,7 @@ "time_zone": "Time zone", "time_zone_auto": "Device time zone", "time_zone_auto_zone": "Device time zone ({zone})", - "time_zone_desc": "Zone used for new events and for interpreting the calendar. \"Device\" follows the phone." + "time_zone_desc": "Zone used for new events and for interpreting the calendar. \"Device\" follows the time zone set on this device." }, "share": { "add_people": "Add people", @@ -901,7 +901,7 @@ "err_pair_connect_detail": "Show a new code on your computer and scan again.", "err_pair_connect_title": "Signed in, but couldn't connect to {host}", "err_pair_insecure_detail": "{host} wants to use an unencrypted connection.", - "err_pair_network_detail": "Check your connection and that your phone can reach the webmail, then try again.", + "err_pair_network_detail": "Check your connection and that this device can reach the webmail, then try again.", "err_pair_rate_detail": "Wait a minute, then try again.", "err_pair_server_detail": "Try again with a new code. If it keeps failing, ask your administrator.", "err_pair_server_title": "The webmail couldn't complete the sign-in", @@ -965,7 +965,7 @@ "server_cert_active": "Signing in with the client certificate \"{alias}\". Tap to choose another.", "server_cert_clear": "Sign in without the certificate", "server_cert_failed": "Could not pick a certificate", - "server_cert_hint": "Does your server ask for a client certificate? Pick one installed on this phone.", + "server_cert_hint": "Does your server ask for a client certificate? Pick one installed on this device.", "server_hint": "Same address you type in the browser. We'll add https:// for you.", "server_label": "Server address", "server_not_found": "We couldn't find a server for {domain}. If you self-host, the address is the one you use for webmail.", @@ -1067,7 +1067,7 @@ "licences_desc": "AGPL-3.0 for the app, and the libraries it uses", "never_checked": "Not checked yet", "privacy": "Privacy", - "privacy_desc": "What stays on this phone and what goes to your server", + "privacy_desc": "What stays on this device and what goes to your server", "report_problem": "Report a problem", "report_problem_desc": "Opens an issue on GitHub with the version filled in", "server": "Server", @@ -1106,10 +1106,10 @@ } }, "reduce_motion": { - "description_mobile": "Turns off sliding panels and transitions. The app also does this when reduced motion is on in your phone's settings." + "description_mobile": "Turns off sliding panels and transitions. The app also does this when reduced motion is on in your device's settings." }, "text_size": { - "description_mobile": "Scales the app's text on top of your phone's text size. 100% is your phone's size." + "description_mobile": "Scales the app's text on top of your device's text size. 100% is your device's size." }, "toast_duration": { "automatic": "Automatic (longer with a screen reader)" @@ -1163,7 +1163,7 @@ } }, "backup": { - "description": "Move your settings to another phone or the webmail.", + "description": "Move your settings to another device or the webmail.", "export_import": "Export or import", "export_import_desc": "A JSON file, same format as the webmail", "reset": "Reset", @@ -1547,7 +1547,7 @@ "clear_title": "Clear offline mail", "days_ago": "{count} d ago", "description": "Download recent messages so they open instantly and remain readable without a connection. Bodies only — attachments are still fetched on demand.", - "description_short": "Keep recent messages on this phone so they open without a connection.", + "description_short": "Keep recent messages on this device so they open without a connection.", "downloading": "Downloading {completed}/{total}…", "enabled": "Cache recent mail", "enabled_desc": "When on, the app keeps message bodies for the selected window on this device.", @@ -1587,7 +1587,7 @@ "other_title": "Other connections", "phone_backup": "None of it goes into Android's cloud backup.", "phone_secure": "Your sign-in details are kept in the system's secure storage. Settings, the offline mail cache and changes waiting to sync are kept in the app's own storage.", - "phone_title": "On this phone", + "phone_title": "On this device", "push": "When notifications are on, your server tells the push relay ({relay}) that something changed, and the relay wakes the app through {service}. The relay sees a device token and a change marker, never your mail. You can use your own relay under Notifications.", "push_title": "Push notifications", "server": "Mail, calendars, contacts and files are loaded from your server and saved back to it. Apart from the connections below, the app talks only to your server.", diff --git a/locales/rn/es.json b/locales/rn/es.json index 11003b46..67e31fcf 100644 --- a/locales/rn/es.json +++ b/locales/rn/es.json @@ -124,7 +124,7 @@ "time_zone": "Zona horaria", "time_zone_auto": "Zona horaria del dispositivo", "time_zone_auto_zone": "Zona horaria del dispositivo ({zone})", - "time_zone_desc": "Zona usada para los eventos nuevos y para interpretar el calendario. \"Dispositivo\" sigue la zona del teléfono." + "time_zone_desc": "Zona usada para los eventos nuevos y para interpretar el calendario. \"Dispositivo\" sigue la zona de este dispositivo." }, "share": { "add_people": "Añadir personas", @@ -681,7 +681,7 @@ "err_pair_connect_detail": "Muestra un código nuevo en tu ordenador y vuelve a escanearlo.", "err_pair_connect_title": "Sesión iniciada, pero no se pudo conectar con {host}", "err_pair_insecure_detail": "{host} quiere usar una conexión sin cifrar.", - "err_pair_network_detail": "Comprueba tu conexión y que tu teléfono pueda acceder al webmail, y vuelve a intentarlo.", + "err_pair_network_detail": "Comprueba tu conexión y que este dispositivo pueda acceder al webmail, y vuelve a intentarlo.", "err_pair_rate_detail": "Espera un minuto y vuelve a intentarlo.", "err_pair_server_detail": "Vuelve a intentarlo con un código nuevo. Si sigue fallando, consulta a tu administrador.", "err_pair_server_title": "El webmail no ha podido completar el inicio de sesión", @@ -769,7 +769,7 @@ "server_cert_active": "Iniciando sesión con el certificado de cliente «{alias}». Toca para elegir otro.", "server_cert_clear": "Iniciar sesión sin el certificado", "server_cert_failed": "No se pudo elegir un certificado", - "server_cert_hint": "¿Tu servidor pide un certificado de cliente? Elige uno instalado en este teléfono." + "server_cert_hint": "¿Tu servidor pide un certificado de cliente? Elige uno instalado en este dispositivo." } }, "offline": { @@ -1244,7 +1244,7 @@ "window_24h": "Últimas 24 horas", "window_days": "Últimos {count} días", "window_desc": "Hasta qué fecha guardar en caché, según la fecha de recepción del mensaje.", - "description_short": "Guarda los mensajes recientes en este teléfono para abrirlos sin conexión.", + "description_short": "Guarda los mensajes recientes en este dispositivo para abrirlos sin conexión.", "keep_24h": "24 horas", "keep_days": "{count} días", "keep_from": "Periodo a conservar", @@ -1444,7 +1444,7 @@ "licences_desc": "AGPL-3.0 para la app, y las bibliotecas que usa", "never_checked": "Aún no comprobado", "privacy": "Privacidad", - "privacy_desc": "Qué se queda en este teléfono y qué va a tu servidor", + "privacy_desc": "Qué se queda en este dispositivo y qué va a tu servidor", "report_problem": "Informar de un problema", "report_problem_desc": "Abre una incidencia en GitHub con la versión ya rellenada", "server": "Servidor", @@ -1483,17 +1483,17 @@ } }, "reduce_motion": { - "description_mobile": "Desactiva los paneles deslizantes y las transiciones. La app también lo hace cuando la reducción de movimiento está activada en los ajustes del teléfono." + "description_mobile": "Desactiva los paneles deslizantes y las transiciones. La app también lo hace cuando la reducción de movimiento está activada en los ajustes del dispositivo." }, "text_size": { - "description_mobile": "Escala el texto de la app sobre el tamaño de texto del teléfono. 100 % es el tamaño del teléfono." + "description_mobile": "Escala el texto de la app sobre el tamaño de texto del dispositivo. 100 % es el tamaño del dispositivo." }, "toast_duration": { "automatic": "Automática (más larga con un lector de pantalla)" } }, "backup": { - "description": "Lleva tu configuración a otro teléfono o al webmail.", + "description": "Lleva tu configuración a otro dispositivo o al webmail.", "export_import": "Exportar o importar", "export_import_desc": "Un archivo JSON, con el mismo formato que el webmail", "reset": "Restablecer", @@ -1512,7 +1512,7 @@ "other_title": "Otras conexiones", "phone_backup": "Nada de esto va a la copia de seguridad en la nube de Android.", "phone_secure": "Tus datos de inicio de sesión se guardan en el almacenamiento seguro del sistema. La configuración, la caché de correo sin conexión y los cambios pendientes de sincronizar se guardan en el almacenamiento propio de la app.", - "phone_title": "En este teléfono", + "phone_title": "En este dispositivo", "push": "Cuando las notificaciones están activadas, tu servidor avisa al relay push ({relay}) de que algo ha cambiado, y el relay despierta la app mediante {service}. El relay ve un token de dispositivo y una marca de cambio, nunca tu correo. Puedes usar tu propio relay en Notificaciones.", "push_title": "Notificaciones push", "server": "El correo, los calendarios, los contactos y los archivos se cargan desde tu servidor y se guardan en él. Aparte de las conexiones de abajo, la app solo se comunica con tu servidor.", diff --git a/locales/rn/et.json b/locales/rn/et.json index 9f5bce91..1a371ff1 100644 --- a/locales/rn/et.json +++ b/locales/rn/et.json @@ -122,7 +122,7 @@ "time_zone": "Ajavöönd", "time_zone_auto": "Seadme ajavöönd", "time_zone_auto_zone": "Seadme ajavöönd ({zone})", - "time_zone_desc": "Ajavöönd, mida kasutatakse uute sündmuste jaoks ja kalendri tõlgendamisel. „Seadme ajavöönd“ järgib telefoni." + "time_zone_desc": "Ajavöönd, mida kasutatakse uute sündmuste jaoks ja kalendri tõlgendamisel. „Seadme ajavöönd“ järgib seda seadet." }, "share": { "add_people": "Lisa inimesi", @@ -587,7 +587,7 @@ "licences_desc": "AGPL-3.0 rakendusele ja selle kasutatavatele teekidele", "never_checked": "Pole veel kontrollitud", "privacy": "Privaatsus", - "privacy_desc": "Mis jääb sellesse telefoni ja mis läheb sinu serverisse", + "privacy_desc": "Mis jääb sellesse seadmesse ja mis läheb sinu serverisse", "report_problem": "Teata probleemist", "report_problem_desc": "Avab GitHubis probleemiteate, kuhu versioon on juba sisestatud", "server": "Server", @@ -626,10 +626,10 @@ } }, "reduce_motion": { - "description_mobile": "Lülitab välja libisevad paneelid ja üleminekud. Rakendus teeb seda ka siis, kui telefoni seadetes on vähendatud liikumine sisse lülitatud." + "description_mobile": "Lülitab välja libisevad paneelid ja üleminekud. Rakendus teeb seda ka siis, kui seadme seadetes on vähendatud liikumine sisse lülitatud." }, "text_size": { - "description_mobile": "Skaleerib rakenduse teksti lisaks telefoni teksti suurusele. 100% on telefoni enda suurus." + "description_mobile": "Skaleerib rakenduse teksti lisaks seadme teksti suurusele. 100% on seadme enda suurus." }, "toast_duration": { "automatic": "Automaatne (ekraanilugejaga pikem)" @@ -683,7 +683,7 @@ } }, "backup": { - "description": "Vii oma seaded teise telefoni või veebiposti.", + "description": "Vii oma seaded teise seadmesse või veebiposti.", "export_import": "Ekspordi või impordi", "export_import_desc": "JSON-fail, samas vormingus kui veebipostis", "reset": "Lähtesta", @@ -907,7 +907,7 @@ "clear_title": "Tühjenda võrguühenduseta kirjad", "days_ago": "{count} p tagasi", "description": "Laadi hiljutised kirjad alla, et need avaneksid kohe ja oleksid loetavad ka ilma ühenduseta. Ainult kirjade sisu — manused laaditakse endiselt vajaduse korral.", - "description_short": "Hoia hiljutisi kirju selles telefonis, et need avaneksid ilma ühenduseta.", + "description_short": "Hoia hiljutisi kirju selles seadmes, et need avaneksid ilma ühenduseta.", "downloading": "Allalaadimine {completed}/{total}…", "enabled": "Salvesta hiljutised kirjad vahemällu", "enabled_desc": "Sisselülitatuna hoiab rakendus valitud ajavahemiku kirjade sisu selles seadmes.", @@ -947,7 +947,7 @@ "other_title": "Muud ühendused", "phone_backup": "Midagi sellest ei lähe Androidi pilvevarundusse.", "phone_secure": "Sinu sisselogimisandmeid hoitakse süsteemi turvalises hoidlas. Seadeid, võrguühenduseta kirjade vahemälu ja sünkroonimist ootavaid muudatusi hoitakse rakenduse enda hoidlas.", - "phone_title": "Selles telefonis", + "phone_title": "Selles seadmes", "push": "Kui teavitused on sees, annab su server push-releele ({relay}) teada, et midagi muutus, ja relee äratab rakenduse (teenus: {service}). Relee näeb seadmetõendit ja muudatuse märget, mitte kunagi su kirju. Oma releed saad kasutada jaotises Teavitused.", "push_title": "Push-teavitused", "server": "Kirjad, kalendrid, kontaktid ja failid laaditakse sinu serverist ja salvestatakse sinna tagasi. Peale allpool toodud ühenduste suhtleb rakendus ainult sinu serveriga.", @@ -1436,7 +1436,7 @@ "err_pair_connect_detail": "Näita arvutis uut koodi ja skaneeri uuesti.", "err_pair_connect_title": "Sisse logitud, kuid ühendus ebaõnnestus: {host}", "err_pair_insecure_detail": "{host} soovib kasutada krüpteerimata ühendust.", - "err_pair_network_detail": "Kontrolli ühendust ja seda, kas telefon jõuab veebipostini, ning proovi uuesti.", + "err_pair_network_detail": "Kontrolli ühendust ja seda, kas see seade jõuab veebipostini, ning proovi uuesti.", "err_pair_rate_detail": "Oota minut ja proovi siis uuesti.", "err_pair_server_detail": "Proovi uuesti uue koodiga. Kui see ikka ebaõnnestub, pöördu administraatori poole.", "err_pair_server_title": "Veebipost ei saanud sisselogimist lõpule viia", @@ -1500,7 +1500,7 @@ "server_cert_active": "Sisselogimine kliendisertifikaadiga „{alias}“. Puuduta, et valida muu.", "server_cert_clear": "Logi sisse ilma sertifikaadita", "server_cert_failed": "Sertifikaati ei õnnestunud valida", - "server_cert_hint": "Kas sinu server küsib kliendisertifikaati? Vali mõni sellesse telefoni paigaldatud sertifikaat.", + "server_cert_hint": "Kas sinu server küsib kliendisertifikaati? Vali mõni sellesse seadmesse paigaldatud sertifikaat.", "server_hint": "Sama aadress, mille sisestad brauserisse. Lisame https:// ise.", "server_label": "Serveri aadress", "server_not_found": "Domeenile {domain} ei leitud serverit. Kui majutad serverit ise, on aadress sama, mida kasutad veebiposti jaoks.", diff --git a/locales/rn/fa.json b/locales/rn/fa.json index 1665196d..d057e491 100644 --- a/locales/rn/fa.json +++ b/locales/rn/fa.json @@ -124,7 +124,7 @@ "time_zone": "منطقه زمانی", "time_zone_auto": "منطقه زمانی دستگاه", "time_zone_auto_zone": "منطقه زمانی دستگاه ({zone})", - "time_zone_desc": "منطقه‌ای که برای رویدادهای جدید و تفسیر تقویم استفاده می‌شود. «منطقه زمانی دستگاه» از تنظیمات تلفن پیروی می‌کند." + "time_zone_desc": "منطقه‌ای که برای رویدادهای جدید و تفسیر تقویم استفاده می‌شود. «منطقه زمانی دستگاه» از تنظیمات این دستگاه پیروی می‌کند." }, "share": { "add_people": "افزودن افراد", @@ -669,7 +669,7 @@ "err_pair_connect_detail": "روی رایانه‌تان کد تازه‌ای نمایش دهید و دوباره اسکن کنید.", "err_pair_connect_title": "وارد شدید، اما اتصال به {host} ممکن نشد", "err_pair_insecure_detail": "{host} می‌خواهد از اتصال رمزگذاری‌نشده استفاده کند.", - "err_pair_network_detail": "اتصال خود را بررسی کنید و مطمئن شوید تلفنتان به ایمیل تحت وب دسترسی دارد، سپس دوباره امتحان کنید.", + "err_pair_network_detail": "اتصال خود را بررسی کنید و مطمئن شوید این دستگاه به ایمیل تحت وب دسترسی دارد، سپس دوباره امتحان کنید.", "err_pair_rate_detail": "یک دقیقه صبر کنید، سپس دوباره امتحان کنید.", "err_pair_server_detail": "با یک کد تازه دوباره امتحان کنید. اگر باز هم ناموفق بود، با مدیر سیستم تماس بگیرید.", "err_pair_server_title": "ایمیل تحت وب نتوانست ورود را کامل کند", @@ -757,7 +757,7 @@ "server_cert_active": "ورود با گواهی کلاینت «{alias}». برای انتخاب گواهی دیگر ضربه بزنید.", "server_cert_clear": "ورود بدون گواهی", "server_cert_failed": "انتخاب گواهی ممکن نشد", - "server_cert_hint": "سرور شما گواهی کلاینت می‌خواهد؟ یکی از گواهی‌های نصب‌شده روی این تلفن را انتخاب کنید." + "server_cert_hint": "سرور شما گواهی کلاینت می‌خواهد؟ یکی از گواهی‌های نصب‌شده روی این دستگاه را انتخاب کنید." } }, "offline": { @@ -1232,7 +1232,7 @@ "window_24h": "24 ساعت گذشته", "window_days": "{count} روز گذشته", "window_desc": "تا چه مدت قبل کش شود، بر اساس تاریخ دریافت پیام.", - "description_short": "پیام‌های اخیر را روی این تلفن نگه دارید تا بدون اتصال هم باز شوند.", + "description_short": "پیام‌های اخیر را روی این دستگاه نگه دارید تا بدون اتصال هم باز شوند.", "keep_24h": "24 ساعت", "keep_days": "{count} روز", "keep_from": "نگه داشتن ایمیل‌های اخیر به مدت", @@ -1432,7 +1432,7 @@ "licences_desc": "AGPL-3.0 برای برنامه، و کتابخانه‌هایی که از آن‌ها استفاده می‌کند", "never_checked": "هنوز بررسی نشده", "privacy": "حریم خصوصی", - "privacy_desc": "چه چیزی روی این تلفن می‌ماند و چه چیزی به سرور شما می‌رود", + "privacy_desc": "چه چیزی روی این دستگاه می‌ماند و چه چیزی به سرور شما می‌رود", "report_problem": "گزارش مشکل", "report_problem_desc": "یک گزارش در GitHub باز می‌کند که نسخهٔ برنامه در آن درج شده است", "server": "سرور", @@ -1471,17 +1471,17 @@ } }, "reduce_motion": { - "description_mobile": "پنل‌ها و جابه‌جایی‌های کشویی را خاموش می‌کند. اگر کاهش حرکت در تنظیمات تلفن شما روشن باشد، برنامه هم همین کار را می‌کند." + "description_mobile": "پنل‌ها و جابه‌جایی‌های کشویی را خاموش می‌کند. اگر کاهش حرکت در تنظیمات دستگاه شما روشن باشد، برنامه هم همین کار را می‌کند." }, "text_size": { - "description_mobile": "متن برنامه را علاوه بر اندازهٔ متن تلفن شما بزرگ یا کوچک می‌کند. 100% همان اندازهٔ تلفن شماست." + "description_mobile": "متن برنامه را علاوه بر اندازهٔ متن دستگاه شما بزرگ یا کوچک می‌کند. 100% همان اندازهٔ دستگاه شماست." }, "toast_duration": { "automatic": "خودکار (با صفحه‌خوان طولانی‌تر)" } }, "backup": { - "description": "تنظیمات خود را به تلفن دیگر یا ایمیل تحت وب منتقل کنید.", + "description": "تنظیمات خود را به دستگاه دیگر یا ایمیل تحت وب منتقل کنید.", "export_import": "خروجی یا وارد کردن", "export_import_desc": "یک فایل JSON، با همان قالب ایمیل تحت وب", "reset": "بازنشانی", @@ -1500,7 +1500,7 @@ "other_title": "اتصال‌های دیگر", "phone_backup": "هیچ‌کدام از این‌ها در پشتیبان ابری Android ذخیره نمی‌شود.", "phone_secure": "اطلاعات ورود شما در فضای امن سیستم نگه داشته می‌شود. تنظیمات، کش ایمیل آفلاین و تغییرات در انتظار همگام‌سازی در فضای خود برنامه نگه داشته می‌شوند.", - "phone_title": "روی این تلفن", + "phone_title": "روی این دستگاه", "push": "وقتی اعلان‌ها روشن است، سرور شما به رلهٔ push ({relay}) خبر می‌دهد که چیزی تغییر کرده، و رله برنامه را از طریق {service} بیدار می‌کند. رله یک توکن دستگاه و یک نشانگر تغییر می‌بیند، هرگز ایمیل شما را نمی‌بیند. در بخش اعلان‌ها می‌توانید از رلهٔ خودتان استفاده کنید.", "push_title": "اعلان‌های Push", "server": "ایمیل، تقویم‌ها، مخاطبین و فایل‌ها از سرور شما بارگذاری و دوباره در آن ذخیره می‌شوند. جز اتصال‌های زیر، برنامه فقط با سرور شما ارتباط دارد.", diff --git a/locales/rn/fr.json b/locales/rn/fr.json index bf8e0a9a..464c6ccc 100644 --- a/locales/rn/fr.json +++ b/locales/rn/fr.json @@ -124,7 +124,7 @@ "time_zone": "Fuseau horaire", "time_zone_auto": "Fuseau horaire de l'appareil", "time_zone_auto_zone": "Fuseau horaire de l'appareil ({zone})", - "time_zone_desc": "Fuseau utilisé pour les nouveaux événements et pour l'affichage du calendrier. « Fuseau horaire de l'appareil » suit le téléphone." + "time_zone_desc": "Fuseau utilisé pour les nouveaux événements et pour l'affichage du calendrier. « Fuseau horaire de l'appareil » suit celui de cet appareil." }, "share": { "add_people": "Ajouter des personnes", @@ -679,7 +679,7 @@ "err_pair_connect_detail": "Affichez un nouveau code sur votre ordinateur et scannez-le à nouveau.", "err_pair_connect_title": "Connecté, mais impossible de joindre {host}", "err_pair_insecure_detail": "{host} veut utiliser une connexion non chiffrée.", - "err_pair_network_detail": "Vérifiez votre connexion et que votre téléphone peut accéder au webmail, puis réessayez.", + "err_pair_network_detail": "Vérifiez votre connexion et que cet appareil peut accéder au webmail, puis réessayez.", "err_pair_rate_detail": "Patientez une minute, puis réessayez.", "err_pair_server_detail": "Réessayez avec un nouveau code. Si le problème persiste, contactez votre administrateur.", "err_pair_server_title": "Le webmail n'a pas pu terminer la connexion", @@ -767,7 +767,7 @@ "server_cert_active": "Connexion avec le certificat client « {alias} ». Touchez pour en choisir un autre.", "server_cert_clear": "Se connecter sans le certificat", "server_cert_failed": "Impossible de choisir un certificat", - "server_cert_hint": "Votre serveur demande-t-il un certificat client ? Choisissez-en un installé sur ce téléphone." + "server_cert_hint": "Votre serveur demande-t-il un certificat client ? Choisissez-en un installé sur cet appareil." } }, "offline": { @@ -1242,7 +1242,7 @@ "window_24h": "Dernières 24 heures", "window_days": "{count} derniers jours", "window_desc": "Jusqu'où remonter pour la mise en cache, selon la date de réception des messages.", - "description_short": "Conservez les messages récents sur ce téléphone pour les ouvrir sans connexion.", + "description_short": "Conservez les messages récents sur cet appareil pour les ouvrir sans connexion.", "keep_24h": "24 heures", "keep_days": "{count} jours", "keep_from": "Conserver les e-mails depuis", @@ -1442,7 +1442,7 @@ "licences_desc": "AGPL-3.0 pour l'application, et les bibliothèques qu'elle utilise", "never_checked": "Pas encore vérifié", "privacy": "Confidentialité", - "privacy_desc": "Ce qui reste sur ce téléphone et ce qui part vers votre serveur", + "privacy_desc": "Ce qui reste sur cet appareil et ce qui part vers votre serveur", "report_problem": "Signaler un problème", "report_problem_desc": "Ouvre un ticket sur GitHub avec la version déjà renseignée", "server": "Serveur", @@ -1481,17 +1481,17 @@ } }, "reduce_motion": { - "description_mobile": "Désactive les panneaux coulissants et les transitions. L'application le fait aussi quand la réduction des mouvements est activée dans les réglages de votre téléphone." + "description_mobile": "Désactive les panneaux coulissants et les transitions. L'application le fait aussi quand la réduction des mouvements est activée dans les réglages de votre appareil." }, "text_size": { - "description_mobile": "Agrandit ou réduit le texte de l'application en plus de la taille de texte de votre téléphone. 100 % correspond à la taille de votre téléphone." + "description_mobile": "Agrandit ou réduit le texte de l'application en plus de la taille de texte de votre appareil. 100 % correspond à la taille de votre appareil." }, "toast_duration": { "automatic": "Automatique (plus long avec un lecteur d'écran)" } }, "backup": { - "description": "Transférez vos paramètres vers un autre téléphone ou le webmail.", + "description": "Transférez vos paramètres vers un autre appareil ou le webmail.", "export_import": "Exporter ou importer", "export_import_desc": "Un fichier JSON, au même format que le webmail", "reset": "Réinitialiser", @@ -1510,7 +1510,7 @@ "other_title": "Autres connexions", "phone_backup": "Rien de tout cela n'est inclus dans la sauvegarde cloud d'Android.", "phone_secure": "Vos identifiants de connexion sont conservés dans le stockage sécurisé du système. Les paramètres, le cache des e-mails hors ligne et les modifications en attente de synchronisation sont conservés dans le stockage propre de l'application.", - "phone_title": "Sur ce téléphone", + "phone_title": "Sur cet appareil", "push": "Quand les notifications sont activées, votre serveur signale au relais push ({relay}) qu'un changement a eu lieu, et le relais réveille l'application via {service}. Le relais voit un jeton d'appareil et un marqueur de changement, jamais vos e-mails. Vous pouvez utiliser votre propre relais dans Notifications.", "push_title": "Notifications push", "server": "Les e-mails, calendriers, contacts et fichiers sont chargés depuis votre serveur et y sont enregistrés. En dehors des connexions ci-dessous, l'application ne communique qu'avec votre serveur.", diff --git a/locales/rn/he.json b/locales/rn/he.json index 6ffe3bf5..fc880fe0 100644 --- a/locales/rn/he.json +++ b/locales/rn/he.json @@ -124,7 +124,7 @@ "time_zone": "אזור זמן", "time_zone_auto": "אזור הזמן של המכשיר", "time_zone_auto_zone": "אזור הזמן של המכשיר ({zone})", - "time_zone_desc": "האזור המשמש לאירועים חדשים ולפירוש לוח השנה. \"אזור הזמן של המכשיר\" פועל לפי הגדרות הטלפון." + "time_zone_desc": "האזור המשמש לאירועים חדשים ולפירוש לוח השנה. \"אזור הזמן של המכשיר\" פועל לפי הגדרות המכשיר הזה." }, "share": { "add_people": "הוסף אנשים", @@ -673,7 +673,7 @@ "err_pair_connect_detail": "הצג קוד חדש במחשב וסרוק שוב.", "err_pair_connect_title": "ההתחברות הצליחה, אך אין חיבור אל {host}", "err_pair_insecure_detail": "{host} רוצה להשתמש בחיבור לא מוצפן.", - "err_pair_network_detail": "בדוק את החיבור ושהטלפון יכול להגיע לדואר האינטרנט, ונסה שוב.", + "err_pair_network_detail": "בדוק את החיבור ושהמכשיר הזה יכול להגיע לדואר האינטרנט, ונסה שוב.", "err_pair_rate_detail": "המתן דקה ונסה שוב.", "err_pair_server_detail": "נסה שוב עם קוד חדש. אם זה ממשיך להיכשל, פנה למנהל המערכת.", "err_pair_server_title": "דואר האינטרנט לא הצליח להשלים את ההתחברות", @@ -761,7 +761,7 @@ "server_cert_active": "מתחבר עם תעודת הלקוח \"{alias}\". הקש כדי לבחור אחרת.", "server_cert_clear": "התחבר בלי התעודה", "server_cert_failed": "לא ניתן היה לבחור תעודה", - "server_cert_hint": "השרת שלך מבקש תעודת לקוח? בחר תעודה שמותקנת בטלפון הזה." + "server_cert_hint": "השרת שלך מבקש תעודת לקוח? בחר תעודה שמותקנת במכשיר הזה." } }, "offline": { @@ -1236,7 +1236,7 @@ "window_24h": "24 השעות האחרונות", "window_days": "{count, plural, one {היום האחרון} two {היומיים האחרונים} other {# הימים האחרונים}}", "window_desc": "כמה זמן אחורה לשמור במטמון, לפי תאריך קבלת ההודעה.", - "description_short": "שמור הודעות אחרונות בטלפון הזה כדי שייפתחו גם בלי חיבור.", + "description_short": "שמור הודעות אחרונות במכשיר הזה כדי שייפתחו גם בלי חיבור.", "keep_24h": "24 שעות", "keep_days": "{count} ימים", "keep_from": "שמור דואר מהתקופה האחרונה של", @@ -1436,7 +1436,7 @@ "licences_desc": "AGPL-3.0 לאפליקציה, והספריות שבהן היא משתמשת", "never_checked": "עדיין לא נבדק", "privacy": "פרטיות", - "privacy_desc": "מה נשאר בטלפון הזה ומה עובר לשרת שלך", + "privacy_desc": "מה נשאר במכשיר הזה ומה עובר לשרת שלך", "report_problem": "דווח על בעיה", "report_problem_desc": "פותח דיווח ב-GitHub עם הגרסה כבר ממולאת", "server": "שרת", @@ -1475,17 +1475,17 @@ } }, "reduce_motion": { - "description_mobile": "מכבה חלוניות מחליקות ומעברים. האפליקציה עושה זאת גם כשהפחתת תנועה מופעלת בהגדרות הטלפון." + "description_mobile": "מכבה חלוניות מחליקות ומעברים. האפליקציה עושה זאת גם כשהפחתת תנועה מופעלת בהגדרות המכשיר." }, "text_size": { - "description_mobile": "משנה את גודל הטקסט באפליקציה מעבר לגודל הטקסט בטלפון. 100% הוא הגודל של הטלפון." + "description_mobile": "משנה את גודל הטקסט באפליקציה מעבר לגודל הטקסט במכשיר. 100% הוא הגודל של המכשיר." }, "toast_duration": { "automatic": "אוטומטי (ארוך יותר עם קורא מסך)" } }, "backup": { - "description": "העבר את ההגדרות לטלפון אחר או לדואר האינטרנט.", + "description": "העבר את ההגדרות למכשיר אחר או לדואר האינטרנט.", "export_import": "ייצוא או ייבוא", "export_import_desc": "קובץ JSON, באותו פורמט כמו בדואר האינטרנט", "reset": "איפוס", @@ -1504,7 +1504,7 @@ "other_title": "חיבורים אחרים", "phone_backup": "שום דבר מזה לא נכלל בגיבוי בענן של Android.", "phone_secure": "פרטי ההתחברות שלך נשמרים באחסון המאובטח של המערכת. ההגדרות, מטמון הדואר הלא מקוון והשינויים שממתינים לסנכרון נשמרים באחסון של האפליקציה עצמה.", - "phone_title": "בטלפון הזה", + "phone_title": "במכשיר הזה", "push": "כשההתראות פועלות, השרת שלך מודיע לממסר ה-Push ({relay}) שמשהו השתנה, והממסר מעיר את האפליקציה דרך {service}. הממסר רואה אסימון מכשיר וסימון שינוי, אף פעם לא את הדואר שלך. אפשר להשתמש בממסר משלך בהתראות.", "push_title": "הודעות דחיפה", "server": "דואר, לוחות שנה, אנשי קשר וקבצים נטענים מהשרת שלך ונשמרים בחזרה אליו. מלבד החיבורים שלהלן, האפליקציה מדברת רק עם השרת שלך.", diff --git a/locales/rn/hu.json b/locales/rn/hu.json index 814fd5c7..9d3fe34a 100644 --- a/locales/rn/hu.json +++ b/locales/rn/hu.json @@ -124,7 +124,7 @@ "time_zone": "Időzóna", "time_zone_auto": "Eszköz időzónája", "time_zone_auto_zone": "Eszköz időzónája ({zone})", - "time_zone_desc": "Az új eseményekhez és a naptár megjelenítéséhez használt zóna. Az „Eszköz időzónája” a telefont követi." + "time_zone_desc": "Az új eseményekhez és a naptár megjelenítéséhez használt zóna. Az „Eszköz időzónája” ezt az eszközt követi." }, "share": { "add_people": "Személyek hozzáadása", @@ -671,7 +671,7 @@ "err_pair_connect_detail": "Jeleníts meg új kódot a számítógépen, és olvasd be újra.", "err_pair_connect_title": "Bejelentkezve, de nem sikerült csatlakozni: {host}", "err_pair_insecure_detail": "A(z) {host} titkosítatlan kapcsolatot akar használni.", - "err_pair_network_detail": "Ellenőrizd a kapcsolatot, és hogy a telefonod eléri-e a webmailt, majd próbáld újra.", + "err_pair_network_detail": "Ellenőrizd a kapcsolatot, és hogy ez az eszköz eléri-e a webmailt, majd próbáld újra.", "err_pair_rate_detail": "Várj egy percet, majd próbáld újra.", "err_pair_server_detail": "Próbáld újra egy új kóddal. Ha továbbra sem sikerül, fordulj a rendszergazdához.", "err_pair_server_title": "A webmail nem tudta befejezni a bejelentkezést", @@ -759,7 +759,7 @@ "server_cert_active": "Bejelentkezés a(z) „{alias}” klienstanúsítvánnyal. Koppints, ha másikat választanál.", "server_cert_clear": "Bejelentkezés tanúsítvány nélkül", "server_cert_failed": "Nem sikerült tanúsítványt választani", - "server_cert_hint": "A szervered klienstanúsítványt kér? Válassz egyet a telefonra telepítettek közül." + "server_cert_hint": "A szervered klienstanúsítványt kér? Válassz egyet az eszközre telepítettek közül." } }, "offline": { @@ -1234,7 +1234,7 @@ "window_24h": "Utolsó 24 óra", "window_days": "Utolsó {count} nap", "window_desc": "Milyen régre visszamenőleg legyen gyorsítótárazva, az üzenet fogadási dátuma alapján.", - "description_short": "A legutóbbi üzenetek tárolása ezen a telefonon, hogy kapcsolat nélkül is megnyíljanak.", + "description_short": "A legutóbbi üzenetek tárolása ezen az eszközön, hogy kapcsolat nélkül is megnyíljanak.", "keep_24h": "24 óra", "keep_days": "{count} nap", "keep_from": "Megőrzött időszak", @@ -1434,7 +1434,7 @@ "licences_desc": "AGPL-3.0 az alkalmazáshoz és az általa használt könyvtárakhoz", "never_checked": "Még nincs ellenőrizve", "privacy": "Adatvédelem", - "privacy_desc": "Mi marad ezen a telefonon, és mi kerül a szerveredre", + "privacy_desc": "Mi marad ezen az eszközön, és mi kerül a szerveredre", "report_problem": "Probléma jelentése", "report_problem_desc": "Hibajegyet nyit a GitHubon, a verzió már kitöltve", "server": "Szerver", @@ -1473,17 +1473,17 @@ } }, "reduce_motion": { - "description_mobile": "Kikapcsolja a csúszó paneleket és az átmeneteket. Az alkalmazás akkor is ezt teszi, ha a telefon beállításaiban be van kapcsolva a csökkentett mozgás." + "description_mobile": "Kikapcsolja a csúszó paneleket és az átmeneteket. Az alkalmazás akkor is ezt teszi, ha az eszköz beállításaiban be van kapcsolva a csökkentett mozgás." }, "text_size": { - "description_mobile": "A telefon szövegméretén felül méretezi az alkalmazás szövegét. A 100% a telefon szövegmérete." + "description_mobile": "Az eszköz szövegméretén felül méretezi az alkalmazás szövegét. A 100% az eszköz szövegmérete." }, "toast_duration": { "automatic": "Automatikus (képernyőolvasóval hosszabb)" } }, "backup": { - "description": "Vidd át a beállításaidat egy másik telefonra vagy a webmailbe.", + "description": "Vidd át a beállításaidat egy másik eszközre vagy a webmailbe.", "export_import": "Exportálás vagy importálás", "export_import_desc": "JSON-fájl, a webmaillel azonos formátumban", "reset": "Visszaállítás", @@ -1502,7 +1502,7 @@ "other_title": "Egyéb kapcsolatok", "phone_backup": "Ezek egyike sem kerül az Android felhőalapú biztonsági mentésébe.", "phone_secure": "A bejelentkezési adataid a rendszer biztonságos tárolójában vannak. A beállításokat, az offline levél-gyorsítótárat és a szinkronizálásra váró módosításokat az alkalmazás saját tárolója őrzi.", - "phone_title": "Ezen a telefonon", + "phone_title": "Ezen az eszközön", "push": "Ha az értesítések be vannak kapcsolva, a szervered szól a push relének ({relay}), hogy valami megváltozott, a relé pedig ezen keresztül ébreszti fel az alkalmazást: {service}. A relé egy eszköztokent és egy változásjelzőt lát, a leveleidet soha. Az Értesítések alatt saját relét is használhatsz.", "push_title": "Push értesítések", "server": "A levelek, naptárak, névjegyek és fájlok a szerveredről töltődnek be, és oda mentődnek vissza. Az alábbi kapcsolatokon kívül az alkalmazás csak a szervereddel kommunikál.", diff --git a/locales/rn/it.json b/locales/rn/it.json index 82de1156..cc9065c1 100644 --- a/locales/rn/it.json +++ b/locales/rn/it.json @@ -124,7 +124,7 @@ "time_zone": "Fuso orario", "time_zone_auto": "Fuso orario del dispositivo", "time_zone_auto_zone": "Fuso orario del dispositivo ({zone})", - "time_zone_desc": "Fuso usato per i nuovi eventi e per interpretare il calendario. \"Dispositivo\" segue quello del telefono." + "time_zone_desc": "Fuso usato per i nuovi eventi e per interpretare il calendario. \"Dispositivo\" segue quello di questo dispositivo." }, "share": { "add_people": "Aggiungi persone", @@ -677,7 +677,7 @@ "err_pair_connect_detail": "Mostra un nuovo codice sul computer e scansionalo di nuovo.", "err_pair_connect_title": "Accesso eseguito, ma impossibile connettersi a {host}", "err_pair_insecure_detail": "{host} vuole usare una connessione non cifrata.", - "err_pair_network_detail": "Controlla la connessione e che il telefono possa raggiungere la webmail, poi riprova.", + "err_pair_network_detail": "Controlla la connessione e che questo dispositivo possa raggiungere la webmail, poi riprova.", "err_pair_rate_detail": "Attendi un minuto, poi riprova.", "err_pair_server_detail": "Riprova con un nuovo codice. Se continua a non funzionare, rivolgiti all'amministratore.", "err_pair_server_title": "La webmail non è riuscita a completare l'accesso", @@ -765,7 +765,7 @@ "server_cert_active": "Accesso con il certificato client “{alias}”. Tocca per sceglierne un altro.", "server_cert_clear": "Accedi senza il certificato", "server_cert_failed": "Impossibile scegliere un certificato", - "server_cert_hint": "Il tuo server richiede un certificato client? Scegline uno installato su questo telefono." + "server_cert_hint": "Il tuo server richiede un certificato client? Scegline uno installato su questo dispositivo." } }, "offline": { @@ -1240,7 +1240,7 @@ "window_24h": "Ultime 24 ore", "window_days": "Ultimi {count} giorni", "window_desc": "Fino a quando salvare in cache, in base alla data di ricezione del messaggio.", - "description_short": "Conserva i messaggi recenti su questo telefono, così si aprono anche senza connessione.", + "description_short": "Conserva i messaggi recenti su questo dispositivo, così si aprono anche senza connessione.", "keep_24h": "24 ore", "keep_days": "{count} giorni", "keep_from": "Conserva la posta degli ultimi", @@ -1440,7 +1440,7 @@ "licences_desc": "AGPL-3.0 per l'app e le librerie che usa", "never_checked": "Non ancora controllato", "privacy": "Privacy", - "privacy_desc": "Cosa resta su questo telefono e cosa va al tuo server", + "privacy_desc": "Cosa resta su questo dispositivo e cosa va al tuo server", "report_problem": "Segnala un problema", "report_problem_desc": "Apre una segnalazione su GitHub con la versione già compilata", "server": "Server", @@ -1479,17 +1479,17 @@ } }, "reduce_motion": { - "description_mobile": "Disattiva i pannelli a scorrimento e le transizioni. L'app lo fa anche quando nelle impostazioni del telefono è attiva la riduzione del movimento." + "description_mobile": "Disattiva i pannelli a scorrimento e le transizioni. L'app lo fa anche quando nelle impostazioni del dispositivo è attiva la riduzione del movimento." }, "text_size": { - "description_mobile": "Ridimensiona il testo dell'app in aggiunta alla dimensione del testo del telefono. 100% corrisponde alla dimensione del telefono." + "description_mobile": "Ridimensiona il testo dell'app in aggiunta alla dimensione del testo del dispositivo. 100% corrisponde alla dimensione del dispositivo." }, "toast_duration": { "automatic": "Automatica (più lunga con uno screen reader)" } }, "backup": { - "description": "Trasferisci le impostazioni su un altro telefono o nella webmail.", + "description": "Trasferisci le impostazioni su un altro dispositivo o nella webmail.", "export_import": "Esporta o importa", "export_import_desc": "Un file JSON, nello stesso formato della webmail", "reset": "Ripristina", @@ -1508,7 +1508,7 @@ "other_title": "Altre connessioni", "phone_backup": "Niente di tutto ciò finisce nel backup cloud di Android.", "phone_secure": "I tuoi dati di accesso sono conservati nell'archivio sicuro del sistema. Impostazioni, cache della posta offline e modifiche in attesa di sincronizzazione sono conservate nell'archivio dell'app.", - "phone_title": "Su questo telefono", + "phone_title": "Su questo dispositivo", "push": "Quando le notifiche sono attive, il tuo server comunica al relay push ({relay}) che qualcosa è cambiato, e il relay riattiva l'app tramite {service}. Il relay vede un token del dispositivo e un indicatore di modifica, mai la tua posta. Puoi usare un relay tuo in Notifiche.", "push_title": "Notifiche push", "server": "Posta, calendari, contatti e file vengono caricati dal tuo server e salvati di nuovo su di esso. A parte le connessioni qui sotto, l'app comunica solo con il tuo server.", diff --git a/locales/rn/ja.json b/locales/rn/ja.json index f6e44ba1..cfe62450 100644 --- a/locales/rn/ja.json +++ b/locales/rn/ja.json @@ -124,7 +124,7 @@ "time_zone": "タイムゾーン", "time_zone_auto": "デバイスのタイムゾーン", "time_zone_auto_zone": "デバイスのタイムゾーン({zone})", - "time_zone_desc": "新しい予定の作成とカレンダーの表示に使うタイムゾーンです。「デバイス」を選ぶとスマートフォンの設定に従います。" + "time_zone_desc": "新しい予定の作成とカレンダーの表示に使うタイムゾーンです。「デバイス」を選ぶとこのデバイスの設定に従います。" }, "share": { "add_people": "ユーザーを追加", @@ -675,7 +675,7 @@ "err_pair_connect_detail": "コンピューターで新しいコードを表示して、もう一度スキャンしてください。", "err_pair_connect_title": "サインインしましたが、{host} に接続できませんでした", "err_pair_insecure_detail": "{host} が暗号化されていない接続を使おうとしています。", - "err_pair_network_detail": "接続状況と、スマートフォンからウェブメールにアクセスできるかを確認して、もう一度お試しください。", + "err_pair_network_detail": "接続状況と、このデバイスからウェブメールにアクセスできるかを確認して、もう一度お試しください。", "err_pair_rate_detail": "1分待ってから、もう一度お試しください。", "err_pair_server_detail": "新しいコードでもう一度お試しください。それでも失敗する場合は、管理者にお問い合わせください。", "err_pair_server_title": "ウェブメールでサインインを完了できませんでした", @@ -763,7 +763,7 @@ "server_cert_active": "クライアント証明書「{alias}」でサインインします。タップして別の証明書を選択します。", "server_cert_clear": "証明書なしでサインイン", "server_cert_failed": "証明書を選択できませんでした", - "server_cert_hint": "サーバーがクライアント証明書を要求しますか?このスマートフォンにインストールされている証明書を選択してください。" + "server_cert_hint": "サーバーがクライアント証明書を要求しますか?このデバイスにインストールされている証明書を選択してください。" } }, "offline": { @@ -1238,7 +1238,7 @@ "window_24h": "過去24時間", "window_days": "過去{count}日間", "window_desc": "どこまでさかのぼってキャッシュするかを、メッセージの受信日で指定します。", - "description_short": "最近のメッセージをこのスマートフォンに保存し、接続がなくても開けるようにします。", + "description_short": "最近のメッセージをこのデバイスに保存し、接続がなくても開けるようにします。", "keep_24h": "24時間", "keep_days": "{count}日", "keep_from": "保存するメールの期間", @@ -1438,7 +1438,7 @@ "licences_desc": "アプリの AGPL-3.0 と、使用しているライブラリのライセンス", "never_checked": "未確認", "privacy": "プライバシー", - "privacy_desc": "このスマートフォンに残るデータとサーバーに送られるデータ", + "privacy_desc": "このデバイスに残るデータとサーバーに送られるデータ", "report_problem": "問題を報告", "report_problem_desc": "バージョンを記入済みの Issue を GitHub で開きます", "server": "サーバー", @@ -1477,17 +1477,17 @@ } }, "reduce_motion": { - "description_mobile": "スライドするパネルや切り替え効果をオフにします。スマートフォンの設定で動きを減らす機能がオンの場合も、アプリは自動的にオフにします。" + "description_mobile": "スライドするパネルや切り替え効果をオフにします。デバイスの設定で動きを減らす機能がオンの場合も、アプリは自動的にオフにします。" }, "text_size": { - "description_mobile": "スマートフォンの文字サイズを基準に、アプリの文字を拡大・縮小します。100% はスマートフォンと同じサイズです。" + "description_mobile": "デバイスの文字サイズを基準に、アプリの文字を拡大・縮小します。100% はデバイスと同じサイズです。" }, "toast_duration": { "automatic": "自動(スクリーンリーダー使用時は長め)" } }, "backup": { - "description": "設定を別のスマートフォンやウェブメールに移行します。", + "description": "設定を別のデバイスやウェブメールに移行します。", "export_import": "エクスポートまたはインポート", "export_import_desc": "ウェブメールと同じ形式の JSON ファイル", "reset": "リセット", @@ -1506,7 +1506,7 @@ "other_title": "その他の接続", "phone_backup": "これらのデータは Android のクラウドバックアップには含まれません。", "phone_secure": "サインイン情報はシステムの安全な保存領域に保管されます。設定、オフラインメールのキャッシュ、同期待ちの変更はアプリ専用の保存領域に保管されます。", - "phone_title": "このスマートフォン", + "phone_title": "このデバイス", "push": "通知がオンの場合、サーバーは変更があったことをプッシュリレー({relay})に伝え、リレーは {service} を通じてアプリを起動します。リレーに伝わるのはデバイストークンと変更の目印だけで、メールの内容は伝わりません。「通知」で独自のリレーを使うこともできます。", "push_title": "プッシュ通知", "server": "メール、カレンダー、連絡先、ファイルはサーバーから読み込まれ、サーバーに保存されます。以下の接続を除き、アプリが通信するのはお使いのサーバーだけです。", diff --git a/locales/rn/ko.json b/locales/rn/ko.json index 2ce82ac6..bc8c3724 100644 --- a/locales/rn/ko.json +++ b/locales/rn/ko.json @@ -124,7 +124,7 @@ "time_zone": "시간대", "time_zone_auto": "기기 시간대", "time_zone_auto_zone": "기기 시간대({zone})", - "time_zone_desc": "새 일정과 캘린더 표시에 사용하는 시간대예요. \"기기\"를 선택하면 휴대폰 설정을 따라요." + "time_zone_desc": "새 일정과 캘린더 표시에 사용하는 시간대예요. \"기기\"를 선택하면 이 기기의 설정을 따라요." }, "share": { "add_people": "사용자 추가", @@ -673,7 +673,7 @@ "err_pair_connect_detail": "컴퓨터에서 새 코드를 표시한 후 다시 스캔해 주세요.", "err_pair_connect_title": "로그인했지만 {host}에 연결하지 못했어요", "err_pair_insecure_detail": "{host}에서 암호화되지 않은 연결을 사용하려고 해요.", - "err_pair_network_detail": "연결 상태와 휴대폰에서 웹메일에 접속할 수 있는지 확인한 후 다시 시도해 주세요.", + "err_pair_network_detail": "연결 상태와 이 기기에서 웹메일에 접속할 수 있는지 확인한 후 다시 시도해 주세요.", "err_pair_rate_detail": "1분 정도 기다린 후 다시 시도해 주세요.", "err_pair_server_detail": "새 코드로 다시 시도해 주세요. 계속 실패하면 관리자에게 문의해 주세요.", "err_pair_server_title": "웹메일에서 로그인을 완료하지 못했어요", @@ -761,7 +761,7 @@ "server_cert_active": "클라이언트 인증서 \"{alias}\"(으)로 로그인해요. 다른 인증서를 선택하려면 탭하세요.", "server_cert_clear": "인증서 없이 로그인", "server_cert_failed": "인증서를 선택하지 못했어요", - "server_cert_hint": "서버에서 클라이언트 인증서를 요구하나요? 이 휴대폰에 설치된 인증서를 선택해 주세요." + "server_cert_hint": "서버에서 클라이언트 인증서를 요구하나요? 이 기기에 설치된 인증서를 선택해 주세요." } }, "offline": { @@ -1236,7 +1236,7 @@ "window_24h": "최근 24시간", "window_days": "최근 {count}일", "window_desc": "메시지를 받은 날짜를 기준으로 얼마나 이전까지 캐시할지 정해요.", - "description_short": "최근 메시지를 이 휴대폰에 보관해서 연결 없이도 열 수 있게 해요.", + "description_short": "최근 메시지를 이 기기에 보관해서 연결 없이도 열 수 있게 해요.", "keep_24h": "24시간", "keep_days": "{count}일", "keep_from": "보관할 메일 기간(최근)", @@ -1436,7 +1436,7 @@ "licences_desc": "앱의 AGPL-3.0 라이선스와 앱이 사용하는 라이브러리", "never_checked": "아직 확인하지 않음", "privacy": "개인정보 보호", - "privacy_desc": "이 휴대폰에 남는 정보와 서버로 전송되는 정보", + "privacy_desc": "이 기기에 남는 정보와 서버로 전송되는 정보", "report_problem": "문제 신고", "report_problem_desc": "버전이 채워진 이슈를 GitHub에서 열어요", "server": "서버", @@ -1475,17 +1475,17 @@ } }, "reduce_motion": { - "description_mobile": "슬라이드 패널과 전환 효과를 꺼요. 휴대폰 설정에서 움직임 줄이기가 켜져 있을 때도 앱이 자동으로 꺼요." + "description_mobile": "슬라이드 패널과 전환 효과를 꺼요. 기기 설정에서 움직임 줄이기가 켜져 있을 때도 앱이 자동으로 꺼요." }, "text_size": { - "description_mobile": "휴대폰의 글자 크기를 기준으로 앱의 글자 크기를 조절해요. 100%는 휴대폰의 글자 크기와 같아요." + "description_mobile": "기기의 글자 크기를 기준으로 앱의 글자 크기를 조절해요. 100%는 기기의 글자 크기와 같아요." }, "toast_duration": { "automatic": "자동(화면 낭독기 사용 시 더 길게)" } }, "backup": { - "description": "설정을 다른 휴대폰이나 웹메일로 옮겨요.", + "description": "설정을 다른 기기나 웹메일로 옮겨요.", "export_import": "내보내기 또는 가져오기", "export_import_desc": "웹메일과 같은 형식의 JSON 파일", "reset": "초기화", @@ -1504,7 +1504,7 @@ "other_title": "기타 연결", "phone_backup": "이 정보는 Android 클라우드 백업에 포함되지 않아요.", "phone_secure": "로그인 정보는 시스템의 보안 저장소에 보관돼요. 설정, 오프라인 메일 캐시, 동기화 대기 중인 변경 사항은 앱 자체 저장소에 보관돼요.", - "phone_title": "이 휴대폰", + "phone_title": "이 기기", "push": "알림이 켜져 있으면 서버가 푸시 릴레이({relay})에 변경 사항이 있다고 알리고, 릴레이는 {service}를 통해 앱을 깨워요. 릴레이가 보는 것은 기기 토큰과 변경 표시뿐이며, 메일 내용은 절대 보지 않아요. 알림에서 직접 운영하는 릴레이를 사용할 수도 있어요.", "push_title": "푸시 알림", "server": "메일, 캘린더, 연락처, 파일은 서버에서 불러오고 서버에 다시 저장해요. 아래 연결을 제외하면 앱은 내 서버와만 통신해요.", diff --git a/locales/rn/lv.json b/locales/rn/lv.json index c0fa0f23..7b4d1f2b 100644 --- a/locales/rn/lv.json +++ b/locales/rn/lv.json @@ -124,7 +124,7 @@ "time_zone": "Laika josla", "time_zone_auto": "Ierīces laika josla", "time_zone_auto_zone": "Ierīces laika josla ({zone})", - "time_zone_desc": "Josla, ko izmanto jauniem notikumiem un kalendāra attēlošanai. \"Ierīces laika josla\" seko tālrunim." + "time_zone_desc": "Josla, ko izmanto jauniem notikumiem un kalendāra attēlošanai. \"Ierīces laika josla\" seko šai ierīcei." }, "share": { "add_people": "Pievienot personas", @@ -674,7 +674,7 @@ "err_pair_connect_detail": "Parādiet jaunu kodu datorā un skenējiet vēlreiz.", "err_pair_connect_title": "Pierakstījāties, bet neizdevās izveidot savienojumu ar {host}", "err_pair_insecure_detail": "{host} vēlas izmantot nešifrētu savienojumu.", - "err_pair_network_detail": "Pārbaudiet savienojumu un to, vai tālrunis var piekļūt tīmekļa pastam, un mēģiniet vēlreiz.", + "err_pair_network_detail": "Pārbaudiet savienojumu un to, vai šī ierīce var piekļūt tīmekļa pastam, un mēģiniet vēlreiz.", "err_pair_rate_detail": "Uzgaidiet minūti un mēģiniet vēlreiz.", "err_pair_server_detail": "Mēģiniet vēlreiz ar jaunu kodu. Ja tas joprojām neizdodas, sazinieties ar administratoru.", "err_pair_server_title": "Tīmekļa pasts nevarēja pabeigt pierakstīšanos", @@ -762,7 +762,7 @@ "server_cert_active": "Pierakstīšanās ar klienta sertifikātu “{alias}”. Pieskarieties, lai izvēlētos citu.", "server_cert_clear": "Pierakstīties bez sertifikāta", "server_cert_failed": "Neizdevās izvēlēties sertifikātu", - "server_cert_hint": "Vai jūsu serveris pieprasa klienta sertifikātu? Izvēlieties kādu, kas instalēts šajā tālrunī." + "server_cert_hint": "Vai jūsu serveris pieprasa klienta sertifikātu? Izvēlieties kādu, kas instalēts šajā ierīcē." } }, "offline": { @@ -1237,7 +1237,7 @@ "window_24h": "Pēdējās 24 stundas", "window_days": "Pēdējās {count} dienas", "window_desc": "Cik tālu pagātnē saglabāt, pēc vēstules saņemšanas datuma.", - "description_short": "Glabājiet jaunākās vēstules šajā tālrunī, lai tās atvērtos arī bez savienojuma.", + "description_short": "Glabājiet jaunākās vēstules šajā ierīcē, lai tās atvērtos arī bez savienojuma.", "keep_24h": "24 stundas", "keep_days": "{count} dienas", "keep_from": "Glabāt pastu par pēdējām", @@ -1437,7 +1437,7 @@ "licences_desc": "AGPL-3.0 lietotnei un tās izmantotajām bibliotēkām", "never_checked": "Vēl nav pārbaudīts", "privacy": "Privātums", - "privacy_desc": "Kas paliek šajā tālrunī un kas tiek sūtīts uz jūsu serveri", + "privacy_desc": "Kas paliek šajā ierīcē un kas tiek sūtīts uz jūsu serveri", "report_problem": "Ziņot par problēmu", "report_problem_desc": "Atver problēmas pieteikumu GitHub ar jau aizpildītu versiju", "server": "Serveris", @@ -1476,17 +1476,17 @@ } }, "reduce_motion": { - "description_mobile": "Izslēdz slīdošos paneļus un pārejas. Lietotne to dara arī tad, ja tālruņa iestatījumos ir ieslēgta samazināta kustība." + "description_mobile": "Izslēdz slīdošos paneļus un pārejas. Lietotne to dara arī tad, ja ierīces iestatījumos ir ieslēgta samazināta kustība." }, "text_size": { - "description_mobile": "Mērogo lietotnes tekstu papildus tālruņa teksta lielumam. 100% ir tālruņa teksta lielums." + "description_mobile": "Mērogo lietotnes tekstu papildus ierīces teksta lielumam. 100% ir ierīces teksta lielums." }, "toast_duration": { "automatic": "Automātiski (ilgāk ar ekrāna lasītāju)" } }, "backup": { - "description": "Pārnesiet iestatījumus uz citu tālruni vai tīmekļa pastu.", + "description": "Pārnesiet iestatījumus uz citu ierīci vai tīmekļa pastu.", "export_import": "Eksportēt vai importēt", "export_import_desc": "JSON fails tādā pašā formātā kā tīmekļa pastā", "reset": "Atiestatīt", @@ -1505,7 +1505,7 @@ "other_title": "Citi savienojumi", "phone_backup": "Nekas no tā netiek iekļauts Android mākoņa dublējumā.", "phone_secure": "Jūsu pierakstīšanās dati tiek glabāti sistēmas drošajā krātuvē. Iestatījumi, bezsaistes pasta kešatmiņa un izmaiņas, kas gaida sinhronizāciju, tiek glabāti pašas lietotnes krātuvē.", - "phone_title": "Šajā tālrunī", + "phone_title": "Šajā ierīcē", "push": "Kad paziņojumi ir ieslēgti, jūsu serveris paziņo push relejam ({relay}), ka kaut kas ir mainījies, un relejs pamodina lietotni ar šī pakalpojuma starpniecību: {service}. Relejs redz ierīces marķieri un izmaiņu atzīmi, bet nekad jūsu pastu. Sadaļā Paziņojumi varat izmantot savu releju.", "push_title": "Push paziņojumi", "server": "Pasts, kalendāri, kontakti un faili tiek ielādēti no jūsu servera un saglabāti atpakaļ tajā. Izņemot tālāk minētos savienojumus, lietotne sazinās tikai ar jūsu serveri.", diff --git a/locales/rn/mn.json b/locales/rn/mn.json index 3f727568..94d80221 100644 --- a/locales/rn/mn.json +++ b/locales/rn/mn.json @@ -124,7 +124,7 @@ "time_zone": "Цагийн бүс", "time_zone_auto": "Төхөөрөмжийн цагийн бүс", "time_zone_auto_zone": "Төхөөрөмжийн цагийн бүс ({zone})", - "time_zone_desc": "Шинэ үйл явдал болон хуанлийг харуулахад ашиглах бүс. \"Төхөөрөмжийн цагийн бүс\" нь утасны тохиргоог дагана." + "time_zone_desc": "Шинэ үйл явдал болон хуанлийг харуулахад ашиглах бүс. \"Төхөөрөмжийн цагийн бүс\" нь энэ төхөөрөмжийн тохиргоог дагана." }, "share": { "add_people": "Хүн нэмэх", @@ -670,7 +670,7 @@ "err_pair_connect_detail": "Компьютер дээрээ шинэ код харуулаад дахин скан хийнэ үү.", "err_pair_connect_title": "Нэвтэрсэн ч {host} руу холбогдож чадсангүй", "err_pair_insecure_detail": "{host} шифрлэгдээгүй холболт ашиглахыг хүсэж байна.", - "err_pair_network_detail": "Холболтоо болон утас тань вэбмэйлд хүрч чадаж байгаа эсэхийг шалгаад дахин оролдоно уу.", + "err_pair_network_detail": "Холболтоо болон энэ төхөөрөмж вэбмэйлд хүрч чадаж байгаа эсэхийг шалгаад дахин оролдоно уу.", "err_pair_rate_detail": "Нэг минут хүлээгээд дахин оролдоно уу.", "err_pair_server_detail": "Шинэ кодоор дахин оролдоно уу. Алдаа үргэлжилбэл админтайгаа холбогдоно уу.", "err_pair_server_title": "Вэбмэйл нэвтрэлтийг дуусгаж чадсангүй", @@ -758,7 +758,7 @@ "server_cert_active": "\"{alias}\" клиентийн гэрчилгээгээр нэвтэрч байна. Өөрийг сонгохын тулд товшино уу.", "server_cert_clear": "Гэрчилгээгүйгээр нэвтрэх", "server_cert_failed": "Гэрчилгээ сонгож чадсангүй", - "server_cert_hint": "Таны сервер клиентийн гэрчилгээ шаарддаг уу? Энэ утсанд суулгасан гэрчилгээг сонгоно уу." + "server_cert_hint": "Таны сервер клиентийн гэрчилгээ шаарддаг уу? Энэ төхөөрөмжид суулгасан гэрчилгээг сонгоно уу." } }, "offline": { @@ -1233,7 +1233,7 @@ "window_24h": "Сүүлийн 24 цаг", "window_days": "Сүүлийн {count} өдөр", "window_desc": "Имэйл хүлээн авсан огноогоор хэр хугацааны өмнөхийг кэшлэх.", - "description_short": "Сүүлийн мессежүүдийг энэ утсанд хадгалж, холболтгүй үед ч нээгддэг болгоно.", + "description_short": "Сүүлийн мессежүүдийг энэ төхөөрөмжид хадгалж, холболтгүй үед ч нээгддэг болгоно.", "keep_24h": "24 цаг", "keep_days": "{count} өдөр", "keep_from": "Имэйл хадгалах хугацаа (сүүлийн)", @@ -1433,7 +1433,7 @@ "licences_desc": "Аппад AGPL-3.0, мөн түүний ашигладаг сангууд", "never_checked": "Одоогоор шалгаагүй", "privacy": "Нууцлал", - "privacy_desc": "Юу энэ утсанд үлдэж, юу таны сервер рүү очдог вэ", + "privacy_desc": "Юу энэ төхөөрөмжид үлдэж, юу таны сервер рүү очдог вэ", "report_problem": "Асуудал мэдээлэх", "report_problem_desc": "GitHub дээр хувилбарын мэдээлэл бөглөгдсөн асуудал нээнэ", "server": "Сервер", @@ -1472,17 +1472,17 @@ } }, "reduce_motion": { - "description_mobile": "Гулсах самбар болон шилжилтийг унтраана. Таны утасны тохиргоонд хөдөлгөөнийг багасгах асаалттай үед апп үүнийг мөн хийнэ." + "description_mobile": "Гулсах самбар болон шилжилтийг унтраана. Таны төхөөрөмжийн тохиргоонд хөдөлгөөнийг багасгах асаалттай үед апп үүнийг мөн хийнэ." }, "text_size": { - "description_mobile": "Аппын текстийг таны утасны текстийн хэмжээн дээр нэмж томруулна. 100% нь таны утасны хэмжээ." + "description_mobile": "Аппын текстийг таны төхөөрөмжийн текстийн хэмжээн дээр нэмж томруулна. 100% нь таны төхөөрөмжийн хэмжээ." }, "toast_duration": { "automatic": "Автомат (дэлгэц уншигчтай үед удаан)" } }, "backup": { - "description": "Тохиргоогоо өөр утас эсвэл вэбмэйл рүү шилжүүлэх.", + "description": "Тохиргоогоо өөр төхөөрөмж эсвэл вэбмэйл рүү шилжүүлэх.", "export_import": "Экспортлох эсвэл импортлох", "export_import_desc": "Вэбмэйлтэй ижил форматтай JSON файл", "reset": "Дахин тохируулах", @@ -1501,7 +1501,7 @@ "other_title": "Бусад холболтууд", "phone_backup": "Эдгээрийн аль нь ч Android-ийн үүлэн нөөцлөлтөд ордоггүй.", "phone_secure": "Таны нэвтрэх мэдээлэл системийн аюулгүй санд хадгалагдана. Тохиргоо, офлайн имэйлийн кэш болон синк хийгдэхийг хүлээж буй өөрчлөлтүүд аппын өөрийн санд хадгалагдана.", - "phone_title": "Энэ утсанд", + "phone_title": "Энэ төхөөрөмжид", "push": "Мэдэгдэл асаалттай үед таны сервер түлхэх релед ({relay}) ямар нэг зүйл өөрчлөгдсөнийг мэдэгдэж, реле {service} ашиглан аппыг сэрээнэ. Реле төхөөрөмжийн токен болон өөрчлөлтийн тэмдэглэгээг хардаг ч таны имэйлийг хэзээ ч хардаггүй. Мэдэгдэл хэсэгт өөрийн релег ашиглаж болно.", "push_title": "Түлхэх мэдэгдэл", "server": "Имэйл, хуанли, харилцагчид болон файлуудыг таны серверээс ачаалж, буцаан түүнд хадгална. Доорх холболтуудаас гадна апп зөвхөн таны сервертэй холбогддог.", diff --git a/locales/rn/nb.json b/locales/rn/nb.json index 1856911f..d7cc52d6 100644 --- a/locales/rn/nb.json +++ b/locales/rn/nb.json @@ -124,7 +124,7 @@ "time_zone": "Tidssone", "time_zone_auto": "Enhetens tidssone", "time_zone_auto_zone": "Enhetens tidssone ({zone})", - "time_zone_desc": "Tidssonen som brukes for nye hendelser og for visning av kalenderen. «Enhetens tidssone» følger telefonen." + "time_zone_desc": "Tidssonen som brukes for nye hendelser og for visning av kalenderen. «Enhetens tidssone» følger denne enheten." }, "share": { "add_people": "Legg til personer", @@ -672,7 +672,7 @@ "err_pair_connect_detail": "Vis en ny kode på datamaskinen, og skann på nytt.", "err_pair_connect_title": "Pålogget, men kunne ikke koble til {host}", "err_pair_insecure_detail": "{host} vil bruke en ukryptert tilkobling.", - "err_pair_network_detail": "Sjekk tilkoblingen og at telefonen kan nå nettposten, og prøv deretter på nytt.", + "err_pair_network_detail": "Sjekk tilkoblingen og at denne enheten kan nå nettposten, og prøv deretter på nytt.", "err_pair_rate_detail": "Vent et minutt, og prøv deretter på nytt.", "err_pair_server_detail": "Prøv på nytt med en ny kode. Hvis det fortsatt mislykkes, kontakt administratoren.", "err_pair_server_title": "Nettposten kunne ikke fullføre påloggingen", @@ -760,7 +760,7 @@ "server_cert_active": "Logger på med klientsertifikatet «{alias}». Trykk for å velge et annet.", "server_cert_clear": "Logg på uten sertifikatet", "server_cert_failed": "Kunne ikke velge et sertifikat", - "server_cert_hint": "Ber serveren din om et klientsertifikat? Velg et som er installert på denne telefonen." + "server_cert_hint": "Ber serveren din om et klientsertifikat? Velg et som er installert på denne enheten." } }, "offline": { @@ -1235,7 +1235,7 @@ "window_24h": "Siste 24 timer", "window_days": "Siste {count} dager", "window_desc": "Hvor langt tilbake som bufres, basert på datoen meldingen ble mottatt.", - "description_short": "Behold nylige meldinger på denne telefonen så de kan åpnes uten tilkobling.", + "description_short": "Behold nylige meldinger på denne enheten så de kan åpnes uten tilkobling.", "keep_24h": "24 timer", "keep_days": "{count} dager", "keep_from": "Behold e-post fra de siste", @@ -1435,7 +1435,7 @@ "licences_desc": "AGPL-3.0 for appen, og bibliotekene den bruker", "never_checked": "Ikke sjekket ennå", "privacy": "Personvern", - "privacy_desc": "Hva som blir på denne telefonen, og hva som går til serveren din", + "privacy_desc": "Hva som blir på denne enheten, og hva som går til serveren din", "report_problem": "Rapporter et problem", "report_problem_desc": "Åpner en sak på GitHub med versjonen fylt ut", "server": "Server", @@ -1474,17 +1474,17 @@ } }, "reduce_motion": { - "description_mobile": "Slår av glidende paneler og overganger. Appen gjør også dette når redusert bevegelse er slått på i telefonens innstillinger." + "description_mobile": "Slår av glidende paneler og overganger. Appen gjør også dette når redusert bevegelse er slått på i enhetens innstillinger." }, "text_size": { - "description_mobile": "Skalerer teksten i appen i tillegg til telefonens tekststørrelse. 100 % er telefonens størrelse." + "description_mobile": "Skalerer teksten i appen i tillegg til enhetens tekststørrelse. 100 % er enhetens størrelse." }, "toast_duration": { "automatic": "Automatisk (lenger med skjermleser)" } }, "backup": { - "description": "Flytt innstillingene dine til en annen telefon eller til nettposten.", + "description": "Flytt innstillingene dine til en annen enhet eller til nettposten.", "export_import": "Eksporter eller importer", "export_import_desc": "En JSON-fil i samme format som nettposten", "reset": "Tilbakestill", @@ -1503,7 +1503,7 @@ "other_title": "Andre tilkoblinger", "phone_backup": "Ingenting av dette havner i Androids skysikkerhetskopi.", "phone_secure": "Påloggingsinformasjonen din oppbevares i systemets sikre lagring. Innstillinger, hurtigbufferen for e-post uten nett og endringer som venter på synkronisering, oppbevares i appens egen lagring.", - "phone_title": "På denne telefonen", + "phone_title": "På denne enheten", "push": "Når varsler er på, forteller serveren din push-reléet ({relay}) at noe er endret, og reléet vekker appen gjennom {service}. Reléet ser et enhetstoken og en endringsmarkør, aldri e-posten din. Du kan bruke ditt eget relé under Varsler.", "push_title": "Pushvarsler", "server": "E-post, kalendere, kontakter og filer lastes inn fra serveren din og lagres tilbake dit. Bortsett fra tilkoblingene nedenfor snakker appen bare med serveren din.", diff --git a/locales/rn/nl.json b/locales/rn/nl.json index d62dae0b..19def41d 100644 --- a/locales/rn/nl.json +++ b/locales/rn/nl.json @@ -124,7 +124,7 @@ "time_zone": "Tijdzone", "time_zone_auto": "Tijdzone van apparaat", "time_zone_auto_zone": "Tijdzone van apparaat ({zone})", - "time_zone_desc": "Tijdzone voor nieuwe afspraken en voor het weergeven van de agenda. \"Tijdzone van apparaat\" volgt de telefoon." + "time_zone_desc": "Tijdzone voor nieuwe afspraken en voor het weergeven van de agenda. \"Tijdzone van apparaat\" volgt dit apparaat." }, "share": { "add_people": "Personen toevoegen", @@ -675,7 +675,7 @@ "err_pair_connect_detail": "Toon een nieuwe code op uw computer en scan opnieuw.", "err_pair_connect_title": "Ingelogd, maar kan geen verbinding maken met {host}", "err_pair_insecure_detail": "{host} wil een onversleutelde verbinding gebruiken.", - "err_pair_network_detail": "Controleer uw verbinding en of uw telefoon de webmail kan bereiken, en probeer het opnieuw.", + "err_pair_network_detail": "Controleer uw verbinding en of dit apparaat de webmail kan bereiken, en probeer het opnieuw.", "err_pair_rate_detail": "Wacht een minuut en probeer het dan opnieuw.", "err_pair_server_detail": "Probeer het opnieuw met een nieuwe code. Blijft het mislukken, neem dan contact op met uw beheerder.", "err_pair_server_title": "De webmail kon het inloggen niet voltooien", @@ -763,7 +763,7 @@ "server_cert_active": "Inloggen met het clientcertificaat \"{alias}\". Tik om een ander te kiezen.", "server_cert_clear": "Inloggen zonder het certificaat", "server_cert_failed": "Kan geen certificaat kiezen", - "server_cert_hint": "Vraagt je server om een clientcertificaat? Kies er een dat op deze telefoon is geïnstalleerd." + "server_cert_hint": "Vraagt je server om een clientcertificaat? Kies er een dat op dit apparaat is geïnstalleerd." } }, "offline": { @@ -1238,7 +1238,7 @@ "window_24h": "Afgelopen 24 uur", "window_days": "Afgelopen {count} dagen", "window_desc": "Hoe ver terug er in de cache wordt bewaard, gemeten naar de ontvangstdatum van het bericht.", - "description_short": "Recente berichten op deze telefoon bewaren, zodat ze ook zonder verbinding openen.", + "description_short": "Recente berichten op dit apparaat bewaren, zodat ze ook zonder verbinding openen.", "keep_24h": "24 uur", "keep_days": "{count} dagen", "keep_from": "E-mail bewaren van de afgelopen", @@ -1438,7 +1438,7 @@ "licences_desc": "AGPL-3.0 voor de app en de bibliotheken die ze gebruikt", "never_checked": "Nog niet gecontroleerd", "privacy": "Privacy", - "privacy_desc": "Wat op deze telefoon blijft en wat naar je server gaat", + "privacy_desc": "Wat op dit apparaat blijft en wat naar je server gaat", "report_problem": "Een probleem melden", "report_problem_desc": "Opent een issue op GitHub met de versie al ingevuld", "server": "Server", @@ -1477,17 +1477,17 @@ } }, "reduce_motion": { - "description_mobile": "Schakelt schuivende panelen en overgangen uit. De app doet dit ook wanneer minder beweging aanstaat in de instellingen van je telefoon." + "description_mobile": "Schakelt schuivende panelen en overgangen uit. De app doet dit ook wanneer minder beweging aanstaat in de instellingen van je apparaat." }, "text_size": { - "description_mobile": "Schaalt de tekst van de app bovenop de tekstgrootte van je telefoon. 100% is de grootte van je telefoon." + "description_mobile": "Schaalt de tekst van de app bovenop de tekstgrootte van je apparaat. 100% is de grootte van je apparaat." }, "toast_duration": { "automatic": "Automatisch (langer met een schermlezer)" } }, "backup": { - "description": "Zet je instellingen over naar een andere telefoon of naar de webmail.", + "description": "Zet je instellingen over naar een ander apparaat of naar de webmail.", "export_import": "Exporteren of importeren", "export_import_desc": "Een JSON-bestand, in hetzelfde formaat als de webmail", "reset": "Herstellen", @@ -1506,7 +1506,7 @@ "other_title": "Andere verbindingen", "phone_backup": "Niets daarvan gaat naar de cloudback-up van Android.", "phone_secure": "Je inloggegevens worden bewaard in de beveiligde opslag van het systeem. Instellingen, de offline e-mailcache en wijzigingen die nog moeten synchroniseren worden in de eigen opslag van de app bewaard.", - "phone_title": "Op deze telefoon", + "phone_title": "Op dit apparaat", "push": "Als meldingen aanstaan, laat je server de push-relay ({relay}) weten dat er iets is veranderd, en de relay wekt de app via {service}. De relay ziet een apparaattoken en een wijzigingsmarkering, nooit je e-mail. Onder Meldingen kun je je eigen relay gebruiken.", "push_title": "Pushmeldingen", "server": "E-mail, agenda's, contacten en bestanden worden van je server geladen en daar weer opgeslagen. Afgezien van de verbindingen hieronder praat de app alleen met je server.", diff --git a/locales/rn/pl.json b/locales/rn/pl.json index c6b65d26..afd5f9d4 100644 --- a/locales/rn/pl.json +++ b/locales/rn/pl.json @@ -124,7 +124,7 @@ "time_zone": "Strefa czasowa", "time_zone_auto": "Strefa czasowa urządzenia", "time_zone_auto_zone": "Strefa czasowa urządzenia ({zone})", - "time_zone_desc": "Strefa używana dla nowych wydarzeń i do odczytu kalendarza. „Strefa czasowa urządzenia” podąża za telefonem." + "time_zone_desc": "Strefa używana dla nowych wydarzeń i do odczytu kalendarza. „Strefa czasowa urządzenia” podąża za tym urządzeniem." }, "share": { "add_people": "Dodaj osoby", @@ -675,7 +675,7 @@ "err_pair_connect_detail": "Wyświetl nowy kod na komputerze i zeskanuj go ponownie.", "err_pair_connect_title": "Zalogowano, ale nie można połączyć się z {host}", "err_pair_insecure_detail": "{host} chce użyć nieszyfrowanego połączenia.", - "err_pair_network_detail": "Sprawdź połączenie i czy telefon ma dostęp do webmaila, a potem spróbuj ponownie.", + "err_pair_network_detail": "Sprawdź połączenie i czy to urządzenie ma dostęp do webmaila, a potem spróbuj ponownie.", "err_pair_rate_detail": "Poczekaj minutę i spróbuj ponownie.", "err_pair_server_detail": "Spróbuj ponownie z nowym kodem. Jeśli nadal się nie udaje, skontaktuj się z administratorem.", "err_pair_server_title": "Webmail nie mógł dokończyć logowania", @@ -763,7 +763,7 @@ "server_cert_active": "Logowanie z certyfikatem klienta „{alias}”. Stuknij, aby wybrać inny.", "server_cert_clear": "Zaloguj się bez certyfikatu", "server_cert_failed": "Nie udało się wybrać certyfikatu", - "server_cert_hint": "Czy Twój serwer wymaga certyfikatu klienta? Wybierz jeden z zainstalowanych na tym telefonie." + "server_cert_hint": "Czy Twój serwer wymaga certyfikatu klienta? Wybierz jeden z zainstalowanych na tym urządzeniu." } }, "offline": { @@ -1238,7 +1238,7 @@ "window_24h": "Ostatnie 24 godziny", "window_days": "Ostatnie {count} dni", "window_desc": "Jak daleko wstecz zapisywać pocztę, według daty otrzymania wiadomości.", - "description_short": "Przechowuj najnowsze wiadomości na tym telefonie, aby otwierały się bez połączenia.", + "description_short": "Przechowuj najnowsze wiadomości na tym urządzeniu, aby otwierały się bez połączenia.", "keep_24h": "24 godziny", "keep_days": "{count} dni", "keep_from": "Zachowuj pocztę z ostatnich", @@ -1438,7 +1438,7 @@ "licences_desc": "AGPL-3.0 dla aplikacji i używanych przez nią bibliotek", "never_checked": "Jeszcze nie sprawdzono", "privacy": "Prywatność", - "privacy_desc": "Co zostaje na tym telefonie, a co trafia na Twój serwer", + "privacy_desc": "Co zostaje na tym urządzeniu, a co trafia na Twój serwer", "report_problem": "Zgłoś problem", "report_problem_desc": "Otwiera zgłoszenie na GitHubie z wypełnioną wersją", "server": "Serwer", @@ -1477,17 +1477,17 @@ } }, "reduce_motion": { - "description_mobile": "Wyłącza wysuwane panele i przejścia. Aplikacja robi to też, gdy w ustawieniach telefonu włączone jest ograniczenie ruchu." + "description_mobile": "Wyłącza wysuwane panele i przejścia. Aplikacja robi to też, gdy w ustawieniach urządzenia włączone jest ograniczenie ruchu." }, "text_size": { - "description_mobile": "Skaluje tekst aplikacji względem rozmiaru tekstu w telefonie. 100% to rozmiar ustawiony w telefonie." + "description_mobile": "Skaluje tekst aplikacji względem rozmiaru tekstu w urządzeniu. 100% to rozmiar ustawiony w urządzeniu." }, "toast_duration": { "automatic": "Automatycznie (dłużej z czytnikiem ekranu)" } }, "backup": { - "description": "Przenieś ustawienia na inny telefon lub do webmaila.", + "description": "Przenieś ustawienia na inne urządzenie lub do webmaila.", "export_import": "Eksport lub import", "export_import_desc": "Plik JSON w tym samym formacie co w webmailu", "reset": "Resetuj", @@ -1506,7 +1506,7 @@ "other_title": "Inne połączenia", "phone_backup": "Nic z tego nie trafia do kopii zapasowej Androida w chmurze.", "phone_secure": "Twoje dane logowania są przechowywane w bezpiecznym magazynie systemu. Ustawienia, pamięć podręczna poczty offline i zmiany czekające na synchronizację są przechowywane we własnej pamięci aplikacji.", - "phone_title": "Na tym telefonie", + "phone_title": "Na tym urządzeniu", "push": "Gdy powiadomienia są włączone, Twój serwer informuje przekaźnik push ({relay}), że coś się zmieniło, a przekaźnik budzi aplikację przez {service}. Przekaźnik widzi token urządzenia i znacznik zmiany, nigdy Twoją pocztę. W ustawieniach Powiadomienia możesz użyć własnego przekaźnika.", "push_title": "Powiadomienia push", "server": "Poczta, kalendarze, kontakty i pliki są wczytywane z Twojego serwera i na nim zapisywane. Poza połączeniami wymienionymi poniżej aplikacja komunikuje się tylko z Twoim serwerem.", diff --git a/locales/rn/pt.json b/locales/rn/pt.json index 402dfb24..b490f2c2 100644 --- a/locales/rn/pt.json +++ b/locales/rn/pt.json @@ -124,7 +124,7 @@ "time_zone": "Fuso horário", "time_zone_auto": "Fuso horário do dispositivo", "time_zone_auto_zone": "Fuso horário do dispositivo ({zone})", - "time_zone_desc": "Fuso usado para novos eventos e para interpretar o calendário. \"Dispositivo\" segue o do celular." + "time_zone_desc": "Fuso usado para novos eventos e para interpretar o calendário. \"Dispositivo\" segue o deste dispositivo." }, "share": { "add_people": "Adicionar pessoas", @@ -678,7 +678,7 @@ "err_pair_connect_detail": "Mostre um novo código no computador e escaneie de novo.", "err_pair_connect_title": "Login feito, mas não foi possível conectar a {host}", "err_pair_insecure_detail": "{host} quer usar uma conexão sem criptografia.", - "err_pair_network_detail": "Verifique sua conexão e se o celular consegue acessar o webmail, depois tente de novo.", + "err_pair_network_detail": "Verifique sua conexão e se este dispositivo consegue acessar o webmail, depois tente de novo.", "err_pair_rate_detail": "Aguarde um minuto e tente de novo.", "err_pair_server_detail": "Tente de novo com um novo código. Se continuar falhando, fale com o administrador.", "err_pair_server_title": "O webmail não conseguiu concluir o login", @@ -766,7 +766,7 @@ "server_cert_active": "Entrando com o certificado de cliente “{alias}”. Toque para escolher outro.", "server_cert_clear": "Entrar sem o certificado", "server_cert_failed": "Não foi possível escolher um certificado", - "server_cert_hint": "Seu servidor pede um certificado de cliente? Escolha um instalado neste celular." + "server_cert_hint": "Seu servidor pede um certificado de cliente? Escolha um instalado neste dispositivo." } }, "offline": { @@ -1241,7 +1241,7 @@ "window_24h": "Últimas 24 horas", "window_days": "Últimos {count} dias", "window_desc": "Até quando armazenar em cache, com base na data de recebimento da mensagem.", - "description_short": "Guardar as mensagens recentes neste celular para que abram sem conexão.", + "description_short": "Guardar as mensagens recentes neste dispositivo para que abram sem conexão.", "keep_24h": "24 horas", "keep_days": "{count} dias", "keep_from": "Guardar e-mails dos últimos", @@ -1441,7 +1441,7 @@ "licences_desc": "AGPL-3.0 para o aplicativo e as bibliotecas que ele usa", "never_checked": "Ainda não verificado", "privacy": "Privacidade", - "privacy_desc": "O que fica neste celular e o que vai para o seu servidor", + "privacy_desc": "O que fica neste dispositivo e o que vai para o seu servidor", "report_problem": "Relatar um problema", "report_problem_desc": "Abre uma issue no GitHub com a versão já preenchida", "server": "Servidor", @@ -1480,17 +1480,17 @@ } }, "reduce_motion": { - "description_mobile": "Desativa painéis deslizantes e transições. O aplicativo também faz isso quando o movimento reduzido está ativado nas configurações do celular." + "description_mobile": "Desativa painéis deslizantes e transições. O aplicativo também faz isso quando o movimento reduzido está ativado nas configurações do dispositivo." }, "text_size": { - "description_mobile": "Ajusta o texto do aplicativo além do tamanho de texto do celular. 100% é o tamanho do celular." + "description_mobile": "Ajusta o texto do aplicativo além do tamanho de texto do dispositivo. 100% é o tamanho do dispositivo." }, "toast_duration": { "automatic": "Automático (mais longo com leitor de tela)" } }, "backup": { - "description": "Leve suas configurações para outro celular ou para o webmail.", + "description": "Leve suas configurações para outro dispositivo ou para o webmail.", "export_import": "Exportar ou importar", "export_import_desc": "Um arquivo JSON, no mesmo formato do webmail", "reset": "Redefinir", @@ -1509,7 +1509,7 @@ "other_title": "Outras conexões", "phone_backup": "Nada disso vai para o backup na nuvem do Android.", "phone_secure": "Seus dados de login ficam no armazenamento seguro do sistema. As configurações, o cache de e-mails offline e as alterações aguardando sincronização ficam no armazenamento do próprio aplicativo.", - "phone_title": "Neste celular", + "phone_title": "Neste dispositivo", "push": "Quando as notificações estão ativadas, seu servidor avisa o relay push ({relay}) de que algo mudou, e o relay desperta o aplicativo por meio do {service}. O relay vê um token do dispositivo e um marcador de alteração, nunca seus e-mails. Você pode usar seu próprio relay em Notificações.", "push_title": "Notificações push", "server": "E-mails, calendários, contatos e arquivos são carregados do seu servidor e salvos de volta nele. Além das conexões abaixo, o aplicativo só se comunica com o seu servidor.", diff --git a/locales/rn/ro.json b/locales/rn/ro.json index fb2f9269..80fae9b5 100644 --- a/locales/rn/ro.json +++ b/locales/rn/ro.json @@ -124,7 +124,7 @@ "time_zone": "Fus orar", "time_zone_auto": "Fusul orar al dispozitivului", "time_zone_auto_zone": "Fusul orar al dispozitivului ({zone})", - "time_zone_desc": "Fusul folosit pentru evenimentele noi și pentru interpretarea calendarului. „Dispozitiv” urmează fusul telefonului." + "time_zone_desc": "Fusul folosit pentru evenimentele noi și pentru interpretarea calendarului. „Dispozitiv” urmează fusul acestui dispozitiv." }, "share": { "add_people": "Adăugați persoane", @@ -675,7 +675,7 @@ "err_pair_connect_detail": "Afișați un cod nou pe computer și scanați-l din nou.", "err_pair_connect_title": "Autentificat, dar nu se poate conecta la {host}", "err_pair_insecure_detail": "{host} vrea să folosească o conexiune necriptată.", - "err_pair_network_detail": "Verificați conexiunea și dacă telefonul poate accesa webmailul, apoi încercați din nou.", + "err_pair_network_detail": "Verificați conexiunea și dacă acest dispozitiv poate accesa webmailul, apoi încercați din nou.", "err_pair_rate_detail": "Așteptați un minut, apoi încercați din nou.", "err_pair_server_detail": "Încercați din nou cu un cod nou. Dacă tot nu funcționează, contactați administratorul.", "err_pair_server_title": "Webmailul nu a putut finaliza autentificarea", @@ -763,7 +763,7 @@ "server_cert_active": "Autentificare cu certificatul de client „{alias}”. Atingeți pentru a alege altul.", "server_cert_clear": "Autentificare fără certificat", "server_cert_failed": "Nu s-a putut alege un certificat", - "server_cert_hint": "Serverul dvs. cere un certificat de client? Alegeți unul instalat pe acest telefon." + "server_cert_hint": "Serverul dvs. cere un certificat de client? Alegeți unul instalat pe acest dispozitiv." } }, "offline": { @@ -1238,7 +1238,7 @@ "window_24h": "Ultimele 24 de ore", "window_days": "{count, plural, one {Ultima zi} few {Ultimele # zile} other {Ultimele # de zile}}", "window_desc": "Cât de departe în trecut să se păstreze în cache, după data primirii mesajului.", - "description_short": "Păstrați mesajele recente pe acest telefon, ca să se deschidă fără conexiune.", + "description_short": "Păstrați mesajele recente pe acest dispozitiv, ca să se deschidă fără conexiune.", "keep_24h": "24 de ore", "keep_days": "{count} zile", "keep_from": "Păstrați e-mailurile din ultimele", @@ -1438,7 +1438,7 @@ "licences_desc": "AGPL-3.0 pentru aplicație și bibliotecile pe care le folosește", "never_checked": "Neverificat încă", "privacy": "Confidențialitate", - "privacy_desc": "Ce rămâne pe acest telefon și ce ajunge la serverul dvs.", + "privacy_desc": "Ce rămâne pe acest dispozitiv și ce ajunge la serverul dvs.", "report_problem": "Raportați o problemă", "report_problem_desc": "Deschide o problemă pe GitHub, cu versiunea deja completată", "server": "Server", @@ -1477,17 +1477,17 @@ } }, "reduce_motion": { - "description_mobile": "Dezactivează panourile glisante și tranzițiile. Aplicația face acest lucru și când mișcarea redusă este activată în setările telefonului." + "description_mobile": "Dezactivează panourile glisante și tranzițiile. Aplicația face acest lucru și când mișcarea redusă este activată în setările dispozitivului." }, "text_size": { - "description_mobile": "Scalează textul aplicației peste dimensiunea textului din telefon. 100% este dimensiunea din telefon." + "description_mobile": "Scalează textul aplicației peste dimensiunea textului din dispozitiv. 100% este dimensiunea din dispozitiv." }, "toast_duration": { "automatic": "Automat (mai lung cu un cititor de ecran)" } }, "backup": { - "description": "Mutați setările pe alt telefon sau în webmail.", + "description": "Mutați setările pe alt dispozitiv sau în webmail.", "export_import": "Export sau import", "export_import_desc": "Un fișier JSON, în același format ca în webmail", "reset": "Resetare", @@ -1506,7 +1506,7 @@ "other_title": "Alte conexiuni", "phone_backup": "Nimic din toate acestea nu ajunge în copia de rezervă în cloud a Android.", "phone_secure": "Datele dvs. de autentificare sunt păstrate în spațiul de stocare securizat al sistemului. Setările, cache-ul e-mailurilor offline și modificările care așteaptă sincronizarea sunt păstrate în spațiul de stocare propriu al aplicației.", - "phone_title": "Pe acest telefon", + "phone_title": "Pe acest dispozitiv", "push": "Când notificările sunt activate, serverul dvs. îi spune releului push ({relay}) că s-a schimbat ceva, iar releul trezește aplicația prin {service}. Releul vede un token al dispozitivului și un marcaj de modificare, niciodată e-mailurile dvs. Puteți folosi propriul releu din Notificări.", "push_title": "Notificări push", "server": "E-mailurile, calendarele, contactele și fișierele sunt încărcate de pe serverul dvs. și salvate înapoi pe el. În afara conexiunilor de mai jos, aplicația comunică doar cu serverul dvs.", diff --git a/locales/rn/ru.json b/locales/rn/ru.json index a02a8e18..dbc93570 100644 --- a/locales/rn/ru.json +++ b/locales/rn/ru.json @@ -124,7 +124,7 @@ "time_zone": "Часовой пояс", "time_zone_auto": "Часовой пояс устройства", "time_zone_auto_zone": "Часовой пояс устройства ({zone})", - "time_zone_desc": "Пояс для новых событий и отображения календаря. «Часовой пояс устройства» берётся из настроек телефона." + "time_zone_desc": "Пояс для новых событий и отображения календаря. «Часовой пояс устройства» берётся из настроек этого устройства." }, "share": { "add_people": "Добавить людей", @@ -678,7 +678,7 @@ "err_pair_connect_detail": "Покажите новый код на компьютере и отсканируйте его снова.", "err_pair_connect_title": "Вход выполнен, но не удалось подключиться к {host}", "err_pair_insecure_detail": "{host} хочет использовать незашифрованное соединение.", - "err_pair_network_detail": "Проверьте подключение и доступна ли веб-почта с телефона, затем повторите попытку.", + "err_pair_network_detail": "Проверьте подключение и доступна ли веб-почта с этого устройства, затем повторите попытку.", "err_pair_rate_detail": "Подождите минуту и повторите попытку.", "err_pair_server_detail": "Попробуйте ещё раз с новым кодом. Если ошибка повторяется, обратитесь к администратору.", "err_pair_server_title": "Веб-почте не удалось завершить вход", @@ -766,7 +766,7 @@ "server_cert_active": "Вход с клиентским сертификатом «{alias}». Нажмите, чтобы выбрать другой.", "server_cert_clear": "Войти без сертификата", "server_cert_failed": "Не удалось выбрать сертификат", - "server_cert_hint": "Ваш сервер запрашивает клиентский сертификат? Выберите один из установленных на этом телефоне." + "server_cert_hint": "Ваш сервер запрашивает клиентский сертификат? Выберите один из установленных на этом устройстве." } }, "offline": { @@ -1241,7 +1241,7 @@ "window_24h": "Последние 24 часа", "window_days": "{count, plural, one {Последний # день} few {Последние # дня} many {Последние # дней} other {Последние # дня}}", "window_desc": "За какой период кэшировать почту (по дате получения письма).", - "description_short": "Хранить недавние письма на этом телефоне, чтобы они открывались без подключения.", + "description_short": "Хранить недавние письма на этом устройстве, чтобы они открывались без подключения.", "keep_24h": "24 часа", "keep_days": "{count} дн.", "keep_from": "Хранить почту за последние", @@ -1441,7 +1441,7 @@ "licences_desc": "AGPL-3.0 для приложения и лицензии используемых библиотек", "never_checked": "Ещё не проверялось", "privacy": "Конфиденциальность", - "privacy_desc": "Что остаётся на этом телефоне и что уходит на ваш сервер", + "privacy_desc": "Что остаётся на этом устройстве и что уходит на ваш сервер", "report_problem": "Сообщить о проблеме", "report_problem_desc": "Открывает задачу на GitHub с уже указанной версией", "server": "Сервер", @@ -1480,17 +1480,17 @@ } }, "reduce_motion": { - "description_mobile": "Отключает выдвижные панели и переходы. Приложение делает это и тогда, когда в настройках телефона включено уменьшение анимации." + "description_mobile": "Отключает выдвижные панели и переходы. Приложение делает это и тогда, когда в настройках устройства включено уменьшение анимации." }, "text_size": { - "description_mobile": "Масштабирует текст приложения поверх размера текста телефона. 100 % — размер, заданный в телефоне." + "description_mobile": "Масштабирует текст приложения поверх размера текста устройства. 100 % — размер, заданный в устройстве." }, "toast_duration": { "automatic": "Автоматически (дольше с программой чтения с экрана)" } }, "backup": { - "description": "Перенесите настройки на другой телефон или в веб-почту.", + "description": "Перенесите настройки на другое устройство или в веб-почту.", "export_import": "Экспорт или импорт", "export_import_desc": "Файл JSON в том же формате, что и в веб-почте", "reset": "Сбросить", @@ -1509,7 +1509,7 @@ "other_title": "Другие подключения", "phone_backup": "Ничего из этого не попадает в облачную резервную копию Android.", "phone_secure": "Ваши данные для входа хранятся в защищённом хранилище системы. Настройки, офлайн-кэш почты и изменения, ожидающие синхронизации, хранятся в собственном хранилище приложения.", - "phone_title": "На этом телефоне", + "phone_title": "На этом устройстве", "push": "Когда уведомления включены, ваш сервер сообщает push-релею ({relay}), что что-то изменилось, и релей будит приложение через {service}. Релей видит токен устройства и отметку об изменении, но никогда не видит вашу почту. В разделе «Уведомления» можно указать собственный релей.", "push_title": "Push-уведомления", "server": "Почта, календари, контакты и файлы загружаются с вашего сервера и сохраняются обратно на него. Кроме перечисленных ниже подключений, приложение связывается только с вашим сервером.", diff --git a/locales/rn/sk.json b/locales/rn/sk.json index 16ce694f..a8fe02c4 100644 --- a/locales/rn/sk.json +++ b/locales/rn/sk.json @@ -124,7 +124,7 @@ "time_zone": "Časové pásmo", "time_zone_auto": "Časové pásmo zariadenia", "time_zone_auto_zone": "Časové pásmo zariadenia ({zone})", - "time_zone_desc": "Pásmo pre nové udalosti a na zobrazenie kalendára. „Časové pásmo zariadenia“ sa riadi telefónom." + "time_zone_desc": "Pásmo pre nové udalosti a na zobrazenie kalendára. „Časové pásmo zariadenia“ sa riadi týmto zariadením." }, "share": { "add_people": "Pridať osoby", @@ -674,7 +674,7 @@ "err_pair_connect_detail": "Zobrazte na počítači nový kód a naskenujte ho znova.", "err_pair_connect_title": "Prihlásené, ale nedá sa pripojiť k {host}", "err_pair_insecure_detail": "{host} chce použiť nešifrované pripojenie.", - "err_pair_network_detail": "Skontrolujte pripojenie a či sa telefón dostane k webmailu, a skúste to znova.", + "err_pair_network_detail": "Skontrolujte pripojenie a či sa toto zariadenie dostane k webmailu, a skúste to znova.", "err_pair_rate_detail": "Počkajte minútu a skúste to znova.", "err_pair_server_detail": "Skúste to znova s novým kódom. Ak to stále zlyháva, obráťte sa na správcu.", "err_pair_server_title": "Webmail nedokázal dokončiť prihlásenie", @@ -762,7 +762,7 @@ "server_cert_active": "Prihlasovanie pomocou klientskeho certifikátu „{alias}“. Ťuknutím vyberiete iný.", "server_cert_clear": "Prihlásiť sa bez certifikátu", "server_cert_failed": "Certifikát sa nepodarilo vybrať", - "server_cert_hint": "Vyžaduje váš server klientsky certifikát? Vyberte niektorý z nainštalovaných v tomto telefóne." + "server_cert_hint": "Vyžaduje váš server klientsky certifikát? Vyberte niektorý z nainštalovaných v tomto zariadení." } }, "offline": { @@ -1237,7 +1237,7 @@ "window_24h": "Posledných 24 hodín", "window_days": "{count, plural, one {Posledný # deň} few {Posledné # dni} many {Posledného # dňa} other {Posledných # dní}}", "window_desc": "Ako ďaleko do minulosti ukladať, podľa dátumu prijatia správy.", - "description_short": "Uchovávať nedávne správy v tomto telefóne, aby sa otvorili aj bez pripojenia.", + "description_short": "Uchovávať nedávne správy v tomto zariadení, aby sa otvorili aj bez pripojenia.", "keep_24h": "24 hodín", "keep_days": "{count} dní", "keep_from": "Uchovávať poštu za posledných", @@ -1437,7 +1437,7 @@ "licences_desc": "AGPL-3.0 pre aplikáciu a licencie knižníc, ktoré používa", "never_checked": "Zatiaľ neskontrolované", "privacy": "Súkromie", - "privacy_desc": "Čo zostáva v tomto telefóne a čo ide na váš server", + "privacy_desc": "Čo zostáva v tomto zariadení a čo ide na váš server", "report_problem": "Nahlásiť problém", "report_problem_desc": "Otvorí hlásenie na GitHube s vyplnenou verziou", "server": "Server", @@ -1476,17 +1476,17 @@ } }, "reduce_motion": { - "description_mobile": "Vypne posuvné panely a prechody. Aplikácia to urobí aj vtedy, keď je v nastaveniach telefónu zapnutý znížený pohyb." + "description_mobile": "Vypne posuvné panely a prechody. Aplikácia to urobí aj vtedy, keď je v nastaveniach zariadenia zapnutý znížený pohyb." }, "text_size": { - "description_mobile": "Mení veľkosť textu aplikácie nad rámec veľkosti textu v telefóne. 100 % zodpovedá veľkosti v telefóne." + "description_mobile": "Mení veľkosť textu aplikácie nad rámec veľkosti textu v zariadení. 100 % zodpovedá veľkosti v zariadení." }, "toast_duration": { "automatic": "Automaticky (dlhšie s čítačkou obrazovky)" } }, "backup": { - "description": "Preneste svoje nastavenia do iného telefónu alebo do webmailu.", + "description": "Preneste svoje nastavenia do iného zariadenia alebo do webmailu.", "export_import": "Export alebo import", "export_import_desc": "Súbor JSON v rovnakom formáte ako vo webmaile", "reset": "Resetovať", @@ -1505,7 +1505,7 @@ "other_title": "Ďalšie pripojenia", "phone_backup": "Nič z toho sa neukladá do cloudovej zálohy Androidu.", "phone_secure": "Vaše prihlasovacie údaje sú uložené v zabezpečenom úložisku systému. Nastavenia, offline vyrovnávacia pamäť pošty a zmeny čakajúce na synchronizáciu sú uložené vo vlastnom úložisku aplikácie.", - "phone_title": "V tomto telefóne", + "phone_title": "V tomto zariadení", "push": "Keď sú oznámenia zapnuté, váš server oznámi push relay ({relay}), že sa niečo zmenilo, a relay prebudí aplikáciu cez {service}. Relay vidí token zariadenia a značku zmeny, nikdy nie vašu poštu. V časti Oznámenia môžete použiť vlastný relay.", "push_title": "Push oznámenia", "server": "Pošta, kalendáre, kontakty a súbory sa načítavajú z vášho servera a ukladajú sa späť naň. Okrem pripojení uvedených nižšie komunikuje aplikácia iba s vaším serverom.", diff --git a/locales/rn/th.json b/locales/rn/th.json index 9d13fac0..f4a4ebe7 100644 --- a/locales/rn/th.json +++ b/locales/rn/th.json @@ -129,7 +129,7 @@ "time_zone": "เขตเวลา", "time_zone_auto": "เขตเวลาของอุปกรณ์", "time_zone_auto_zone": "เขตเวลาของอุปกรณ์ ({zone})", - "time_zone_desc": "เขตเวลาที่ใช้กับกิจกรรมใหม่และการแสดงปฏิทิน “เขตเวลาของอุปกรณ์” จะใช้ตามโทรศัพท์" + "time_zone_desc": "เขตเวลาที่ใช้กับกิจกรรมใหม่และการแสดงปฏิทิน “เขตเวลาของอุปกรณ์” จะใช้ตามอุปกรณ์นี้" }, "share": { "add_people": "เพิ่มบุคคล", @@ -706,7 +706,7 @@ "err_pair_connect_detail": "แสดงรหัสใหม่บนคอมพิวเตอร์แล้วสแกนอีกครั้ง", "err_pair_connect_title": "ลงชื่อเข้าใช้แล้ว แต่เชื่อมต่อกับ {host} ไม่ได้", "err_pair_insecure_detail": "{host} ต้องการใช้การเชื่อมต่อที่ไม่ได้เข้ารหัส", - "err_pair_network_detail": "ตรวจสอบการเชื่อมต่อ และตรวจสอบว่าโทรศัพท์ของคุณเข้าถึงเว็บเมลได้ แล้วลองอีกครั้ง", + "err_pair_network_detail": "ตรวจสอบการเชื่อมต่อ และตรวจสอบว่าอุปกรณ์นี้เข้าถึงเว็บเมลได้ แล้วลองอีกครั้ง", "err_pair_rate_detail": "รอสักหนึ่งนาที แล้วลองอีกครั้ง", "err_pair_server_detail": "ลองอีกครั้งด้วยรหัสใหม่ หากยังล้มเหลวอยู่ ให้ติดต่อผู้ดูแลระบบ", "err_pair_server_title": "เว็บเมลดำเนินการลงชื่อเข้าใช้ให้เสร็จไม่ได้", @@ -770,7 +770,7 @@ "server_cert_active": "กำลังลงชื่อเข้าใช้ด้วยใบรับรองไคลเอนต์ \"{alias}\" แตะเพื่อเลือกใบอื่น", "server_cert_clear": "ลงชื่อเข้าใช้โดยไม่ใช้ใบรับรอง", "server_cert_failed": "เลือกใบรับรองไม่สำเร็จ", - "server_cert_hint": "เซิร์ฟเวอร์ของคุณขอใบรับรองไคลเอนต์หรือไม่? เลือกใบรับรองที่ติดตั้งไว้บนโทรศัพท์นี้", + "server_cert_hint": "เซิร์ฟเวอร์ของคุณขอใบรับรองไคลเอนต์หรือไม่? เลือกใบรับรองที่ติดตั้งไว้บนอุปกรณ์นี้", "server_hint": "ที่อยู่เดียวกับที่คุณพิมพ์ในเบราว์เซอร์ เราจะเติม https:// ให้", "server_label": "ที่อยู่เซิร์ฟเวอร์", "server_not_found": "เราไม่พบเซิร์ฟเวอร์สำหรับ {domain} หากคุณโฮสต์เอง ที่อยู่คือที่อยู่ที่คุณใช้สำหรับเว็บเมล", @@ -865,7 +865,7 @@ "licences_desc": "AGPL-3.0 สำหรับแอป และไลบรารีที่แอปใช้", "never_checked": "ยังไม่ได้ตรวจสอบ", "privacy": "ความเป็นส่วนตัว", - "privacy_desc": "สิ่งที่อยู่บนโทรศัพท์นี้ และสิ่งที่ส่งไปยังเซิร์ฟเวอร์ของคุณ", + "privacy_desc": "สิ่งที่อยู่บนอุปกรณ์นี้ และสิ่งที่ส่งไปยังเซิร์ฟเวอร์ของคุณ", "report_problem": "รายงานปัญหา", "report_problem_desc": "เปิด issue บน GitHub พร้อมกรอกเวอร์ชันไว้ให้", "server": "เซิร์ฟเวอร์", @@ -904,10 +904,10 @@ } }, "reduce_motion": { - "description_mobile": "ปิดแผงแบบเลื่อนและการเปลี่ยนหน้า แอปจะทำเช่นนี้ด้วยเมื่อเปิดการลดการเคลื่อนไหวในการตั้งค่าโทรศัพท์ของคุณ" + "description_mobile": "ปิดแผงแบบเลื่อนและการเปลี่ยนหน้า แอปจะทำเช่นนี้ด้วยเมื่อเปิดการลดการเคลื่อนไหวในการตั้งค่าอุปกรณ์ของคุณ" }, "text_size": { - "description_mobile": "ปรับขนาดข้อความของแอปเพิ่มจากขนาดข้อความของโทรศัพท์ 100% คือขนาดเดียวกับโทรศัพท์" + "description_mobile": "ปรับขนาดข้อความของแอปเพิ่มจากขนาดข้อความของอุปกรณ์ 100% คือขนาดเดียวกับอุปกรณ์" }, "toast_duration": { "automatic": "อัตโนมัติ (นานขึ้นเมื่อใช้โปรแกรมอ่านหน้าจอ)" @@ -961,7 +961,7 @@ } }, "backup": { - "description": "ย้ายการตั้งค่าของคุณไปยังโทรศัพท์เครื่องอื่นหรือเว็บเมล", + "description": "ย้ายการตั้งค่าของคุณไปยังอุปกรณ์เครื่องอื่นหรือเว็บเมล", "export_import": "ส่งออกหรือนำเข้า", "export_import_desc": "ไฟล์ JSON รูปแบบเดียวกับเว็บเมล", "reset": "รีเซ็ต", @@ -1338,7 +1338,7 @@ "clear_title": "ล้างอีเมลออฟไลน์", "days_ago": "{count} วันที่แล้ว", "description": "ดาวน์โหลดข้อความล่าสุดเพื่อให้เปิดได้ทันทีและอ่านได้แม้ไม่มีการเชื่อมต่อ เฉพาะเนื้อหาข้อความเท่านั้น — ไฟล์แนบยังคงดึงมาเมื่อต้องการ", - "description_short": "เก็บข้อความล่าสุดไว้บนโทรศัพท์นี้เพื่อให้เปิดได้โดยไม่ต้องเชื่อมต่อ", + "description_short": "เก็บข้อความล่าสุดไว้บนอุปกรณ์นี้เพื่อให้เปิดได้โดยไม่ต้องเชื่อมต่อ", "downloading": "กำลังดาวน์โหลด {completed}/{total}…", "enabled": "แคชอีเมลล่าสุด", "enabled_desc": "เมื่อเปิดไว้ แอปจะเก็บเนื้อหาข้อความในช่วงเวลาที่เลือกไว้บนอุปกรณ์นี้", @@ -1378,7 +1378,7 @@ "other_title": "การเชื่อมต่ออื่นๆ", "phone_backup": "ข้อมูลเหล่านี้จะไม่ถูกรวมในการสำรองข้อมูลบนคลาวด์ของ Android", "phone_secure": "ข้อมูลการลงชื่อเข้าใช้ของคุณเก็บไว้ในที่จัดเก็บที่ปลอดภัยของระบบ ส่วนการตั้งค่า แคชอีเมลออฟไลน์ และการเปลี่ยนแปลงที่รอการซิงค์เก็บไว้ในที่จัดเก็บของแอปเอง", - "phone_title": "บนโทรศัพท์นี้", + "phone_title": "บนอุปกรณ์นี้", "push": "เมื่อเปิดการแจ้งเตือน เซิร์ฟเวอร์ของคุณจะแจ้งพุชรีเลย์ ({relay}) ว่ามีบางอย่างเปลี่ยนแปลง และรีเลย์จะปลุกแอปผ่าน {service} รีเลย์เห็นเพียงโทเค็นอุปกรณ์และตัวบ่งชี้การเปลี่ยนแปลง ไม่เคยเห็นอีเมลของคุณ คุณใช้รีเลย์ของคุณเองได้ที่ การแจ้งเตือน", "push_title": "การแจ้งเตือนพุช", "server": "อีเมล ปฏิทิน ผู้ติดต่อ และไฟล์จะโหลดจากเซิร์ฟเวอร์ของคุณและบันทึกกลับไปที่นั่น นอกจากการเชื่อมต่อด้านล่างแล้ว แอปจะติดต่อเฉพาะเซิร์ฟเวอร์ของคุณเท่านั้น", diff --git a/locales/rn/tr.json b/locales/rn/tr.json index efd7f9d8..66ab5c47 100644 --- a/locales/rn/tr.json +++ b/locales/rn/tr.json @@ -124,7 +124,7 @@ "time_zone": "Saat dilimi", "time_zone_auto": "Cihaz saat dilimi", "time_zone_auto_zone": "Cihaz saat dilimi ({zone})", - "time_zone_desc": "Yeni etkinlikler ve takvimin yorumlanması için kullanılan saat dilimi. \"Cihaz saat dilimi\" telefonu izler." + "time_zone_desc": "Yeni etkinlikler ve takvimin yorumlanması için kullanılan saat dilimi. \"Cihaz saat dilimi\" bu cihazı izler." }, "share": { "add_people": "Kişi ekle", @@ -676,7 +676,7 @@ "err_pair_connect_detail": "Bilgisayarınızda yeni bir kod gösterip tekrar tarayın.", "err_pair_connect_title": "Giriş yapıldı ancak {host} ile bağlantı kurulamadı", "err_pair_insecure_detail": "{host} şifrelenmemiş bir bağlantı kullanmak istiyor.", - "err_pair_network_detail": "Bağlantınızı ve telefonunuzun webmail'e erişip erişemediğini kontrol edin, ardından tekrar deneyin.", + "err_pair_network_detail": "Bağlantınızı ve bu cihazın webmail'e erişip erişemediğini kontrol edin, ardından tekrar deneyin.", "err_pair_rate_detail": "Bir dakika bekleyip tekrar deneyin.", "err_pair_server_detail": "Yeni bir kodla tekrar deneyin. Sorun devam ederse yöneticinize başvurun.", "err_pair_server_title": "Webmail girişi tamamlayamadı", @@ -764,7 +764,7 @@ "server_cert_active": "\"{alias}\" istemci sertifikasıyla giriş yapılıyor. Başka birini seçmek için dokunun.", "server_cert_clear": "Sertifika olmadan giriş yap", "server_cert_failed": "Sertifika seçilemedi", - "server_cert_hint": "Sunucunuz istemci sertifikası mı istiyor? Bu telefonda yüklü olan birini seçin." + "server_cert_hint": "Sunucunuz istemci sertifikası mı istiyor? Bu cihazda yüklü olan birini seçin." } }, "offline": { @@ -1239,7 +1239,7 @@ "window_24h": "Son 24 saat", "window_days": "Son {count} gün", "window_desc": "İletinin alınma tarihine göre ne kadar geriye kadar önbelleğe alınacağı.", - "description_short": "Son iletileri bu telefonda tutun; böylece bağlantı olmadan da açılırlar.", + "description_short": "Son iletileri bu cihazda tutun; böylece bağlantı olmadan da açılırlar.", "keep_24h": "24 saat", "keep_days": "{count} gün", "keep_from": "Postaları saklama süresi", @@ -1439,7 +1439,7 @@ "licences_desc": "Uygulama için AGPL-3.0 ve kullandığı kütüphaneler", "never_checked": "Henüz kontrol edilmedi", "privacy": "Gizlilik", - "privacy_desc": "Bu telefonda ne kalır, sunucunuza ne gider", + "privacy_desc": "Bu cihazda ne kalır, sunucunuza ne gider", "report_problem": "Sorun bildir", "report_problem_desc": "GitHub'da sürüm bilgisi doldurulmuş bir kayıt açar", "server": "Sunucu", @@ -1478,17 +1478,17 @@ } }, "reduce_motion": { - "description_mobile": "Kayan panelleri ve geçişleri kapatır. Telefonunuzun ayarlarında hareketi azaltma açıksa uygulama bunu kendiliğinden yapar." + "description_mobile": "Kayan panelleri ve geçişleri kapatır. Cihazınızın ayarlarında hareketi azaltma açıksa uygulama bunu kendiliğinden yapar." }, "text_size": { - "description_mobile": "Uygulamanın metnini telefonunuzun metin boyutunun üzerine ölçekler. %100, telefonunuzun boyutudur." + "description_mobile": "Uygulamanın metnini cihazınızın metin boyutunun üzerine ölçekler. %100, cihazınızın boyutudur." }, "toast_duration": { "automatic": "Otomatik (ekran okuyucuyla daha uzun)" } }, "backup": { - "description": "Ayarlarınızı başka bir telefona veya webmail'e taşıyın.", + "description": "Ayarlarınızı başka bir cihaza veya webmail'e taşıyın.", "export_import": "Dışa veya içe aktar", "export_import_desc": "Webmail ile aynı biçimde bir JSON dosyası", "reset": "Sıfırla", @@ -1507,7 +1507,7 @@ "other_title": "Diğer bağlantılar", "phone_backup": "Bunların hiçbiri Android'in bulut yedeğine gitmez.", "phone_secure": "Giriş bilgileriniz sistemin güvenli depolama alanında saklanır. Ayarlar, çevrimdışı posta önbelleği ve senkronizasyon bekleyen değişiklikler uygulamanın kendi depolama alanında saklanır.", - "phone_title": "Bu telefonda", + "phone_title": "Bu cihazda", "push": "Bildirimler açıkken sunucunuz push rölesine ({relay}) bir şeyin değiştiğini bildirir ve röle uygulamayı {service} aracılığıyla uyandırır. Röle bir cihaz belirteci ve bir değişiklik işareti görür, postanızı asla görmez. Bildirimler bölümünde kendi rölenizi kullanabilirsiniz.", "push_title": "Anlık Bildirimler", "server": "Posta, takvimler, kişiler ve dosyalar sunucunuzdan yüklenir ve yine oraya kaydedilir. Aşağıdaki bağlantılar dışında uygulama yalnızca sunucunuzla iletişim kurar.", diff --git a/locales/rn/uk.json b/locales/rn/uk.json index c4ed4555..0540a277 100644 --- a/locales/rn/uk.json +++ b/locales/rn/uk.json @@ -124,7 +124,7 @@ "time_zone": "Часовий пояс", "time_zone_auto": "Часовий пояс пристрою", "time_zone_auto_zone": "Часовий пояс пристрою ({zone})", - "time_zone_desc": "Пояс для нових подій і відображення календаря. «Часовий пояс пристрою» береться з налаштувань телефона." + "time_zone_desc": "Пояс для нових подій і відображення календаря. «Часовий пояс пристрою» береться з налаштувань цього пристрою." }, "share": { "add_people": "Додати людей", @@ -677,7 +677,7 @@ "err_pair_connect_detail": "Покажіть новий код на комп'ютері та відскануйте його знову.", "err_pair_connect_title": "Вхід виконано, але не вдалося під'єднатися до {host}", "err_pair_insecure_detail": "{host} хоче використати незашифроване з'єднання.", - "err_pair_network_detail": "Перевірте підключення та чи доступна веб-пошта з телефона, потім спробуйте ще раз.", + "err_pair_network_detail": "Перевірте підключення та чи доступна веб-пошта з цього пристрою, потім спробуйте ще раз.", "err_pair_rate_detail": "Зачекайте хвилину, потім спробуйте ще раз.", "err_pair_server_detail": "Спробуйте ще раз із новим кодом. Якщо помилка повторюється, зверніться до адміністратора.", "err_pair_server_title": "Веб-пошті не вдалося завершити вхід", @@ -765,7 +765,7 @@ "server_cert_active": "Вхід із клієнтським сертифікатом «{alias}». Торкніться, щоб вибрати інший.", "server_cert_clear": "Увійти без сертифіката", "server_cert_failed": "Не вдалося вибрати сертифікат", - "server_cert_hint": "Ваш сервер запитує клієнтський сертифікат? Виберіть один зі встановлених на цьому телефоні." + "server_cert_hint": "Ваш сервер запитує клієнтський сертифікат? Виберіть один зі встановлених на цьому пристрої." } }, "offline": { @@ -1240,7 +1240,7 @@ "window_24h": "Останні 24 години", "window_days": "{count, plural, one {Останній # день} few {Останні # дні} many {Останні # днів} other {Останні # дня}}", "window_desc": "За який період кешувати пошту (за датою отримання листа).", - "description_short": "Зберігайте нещодавні листи на цьому телефоні, щоб вони відкривалися без з'єднання.", + "description_short": "Зберігайте нещодавні листи на цьому пристрої, щоб вони відкривалися без з'єднання.", "keep_24h": "24 години", "keep_days": "{count} дн.", "keep_from": "Зберігати пошту за останні", @@ -1440,7 +1440,7 @@ "licences_desc": "AGPL-3.0 для застосунку та бібліотеки, які він використовує", "never_checked": "Ще не перевірено", "privacy": "Конфіденційність", - "privacy_desc": "Що залишається на цьому телефоні, а що надходить на ваш сервер", + "privacy_desc": "Що залишається на цьому пристрої, а що надходить на ваш сервер", "report_problem": "Повідомити про проблему", "report_problem_desc": "Створює звернення на GitHub із уже вказаною версією", "server": "Сервер", @@ -1479,17 +1479,17 @@ } }, "reduce_motion": { - "description_mobile": "Вимикає висувні панелі й переходи. Застосунок робить це й тоді, коли в налаштуваннях телефона ввімкнено зменшення руху." + "description_mobile": "Вимикає висувні панелі й переходи. Застосунок робить це й тоді, коли в налаштуваннях пристрою ввімкнено зменшення руху." }, "text_size": { - "description_mobile": "Масштабує текст застосунку поверх розміру тексту телефона. 100% — це розмір тексту вашого телефона." + "description_mobile": "Масштабує текст застосунку поверх розміру тексту пристрою. 100% — це розмір тексту вашого пристрою." }, "toast_duration": { "automatic": "Автоматично (довше з програмою зчитування з екрана)" } }, "backup": { - "description": "Перенесіть налаштування на інший телефон або у веб-пошту.", + "description": "Перенесіть налаштування на інший пристрій або у веб-пошту.", "export_import": "Експорт або імпорт", "export_import_desc": "Файл JSON у тому самому форматі, що й у веб-пошті", "reset": "Скинути", @@ -1508,7 +1508,7 @@ "other_title": "Інші з'єднання", "phone_backup": "Ніщо з цього не потрапляє до хмарної резервної копії Android.", "phone_secure": "Ваші дані для входу зберігаються в захищеному сховищі системи. Налаштування, офлайн-кеш пошти та зміни, що очікують синхронізації, зберігаються у власному сховищі застосунку.", - "phone_title": "На цьому телефоні", + "phone_title": "На цьому пристрої", "push": "Коли сповіщення ввімкнено, ваш сервер повідомляє push-реле ({relay}), що щось змінилося, і реле пробуджує застосунок через {service}. Реле бачить токен пристрою та позначку зміни, але ніколи не бачить вашої пошти. Власне реле можна вказати в розділі «Сповіщення».", "push_title": "Push-сповіщення", "server": "Пошта, календарі, контакти та файли завантажуються з вашого сервера й зберігаються на ньому. Окрім з'єднань, наведених нижче, застосунок зв'язується лише з вашим сервером.", diff --git a/locales/rn/zh-TW.json b/locales/rn/zh-TW.json index b95117b6..11007890 100644 --- a/locales/rn/zh-TW.json +++ b/locales/rn/zh-TW.json @@ -124,7 +124,7 @@ "time_zone": "時區", "time_zone_auto": "裝置時區", "time_zone_auto_zone": "裝置時區({zone})", - "time_zone_desc": "用於新增活動與顯示行事曆的時區。選擇「裝置」時會依照手機的設定。" + "time_zone_desc": "用於新增活動與顯示行事曆的時區。選擇「裝置」時會依照此裝置的設定。" }, "share": { "add_people": "新增使用者", @@ -674,7 +674,7 @@ "err_pair_connect_detail": "請在電腦上顯示新的登入碼,再重新掃描。", "err_pair_connect_title": "已登入,但無法連線至 {host}", "err_pair_insecure_detail": "{host} 想要使用未加密的連線。", - "err_pair_network_detail": "請檢查網路連線,並確認手機可以連上網頁郵件,然後再試一次。", + "err_pair_network_detail": "請檢查網路連線,並確認此裝置可以連上網頁郵件,然後再試一次。", "err_pair_rate_detail": "請等候一分鐘後再試一次。", "err_pair_server_detail": "請使用新的登入碼再試一次。如果仍然失敗,請洽詢管理員。", "err_pair_server_title": "網頁郵件無法完成登入", @@ -762,7 +762,7 @@ "server_cert_active": "正在使用用戶端憑證「{alias}」登入。點一下可選擇其他憑證。", "server_cert_clear": "不使用憑證登入", "server_cert_failed": "無法選擇憑證", - "server_cert_hint": "您的伺服器要求用戶端憑證嗎?請選擇此手機上已安裝的憑證。" + "server_cert_hint": "您的伺服器要求用戶端憑證嗎?請選擇此裝置上已安裝的憑證。" } }, "offline": { @@ -1237,7 +1237,7 @@ "window_24h": "過去 24 小時", "window_days": "過去 {count} 天", "window_desc": "依郵件接收日期計算,要快取多久以前的郵件。", - "description_short": "在此手機上保留最近的郵件,沒有網路時也能開啟。", + "description_short": "在此裝置上保留最近的郵件,沒有網路時也能開啟。", "keep_24h": "24 小時", "keep_days": "{count} 天", "keep_from": "保留以下期間的郵件", @@ -1437,7 +1437,7 @@ "licences_desc": "應用程式採用 AGPL-3.0,以及所用程式庫的授權", "never_checked": "尚未檢查", "privacy": "隱私權", - "privacy_desc": "哪些內容留在此手機上,哪些會傳送到您的伺服器", + "privacy_desc": "哪些內容留在此裝置上,哪些會傳送到您的伺服器", "report_problem": "回報問題", "report_problem_desc": "在 GitHub 上建立 issue,並自動填入版本號碼", "server": "伺服器", @@ -1476,17 +1476,17 @@ } }, "reduce_motion": { - "description_mobile": "關閉滑動面板和轉場效果。手機設定中開啟減少動態效果時,應用程式也會這麼做。" + "description_mobile": "關閉滑動面板和轉場效果。裝置設定中開啟減少動態效果時,應用程式也會這麼做。" }, "text_size": { - "description_mobile": "在手機文字大小的基礎上縮放應用程式的文字。100% 即手機的文字大小。" + "description_mobile": "在裝置文字大小的基礎上縮放應用程式的文字。100% 即裝置的文字大小。" }, "toast_duration": { "automatic": "自動(使用螢幕閱讀器時較長)" } }, "backup": { - "description": "將您的設定移至另一支手機或網頁郵件。", + "description": "將您的設定移至另一台裝置或網頁郵件。", "export_import": "匯出或匯入", "export_import_desc": "JSON 檔案,格式與網頁郵件相同", "reset": "重設", @@ -1505,7 +1505,7 @@ "other_title": "其他連線", "phone_backup": "這些資料都不會進入 Android 的雲端備份。", "phone_secure": "您的登入資訊存放在系統的安全儲存空間中。設定、離線郵件快取以及等待同步的變更則存放在應用程式自己的儲存空間中。", - "phone_title": "在此手機上", + "phone_title": "在此裝置上", "push": "開啟通知後,您的伺服器會告知推送中繼({relay})有內容變更,中繼再透過{service}喚醒應用程式。中繼只會看到裝置權杖和變更標記,看不到您的郵件。您可以在「通知」中使用自己的中繼。", "push_title": "推送通知", "server": "郵件、行事曆、聯絡人和檔案從您的伺服器載入,並儲存回伺服器。除了下列連線外,應用程式只會與您的伺服器通訊。", diff --git a/locales/rn/zh.json b/locales/rn/zh.json index 79bb774e..220da131 100644 --- a/locales/rn/zh.json +++ b/locales/rn/zh.json @@ -124,7 +124,7 @@ "time_zone": "时区", "time_zone_auto": "设备时区", "time_zone_auto_zone": "设备时区({zone})", - "time_zone_desc": "用于新建活动和显示日历的时区。“设备”将跟随手机设置。" + "time_zone_desc": "用于新建活动和显示日历的时区。“设备”将跟随此设备的设置。" }, "share": { "add_people": "添加用户", @@ -674,7 +674,7 @@ "err_pair_connect_detail": "请在电脑上显示新的登录码,然后重新扫描。", "err_pair_connect_title": "已登录,但无法连接到 {host}", "err_pair_insecure_detail": "{host} 想要使用未加密的连接。", - "err_pair_network_detail": "请检查网络连接,并确认手机可以访问网页邮箱,然后重试。", + "err_pair_network_detail": "请检查网络连接,并确认此设备可以访问网页邮箱,然后重试。", "err_pair_rate_detail": "请等待一分钟后重试。", "err_pair_server_detail": "请使用新的登录码重试。如果仍然失败,请联系管理员。", "err_pair_server_title": "网页邮箱无法完成登录", @@ -762,7 +762,7 @@ "server_cert_active": "正在使用客户端证书“{alias}”登录。点按可选择其他证书。", "server_cert_clear": "不使用证书登录", "server_cert_failed": "无法选择证书", - "server_cert_hint": "您的服务器要求客户端证书吗?请选择此手机上已安装的证书。" + "server_cert_hint": "您的服务器要求客户端证书吗?请选择此设备上已安装的证书。" } }, "offline": { @@ -1237,7 +1237,7 @@ "window_24h": "过去 24 小时", "window_days": "过去 {count} 天", "window_desc": "按邮件接收日期计算,缓存多久以前的邮件。", - "description_short": "在此手机上保留最近的邮件,无网络时也能打开。", + "description_short": "在此设备上保留最近的邮件,无网络时也能打开。", "keep_24h": "24 小时", "keep_days": "{count} 天", "keep_from": "保留以下时间内的邮件", @@ -1437,7 +1437,7 @@ "licences_desc": "应用采用 AGPL-3.0,以及所用库的许可证", "never_checked": "尚未检查", "privacy": "隐私", - "privacy_desc": "哪些内容留在此手机上,哪些发送到您的服务器", + "privacy_desc": "哪些内容留在此设备上,哪些发送到您的服务器", "report_problem": "报告问题", "report_problem_desc": "在 GitHub 上新建 issue,并自动填写版本号", "server": "服务器", @@ -1476,17 +1476,17 @@ } }, "reduce_motion": { - "description_mobile": "关闭滑动面板和过渡效果。手机设置中开启减少动效时,应用也会这样做。" + "description_mobile": "关闭滑动面板和过渡效果。设备设置中开启减少动效时,应用也会这样做。" }, "text_size": { - "description_mobile": "在手机文字大小的基础上缩放应用文字。100% 即手机的文字大小。" + "description_mobile": "在设备文字大小的基础上缩放应用文字。100% 即设备的文字大小。" }, "toast_duration": { "automatic": "自动(使用屏幕阅读器时更长)" } }, "backup": { - "description": "将您的设置转移到另一部手机或网页邮箱。", + "description": "将您的设置转移到另一台设备或网页邮箱。", "export_import": "导出或导入", "export_import_desc": "JSON 文件,格式与网页邮箱相同", "reset": "重置", @@ -1505,7 +1505,7 @@ "other_title": "其他连接", "phone_backup": "这些数据都不会进入 Android 的云备份。", "phone_secure": "您的登录信息保存在系统的安全存储中。设置、离线邮件缓存以及等待同步的更改保存在应用自己的存储中。", - "phone_title": "在此手机上", + "phone_title": "在此设备上", "push": "开启通知后,您的服务器会告知推送中继({relay})有内容发生变化,中继再通过{service}唤醒应用。中继只能看到设备令牌和变更标记,看不到您的邮件。您可以在“通知”中使用自己的中继。", "push_title": "推送通知", "server": "邮件、日历、联系人和文件从您的服务器加载,并保存回服务器。除下列连接外,应用只与您的服务器通信。", diff --git a/src/components/settings/AboutSettings.tsx b/src/components/settings/AboutSettings.tsx index 025cb7ae..434280d1 100644 --- a/src/components/settings/AboutSettings.tsx +++ b/src/components/settings/AboutSettings.tsx @@ -166,7 +166,7 @@ export function AboutSettings({ onOpen }: AboutSettingsProps) { /> onOpen?.('privacy')} /> {/* The operator's accessibility and help links, from the account's webmail. */} diff --git a/src/components/settings/AccessibilitySettings.tsx b/src/components/settings/AccessibilitySettings.tsx index e62891ea..305ec63e 100644 --- a/src/components/settings/AccessibilitySettings.tsx +++ b/src/components/settings/AccessibilitySettings.tsx @@ -166,7 +166,7 @@ export function AccessibilitySettings() { - + {paragraph(t( 'settings.privacy.phone_secure', 'Your sign-in details are kept in the system\'s secure storage. Settings, the offline mail cache and changes waiting to sync are kept in the app\'s own storage.', diff --git a/src/components/settings/StorageSettings.tsx b/src/components/settings/StorageSettings.tsx index c3314043..1701c339 100644 --- a/src/components/settings/StorageSettings.tsx +++ b/src/components/settings/StorageSettings.tsx @@ -205,7 +205,7 @@ export function StorageSettings({ onOpenTroubleshooting }: StorageSettingsProps) {exportAllowed && ( , prefix = '', out = new Map()): Map { + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k; + if (v && typeof v === 'object' && !Array.isArray(v)) flatten(v as Record, key, out); + else out.set(key, v); + } + return out; +} + +const read = (code: string) => flatten(JSON.parse(readFileSync(join(RN_DIR, `${code}.json`), 'utf8'))); + +// The keys that spoke of the user's phone. +const DEVICE_KEYS = [ + 'calendar.settings.time_zone_desc', + 'login.mobile.err_pair_network_detail', + 'login.mobile.server_cert_hint', + 'settings.about.privacy_desc', + 'settings.accessibility.reduce_motion.description_mobile', + 'settings.accessibility.text_size.description_mobile', + 'settings.backup.description', + 'settings.offline.description_short', + 'settings.privacy.phone_title', +]; + +// "Phone" in each language, as the overlays had it. +const PHONE: Record = { + en: /\bphone/i, ar: /هاتف/, ca: /telèfon/i, cs: /telefon/i, da: /telefon/i, de: /telefon/i, + es: /teléfono/i, et: /telefon/i, fa: /تلفن/, fr: /téléphone/i, he: /טלפון/, hu: /telefon/i, + it: /telefono/i, ja: /スマートフォン/, ko: /휴대폰/, lv: /tālru/i, mn: /утас|утсанд|утсан/i, nb: /telefon/i, + nl: /telefoon/i, pl: /telefon/i, pt: /celular/i, ro: /telefon/i, ru: /телефон/i, sk: /telefón/i, + th: /โทรศัพท์/, tr: /telefon/i, uk: /телефон/i, zh: /手机/, 'zh-TW': /手機/, +}; + +describe('device-neutral wording', () => { + const codes = readdirSync(RN_DIR).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5)); + + it('covers every overlay', () => { + expect(codes.sort()).toEqual(Object.keys(PHONE).sort()); + }); + + it.each(codes)('%s says device, not phone', (code) => { + const overlay = read(code); + for (const key of DEVICE_KEYS) { + const value = overlay.get(key); + expect(typeof value, `${code} ${key}`).toBe('string'); + expect(value as string, `${code} ${key}`).not.toMatch(PHONE[code]); + } + }); + + it('leaves no "phone" for the device in the English app copy', () => { + // A phone call and a contact's phone number are still phones. + const allowed = new Set(['encryption.lookup_confirm', 'structured_mail_calendar.description.phone']); + const offenders = [...read('en')] + .filter(([key, value]) => typeof value === 'string' && /\bphone\b/i.test(value) && !allowed.has(key)) + .map(([key]) => key); + expect(offenders).toEqual([]); + }); +}); diff --git a/src/lib/__tests__/settings-search.test.ts b/src/lib/__tests__/settings-search.test.ts index 4a2885cc..e8bbbde8 100644 --- a/src/lib/__tests__/settings-search.test.ts +++ b/src/lib/__tests__/settings-search.test.ts @@ -187,7 +187,7 @@ describe('matching', () => { it('returns the matching settings of a pane, by label or description', () => { expect(subResultsForQuery(index, 'accessibility', 'text size')).toEqual([ - { label: 'Text size', description: "Scales the app's text on top of your phone's text size. 100% is your phone's size." }, + { label: 'Text size', description: "Scales the app's text on top of your device's text size. 100% is your device's size." }, ]); expect(subResultsForQuery(index, 'accessibility', 'archived').map((r) => r.label)).toEqual(['Confirm before archiving']); expect(subResultsForQuery(index, 'accessibility', '')).toEqual([]); From 34c65765ba1c6bf7c143e7bd076f27809d6a3c70 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:39:56 +0100 Subject: [PATCH 11/13] fix: call a sharee without a name unknown person in every share sheet --- src/components/files/ShareSheet.tsx | 3 ++- .../sharing/ShareCollectionSheet.tsx | 3 ++- .../sharing/__tests__/unknown-person.test.ts | 25 +++++++++++++++++++ src/components/sharing/unknown-person.ts | 10 ++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/components/sharing/__tests__/unknown-person.test.ts create mode 100644 src/components/sharing/unknown-person.ts diff --git a/src/components/files/ShareSheet.tsx b/src/components/files/ShareSheet.tsx index 3fee704d..7121f29e 100644 --- a/src/components/files/ShareSheet.tsx +++ b/src/components/files/ShareSheet.tsx @@ -21,6 +21,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke import { useColors } from '../../theme/colors'; import { useLocaleStore } from '../../stores/locale-store'; import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations'; +import { unknownPersonLabel } from '../sharing/unknown-person'; import { useSheetInsets } from '../../lib/use-layout'; import { useSheetBottomInset } from '../../lib/sheet-bottom-inset'; @@ -172,7 +173,7 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps // Stalwart 1.0 with directory queries off hides every principal the user // has no relationship with, so a sharee may have no name. const unresolved = !p && !loadingPrincipals; - return p?.description || p?.name || (unresolved ? t('sharing.unknown_user', 'Unknown user') : principalId); + return p?.description || p?.name || (unresolved ? unknownPersonLabel(t) : principalId); }; const renderPrincipalLabel = (principalId: string) => { diff --git a/src/components/sharing/ShareCollectionSheet.tsx b/src/components/sharing/ShareCollectionSheet.tsx index 5d2b3d69..e288c153 100644 --- a/src/components/sharing/ShareCollectionSheet.tsx +++ b/src/components/sharing/ShareCollectionSheet.tsx @@ -21,6 +21,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke import { useColors } from '../../theme/colors'; import { useLocaleStore } from '../../stores/locale-store'; import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations'; +import { unknownPersonLabel } from './unknown-person'; import { useSheetInsets } from '../../lib/use-layout'; import { useSheetBottomInset } from '../../lib/sheet-bottom-inset'; @@ -200,7 +201,7 @@ export function ShareCollectionSheet({ return ( - {p?.description || p?.name || (unresolved ? t('sharing.unknown_user', 'Unknown user') : principalId)} + {p?.description || p?.name || (unresolved ? unknownPersonLabel(t) : principalId)} {/* Never the id in its place: Stalwart 1.0's are a letter or two ("e") and read as a broken address. */} diff --git a/src/components/sharing/__tests__/unknown-person.test.ts b/src/components/sharing/__tests__/unknown-person.test.ts new file mode 100644 index 00000000..31f37554 --- /dev/null +++ b/src/components/sharing/__tests__/unknown-person.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { unknownPersonLabel } from '../unknown-person'; +import { principalRowDisplay } from '../../../lib/delegation/display'; + +// Settings > Sharing said "Unknown person", the folder share sheet "Unknown user". + +const t = (_key: string, fallback?: string) => fallback ?? ''; +const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', '..', rel), 'utf8'); + +describe('a sharee without a name', () => { + it('reads like Settings > Sharing', () => { + expect(unknownPersonLabel(t)).toBe('Unknown person'); + expect(unknownPersonLabel((key) => key)).toBe(principalRowDisplay(null, (key) => key).name); + }); + + it('is what the folder, calendar, address book and file share sheets show', () => { + for (const file of ['components/sharing/ShareCollectionSheet.tsx', 'components/files/ShareSheet.tsx']) { + const src = read(file); + expect(src, file).not.toMatch(/sharing\.unknown_user|Unknown user/); + expect(src, file).toMatch(/unresolved \? unknownPersonLabel\(t\) : principalId/); + } + }); +}); diff --git a/src/components/sharing/unknown-person.ts b/src/components/sharing/unknown-person.ts new file mode 100644 index 00000000..e36634ce --- /dev/null +++ b/src/components/sharing/unknown-person.ts @@ -0,0 +1,10 @@ +type Translate = (key: string, fallback?: string) => string; + +/** + * A sharee the server shows no name for (Stalwart 1.0 with directory queries + * off). One wording everywhere: Settings > Sharing (lib/delegation/display) + * said "Unknown person", the folder and file share sheets "Unknown user". + */ +export function unknownPersonLabel(t: Translate): string { + return t('delegates.unknown_person', 'Unknown person'); +} From 3c95bba0d25ff089b54e3e90c7ec577457ff11e9 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:42:59 +0100 Subject: [PATCH 12/13] fix: give the folder name its own line next to the docked file preview --- src/lib/__tests__/files-split.test.ts | 28 ++++++++++++++++++++++++++- src/lib/files-split.ts | 24 +++++++++++++++++++++++ src/screens/FilesScreen.tsx | 24 ++++++++++++++++++----- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/lib/__tests__/files-split.test.ts b/src/lib/__tests__/files-split.test.ts index c6eb3eb3..20cb4d0a 100644 --- a/src/lib/__tests__/files-split.test.ts +++ b/src/lib/__tests__/files-split.test.ts @@ -5,7 +5,10 @@ import path from 'node:path'; vi.mock('react-native', () => ({ useWindowDimensions: () => ({ width: 800, height: 1280 }) })); vi.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }) })); -import { FILES_LIST_MIN, PREVIEW_HEADER_CHROME, PREVIEW_TITLE_MIN, filesListWidth } from '../files-split'; +import { + FILES_LIST_MIN, FILES_TITLE_MIN, PREVIEW_HEADER_CHROME, PREVIEW_TITLE_MIN, + filesHeaderTitleRoom, filesListWidth, filesTitleOnOwnRow, +} from '../files-split'; // In the tablet split view the docked file preview had a fixed 45 % of the // row. On a Pixel Tablet in portrait (about 720 dp next to the tab rail) @@ -28,6 +31,29 @@ describe('filesListWidth', () => { }); }); +describe('the folder name in the Files header next to the docked preview', () => { + it('had about 50 dp beside four buttons and Back in a 320 dp list column', () => { + expect(filesHeaderTitleRoom(320, 4, true)).toBe(52); + expect(filesHeaderTitleRoom(320, 5, false)).toBe(52); + }); + + it('moves under the buttons when that is too little, and stays beside them when there is room', () => { + expect(filesTitleOnOwnRow(filesListWidth(720), 4, true)).toBe(true); + expect(filesTitleOnOwnRow(FILES_LIST_MIN, 5, false)).toBe(true); + expect(filesTitleOnOwnRow(800, 4, true)).toBe(false); + expect(filesHeaderTitleRoom(800, 4, true)).toBeGreaterThanOrEqual(FILES_TITLE_MIN); + // Not measured yet: beside the buttons, as before. + expect(filesTitleOnOwnRow(0, 5, true)).toBe(false); + }); + + it('is how the header lays out its title', () => { + const source = fs.readFileSync(path.resolve(__dirname, '../../screens/FilesScreen.tsx'), 'utf8'); + expect(source).toMatch(/const titleOwnRow = isSplit && previewTarget != null && filesTitleOnOwnRow\(headerWidth, headerButtons, canBack\);/); + expect(source).toMatch(/onLayout=\{\(e\) => setHeaderWidth\(e\.nativeEvent\.layout\.width\)\}/); + expect(source).toMatch(/\{titleOwnRow \? \(\s*/); + }); +}); + describe('the Files split view', () => { it('sizes the list column with filesListWidth and lets the preview take the rest', () => { const source = fs.readFileSync(path.resolve(__dirname, '../../screens/FilesScreen.tsx'), 'utf8'); diff --git a/src/lib/files-split.ts b/src/lib/files-split.ts index 5ba424fb..b8600bcb 100644 --- a/src/lib/files-split.ts +++ b/src/lib/files-split.ts @@ -22,3 +22,27 @@ export function filesListWidth(contentWidth: number): number { const previewMin = PREVIEW_HEADER_CHROME + PREVIEW_TITLE_MIN; return Math.max(FILES_LIST_MIN, Math.min(listPaneWidth(contentWidth), contentWidth - previewMin)); } + +/** Room the folder name keeps beside the Files header buttons before it moves to a line of its own. */ +export const FILES_TITLE_MIN = 120; +const HEADER_PADDING = 12; // spacing.md, each side +const HEADER_BUTTON = 44; // componentSizes.buttonLg +const HEADER_GAP = 4; // spacing.xs, between buttons +const HEADER_ROW_GAP = 8; // spacing.sm, title side to buttons + +/** + * Width left for the folder name in the Files header, `width` wide, with + * `buttons` buttons on the right and a back button or not. Next to the docked + * preview (280 to 420 dp) the four or five buttons left a folder name about + * 50 dp: "…". + */ +export function filesHeaderTitleRoom(width: number, buttons: number, back: boolean): number { + const actions = buttons * HEADER_BUTTON + Math.max(0, buttons - 1) * HEADER_GAP; + const left = back ? HEADER_BUTTON + HEADER_GAP : 0; + return width - 2 * HEADER_PADDING - actions - HEADER_ROW_GAP - left; +} + +/** Whether the folder name goes on its own line under the header buttons. A width not measured yet keeps it beside them. */ +export function filesTitleOnOwnRow(width: number, buttons: number, back: boolean): boolean { + return width > 0 && filesHeaderTitleRoom(width, buttons, back) < FILES_TITLE_MIN; +} diff --git a/src/screens/FilesScreen.tsx b/src/screens/FilesScreen.tsx index 3b19ef67..d43e951f 100644 --- a/src/screens/FilesScreen.tsx +++ b/src/screens/FilesScreen.tsx @@ -71,7 +71,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { formatListDate } from '../lib/date-format'; import { useLayout, useSheetInsets } from '../lib/use-layout'; import { useSheetBottomInset } from '../lib/sheet-bottom-inset'; -import { filesListWidth } from '../lib/files-split'; +import { filesListWidth, filesTitleOnOwnRow } from '../lib/files-split'; import { radioOptionA11y } from '../components/radio-a11y'; import { useBackWhileFocused } from '../lib/use-back-while-focused'; import { filesBackStep } from '../lib/files-back'; @@ -232,6 +232,9 @@ export default function FilesScreen() { // whole screen keeps clear of the side insets in landscape. const insets = useSafeAreaInsets(); const headerInsets = { paddingTop: insets.top + spacing.sm, paddingHorizontal: spacing.md }; + // The header's width: next to the docked preview the folder name moves + // under the buttons when they leave it too little room. + const [headerWidth, setHeaderWidth] = useState(0); // Folder navigation stack. Hierarchy is real parentId nesting (#379) — each // entry is the folder's node id (namespaced "accountId:nodeId" inside a @@ -1015,8 +1018,12 @@ export default function FilesScreen() { ); } + // Search and view toggle; New folder and Upload; Trash at the top level. + // Only the list column next to the docked preview is that narrow. + const headerButtons = 2 + (canCreateHere ? 2 : 0) + (path.length === 0 && !inTrash ? 1 : 0); + const titleOwnRow = isSplit && previewTarget != null && filesTitleOnOwnRow(headerWidth, headerButtons, canBack); return ( - + setHeaderWidth(e.nativeEvent.layout.width)}> {canBack ? ( @@ -1030,9 +1037,11 @@ export default function FilesScreen() { ) : null} - - {headerName} - + {!titleOwnRow ? ( + + {headerName} + + ) : null} + {titleOwnRow ? ( + + {headerName} + + ) : null} {canBack ? ( {[rootLabel, ...(inTrash ? [trashLabel] : path.map((p) => p.name))].join(' / ')} From 10f0066ed15e03ed6a29adefd6a34ae7a4b0506e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:44:42 +0100 Subject: [PATCH 13/13] fix: name the server when two accounts with one address sync to this device --- .../device-sync/DeviceSyncAccountRow.tsx | 6 ++-- .../__tests__/account-label.test.ts | 31 +++++++++++++++++++ .../settings/device-sync/account-label.ts | 18 +++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/device-sync/__tests__/account-label.test.ts create mode 100644 src/components/settings/device-sync/account-label.ts diff --git a/src/components/settings/device-sync/DeviceSyncAccountRow.tsx b/src/components/settings/device-sync/DeviceSyncAccountRow.tsx index 814783cb..602f8cfd 100644 --- a/src/components/settings/device-sync/DeviceSyncAccountRow.tsx +++ b/src/components/settings/device-sync/DeviceSyncAccountRow.tsx @@ -15,7 +15,7 @@ import { spacing, typography, type ThemePalette } from '../../../theme/tokens'; import { useColors } from '../../../theme/colors'; import { useLocaleStore, type TranslateFn } from '../../../stores/locale-store'; import { useSettingsStore } from '../../../stores/settings-store'; -import type { AccountEntry } from '../../../stores/account-store'; +import { useAccountStore, type AccountEntry } from '../../../stores/account-store'; import { effectiveSelection, emptyAccountDeviceSync, @@ -41,6 +41,7 @@ import { describeSyncStatus, type StatusAction } from './status'; import { useAndroidSync } from './use-android-sync'; import { useSyncCollections } from './use-sync-collections'; import { nameWithAccount } from '../../../lib/address-book-label'; +import { deviceSyncAccountLabel } from './account-label'; interface Props { account: AccountEntry; @@ -117,7 +118,8 @@ export function DeviceSyncAccountRow({ account, authority }: Props) { [collections, entry, authority], ); - const accountLabel = account.email || account.username; + const accounts = useAccountStore((s) => s.accounts); + const accountLabel = React.useMemo(() => deviceSyncAccountLabel(account, accounts), [account, accounts]); const status = describeSyncStatus( { authority, diff --git a/src/components/settings/device-sync/__tests__/account-label.test.ts b/src/components/settings/device-sync/__tests__/account-label.test.ts new file mode 100644 index 00000000..637826c8 --- /dev/null +++ b/src/components/settings/device-sync/__tests__/account-label.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { deviceSyncAccountLabel } from '../account-label'; + +const account = (id: string, email: string, serverUrl: string) => ({ id, email, username: email, serverUrl }); + +describe('the account name in Sync to this device', () => { + const a = account('usera@10.10.10.62:8016', 'usera@example.org', 'http://10.10.10.62:8016'); + const b = account('usera@10.10.10.62:8120', 'usera@example.org', 'http://10.10.10.62:8120'); + const c = account('userb@mail.example.org', 'userb@example.org', 'https://mail.example.org'); + + it('tells two accounts with the same address apart by their server', () => { + expect(deviceSyncAccountLabel(a, [a, b, c])).toBe('usera@example.org (10.10.10.62:8016)'); + expect(deviceSyncAccountLabel(b, [a, b, c])).toBe('usera@example.org (10.10.10.62:8120)'); + }); + + it('is just the address when it is the only one with it', () => { + expect(deviceSyncAccountLabel(c, [a, b, c])).toBe('userb@example.org'); + expect(deviceSyncAccountLabel(a, [a, c])).toBe('usera@example.org'); + // A default port is left out of the server. + const d = account('usera@mail.example.org', 'USERA@example.org', 'https://mail.example.org'); + expect(deviceSyncAccountLabel(d, [a, d])).toBe('USERA@example.org (mail.example.org)'); + }); + + it('is what the row and its switch are labelled with', () => { + const src = fs.readFileSync(path.resolve(__dirname, '..', 'DeviceSyncAccountRow.tsx'), 'utf8'); + expect(src).toMatch(/const accountLabel = React\.useMemo\(\(\) => deviceSyncAccountLabel\(account, accounts\), \[account, accounts\]\);/); + expect(src).toMatch(/'Sync \{account\} to this device', \{ account: accountLabel \}/); + }); +}); diff --git a/src/components/settings/device-sync/account-label.ts b/src/components/settings/device-sync/account-label.ts new file mode 100644 index 00000000..a452eeca --- /dev/null +++ b/src/components/settings/device-sync/account-label.ts @@ -0,0 +1,18 @@ +// The account's name in "Sync to this device": its address, plus the server +// when another signed-in account has the same address (usera@example.org on +// two servers showed two identical "Sync usera@example.org to this device"). +import { serverHostLabel } from '../../../lib/account-utils'; + +interface LabelledAccount { + id: string; + email: string; + username: string; + serverUrl: string; +} + +export function deviceSyncAccountLabel(account: LabelledAccount, accounts: readonly LabelledAccount[]): string { + const address = account.email || account.username; + const key = address.trim().toLowerCase(); + const twin = accounts.some((a) => a.id !== account.id && (a.email || a.username).trim().toLowerCase() === key); + return twin ? `${address} (${serverHostLabel(account.serverUrl)})` : address; +}