From 22903cdfc059a5eff464fc3f78860a277973a76e Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Wed, 4 Nov 2026 10:51:19 +0100
Subject: [PATCH] feat: search the screen you're on and import calendars and
contacts from the desktop app's menus
---
.../calendar-desktop-menu-ux-pim.test.tsx | 86 +++++++++++++++++
components/calendar/calendar-app.tsx | 2 +
.../contacts-desktop-menu-ux-pim.test.tsx | 96 +++++++++++++++++++
components/contacts/contact-list.tsx | 3 +
components/contacts/contacts-app.tsx | 2 +-
.../desktop/__tests__/desktop-host.test.tsx | 30 ++++++
components/desktop/desktop-host.tsx | 16 +++-
.../file-browser-desktop-menu-ux-pim.test.tsx | 90 +++++++++++++++++
components/files/file-browser.tsx | 4 +
lib/app-commands.ts | 2 +-
lib/desktop/languages.ts | 6 ++
locales/ar/common.json | 8 ++
locales/ca/common.json | 8 ++
locales/cs/common.json | 8 ++
locales/da/common.json | 8 ++
locales/de/common.json | 8 ++
locales/en/common.json | 8 ++
locales/es/common.json | 8 ++
locales/et/common.json | 8 ++
locales/fa/common.json | 8 ++
locales/fr/common.json | 8 ++
locales/he/common.json | 8 ++
locales/hu/common.json | 8 ++
locales/it/common.json | 8 ++
locales/ja/common.json | 8 ++
locales/ko/common.json | 8 ++
locales/lv/common.json | 8 ++
locales/mn/common.json | 8 ++
locales/nb/common.json | 8 ++
locales/nl/common.json | 8 ++
locales/pl/common.json | 8 ++
locales/pt/common.json | 8 ++
locales/ro/common.json | 8 ++
locales/ru/common.json | 8 ++
locales/sk/common.json | 8 ++
locales/th/common.json | 8 ++
locales/tr/common.json | 8 ++
locales/uk/common.json | 8 ++
locales/vi/common.json | 8 ++
locales/zh-TW/common.json | 8 ++
locales/zh/common.json | 8 ++
41 files changed, 574 insertions(+), 3 deletions(-)
create mode 100644 components/calendar/__tests__/calendar-desktop-menu-ux-pim.test.tsx
create mode 100644 components/contacts/__tests__/contacts-desktop-menu-ux-pim.test.tsx
create mode 100644 components/files/__tests__/file-browser-desktop-menu-ux-pim.test.tsx
diff --git a/components/calendar/__tests__/calendar-desktop-menu-ux-pim.test.tsx b/components/calendar/__tests__/calendar-desktop-menu-ux-pim.test.tsx
new file mode 100644
index 000000000..0fc99ee3e
--- /dev/null
+++ b/components/calendar/__tests__/calendar-desktop-menu-ux-pim.test.tsx
@@ -0,0 +1,86 @@
+import { act, cleanup, render, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Calendar } from '@/lib/jmap/types';
+import { useCalendarStore } from '@/stores/calendar-store';
+import { useAuthStore } from '@/stores/auth-store';
+import { useSettingsStore } from '@/stores/settings-store';
+import { useTaskStore } from '@/stores/task-store';
+
+// pim-15: the desktop app's File > Import > Calendar…, on the whole calendar page
+// with the shell around it stubbed out.
+
+vi.mock('@/hooks/use-media-query', () => ({
+ useIsMobile: () => false,
+ useIsDesktop: () => true,
+ useIsFullDesktop: () => true,
+ useIsTablet: () => false,
+ useMediaQuery: () => false,
+}));
+vi.mock('@/components/layout/navigation-rail', () => ({ NavigationRail: () => null }));
+vi.mock('@/components/layout/sidebar-apps-modal', () => ({ SidebarAppsModal: () => null }));
+vi.mock('@/components/layout/inline-app-view', () => ({ InlineAppView: () => null }));
+vi.mock('@/components/layout/mv-top-bar', () => ({ MvTopBar: () => null }));
+vi.mock('@/components/plugins/app-top-banner-slot', () => ({ AppTopBannerSlot: () => null }));
+vi.mock('@/hooks/use-refresh-gesture', () => ({ useRefreshGesture: () => ({ indicator: null }) }));
+vi.mock('@/hooks/use-calendar-push-refresh', () => ({ useCalendarPushRefresh: () => {} }));
+vi.mock('@/hooks/use-account-principal-emails', () => ({ useAccountPrincipalEmails: () => [] }));
+vi.mock('../participant-availability', () => ({ ParticipantAvailability: () => null }));
+const config = vi.hoisted(() => ({ demoMode: false }));
+vi.mock('@/hooks/use-config', () => ({ useConfig: () => config }));
+
+import { CalendarApp } from '../calendar-app';
+import { requestAppCommand } from '@/lib/app-commands';
+
+vi.setConfig({ testTimeout: 30000 });
+
+const RIGHTS = {
+ mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true,
+ mayUpdatePrivate: true, mayRSVP: true, mayShare: true, mayDelete: true,
+};
+
+const calendars = [{
+ id: 'cal', name: 'Personal', color: '#123456', isDefault: true, sortOrder: 0, description: null,
+ isSubscribed: true, isVisible: true, includeInAvailability: 'all', defaultAlertsWithTime: null,
+ defaultAlertsWithoutTime: null, timeZone: null, shareWith: null, myRights: RIGHTS,
+}] as unknown as Calendar[];
+
+const client = {
+ getAccountId: () => 'acc',
+ getMaxParticipantsPerEvent: () => null,
+ supportsCalendars: () => true,
+ queryCalendarEvents: vi.fn().mockResolvedValue([]),
+ getCalendarEvent: vi.fn().mockResolvedValue(null),
+ getCalendarTasks: vi.fn().mockResolvedValue([]),
+};
+
+beforeEach(() => {
+ config.demoMode = false;
+ useAuthStore.setState({ isAuthenticated: true, client: client as never, isLoading: false, activeAccountId: 'acc' });
+ useCalendarStore.setState({
+ calendars, events: [], supportsCalendar: true, selectedCalendarIds: ['cal'], viewMode: 'week',
+ selectedDate: new Date(2026, 9, 12), isLoading: false, isLoadingEvents: false, error: null,
+ fetchCalendars: vi.fn(), fetchEvents: vi.fn(), refreshAllSubscriptions: vi.fn(),
+ } as never);
+ useSettingsStore.setState({ enableCalendarTasks: true, proInterface: false } as never);
+ useTaskStore.setState({ tasks: [] } as never);
+});
+
+afterEach(() => cleanup());
+
+describe('Calendar in the desktop menus', () => {
+ it('File > Import > Calendar… opens the calendar import', () => {
+ render();
+ expect(screen.queryByRole('dialog', { name: 'title' })).toBeNull();
+ let taken = false;
+ act(() => { taken = requestAppCommand('import-calendar'); });
+ expect(taken).toBe(true);
+ expect(screen.getByRole('dialog', { name: 'title' })).toBeInTheDocument();
+ });
+
+ it('opens nothing in demo mode, where the toolbar has no import either', () => {
+ config.demoMode = true;
+ render();
+ act(() => { requestAppCommand('import-calendar'); });
+ expect(screen.queryByRole('dialog', { name: 'title' })).toBeNull();
+ });
+});
diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx
index fb964a7d7..34cd703a7 100644
--- a/components/calendar/calendar-app.tsx
+++ b/components/calendar/calendar-app.tsx
@@ -1677,6 +1677,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
useAppCommands({
"new-event": () => openCreateModal(),
"new-task": () => { if (enableCalendarTasks) openCreateTaskModal(); },
+ "import-calendar": () => { if (!demoMode) setShowImportModal(true); },
}, true);
// Its View menu's Day, Week, Month and Agenda, and Edit > Search events.
const viewFromMenu = (mode: CalendarViewMode) => () => { setMobileReturnToMonth(false); leaveSearch(); setViewMode(mode); };
@@ -1693,6 +1694,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
"view.calendar.agenda": { enabled: true, checked: normalizedViewMode === "agenda" },
"edit.search": normalizedViewMode !== "tasks",
"file.newTask": enableCalendarTasks,
+ "file.importCalendar": !demoMode,
});
useRegisterCommands(tSidebar("calendar"), [
diff --git a/components/contacts/__tests__/contacts-desktop-menu-ux-pim.test.tsx b/components/contacts/__tests__/contacts-desktop-menu-ux-pim.test.tsx
new file mode 100644
index 000000000..31d674048
--- /dev/null
+++ b/components/contacts/__tests__/contacts-desktop-menu-ux-pim.test.tsx
@@ -0,0 +1,96 @@
+import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import type { ContactCard } from '@/lib/jmap/types';
+import type { IJMAPClient } from '@/lib/jmap/client-interface';
+import type { BulwarkDesktopBridge } from '@/lib/desktop/types';
+
+// pim-15: the desktop app's Edit > Search contacts and File > Import > Contacts…,
+// on the whole Contacts app with the shell around it stubbed out.
+
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({ push: vi.fn(), back: vi.fn() }),
+ useParams: () => ({ locale: 'en' }),
+ usePathname: () => '/en/contacts',
+ useSearchParams: () => new URLSearchParams(),
+}));
+vi.mock('@/components/contacts/contact-activity', () => ({ ContactActivity: () => null }));
+vi.mock('@/components/layout/navigation-rail', () => ({ NavigationRail: () => }));
+vi.mock('@/components/layout/sidebar-apps-modal', () => ({ SidebarAppsModal: () => null }));
+vi.mock('@/components/layout/inline-app-view', () => ({ InlineAppView: () => null }));
+vi.mock('@/components/layout/mv-top-bar', () => ({ MvTopBar: () => null }));
+vi.mock('@/components/plugins/app-top-banner-slot', () => ({ AppTopBannerSlot: () => null }));
+vi.mock('@/hooks/use-sidebar-apps', () => ({
+ useSidebarApps: () => ({
+ showAppsModal: false, inlineApp: null, loadedApps: [], handleManageApps: vi.fn(),
+ handleInlineApp: vi.fn(), closeInlineApp: vi.fn(), closeAppsModal: vi.fn(),
+ }),
+}));
+
+import { ContactsApp } from '../contacts-app';
+import { requestAppCommand } from '@/lib/app-commands';
+import { resetDesktopCommands, runDesktopCommand } from '@/lib/desktop/commands';
+import { useAuthStore } from '@/stores/auth-store';
+import { useContactStore } from '@/stores/contact-store';
+
+const alice: ContactCard = {
+ id: 'c-alice',
+ addressBookIds: {},
+ name: { components: [{ kind: 'given', value: 'Alice' }, { kind: 'surname', value: 'Smith' }], isOrdered: true },
+ emails: { e0: { address: 'alice@example.com' } },
+};
+
+const FAKE_CLIENT = { supportsPrincipals: () => false } as unknown as IJMAPClient;
+
+function renderContacts() {
+ act(() => {
+ useAuthStore.setState({ isAuthenticated: true, client: FAKE_CLIENT, isLoading: false });
+ useContactStore.setState({
+ contacts: [alice],
+ addressBooks: [],
+ selectedContactId: null,
+ selectedContactIds: new Set(),
+ searchQuery: '',
+ supportsSync: false,
+ contactsLoaded: true,
+ });
+ });
+ return render();
+}
+
+function fakeBridge() {
+ const setState = vi.fn();
+ window.bulwarkDesktop = { version: 1, platform: 'win32', menu: { setState, onCommand: () => () => {} } } as unknown as BulwarkDesktopBridge;
+ return { setState };
+}
+
+afterEach(() => {
+ cleanup();
+ window.bulwarkDesktop = undefined;
+ resetDesktopCommands();
+ act(() => {
+ useAuthStore.setState({ isAuthenticated: false, client: null });
+ useContactStore.setState({ contacts: [], selectedContactId: null, selectedContactIds: new Set() });
+ });
+});
+
+describe('Contacts in the desktop menus', () => {
+ it('Edit > Search contacts is on offer and puts the cursor in the contacts search', async () => {
+ const shell = fakeBridge();
+ renderContacts();
+ await waitFor(() => expect(shell.setState).toHaveBeenCalled());
+ expect(shell.setState.mock.lastCall![0]).toMatchObject({ 'edit.search': true });
+ let ran = false;
+ act(() => { ran = runDesktopCommand('edit.search'); });
+ expect(ran).toBe(true);
+ expect(document.activeElement).toBe(screen.getAllByRole('searchbox', { name: 'search_placeholder' })[0]);
+ });
+
+ it('File > Import > Contacts… opens the contacts import', () => {
+ renderContacts();
+ expect(screen.queryByRole('dialog', { name: 'import.title' })).toBeNull();
+ let taken = false;
+ act(() => { taken = requestAppCommand('import-contacts'); });
+ expect(taken).toBe(true);
+ expect(screen.getByRole('dialog', { name: 'import.title' })).toBeInTheDocument();
+ });
+});
diff --git a/components/contacts/contact-list.tsx b/components/contacts/contact-list.tsx
index c3eef9c7a..57578c81e 100644
--- a/components/contacts/contact-list.tsx
+++ b/components/contacts/contact-list.tsx
@@ -21,6 +21,7 @@ import { getContactDisplayName, getContactSortName, hasContactPhoto } from "@/st
import { useSettingsStore } from "@/stores/settings-store";
import { isAppShortcutBlocked } from "@/lib/keyboard";
import { isSingleKeyShortcutOff } from "@/lib/single-key-shortcuts";
+import { useDesktopCommands } from "@/lib/desktop/commands";
import { ContactRowsSkeleton } from "@/components/boot-skeleton/contacts-skeleton";
type TriState = boolean | null;
@@ -389,6 +390,8 @@ export function ContactList({
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
+ // Edit > Search contacts in the desktop app's menus.
+ useDesktopCommands({ "edit.search": () => searchRef.current?.focus() }, { "edit.search": true });
return (
diff --git a/components/contacts/contacts-app.tsx b/components/contacts/contacts-app.tsx
index 3a3b7b004..8ecf57384 100644
--- a/components/contacts/contacts-app.tsx
+++ b/components/contacts/contacts-app.tsx
@@ -971,7 +971,7 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
};
// File > New contact in the desktop app's menus, from any app.
- useAppCommands({ "new-contact": () => guard(handleCreateNew)() }, contactsEnabled);
+ useAppCommands({ "new-contact": () => guard(handleCreateNew)(), "import-contacts": () => setShowImportDialog(true) }, contactsEnabled);
// The command palette's Contacts group (Ctrl/⌘+K).
useRegisterCommands(tSidebar("contacts"), [
diff --git a/components/desktop/__tests__/desktop-host.test.tsx b/components/desktop/__tests__/desktop-host.test.tsx
index 29839f084..e911fd4c3 100644
--- a/components/desktop/__tests__/desktop-host.test.tsx
+++ b/components/desktop/__tests__/desktop-host.test.tsx
@@ -136,6 +136,36 @@ describe('DesktopHost', () => {
expect(state['section.mail']).toBeUndefined();
});
+ // pim-15: File > Import has Mail…, Calendar… and Contacts….
+ it('File > Import > Calendar… and Contacts… open that app with its import', async () => {
+ const { peekParkedAppCommand } = await import('@/lib/app-commands');
+ const shell = fakeBridge();
+ render();
+ await waitFor(() => expect(shell.setState).toHaveBeenCalled());
+ expect(shell.setState.mock.lastCall![0]).toMatchObject({ 'file.import': true, 'file.importCalendar': true, 'file.importContacts': true });
+ shell.run('file.importCalendar');
+ expect(push).toHaveBeenLastCalledWith('/calendar');
+ expect(peekParkedAppCommand()).toBe('import-calendar');
+ shell.run('file.importContacts');
+ expect(push).toHaveBeenLastCalledWith('/contacts');
+ expect(peekParkedAppCommand()).toBe('import-contacts');
+ });
+
+ // pim-15: Edit > Search searches what's on screen, so mail's search is only Mail's.
+ it.each([
+ ['/', true],
+ ['/settings', true],
+ ['/calendar', false],
+ ['/contacts', false],
+ ['/files', false],
+ ])('on %s, the search in Edit goes to mail: %s', async (pathname, mail) => {
+ route.pathname = pathname;
+ const shell = fakeBridge();
+ render();
+ await waitFor(() => expect(shell.setState).toHaveBeenCalled());
+ expect(shell.setState.mock.lastCall![0]['edit.search']).toBe(mail);
+ });
+
it('runs a command from what\'s on the page first', () => {
const shell = fakeBridge();
render();
diff --git a/components/desktop/desktop-host.tsx b/components/desktop/desktop-host.tsx
index 14930395e..3f5604767 100644
--- a/components/desktop/desktop-host.tsx
+++ b/components/desktop/desktop-host.tsx
@@ -95,6 +95,17 @@ export function DesktopHost() {
routerRef.current.push("/contacts");
},
"file.import": () => openMailImport(),
+ // File > Import > Calendar… and Contacts…: that app's own import, once it's on screen.
+ "file.importCalendar": () => {
+ if (requestAppCommand("import-calendar")) return;
+ parkAppCommand("import-calendar");
+ routerRef.current.push("/calendar");
+ },
+ "file.importContacts": () => {
+ if (requestAppCommand("import-contacts")) return;
+ parkAppCommand("import-contacts");
+ routerRef.current.push("/contacts");
+ },
"go.inbox": () => routerRef.current.push("/"),
"go.mail": () => routerRef.current.push("/"),
"go.calendar": () => routerRef.current.push("/calendar"),
@@ -159,9 +170,12 @@ export function DesktopHost() {
"file.newTask": signedIn,
"file.newContact": signedIn,
"file.import": signedIn && !popout,
+ "file.importCalendar": signedIn && !popout,
+ "file.importContacts": signedIn && !popout,
"file.settings": signedIn,
[`section.${section}`]: true,
- "edit.search": signedIn && !popout,
+ // Search mail. Calendar, Contacts and Files search themselves, and say so while they can.
+ "edit.search": signedIn && !popout && (section === "mail" || section === "settings"),
"go.inbox": signedIn,
"go.mail": signedIn,
"go.calendar": signedIn,
diff --git a/components/files/__tests__/file-browser-desktop-menu-ux-pim.test.tsx b/components/files/__tests__/file-browser-desktop-menu-ux-pim.test.tsx
new file mode 100644
index 000000000..03c5fe7bf
--- /dev/null
+++ b/components/files/__tests__/file-browser-desktop-menu-ux-pim.test.tsx
@@ -0,0 +1,90 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
+import { FileBrowser } from '../file-browser';
+import { useFileStore, type FileResource } from '@/stores/file-store';
+import { resetDesktopCommands, runDesktopCommand } from '@/lib/desktop/commands';
+import type { BulwarkDesktopBridge } from '@/lib/desktop/types';
+import type { IJMAPClient } from '@/lib/jmap/client-interface';
+
+// pim-15: the desktop app's Edit > Search files.
+
+vi.mock('@/lib/debug', () => ({ debug: { log: () => {}, warn: () => {}, error: () => {} } }));
+vi.mock('@/components/settings/share-collection-dialog', () => ({ ShareCollectionDialog: () => null }));
+
+const NOTES = {
+ id: 'f-notes', name: 'notes.txt', serverName: 'notes.txt', isDirectory: false, contentType: 'text/plain',
+ contentLength: 10, lastModified: '2026-10-01T00:00:00Z', blobId: 'b-notes', parentId: null,
+} as FileResource;
+
+function renderBrowser() {
+ const client = { listAllFileNodesAcrossAccounts: vi.fn(async () => []) } as unknown as IJMAPClient;
+ act(() => {
+ useFileStore.setState({ client, sharedRoots: [], allFolders: [], currentParentId: null, pathStack: [{ id: null, name: '' }], hasListed: true });
+ });
+ const noop = vi.fn(async () => {});
+ render(
+ ''}
+ favorites={[]}
+ recentFiles={[]}
+ onToggleFavorite={vi.fn()}
+ onOpenRecent={vi.fn()}
+ client={client}
+ ownAccountId="a"
+ onNavigate={vi.fn()}
+ onSetSelection={vi.fn()}
+ onToggleSelect={vi.fn()}
+ onPreviewFile={vi.fn()}
+ onDownload={noop}
+ onRenameNode={noop}
+ onDeleteNode={noop}
+ onDownloadNode={noop}
+ onRevealNode={noop}
+ onRemoveRecent={vi.fn()}
+ />,
+ );
+}
+
+afterEach(() => {
+ cleanup();
+ window.bulwarkDesktop = undefined;
+ resetDesktopCommands();
+});
+
+describe('Files in the desktop menus', () => {
+ it('Edit > Search files is on offer and puts the cursor in the files search', async () => {
+ const setState = vi.fn();
+ window.bulwarkDesktop = { version: 1, platform: 'win32', menu: { setState, onCommand: () => () => {} } } as unknown as BulwarkDesktopBridge;
+ renderBrowser();
+ await waitFor(() => expect(setState).toHaveBeenCalled());
+ expect(setState.mock.lastCall![0]).toMatchObject({ 'edit.search': true });
+ let ran = false;
+ act(() => { ran = runDesktopCommand('edit.search'); });
+ expect(ran).toBe(true);
+ expect(document.activeElement).toBe(screen.getByRole('searchbox', { name: 'search_placeholder' }));
+ });
+});
diff --git a/components/files/file-browser.tsx b/components/files/file-browser.tsx
index 2b514c5ef..b73dc009e 100644
--- a/components/files/file-browser.tsx
+++ b/components/files/file-browser.tsx
@@ -45,6 +45,7 @@ import type { FileNodeRights } from "@/lib/jmap/types";
import type { FileResourceView } from "@/lib/plugin-types";
import { isAppShortcutBlocked, isArrowKeyWidgetTarget } from "@/lib/keyboard";
import { isSingleKeyShortcutOff } from "@/lib/single-key-shortcuts";
+import { useDesktopCommands } from "@/lib/desktop/commands";
import { FilesListingSkeleton } from "@/components/boot-skeleton/files-skeleton";
const NO_FORMATS: string[] = [];
@@ -754,6 +755,9 @@ export function FileBrowser({
return Math.max(1, tops.filter((top) => top === tops[0]).length);
}, [focusedName]);
+ // Edit > Search files in the desktop app's menus.
+ useDesktopCommands({ "edit.search": () => searchInputRef.current?.focus() }, { "edit.search": !accountPickerMode });
+
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (isTypingTarget(e.target) || isAppShortcutBlocked(e)) return;
diff --git a/lib/app-commands.ts b/lib/app-commands.ts
index 23eba902e..0afb62377 100644
--- a/lib/app-commands.ts
+++ b/lib/app-commands.ts
@@ -8,7 +8,7 @@
import { useEffect, useRef } from 'react';
-export type AppCommand = 'compose' | 'search' | 'all-inboxes' | 'show-list' | 'tour-end' | 'new-event' | 'new-task' | 'new-contact';
+export type AppCommand = 'compose' | 'search' | 'all-inboxes' | 'show-list' | 'tour-end' | 'new-event' | 'new-task' | 'new-contact' | 'import-calendar' | 'import-contacts';
export const COMMAND_PALETTE_OPEN_EVENT = 'bulwark:command-palette';
diff --git a/lib/desktop/languages.ts b/lib/desktop/languages.ts
index cee9d4a84..d531362e0 100644
--- a/lib/desktop/languages.ts
+++ b/lib/desktop/languages.ts
@@ -126,6 +126,12 @@ export function shellLabels(t: (key: string, values?: Record) =>
menuNewContact: t('contacts.create_new'),
menuImport: t('mail_import.menu_import'),
menuSearchEvents: t('calendar.search.title'),
+ menuSearchContacts: t('ux_pim_menu.search_contacts'),
+ menuSearchFiles: t('ux_pim_menu.search_files'),
+ menuImportMenu: t('ux_pim_menu.import'),
+ menuImportMail: t('ux_pim_menu.import_mail'),
+ menuImportCalendar: t('ux_pim_menu.import_calendar'),
+ menuImportContacts: t('ux_pim_menu.import_contacts'),
menuDay: t('calendar.views.day'),
menuWeek: t('calendar.views.week'),
menuMonth: t('calendar.views.month'),
diff --git a/locales/ar/common.json b/locales/ar/common.json
index f8800991e..4965f8a6a 100644
--- a/locales/ar/common.json
+++ b/locales/ar/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "تعذّر نسخ الملف.",
"reply_lost": "تعذّر الاحتفاظ بالرد الذي كتبته. افتح الرسالة وأجب من هناك."
},
+ "ux_pim_menu": {
+ "search_contacts": "البحث في جهات الاتصال",
+ "search_files": "البحث في الملفات",
+ "import": "استيراد",
+ "import_mail": "البريد…",
+ "import_calendar": "التقويم…",
+ "import_contacts": "جهات الاتصال…"
+ },
"desktopEdit": {
"edit": "تعديل",
"edit_named": "تعديل {name}",
diff --git a/locales/ca/common.json b/locales/ca/common.json
index 62c75d062..1aba82bc5 100644
--- a/locales/ca/common.json
+++ b/locales/ca/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "No s'ha pogut copiar el fitxer.",
"reply_lost": "No s'ha pogut conservar la resposta que heu escrit. Obriu el missatge i responeu des d'allà."
},
+ "ux_pim_menu": {
+ "search_contacts": "Cerca contactes",
+ "search_files": "Cerca fitxers",
+ "import": "Importa",
+ "import_mail": "Correu…",
+ "import_calendar": "Calendari…",
+ "import_contacts": "Contactes…"
+ },
"desktopEdit": {
"edit": "Edita",
"edit_named": "Edita {name}",
diff --git a/locales/cs/common.json b/locales/cs/common.json
index cd765731a..e5b6cd20f 100644
--- a/locales/cs/common.json
+++ b/locales/cs/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Soubor se nepodařilo zkopírovat.",
"reply_lost": "Napsanou odpověď se nepodařilo zachovat. Otevřete zprávu a odpovězte v ní."
},
+ "ux_pim_menu": {
+ "search_contacts": "Hledat kontakty",
+ "search_files": "Hledat soubory",
+ "import": "Importovat",
+ "import_mail": "Pošta…",
+ "import_calendar": "Kalendář…",
+ "import_contacts": "Kontakty…"
+ },
"desktopEdit": {
"edit": "Upravit",
"edit_named": "Upravit {name}",
diff --git a/locales/da/common.json b/locales/da/common.json
index 68c3ace47..5e293f314 100644
--- a/locales/da/common.json
+++ b/locales/da/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Filen kunne ikke kopieres.",
"reply_lost": "Dit skrevne svar kunne ikke gemmes. Åbn beskeden, og svar der."
},
+ "ux_pim_menu": {
+ "search_contacts": "Søg i kontakter",
+ "search_files": "Søg i filer",
+ "import": "Importér",
+ "import_mail": "Mail…",
+ "import_calendar": "Kalender…",
+ "import_contacts": "Kontakter…"
+ },
"desktopEdit": {
"edit": "Rediger",
"edit_named": "Rediger {name}",
diff --git a/locales/de/common.json b/locales/de/common.json
index 6acbe0328..d955244cd 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Die Datei konnte nicht kopiert werden.",
"reply_lost": "Ihre eingegebene Antwort konnte nicht übernommen werden. Öffnen Sie die Nachricht und antworten Sie dort."
},
+ "ux_pim_menu": {
+ "search_contacts": "Kontakte durchsuchen",
+ "search_files": "Dateien durchsuchen",
+ "import": "Importieren",
+ "import_mail": "E-Mails…",
+ "import_calendar": "Kalender…",
+ "import_contacts": "Kontakte…"
+ },
"desktopEdit": {
"edit": "Bearbeiten",
"edit_named": "{name} bearbeiten",
diff --git a/locales/en/common.json b/locales/en/common.json
index 9fe28226c..591ba7904 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "The file could not be copied.",
"reply_lost": "Your typed reply could not be kept. Open the message and reply there."
},
+ "ux_pim_menu": {
+ "search_contacts": "Search contacts",
+ "search_files": "Search files",
+ "import": "Import",
+ "import_mail": "Mail…",
+ "import_calendar": "Calendar…",
+ "import_contacts": "Contacts…"
+ },
"desktopEdit": {
"edit": "Edit",
"edit_named": "Edit {name}",
diff --git a/locales/es/common.json b/locales/es/common.json
index e1f5a5ebe..03e186eb7 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "No se pudo copiar el archivo.",
"reply_lost": "No se pudo conservar la respuesta que escribiste. Abre el mensaje y responde desde allí."
},
+ "ux_pim_menu": {
+ "search_contacts": "Buscar contactos",
+ "search_files": "Buscar archivos",
+ "import": "Importar",
+ "import_mail": "Correo…",
+ "import_calendar": "Calendario…",
+ "import_contacts": "Contactos…"
+ },
"desktopEdit": {
"edit": "Editar",
"edit_named": "Editar {name}",
diff --git a/locales/et/common.json b/locales/et/common.json
index f77713d19..f14c30a80 100644
--- a/locales/et/common.json
+++ b/locales/et/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Faili ei saanud kopeerida.",
"reply_lost": "Kirjutatud vastust ei saanud säilitada. Ava kiri ja vasta seal."
},
+ "ux_pim_menu": {
+ "search_contacts": "Otsi kontakte",
+ "search_files": "Otsi faile",
+ "import": "Impordi",
+ "import_mail": "Post…",
+ "import_calendar": "Kalender…",
+ "import_contacts": "Kontaktid…"
+ },
"desktopEdit": {
"edit": "Muuda",
"edit_named": "Muuda faili {name}",
diff --git a/locales/fa/common.json b/locales/fa/common.json
index 36f5febbd..471b5131f 100644
--- a/locales/fa/common.json
+++ b/locales/fa/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "رونوشت پرونده ممکن نشد.",
"reply_lost": "پاسخی که نوشتید نگه داشته نشد. پیام را باز کنید و از آنجا پاسخ دهید."
},
+ "ux_pim_menu": {
+ "search_contacts": "جستجوی مخاطبین",
+ "search_files": "جستجوی پروندهها",
+ "import": "وارد کردن",
+ "import_mail": "نامه…",
+ "import_calendar": "تقویم…",
+ "import_contacts": "مخاطبین…"
+ },
"desktopEdit": {
"edit": "ویرایش",
"edit_named": "ویرایش {name}",
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 96ad6b352..c54e48712 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Le fichier n'a pas pu être copié.",
"reply_lost": "Votre réponse saisie n'a pas pu être conservée. Ouvrez le message et répondez depuis celui-ci."
},
+ "ux_pim_menu": {
+ "search_contacts": "Rechercher des contacts",
+ "search_files": "Rechercher des fichiers",
+ "import": "Importer",
+ "import_mail": "Courrier…",
+ "import_calendar": "Calendrier…",
+ "import_contacts": "Contacts…"
+ },
"desktopEdit": {
"edit": "Modifier",
"edit_named": "Modifier {name}",
diff --git a/locales/he/common.json b/locales/he/common.json
index 8190e738d..4b38b5f7e 100644
--- a/locales/he/common.json
+++ b/locales/he/common.json
@@ -7262,6 +7262,14 @@
"copy_failed": "לא ניתן להעתיק את הקובץ.",
"reply_lost": "לא ניתן היה לשמור את התשובה שהקלדת. פתחו את ההודעה והשיבו משם."
},
+ "ux_pim_menu": {
+ "search_contacts": "חיפוש אנשי קשר",
+ "search_files": "חיפוש קבצים",
+ "import": "ייבוא",
+ "import_mail": "דואר…",
+ "import_calendar": "לוח שנה…",
+ "import_contacts": "אנשי קשר…"
+ },
"desktopEdit": {
"edit": "עריכה",
"edit_named": "עריכת {name}",
diff --git a/locales/hu/common.json b/locales/hu/common.json
index 9d7abd079..4727b4027 100644
--- a/locales/hu/common.json
+++ b/locales/hu/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "A fájlt nem sikerült másolni.",
"reply_lost": "A beírt válasz nem maradt meg. Nyissa meg az üzenetet, és válaszoljon ott."
},
+ "ux_pim_menu": {
+ "search_contacts": "Névjegyek keresése",
+ "search_files": "Fájlok keresése",
+ "import": "Importálás",
+ "import_mail": "Levelek…",
+ "import_calendar": "Naptár…",
+ "import_contacts": "Névjegyek…"
+ },
"desktopEdit": {
"edit": "Szerkesztés",
"edit_named": "{name} szerkesztése",
diff --git a/locales/it/common.json b/locales/it/common.json
index c230dd475..5aa0823e7 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Impossibile copiare il file.",
"reply_lost": "Non è stato possibile conservare la risposta digitata. Apri il messaggio e rispondi da lì."
},
+ "ux_pim_menu": {
+ "search_contacts": "Cerca contatti",
+ "search_files": "Cerca file",
+ "import": "Importa",
+ "import_mail": "Posta…",
+ "import_calendar": "Calendario…",
+ "import_contacts": "Contatti…"
+ },
"desktopEdit": {
"edit": "Modifica",
"edit_named": "Modifica {name}",
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 859b086d6..69f2466fb 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "ファイルをコピーできませんでした。",
"reply_lost": "入力した返信を保持できませんでした。メッセージを開いて返信してください。"
},
+ "ux_pim_menu": {
+ "search_contacts": "連絡先を検索",
+ "search_files": "ファイルを検索",
+ "import": "インポート",
+ "import_mail": "メール…",
+ "import_calendar": "カレンダー…",
+ "import_contacts": "連絡先…"
+ },
"desktopEdit": {
"edit": "編集",
"edit_named": "{name} を編集",
diff --git a/locales/ko/common.json b/locales/ko/common.json
index b26122750..00b51231e 100644
--- a/locales/ko/common.json
+++ b/locales/ko/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "파일을 복사할 수 없습니다.",
"reply_lost": "입력한 답장을 유지할 수 없습니다. 메시지를 열어 답장하세요."
},
+ "ux_pim_menu": {
+ "search_contacts": "연락처 검색",
+ "search_files": "파일 검색",
+ "import": "가져오기",
+ "import_mail": "메일…",
+ "import_calendar": "캘린더…",
+ "import_contacts": "연락처…"
+ },
"desktopEdit": {
"edit": "편집",
"edit_named": "{name} 편집",
diff --git a/locales/lv/common.json b/locales/lv/common.json
index 6474a45eb..42e853ab5 100644
--- a/locales/lv/common.json
+++ b/locales/lv/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Failu neizdevās nokopēt.",
"reply_lost": "Ievadīto atbildi neizdevās saglabāt. Atveriet ziņojumu un atbildiet tur."
},
+ "ux_pim_menu": {
+ "search_contacts": "Meklēt kontaktus",
+ "search_files": "Meklēt failus",
+ "import": "Importēt",
+ "import_mail": "Pasts…",
+ "import_calendar": "Kalendārs…",
+ "import_contacts": "Kontakti…"
+ },
"desktopEdit": {
"edit": "Rediģēt",
"edit_named": "Rediģēt {name}",
diff --git a/locales/mn/common.json b/locales/mn/common.json
index 49f47d310..b36145431 100644
--- a/locales/mn/common.json
+++ b/locales/mn/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Файлыг хуулж чадсангүй.",
"reply_lost": "Бичсэн хариуг хадгалж чадсангүй. Захиаг нээгээд тэндээс хариулна уу."
},
+ "ux_pim_menu": {
+ "search_contacts": "Харилцагч хайх",
+ "search_files": "Файл хайх",
+ "import": "Импортлох",
+ "import_mail": "Шуудан…",
+ "import_calendar": "Хуанли…",
+ "import_contacts": "Харилцагчид…"
+ },
"desktopEdit": {
"edit": "Засах",
"edit_named": "{name} засах",
diff --git a/locales/nb/common.json b/locales/nb/common.json
index 6c54c14c6..3b1218a5b 100644
--- a/locales/nb/common.json
+++ b/locales/nb/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Filen kunne ikke kopieres.",
"reply_lost": "Svaret du skrev, kunne ikke beholdes. Åpne meldingen og svar der."
},
+ "ux_pim_menu": {
+ "search_contacts": "Søk i kontakter",
+ "search_files": "Søk i filer",
+ "import": "Importer",
+ "import_mail": "E-post…",
+ "import_calendar": "Kalender…",
+ "import_contacts": "Kontakter…"
+ },
"desktopEdit": {
"edit": "Rediger",
"edit_named": "Rediger {name}",
diff --git a/locales/nl/common.json b/locales/nl/common.json
index caa6592d6..dfd19e628 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Het bestand kon niet worden gekopieerd.",
"reply_lost": "Je getypte antwoord kon niet worden bewaard. Open het bericht en antwoord daar."
},
+ "ux_pim_menu": {
+ "search_contacts": "Contacten zoeken",
+ "search_files": "Bestanden zoeken",
+ "import": "Importeren",
+ "import_mail": "E-mail…",
+ "import_calendar": "Agenda…",
+ "import_contacts": "Contacten…"
+ },
"desktopEdit": {
"edit": "Bewerken",
"edit_named": "{name} bewerken",
diff --git a/locales/pl/common.json b/locales/pl/common.json
index a42440af6..98b199c2d 100644
--- a/locales/pl/common.json
+++ b/locales/pl/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Nie udało się skopiować pliku.",
"reply_lost": "Nie udało się zachować wpisanej odpowiedzi. Otwórz wiadomość i odpowiedz w niej."
},
+ "ux_pim_menu": {
+ "search_contacts": "Szukaj kontaktów",
+ "search_files": "Szukaj plików",
+ "import": "Importuj",
+ "import_mail": "Poczta…",
+ "import_calendar": "Kalendarz…",
+ "import_contacts": "Kontakty…"
+ },
"desktopEdit": {
"edit": "Edytuj",
"edit_named": "Edytuj {name}",
diff --git a/locales/pt/common.json b/locales/pt/common.json
index bd73fa852..e4d16bbf7 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Não foi possível copiar o ficheiro.",
"reply_lost": "Não foi possível guardar a resposta escrita. Abra a mensagem e responda a partir dela."
},
+ "ux_pim_menu": {
+ "search_contacts": "Pesquisar contactos",
+ "search_files": "Pesquisar ficheiros",
+ "import": "Importar",
+ "import_mail": "Correio…",
+ "import_calendar": "Calendário…",
+ "import_contacts": "Contactos…"
+ },
"desktopEdit": {
"edit": "Editar",
"edit_named": "Editar {name}",
diff --git a/locales/ro/common.json b/locales/ro/common.json
index ad9938c1a..afc3428bc 100644
--- a/locales/ro/common.json
+++ b/locales/ro/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Fișierul nu a putut fi copiat.",
"reply_lost": "Răspunsul scris nu a putut fi păstrat. Deschide mesajul și răspunde de acolo."
},
+ "ux_pim_menu": {
+ "search_contacts": "Caută contacte",
+ "search_files": "Caută fișiere",
+ "import": "Importă",
+ "import_mail": "E-mail…",
+ "import_calendar": "Calendar…",
+ "import_contacts": "Contacte…"
+ },
"desktopEdit": {
"edit": "Editează",
"edit_named": "Editează {name}",
diff --git a/locales/ru/common.json b/locales/ru/common.json
index e32676136..b8f899d23 100644
--- a/locales/ru/common.json
+++ b/locales/ru/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Не удалось скопировать файл.",
"reply_lost": "Не удалось сохранить введённый ответ. Откройте письмо и ответьте из него."
},
+ "ux_pim_menu": {
+ "search_contacts": "Поиск контактов",
+ "search_files": "Поиск файлов",
+ "import": "Импорт",
+ "import_mail": "Почта…",
+ "import_calendar": "Календарь…",
+ "import_contacts": "Контакты…"
+ },
"desktopEdit": {
"edit": "Изменить",
"edit_named": "Изменить {name}",
diff --git a/locales/sk/common.json b/locales/sk/common.json
index 3da1ed6c4..128a49daf 100644
--- a/locales/sk/common.json
+++ b/locales/sk/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Súbor sa nepodarilo skopírovať.",
"reply_lost": "Napísanú odpoveď sa nepodarilo zachovať. Otvorte správu a odpovedzte v nej."
},
+ "ux_pim_menu": {
+ "search_contacts": "Hľadať kontakty",
+ "search_files": "Hľadať súbory",
+ "import": "Importovať",
+ "import_mail": "Pošta…",
+ "import_calendar": "Kalendár…",
+ "import_contacts": "Kontakty…"
+ },
"desktopEdit": {
"edit": "Upraviť",
"edit_named": "Upraviť {name}",
diff --git a/locales/th/common.json b/locales/th/common.json
index d8fbce3f9..21d534e6b 100644
--- a/locales/th/common.json
+++ b/locales/th/common.json
@@ -7269,6 +7269,14 @@
"copy_failed": "คัดลอกไฟล์ไม่ได้",
"reply_lost": "ไม่สามารถเก็บคำตอบที่พิมพ์ไว้ได้ เปิดข้อความแล้วตอบจากที่นั่น"
},
+ "ux_pim_menu": {
+ "search_contacts": "ค้นหาผู้ติดต่อ",
+ "search_files": "ค้นหาไฟล์",
+ "import": "นำเข้า",
+ "import_mail": "อีเมล…",
+ "import_calendar": "ปฏิทิน…",
+ "import_contacts": "ผู้ติดต่อ…"
+ },
"desktopEdit": {
"edit": "แก้ไข",
"edit_named": "แก้ไข {name}",
diff --git a/locales/tr/common.json b/locales/tr/common.json
index 59e4b73db..5bb93e17c 100644
--- a/locales/tr/common.json
+++ b/locales/tr/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Dosya kopyalanamadı.",
"reply_lost": "Yazdığınız yanıt korunamadı. İletiyi açıp oradan yanıtlayın."
},
+ "ux_pim_menu": {
+ "search_contacts": "Kişilerde ara",
+ "search_files": "Dosyalarda ara",
+ "import": "İçe aktar",
+ "import_mail": "E-posta…",
+ "import_calendar": "Takvim…",
+ "import_contacts": "Kişiler…"
+ },
"desktopEdit": {
"edit": "Düzenle",
"edit_named": "{name} dosyasını düzenle",
diff --git a/locales/uk/common.json b/locales/uk/common.json
index be354b49c..aa0eb143e 100644
--- a/locales/uk/common.json
+++ b/locales/uk/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "Не вдалося скопіювати файл.",
"reply_lost": "Не вдалося зберегти введену відповідь. Відкрийте лист і дайте відповідь у ньому."
},
+ "ux_pim_menu": {
+ "search_contacts": "Пошук контактів",
+ "search_files": "Пошук файлів",
+ "import": "Імпорт",
+ "import_mail": "Пошта…",
+ "import_calendar": "Календар…",
+ "import_contacts": "Контакти…"
+ },
"desktopEdit": {
"edit": "Редагувати",
"edit_named": "Редагувати {name}",
diff --git a/locales/vi/common.json b/locales/vi/common.json
index fe1be93e4..f90df059a 100644
--- a/locales/vi/common.json
+++ b/locales/vi/common.json
@@ -7273,6 +7273,14 @@
"copy_failed": "Không thể sao chép tệp.",
"reply_lost": "Không thể giữ câu trả lời bạn đã nhập. Hãy mở thư và trả lời ở đó."
},
+ "ux_pim_menu": {
+ "search_contacts": "Tìm liên hệ",
+ "search_files": "Tìm tệp",
+ "import": "Nhập",
+ "import_mail": "Thư…",
+ "import_calendar": "Lịch…",
+ "import_contacts": "Danh bạ…"
+ },
"desktopEdit": {
"edit": "Chỉnh sửa",
"edit_named": "Chỉnh sửa {name}",
diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json
index 205e145f6..3e5f835da 100644
--- a/locales/zh-TW/common.json
+++ b/locales/zh-TW/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "無法複製檔案。",
"reply_lost": "無法保留您輸入的回覆。請開啟郵件並在其中回覆。"
},
+ "ux_pim_menu": {
+ "search_contacts": "搜尋聯絡人",
+ "search_files": "搜尋檔案",
+ "import": "匯入",
+ "import_mail": "郵件…",
+ "import_calendar": "行事曆…",
+ "import_contacts": "聯絡人…"
+ },
"desktopEdit": {
"edit": "編輯",
"edit_named": "編輯 {name}",
diff --git a/locales/zh/common.json b/locales/zh/common.json
index 0e601ed20..ae11bb1fe 100644
--- a/locales/zh/common.json
+++ b/locales/zh/common.json
@@ -7261,6 +7261,14 @@
"copy_failed": "无法复制文件。",
"reply_lost": "无法保留你输入的回复。请打开邮件并在其中回复。"
},
+ "ux_pim_menu": {
+ "search_contacts": "搜索联系人",
+ "search_files": "搜索文件",
+ "import": "导入",
+ "import_mail": "邮件…",
+ "import_calendar": "日历…",
+ "import_contacts": "联系人…"
+ },
"desktopEdit": {
"edit": "编辑",
"edit_named": "编辑 {name}",