diff --git a/components/settings/__tests__/calendar-management-settings.test.tsx b/components/settings/__tests__/calendar-management-settings.test.tsx
index 4f2b4bdce..614e64976 100644
--- a/components/settings/__tests__/calendar-management-settings.test.tsx
+++ b/components/settings/__tests__/calendar-management-settings.test.tsx
@@ -68,6 +68,8 @@ describe('CalendarManagementSettings CalDAV fallback address', () => {
render();
+ // The address waits behind "Use in other apps" (usability pass 2026-11-04).
+ fireEvent.click(await screen.findByTestId('calendar-use-in-other-apps'));
const url = await screen.findByText('https://host.example/stalwart/.well-known/caldav');
// Left to right in Arabic too: the start of the address stays in view.
expect(url).toHaveAttribute('dir', 'ltr');
diff --git a/components/settings/__tests__/settings-page-wording.test.tsx b/components/settings/__tests__/settings-page-wording.test.tsx
new file mode 100644
index 000000000..069be3b8d
--- /dev/null
+++ b/components/settings/__tests__/settings-page-wording.test.tsx
@@ -0,0 +1,133 @@
+import { act, fireEvent, render, screen, within } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { AddressBook, Calendar } from '@/lib/jmap/types';
+import { useAuthStore } from '@/stores/auth-store';
+import { useCalendarStore } from '@/stores/calendar-store';
+import { useContactStore } from '@/stores/contact-store';
+import { useThemeStore } from '@/stores/theme-store';
+import { ThemesSettings } from '../themes-settings';
+import { AddressBookManagementSettings } from '../address-book-management-settings';
+import { CalendarManagementSettings } from '../calendar-management-settings';
+import { FilesSettingsComponent } from '../files-settings';
+import { DownloadsSettings } from '../downloads-settings';
+
+// Usability pass 2026-11-04, settings-11, -33 and -36: settings pages showed
+// the server's names ("Stalwart Calendar (me@example.org)"), a raw CalDAV
+// address on every calendar, English sample text in every language and a
+// "Default" badge on the theme called Default. vitest.setup.ts renders
+// strings as their raw keys.
+
+vi.mock('../settings-section', async (importOriginal) => ({
+ ...(await importOriginal()),
+ SettingsSection: ({ children }: { children: React.ReactNode }) =>
{children}
,
+}));
+
+const initial = {
+ auth: useAuthStore.getState(),
+ contacts: useContactStore.getState(),
+ calendars: useCalendarStore.getState(),
+};
+afterEach(() => {
+ useAuthStore.setState(initial.auth, true);
+ useContactStore.setState(initial.contacts, true);
+ useCalendarStore.setState(initial.calendars, true);
+ vi.restoreAllMocks();
+});
+
+describe('theme gallery', () => {
+ beforeEach(() => {
+ act(() => {
+ useThemeStore.setState({ activeThemeId: null, syncServerThemes: vi.fn().mockResolvedValue(undefined) });
+ });
+ });
+
+ it('puts no "Default" badge on the theme that is called Default', () => {
+ render();
+ const card = document.querySelector('[data-search-label="default_name"]') as HTMLElement;
+ expect(card).toBeTruthy();
+ expect(within(card).queryByText('default_badge')).toBeNull();
+ });
+});
+
+describe('server default names', () => {
+ const rights = { mayRead: true, mayWrite: true, mayShare: true, mayDelete: true };
+
+ it('shows the address book Stalwart made as "Contacts", and a chosen name as it is', () => {
+ useAuthStore.setState({ client: { getAccountId: () => 'acc' } } as never);
+ useContactStore.setState({
+ addressBooks: [
+ { id: 'd', name: 'Stalwart Address Book (me@example.org)', isDefault: true, isShared: false, myRights: rights, shareWith: null },
+ { id: 'c', name: 'Club', isDefault: false, isShared: false, myRights: rights, shareWith: null },
+ ] as unknown as AddressBook[],
+ contacts: [],
+ supportsSync: true,
+ fetchAddressBooks: vi.fn(async () => {}),
+ } as never);
+ render();
+ expect(screen.getByText('default_address_book')).toBeInTheDocument();
+ expect(screen.getByText('Club')).toBeInTheDocument();
+ expect(screen.queryByText(/Stalwart Address Book/)).toBeNull();
+ });
+
+ it('shows the calendar Stalwart made as "Calendar" and keeps its name when saved unchanged', async () => {
+ const updateCalendar = vi.fn().mockResolvedValue(undefined);
+ useAuthStore.setState({ client: { getAccountId: () => 'acct-1' }, serverUrl: null, username: null } as never);
+ useCalendarStore.setState({
+ calendars: [{
+ id: 'cal', name: 'Stalwart Calendar (me@example.org)', color: '#3b82f6', isDefault: true, isShared: false, shareWith: null,
+ myRights: { mayShare: true, mayDelete: true, mayWriteAll: true },
+ } as unknown as Calendar],
+ icalSubscriptions: [],
+ updateCalendar,
+ } as never);
+ render();
+ expect(screen.getByText('default_calendar')).toBeInTheDocument();
+ expect(screen.queryByText(/Stalwart Calendar/)).toBeNull();
+
+ fireEvent.click(screen.getByTitle('edit'));
+ fireEvent.click(screen.getByText('save'));
+ await vi.waitFor(() => expect(updateCalendar).toHaveBeenCalledTimes(1));
+ expect(updateCalendar.mock.calls[0][2]).toEqual({ name: 'Stalwart Calendar (me@example.org)' });
+ });
+});
+
+describe('calendar address for other apps', () => {
+ it('keeps the CalDAV address behind "Use in other apps"', async () => {
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('{}', { status: 502 }));
+ useAuthStore.setState({
+ client: { getAccountId: () => 'acct-1' }, serverUrl: 'https://host.example/', username: 'alice@example.com',
+ } as never);
+ useCalendarStore.setState({
+ calendars: [{
+ id: 'cal', name: 'Home', color: '#3b82f6', isDefault: true, isShared: false, shareWith: null,
+ myRights: { mayShare: true, mayDelete: true, mayWriteAll: true },
+ } as unknown as Calendar],
+ icalSubscriptions: [],
+ updateCalendar: vi.fn(),
+ } as never);
+ render();
+
+ const button = await screen.findByTestId('calendar-use-in-other-apps');
+ expect(button).toHaveTextContent('use_in_other_apps');
+ expect(screen.queryByText('https://host.example/.well-known/caldav')).toBeNull();
+ fireEvent.click(button);
+ expect(screen.getByText('https://host.example/.well-known/caldav')).toBeInTheDocument();
+ });
+});
+
+describe('previews in the user language', () => {
+ it('labels the Files preview through the translations', () => {
+ render();
+ for (const key of ['name', 'size', 'modified', 'preview_documents', 'preview_photos']) {
+ expect(screen.getAllByText(key).length).toBeGreaterThan(0);
+ }
+ expect(screen.queryByText('Documents')).toBeNull();
+ expect(screen.queryByText('Modified')).toBeNull();
+ });
+
+ it('builds the file name preview from a translated sample subject', () => {
+ render();
+ expect(document.body.textContent).toContain('sample_subject');
+ expect(document.body.textContent).not.toContain('Benachrichtigung');
+ });
+});
diff --git a/components/settings/address-book-management-settings.tsx b/components/settings/address-book-management-settings.tsx
index b97f5cb91..ed9a1fc61 100644
--- a/components/settings/address-book-management-settings.tsx
+++ b/components/settings/address-book-management-settings.tsx
@@ -9,6 +9,7 @@ import { useManagedAccountStore } from "@/stores/managed-account-store";
import { toast } from "@/stores/toast-store";
import { SettingsSection } from "./settings-section";
import { cn } from "@/lib/utils";
+import { collectionDisplayName } from "@/lib/default-collection-name";
import { IconButton } from "@/components/ui/icon-button";
import type { AddressBook, AddressBookRights } from "@/lib/jmap/types";
import { ShareCollectionDialog } from "./share-collection-dialog";
@@ -79,6 +80,8 @@ export function AddressBookManagementSettings() {
const t = useTranslations("contacts.address_books");
const tContacts = useTranslations("contacts");
const tSettings = useTranslations("settings.contacts");
+ const tWording = useTranslations("uxs_wording");
+ const bookName = (book: AddressBook) => collectionDisplayName(book.name, "addressBook", tWording("default_address_book"));
const { client } = useAuthStore();
const managedAccountId = useManagedAccountStore((s) => s.managedAccountId);
const { addressBooks, contacts, supportsSync, fetchAddressBooks, createAddressBook, renameAddressBook, shareAddressBook, setDefaultAddressBook, removeAddressBook, renameKeyword } = useContactStore();
@@ -144,7 +147,7 @@ export function AddressBookManagementSettings() {
if (!client) return;
const ok = await confirm({
title: t("delete_title"),
- message: t("confirm_delete", { name: book.name }),
+ message: t("confirm_delete", { name: bookName(book) }),
confirmText: t("delete"),
variant: "destructive",
});
@@ -225,8 +228,9 @@ export function AddressBookManagementSettings() {
return (
handleUpdate(book, name)}
+ initial={bookName(book)}
+ // Saved unchanged, the shown name keeps the server's own.
+ onSave={(name) => handleUpdate(book, name === bookName(book) ? book.name : name)}
onCancel={() => setEditingId(null)}
isLoading={isLoading}
/>
@@ -245,7 +249,7 @@ export function AddressBookManagementSettings() {
>
- {book.name}
+ {bookName(book)}
{book.isDefault && (
@@ -269,7 +273,7 @@ export function AddressBookManagementSettings() {
actions sit behind a ⋮ there, as for calendars. */}
{actions.length > 0 && (
openContextMenuFrom(e.currentTarget, book, "end")}
>
@@ -436,7 +440,7 @@ export function AddressBookManagementSettings() {
{
diff --git a/components/settings/calendar-management-settings.tsx b/components/settings/calendar-management-settings.tsx
index 5713ff424..472b72417 100644
--- a/components/settings/calendar-management-settings.tsx
+++ b/components/settings/calendar-management-settings.tsx
@@ -18,6 +18,7 @@ import { alertsToRows, rowsToAlerts, type AlertRow } from '@/lib/calendar-alerts
import { ShareCollectionDialog } from './share-collection-dialog';
import type { CalendarRights } from '@/lib/jmap/types';
import { cn, formatDateTime, redactUrlCredentials } from '@/lib/utils';
+import { collectionDisplayName } from '@/lib/default-collection-name';
import { ICalImportModal } from '@/components/calendar/ical-import-modal';
import { ICalSubscriptionModal } from '@/components/calendar/ical-subscription-modal';
import { useSettingsStore } from '@/stores/settings-store';
@@ -198,6 +199,7 @@ export function CalendarDefaultAlertsForm({
isLoading: boolean;
}) {
const t = useTranslations('calendar.management');
+ const tWording = useTranslations('uxs_wording');
// Alerts the rows can't show are written back unchanged.
const [timed] = useState(() => alertsToRows(calendar.defaultAlertsWithTime));
const [allDay] = useState(() => alertsToRows(calendar.defaultAlertsWithoutTime));
@@ -219,7 +221,7 @@ export function CalendarDefaultAlertsForm({
return (
{sortedFiles.map((file) => (
)}
- {isDefault && (
+ {/* The built-in theme is called Default already: a badge saying so again is noise. */}
+ {isDefault && !isDefaultTheme && (
{t('default_badge')}
)}
{isActive && }
diff --git a/lib/default-collection-name.ts b/lib/default-collection-name.ts
new file mode 100644
index 000000000..dd2eff62a
--- /dev/null
+++ b/lib/default-collection-name.ts
@@ -0,0 +1,23 @@
+/**
+ * Stalwart names the calendar and address book it creates for a new account
+ * "Stalwart Calendar ()" and "Stalwart Address Book ()"
+ * (registry defaults, 0.16 and 1.0). Settings show those as plain "Calendar"
+ * and "Contacts" in the user's language, the way the sidebar shows "Sent"
+ * instead of the server's folder name. A name the user chose stays as it is.
+ */
+
+const SERVER_DEFAULT_NAMES = {
+ calendar: /^Stalwart Calendar(?: \([^()]*\))?$/,
+ addressBook: /^Stalwart Address Book(?: \([^()]*\))?$/,
+} as const;
+
+export type DefaultCollectionKind = keyof typeof SERVER_DEFAULT_NAMES;
+
+export function isServerDefaultCollectionName(name: string | undefined, kind: DefaultCollectionKind): boolean {
+ return !!name && SERVER_DEFAULT_NAMES[kind].test(name.trim());
+}
+
+/** The name to show: `plainName` for the server's own default name, else the collection's name. */
+export function collectionDisplayName(name: string, kind: DefaultCollectionKind, plainName: string): string {
+ return isServerDefaultCollectionName(name, kind) ? plainName : name;
+}
diff --git a/lib/download-filename.ts b/lib/download-filename.ts
index 9b60f0888..ea26665b1 100644
--- a/lib/download-filename.ts
+++ b/lib/download-filename.ts
@@ -269,7 +269,7 @@ export function bundleExportFilename(
}
// Build a synthetic email for previewing templates in the settings UI.
-export function buildSampleEmail(): Email {
+export function buildSampleEmail(subject = "Benachrichtigung von Ihrem Gerät"): Email {
// Use a fixed date so the preview doesn't churn as the user types.
const iso = "2026-05-22T14:05:33Z";
return {
@@ -283,7 +283,7 @@ export function buildSampleEmail(): Email {
from: [{ name: "Alice Sender", email: "alice@example.com" }],
to: [{ name: "Bob Recipient", email: "bob@example.com" }],
cc: [],
- subject: "Benachrichtigung von Ihrem Gerät",
+ subject,
preview: "",
hasAttachment: true,
blobId: "sample-blob",