diff --git a/components/contacts/__tests__/contact-list.test.tsx b/components/contacts/__tests__/contact-list.test.tsx index 915a1e3f8..ffdc35d84 100644 --- a/components/contacts/__tests__/contact-list.test.tsx +++ b/components/contacts/__tests__/contact-list.test.tsx @@ -193,4 +193,17 @@ describe('ContactList', () => { expect(onComposeContact).toHaveBeenCalledWith(alice); expect(window.location.href).toBe(href); }); + + it("moves a contact to another address book from its right-click menu, and the selection from the bulk bar", () => { + const onMoveContacts = vi.fn(); + const { unmount } = render(); + fireEvent.contextMenu(screen.getByText('Alice Smith')); + fireEvent.click(screen.getByText('address_books.move_to')); + expect(onMoveContacts).toHaveBeenCalledWith(['1']); + unmount(); + + render(); + fireEvent.click(screen.getByRole('button', { name: /address_books.move_to/ })); + expect(onMoveContacts).toHaveBeenLastCalledWith(['1', '2']); + }); }); diff --git a/components/contacts/__tests__/contacts-app.test.tsx b/components/contacts/__tests__/contacts-app.test.tsx index 76d8e3578..121c1edef 100644 --- a/components/contacts/__tests__/contacts-app.test.tsx +++ b/components/contacts/__tests__/contacts-app.test.tsx @@ -28,6 +28,7 @@ vi.mock('@/hooks/use-sidebar-apps', () => ({ import { ContactsApp } from '../contacts-app'; import { useAuthStore } from '@/stores/auth-store'; import { useContactStore } from '@/stores/contact-store'; +import { useToastStore } from '@/stores/toast-store'; export const alice: ContactCard = { id: 'c-alice', @@ -151,4 +152,19 @@ describe('ContactsApp', () => { fireEvent.keyDown(dialog, { key: 'Escape' }); expect(screen.queryByRole('dialog', { name: 'import.title' })).toBeNull(); }); + + it('deletes a category without asking and brings it back on Undo', async () => { + const tagged = { ...alice, keywords: { Friends: true } }; + renderContacts([tagged, bob]); + fireEvent.click(screen.getAllByRole('button', { name: 'open_categories' })[0]); + fireEvent.click(within(drawer()).getByRole('button', { name: 'row_actions' })); + fireEvent.click(screen.getByText('delete_category')); + + await waitFor(() => expect(useContactStore.getState().contacts.find((c) => c.id === 'c-alice')?.keywords?.Friends).toBeUndefined()); + expect(screen.queryByRole('alertdialog')).toBeNull(); + const toastState = useToastStore.getState().toasts.at(-1)!; + expect(toastState.title).toBe('category_deleted'); + await act(async () => { toastState.action!.onClick(); }); + await waitFor(() => expect(useContactStore.getState().contacts.find((c) => c.id === 'c-alice')?.keywords?.Friends).toBe(true)); + }); }); diff --git a/components/contacts/__tests__/contacts-sidebar.test.tsx b/components/contacts/__tests__/contacts-sidebar.test.tsx index 11a15b9a3..30b3f73a2 100644 --- a/components/contacts/__tests__/contacts-sidebar.test.tsx +++ b/components/contacts/__tests__/contacts-sidebar.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { act, render, screen, within } from '@testing-library/react'; import { fireEvent } from '@testing-library/dom'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ContactsSidebar } from '../contacts-sidebar'; @@ -111,3 +111,55 @@ describe('ContactsSidebar - set default address book', () => { expect(screen.queryByText('address_books.set_default')).not.toBeInTheDocument(); }); }); + +describe('ContactsSidebar — row menus without a right-click', () => { + const carol = { + id: 'c1', + addressBookIds: {}, + name: { full: 'Carol' }, + keywords: { Friends: true }, + } as unknown as ContactCard; + + function renderRows(onDeleteKeyword = vi.fn(), onRenameKeyword = vi.fn()) { + render( + , + ); + return { onDeleteKeyword, onRenameKeyword }; + } + + it('opens a category menu from its ⋮, with Delete category', () => { + const { onDeleteKeyword } = renderRows(); + const row = screen.getByText('Friends').closest('div')!; + fireEvent.click(within(row).getByRole('button', { name: 'row_actions' })); + expect(screen.getByText('rename_category')).toBeInTheDocument(); + fireEvent.click(screen.getByText('delete_category')); + expect(onDeleteKeyword).toHaveBeenCalledWith('Friends'); + }); + + it('opens a group menu on long-press', () => { + vi.useFakeTimers(); + try { + renderRows(); + const row = screen.getByText('Team').closest('div')!; + const touchStart = new Event('touchstart', { bubbles: true }); + Object.defineProperty(touchStart, 'touches', { value: [{ clientX: 5, clientY: 5 }] }); + act(() => { row.dispatchEvent(touchStart); }); + act(() => { vi.advanceTimersByTime(600); }); + expect(screen.getByText('groups.edit')).toBeInTheDocument(); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/components/contacts/address-book-picker-dialog.tsx b/components/contacts/address-book-picker-dialog.tsx new file mode 100644 index 000000000..96843f7a1 --- /dev/null +++ b/components/contacts/address-book-picker-dialog.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { Book, Check } from "@/components/icons"; +import { Button } from "@/components/ui/button"; +import { Dialog } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import type { AddressBook } from "@/lib/jmap/types"; + +interface AddressBookPickerDialogProps { + isOpen: boolean; + onClose: () => void; + /** The books contacts can be moved to (writable ones). */ + addressBooks: AddressBook[]; + /** The book the contacts are in now, when they are all in one. */ + currentId?: string | null; + count: number; + onPick: (book: AddressBook) => void; +} + +/** "Move to…" for contacts: pick the address book they go to. */ +export function AddressBookPickerDialog({ isOpen, onClose, addressBooks, currentId, count, onPick }: AddressBookPickerDialogProps) { + const t = useTranslations("contacts"); + const [selected, setSelected] = useState(null); + const choice = addressBooks.find((b) => b.id === selected && b.id !== currentId) ?? null; + + return ( + { + e.preventDefault(); + if (!choice) return; + onPick(choice); + onClose(); + }} + footer={ + <> + + + + } + > +
+ {addressBooks.map((book) => { + const isCurrent = book.id === currentId; + const isSelected = selected === book.id; + return ( + + ); + })} +
+
+ ); +} diff --git a/components/contacts/contact-context-menu.tsx b/components/contacts/contact-context-menu.tsx index 67c445e5e..85120125f 100644 --- a/components/contacts/contact-context-menu.tsx +++ b/components/contacts/contact-context-menu.tsx @@ -18,6 +18,7 @@ import { Trash2, Copy, Printer, + FolderInput, } from "@/components/icons"; import type { ContactCard } from "@/lib/jmap/types"; import { getContactPrimaryEmail } from "@/stores/contact-store"; @@ -50,6 +51,9 @@ interface ContactContextMenuProps { /** Writes to the contact in the app's composer. */ onCompose?: () => void; onDuplicate?: () => void; + /** Move to another address book; undefined where there is none to move to. */ + onMoveTo?: () => void; + onBatchMoveTo?: () => void; onBatchExport?: () => void; onBatchAddToGroup?: () => void; onBatchDelete?: () => void; @@ -69,6 +73,8 @@ export function ContactContextMenu({ onAddToGroup, onCompose, onDuplicate, + onMoveTo, + onBatchMoveTo, onBatchExport, onBatchAddToGroup, onBatchDelete, @@ -121,6 +127,9 @@ export function ContactContextMenu({ onClick={handle(() => onBatchAddToGroup?.())} disabled={!onBatchAddToGroup} /> + {onBatchMoveTo && ( + + )} )} + {onMoveTo && ( + + )} void; onAddToGroup?: () => void; onDuplicate?: () => void; + /** Move to another address book; undefined where there is none to move to. */ + onMoveTo?: () => void; /** Compose an email to this contact in the app (no OS mailto handoff). */ onCompose?: () => void; isMobile?: boolean; @@ -138,7 +140,7 @@ function formatDate(dateInput: AnniversaryDate): string { return dateStr; } -export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDuplicate, onCompose, isMobile, className }: ContactDetailProps) { +export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDuplicate, onMoveTo, onCompose, isMobile, className }: ContactDetailProps) { const t = useTranslations("contacts"); const contextLabel = (key: string) => (CONTEXT_LABEL_KEYS[key] ? t(CONTEXT_LABEL_KEYS[key]) : key); const featureLabel = (key: string) => (PHONE_FEATURE_LABEL_KEYS[key] ? t(PHONE_FEATURE_LABEL_KEYS[key]) : key); @@ -187,6 +189,9 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli if (onDuplicate) { moreItems.push({ icon: Copy, label: t("context_menu.duplicate"), onClick: onDuplicate }); } + if (onMoveTo) { + moreItems.push({ icon: FolderInput, label: t("address_books.move_to"), onClick: onMoveTo }); + } moreItems.push({ icon: Download, label: t("context_menu.export_vcard"), onClick: handleExport, disabled: exporting }); moreItems.push({ icon: Printer, label: t("context_menu.print"), onClick: handlePrint }); moreItems.push({ separator: true }); diff --git a/components/contacts/contact-list.tsx b/components/contacts/contact-list.tsx index 0428cc0b5..f99e2c976 100644 --- a/components/contacts/contact-list.tsx +++ b/components/contacts/contact-list.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { useTranslations, useLocale } from "next-intl"; -import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu, ArrowDownAZ, Loader2 } from "@/components/icons"; +import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu, ArrowDownAZ, Loader2, FolderInput } from "@/components/icons"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { ContactListItem } from "./contact-list-item"; @@ -116,6 +116,8 @@ interface ContactListProps { onAddContactToGroup: (id: string) => void; /** Opens the app's composer to the contact. */ onComposeContact?: (contact: ContactCard) => void; + /** Move contacts to another address book; undefined where there is none. */ + onMoveContacts?: (ids: string[]) => void; /** Show a burger button at the start that opens the (overlay) categories sidebar. */ onMenuClick?: () => void; /** The first fetch is still running: show placeholders, not "No contacts yet". */ @@ -142,6 +144,7 @@ export function ContactList({ onDeleteContact, onAddContactToGroup, onComposeContact, + onMoveContacts, onMenuClick, isLoading = false, }: ContactListProps) { @@ -515,6 +518,12 @@ export function ContactList({ {t("bulk.add_to_group")} + {onMoveContacts && ( + + )} + onOpenMenu={(anchor) => openGroupMenuFrom(anchor, group, "end")} + menuLabel={t("row_actions", { name: groupName })} + /> ); })} @@ -576,7 +582,9 @@ export function ContactsSidebar({ isActive={isActive} onSelect={() => onSelectCategory({ keyword })} onDropContacts={onDropContactsToCategory} - onContextMenu={onRenameKeyword ? (e) => openKeywordContextMenu(e, keyword) : undefined} + onContextMenu={hasKeywordMenu ? (e) => openKeywordContextMenu(e, keyword) : undefined} + onOpenMenu={hasKeywordMenu ? (anchor) => openKeywordMenuFrom(anchor, keyword, "end") : undefined} + menuLabel={t("row_actions", { name: keyword })} /> ); })} @@ -623,7 +631,7 @@ export function ContactsSidebar({ contactCount={contactCountByBook[book.id] || 0} onSelect={() => onSelectCategory({ addressBookId: book.id })} onDropContacts={onDropContacts} - onContextMenu={hasBookMenu ? (e) => openBookContextMenu(e, book) : undefined} + {...bookMenu(book)} /> ))} @@ -705,22 +713,37 @@ export function ContactsSidebar({ })()} {/* Keyword (category) context menu */} - {keywordContextMenu.data && onRenameKeyword && ( + {keywordContextMenu.data && hasKeywordMenu && ( - { - const kw = keywordContextMenu.data!; - closeKeywordContextMenu(); - onRenameKeyword(kw); - }} - /> + {onRenameKeyword && ( + { + const kw = keywordContextMenu.data!; + closeKeywordContextMenu(); + onRenameKeyword(kw); + }} + /> + )} + {onRenameKeyword && onDeleteKeyword && } + {onDeleteKeyword && ( + { + const kw = keywordContextMenu.data!; + closeKeywordContextMenu(); + onDeleteKeyword(kw); + }} + /> + )} )} @@ -771,6 +794,83 @@ export function ContactsSidebar({ ); } +const ROW_STYLE = { paddingBlock: "var(--density-sidebar-py, 4px)", minHeight: "32px" } as const; + +interface RowMenuProps { + /** Right-click: the menu at the pointer. */ + onContextMenu?: (e: React.MouseEvent) => void; + /** The ⋮, long-press and the menu key: the menu under the element. */ + onOpenMenu?: (anchor: HTMLElement) => void; + menuLabel?: string; +} + +/** + * A sidebar row: the row itself selects, a ⋮ at its end (shown on hover and + * focus, always on touch) opens its menu, as do right-click, long-press and + * Shift+F10 or the Menu key (R6, R7). + */ +function SidebarRow({ + isActive, isDragOver, icon, label, count, extra, onSelect, onContextMenu, onOpenMenu, menuLabel, dropProps, rowProps, +}: RowMenuProps & { + isActive: boolean; + isDragOver?: boolean; + icon: React.ReactNode; + label: string; + count: number; + extra?: React.ReactNode; + onSelect: () => void; + dropProps?: React.HTMLAttributes; + rowProps?: Record; +}) { + const rowRef = useRef(null); + const longPress = useLongPress(() => { + if (rowRef.current && onOpenMenu) onOpenMenu(rowRef.current); + }, !!onOpenMenu); + return ( +
+ + {onOpenMenu && menuLabel && ( + onOpenMenu(e.currentTarget)} + className="shrink-0 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100" + > + + + )} +
+ ); +} + function CategoryItem({ keyword, count, @@ -778,17 +878,18 @@ function CategoryItem({ onSelect, onDropContacts, onContextMenu, -}: { + onOpenMenu, + menuLabel, +}: RowMenuProps & { keyword: string; count: number; isActive: boolean; onSelect: () => void; onDropContacts?: (contactIds: string[], keyword: string) => void; - onContextMenu?: (e: React.MouseEvent) => void; }) { const [isDragOver, setIsDragOver] = useState(false); - const handleDragOver = useCallback((e: DragEvent) => { + const handleDragOver = useCallback((e: DragEvent) => { if (!e.dataTransfer.types.includes("application/x-contact-ids")) return; e.preventDefault(); e.dataTransfer.dropEffect = "copy"; @@ -799,7 +900,7 @@ function CategoryItem({ setIsDragOver(false); }, []); - const handleDrop = useCallback((e: DragEvent) => { + const handleDrop = useCallback((e: DragEvent) => { e.preventDefault(); setIsDragOver(false); const data = e.dataTransfer.getData("application/x-contact-ids"); @@ -815,27 +916,18 @@ function CategoryItem({ }, [keyword, onDropContacts]); return ( - + onOpenMenu={onOpenMenu} + menuLabel={menuLabel} + dropProps={{ onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop }} + /> ); } @@ -846,17 +938,18 @@ function AddressBookItem({ onSelect, onDropContacts, onContextMenu, -}: { + onOpenMenu, + menuLabel, +}: RowMenuProps & { book: AddressBook; isActive: boolean; contactCount: number; onSelect: () => void; onDropContacts?: (contactIds: string[], addressBook: AddressBook) => void; - onContextMenu?: (e: React.MouseEvent) => void; }) { const [isDragOver, setIsDragOver] = useState(false); - const handleDragOver = useCallback((e: DragEvent) => { + const handleDragOver = useCallback((e: DragEvent) => { if (!e.dataTransfer.types.includes("application/x-contact-ids")) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; @@ -867,7 +960,7 @@ function AddressBookItem({ setIsDragOver(false); }, []); - const handleDrop = useCallback((e: DragEvent) => { + const handleDrop = useCallback((e: DragEvent) => { e.preventDefault(); setIsDragOver(false); const data = e.dataTransfer.getData("application/x-contact-ids"); @@ -882,36 +975,21 @@ function AddressBookItem({ } }, [book, onDropContacts]); + const sharedOut = !book.isShared && Object.keys(book.shareWith || {}).length > 0; return ( - + onOpenMenu={onOpenMenu} + menuLabel={menuLabel} + dropProps={{ onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop }} + rowProps={{ "data-testid": "address-book-item", "data-book-name": book.name, "data-account": book.accountName ?? "" }} + /> ); } diff --git a/locales/ar/common.json b/locales/ar/common.json index c02fb418c..f8e61cace 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "اسم الفئة", "category_renamed": "تمت إعادة تسمية الفئة", "category_rename_failed": "فشلت إعادة تسمية الفئة", + "delete_category": "حذف الفئة", + "category_deleted": "حُذفت الفئة \"{name}\"", + "category_delete_failed": "تعذّر حذف الفئة", + "undo": "تراجع", "category_added": "تمت إضافة جهة الاتصال إلى {name}", "category_added_plural": "تمت إضافة {count} جهات اتصال إلى {name}", "empty_state": "لا توجد جهات اتصال بعد", @@ -2928,6 +2932,7 @@ "back_to_contacts": "العودة إلى جهات الاتصال", "back_to_email": "العودة إلى البريد", "open_categories": "فتح الفئات", + "row_actions": "إجراءات {name}", "tabs": { "all": "الكل", "groups": "المجموعات" @@ -2960,7 +2965,10 @@ "delete": "حذف دفتر العناوين", "confirm_delete": "حذف \"{name}\"؟ ستتم إزالة جميع جهات الاتصال في دفتر العناوين هذا.", "deleted": "تم حذف دفتر العناوين", - "delete_failed": "فشل حذف دفتر العناوين" + "delete_failed": "فشل حذف دفتر العناوين", + "move_to": "نقل إلى…", + "move_title": "نقل إلى دفتر عناوين", + "move_here": "نقل إلى هنا" }, "detail": { "emails": "عناوين البريد الإلكتروني", diff --git a/locales/ca/common.json b/locales/ca/common.json index 55ee5365d..660744acb 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Nom de la categoria", "category_renamed": "Nom de la categoria canviat", "category_rename_failed": "No s'ha pogut canviar el nom de la categoria", + "delete_category": "Suprimeix la categoria", + "category_deleted": "S'ha suprimit la categoria «{name}»", + "category_delete_failed": "No s'ha pogut suprimir la categoria", + "undo": "Desfés", "category_added": "Contacte afegit a {name}", "category_added_plural": "{count} contactes afegits a {name}", "empty_state": "Encara no hi ha contactes", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Torna als contactes", "back_to_email": "Torna al correu", "open_categories": "Obre les categories", + "row_actions": "Accions per a {name}", "tabs": { "all": "Tots", "groups": "Grups" @@ -2960,7 +2965,10 @@ "delete": "Suprimeix la llibreta d'adreces", "confirm_delete": "Voleu suprimir «{name}»? S'eliminaran tots els contactes d'aquesta llibreta d'adreces.", "deleted": "Llibreta d'adreces suprimida", - "delete_failed": "No s'ha pogut suprimir la llibreta d'adreces" + "delete_failed": "No s'ha pogut suprimir la llibreta d'adreces", + "move_to": "Mou a…", + "move_title": "Mou a una llibreta d'adreces", + "move_here": "Mou aquí" }, "detail": { "emails": "Adreces electròniques", diff --git a/locales/cs/common.json b/locales/cs/common.json index a7b7e3623..9fe08f6f1 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Název kategorie", "category_renamed": "Kategorie byla přejmenována", "category_rename_failed": "Přejmenování kategorie selhalo", + "delete_category": "Smazat kategorii", + "category_deleted": "Kategorie „{name}“ byla smazána", + "category_delete_failed": "Kategorii se nepodařilo smazat", + "undo": "Zpět", "category_added": "Kontakt přidán do {name}", "category_added_plural": "{count} kontaktů bylo přidáno do {name}", "empty_state": "Žádné kontakty", @@ -2959,7 +2963,10 @@ "delete": "Smazat adresář", "confirm_delete": "Smazat „{name}\"? Všechny kontakty v tomto adresáři budou odstraněny.", "deleted": "Adresář smazán", - "delete_failed": "Adresář se nepodařilo smazat" + "delete_failed": "Adresář se nepodařilo smazat", + "move_to": "Přesunout do…", + "move_title": "Přesunout do adresáře", + "move_here": "Přesunout sem" }, "detail": { "emails": "E-mailové adresy", @@ -3248,7 +3255,8 @@ "sort_first_name": "Jméno", "sort_last_name": "Příjmení" }, - "open_categories": "Otevřít kategorie" + "open_categories": "Otevřít kategorie", + "row_actions": "Akce pro {name}" }, "calendar": { "title": "Kalendář", diff --git a/locales/da/common.json b/locales/da/common.json index d5fcd3e20..c218465e9 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Kategorinavn", "category_renamed": "Kategori omdøbt", "category_rename_failed": "Kunne ikke omdøbe kategori", + "delete_category": "Slet kategori", + "category_deleted": "Kategorien \"{name}\" er slettet", + "category_delete_failed": "Kunne ikke slette kategorien", + "undo": "Fortryd", "category_added": "Kontakt tilføjet til {name}", "category_added_plural": "{count} kontakter tilføjet til {name}", "empty_state": "Ingen kontakter endnu", @@ -2959,7 +2963,10 @@ "delete": "Slet adressebog", "confirm_delete": "Slet \"{name}\"? Alle kontakter i denne adressebog fjernes.", "deleted": "Adressebog slettet", - "delete_failed": "Kunne ikke slette adressebog" + "delete_failed": "Kunne ikke slette adressebog", + "move_to": "Flyt til…", + "move_title": "Flyt til adressebog", + "move_here": "Flyt hertil" }, "detail": { "emails": "E-mailadresser", @@ -3248,7 +3255,8 @@ "sort_first_name": "Fornavn", "sort_last_name": "Efternavn" }, - "open_categories": "Åbn kategorier" + "open_categories": "Åbn kategorier", + "row_actions": "Handlinger for {name}" }, "calendar": { "title": "Kalender", diff --git a/locales/de/common.json b/locales/de/common.json index 376fc5970..44cf45c26 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Kategoriename", "category_renamed": "Kategorie umbenannt", "category_rename_failed": "Kategorie konnte nicht umbenannt werden", + "delete_category": "Kategorie löschen", + "category_deleted": "Kategorie „{name}“ gelöscht", + "category_delete_failed": "Kategorie konnte nicht gelöscht werden", + "undo": "Rückgängig", "category_added": "Kontakt zu {name} hinzugefügt", "category_added_plural": "{count} Kontakte zu {name} hinzugefügt", "empty_state": "Keine Kontakte", @@ -2959,7 +2963,10 @@ "delete": "Adressbuch löschen", "confirm_delete": "„{name}\" löschen? Alle Kontakte in diesem Adressbuch werden entfernt.", "deleted": "Adressbuch gelöscht", - "delete_failed": "Adressbuch konnte nicht gelöscht werden" + "delete_failed": "Adressbuch konnte nicht gelöscht werden", + "move_to": "Verschieben nach…", + "move_title": "In Adressbuch verschieben", + "move_here": "Hierher verschieben" }, "detail": { "emails": "E-Mail-Adressen", @@ -3248,7 +3255,8 @@ "sort_first_name": "Vorname", "sort_last_name": "Nachname" }, - "open_categories": "Kategorien öffnen" + "open_categories": "Kategorien öffnen", + "row_actions": "Aktionen für {name}" }, "calendar": { "title": "Kalender", diff --git a/locales/en/common.json b/locales/en/common.json index bf8b9cd1b..f75b52090 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Category name", "category_renamed": "Category renamed", "category_rename_failed": "Failed to rename category", + "delete_category": "Delete category", + "category_deleted": "Category \"{name}\" deleted", + "category_delete_failed": "Couldn't delete the category", + "undo": "Undo", "category_added": "Contact added to {name}", "category_added_plural": "{count} contacts added to {name}", "empty_state": "No contacts yet", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Back to contacts", "back_to_email": "Back to email", "open_categories": "Open categories", + "row_actions": "Actions for {name}", "tabs": { "all": "All", "groups": "Groups" @@ -2960,7 +2965,10 @@ "delete": "Delete address book", "confirm_delete": "Delete \"{name}\"? All contacts in this address book will be removed.", "deleted": "Address book deleted", - "delete_failed": "Failed to delete address book" + "delete_failed": "Failed to delete address book", + "move_to": "Move to…", + "move_title": "Move to address book", + "move_here": "Move here" }, "detail": { "emails": "Email Addresses", diff --git a/locales/es/common.json b/locales/es/common.json index 8fad33d24..a2d8b12f8 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Nombre de la categoría", "category_renamed": "Categoría renombrada", "category_rename_failed": "Error al renombrar la categoría", + "delete_category": "Eliminar categoría", + "category_deleted": "Categoría «{name}» eliminada", + "category_delete_failed": "No se ha podido eliminar la categoría", + "undo": "Deshacer", "category_added": "Contacto añadido a {name}", "category_added_plural": "{count} contactos añadidos a {name}", "empty_state": "No hay contactos", @@ -2959,7 +2963,10 @@ "delete": "Eliminar libreta de direcciones", "confirm_delete": "¿Eliminar «{name}»? Todos los contactos de esta libreta se eliminarán.", "deleted": "Libreta de direcciones eliminada", - "delete_failed": "No se pudo eliminar la libreta de direcciones" + "delete_failed": "No se pudo eliminar la libreta de direcciones", + "move_to": "Mover a…", + "move_title": "Mover a una libreta de direcciones", + "move_here": "Mover aquí" }, "detail": { "emails": "Direcciones de correo", @@ -3248,7 +3255,8 @@ "sort_first_name": "Nombre", "sort_last_name": "Apellido" }, - "open_categories": "Abrir categorías" + "open_categories": "Abrir categorías", + "row_actions": "Acciones para {name}" }, "calendar": { "title": "Calendario", diff --git a/locales/et/common.json b/locales/et/common.json index 221efa0cc..3b4b3ae31 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Kategooria nimi", "category_renamed": "Kategooria nimetati ümber", "category_rename_failed": "Kategooria ümbernimetamine ebaõnnestus", + "delete_category": "Kustuta kategooria", + "category_deleted": "Kategooria „{name}“ kustutati", + "category_delete_failed": "Kategooriat ei õnnestunud kustutada", + "undo": "Võta tagasi", "category_added": "Kontakt lisati kategooriasse {name}", "category_added_plural": "{count} kontakti lisati kategooriasse {name}", "empty_state": "Kontakte veel pole", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Tagasi kontaktide juurde", "back_to_email": "Tagasi e-posti juurde", "open_categories": "Ava kategooriad", + "row_actions": "Toimingud: {name}", "tabs": { "all": "Kõik", "groups": "Grupid" @@ -2960,7 +2965,10 @@ "delete": "Kustuta aadressiraamat", "confirm_delete": "Kustutada \"{name}\"? Kõik selle aadressiraamatu kontaktid eemaldatakse.", "deleted": "Aadressiraamat kustutatud", - "delete_failed": "Aadressiraamatu kustutamine ebaõnnestus" + "delete_failed": "Aadressiraamatu kustutamine ebaõnnestus", + "move_to": "Teisalda…", + "move_title": "Teisalda aadressiraamatusse", + "move_here": "Teisalda siia" }, "detail": { "emails": "E-posti aadressid", diff --git a/locales/fa/common.json b/locales/fa/common.json index faf04355c..ed4b75a0c 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "نام دسته‌بندی", "category_renamed": "نام دسته‌بندی تغییر کرد", "category_rename_failed": "تغییر نام دسته‌بندی ناموفق بود", + "delete_category": "حذف دسته", + "category_deleted": "دسته «{name}» حذف شد", + "category_delete_failed": "دسته حذف نشد", + "undo": "واگرد", "category_added": "مخاطب به {name} اضافه شد", "category_added_plural": "{count} مخاطب به {name} اضافه شد", "empty_state": "هنوز مخاطبی نیست", @@ -2928,6 +2932,7 @@ "back_to_contacts": "بازگشت به مخاطبین", "back_to_email": "بازگشت به ایمیل", "open_categories": "باز کردن دسته‌بندی‌ها", + "row_actions": "کنش‌های {name}", "tabs": { "all": "همه", "groups": "گروه‌ها" @@ -2960,7 +2965,10 @@ "delete": "حذف دفترچه آدرس", "confirm_delete": "حذف \"{name}\"؟ همه مخاطبین این دفترچه آدرس حذف خواهند شد.", "deleted": "دفترچه آدرس حذف شد", - "delete_failed": "حذف دفترچه آدرس ناموفق بود" + "delete_failed": "حذف دفترچه آدرس ناموفق بود", + "move_to": "انتقال به…", + "move_title": "انتقال به دفترچه نشانی", + "move_here": "انتقال به اینجا" }, "detail": { "emails": "آدرس‌های ایمیل", diff --git a/locales/fr/common.json b/locales/fr/common.json index e5a949fca..9190e5cb1 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Nom de la catégorie", "category_renamed": "Catégorie renommée", "category_rename_failed": "Échec du renommage de la catégorie", + "delete_category": "Supprimer la catégorie", + "category_deleted": "Catégorie « {name} » supprimée", + "category_delete_failed": "Impossible de supprimer la catégorie", + "undo": "Annuler", "category_added": "Contact ajouté à {name}", "category_added_plural": "{count} contacts ajoutés à {name}", "empty_state": "Aucun contact", @@ -2959,7 +2963,10 @@ "delete": "Supprimer le carnet d'adresses", "confirm_delete": "Supprimer « {name} » ? Tous les contacts de ce carnet d'adresses seront supprimés.", "deleted": "Carnet d'adresses supprimé", - "delete_failed": "Échec de la suppression du carnet d'adresses" + "delete_failed": "Échec de la suppression du carnet d'adresses", + "move_to": "Déplacer vers…", + "move_title": "Déplacer vers un carnet d'adresses", + "move_here": "Déplacer ici" }, "detail": { "emails": "Adresses e-mail", @@ -3248,7 +3255,8 @@ "sort_first_name": "Prénom", "sort_last_name": "Nom de famille" }, - "open_categories": "Ouvrir les catégories" + "open_categories": "Ouvrir les catégories", + "row_actions": "Actions pour {name}" }, "calendar": { "title": "Calendrier", diff --git a/locales/he/common.json b/locales/he/common.json index 9019aef48..b23020009 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -2829,6 +2829,10 @@ "category_name_label": "שם קטגוריה", "category_renamed": "שם קטגוריה שונה", "category_rename_failed": "שינוי שם הקטגוריה נכשל", + "delete_category": "מחק קטגוריה", + "category_deleted": "הקטגוריה \"{name}\" נמחקה", + "category_delete_failed": "לא ניתן היה למחוק את הקטגוריה", + "undo": "בטל", "category_added": "איש קשר נוסף ל-{name}", "category_added_plural": "אנשי הקשר{count}נוספו ל-{name}", "empty_state": "עדיין אין אנשי קשר", @@ -2874,7 +2878,10 @@ "delete": "מחק ספר כתובות", "confirm_delete": "מחק \"{name}\"? כל אנשי הקשר בספר כתובות זה יוסרו.", "deleted": "ספר כתובות נמחק", - "delete_failed": "נכשל במחיקת ספר כתובות" + "delete_failed": "נכשל במחיקת ספר כתובות", + "move_to": "העבר אל…", + "move_title": "העבר לפנקס כתובות", + "move_here": "העבר לכאן" }, "detail": { "emails": "כתובות אימייל", @@ -3122,6 +3129,7 @@ "empty_filtered_hint": "נסה להתאים או להביא סננים", "back_to_email": "חזור לדוא״ל", "open_categories": "קטגוריות פתוחות", + "row_actions": "פעולות עבור {name}", "activity": { "recent_emails": "דוא״ל אחרונים", "upcoming_events": "אירועים קרובים", diff --git a/locales/hu/common.json b/locales/hu/common.json index f7690238f..73c266537 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Kategória neve", "category_renamed": "Kategória átnevezve", "category_rename_failed": "Nem sikerült átnevezni a kategóriát", + "delete_category": "Kategória törlése", + "category_deleted": "A(z) „{name}” kategória törölve", + "category_delete_failed": "Nem sikerült törölni a kategóriát", + "undo": "Visszavonás", "category_added": "Névjegy hozzáadva ide: {name}", "category_added_plural": "{count} névjegy hozzáadva ide: {name}", "empty_state": "Még nincsenek névjegyek", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Vissza a névjegyekhez", "back_to_email": "Vissza az e-mailekhez", "open_categories": "Kategóriák megnyitása", + "row_actions": "Műveletek: {name}", "tabs": { "all": "Összes", "groups": "Csoportok" @@ -2960,7 +2965,10 @@ "delete": "Címjegyzék törlése", "confirm_delete": "Törlöd a \"{name}\" címjegyzéket? Az összes névjegy eltávolításra kerül.", "deleted": "Címjegyzék törölve", - "delete_failed": "Nem sikerült törölni a címjegyzéket" + "delete_failed": "Nem sikerült törölni a címjegyzéket", + "move_to": "Áthelyezés ide…", + "move_title": "Áthelyezés címjegyzékbe", + "move_here": "Áthelyezés ide" }, "detail": { "emails": "E-mail címek", diff --git a/locales/it/common.json b/locales/it/common.json index dbcdaf241..a06f286f9 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Nome della categoria", "category_renamed": "Categoria rinominata", "category_rename_failed": "Impossibile rinominare la categoria", + "delete_category": "Elimina categoria", + "category_deleted": "Categoria «{name}» eliminata", + "category_delete_failed": "Impossibile eliminare la categoria", + "undo": "Annulla", "category_added": "Contatto aggiunto a {name}", "category_added_plural": "{count} contatti aggiunti a {name}", "empty_state": "Nessun contatto", @@ -2959,7 +2963,10 @@ "delete": "Elimina rubrica", "confirm_delete": "Eliminare \"{name}\"? Tutti i contatti in questa rubrica verranno rimossi.", "deleted": "Rubrica eliminata", - "delete_failed": "Impossibile eliminare la rubrica" + "delete_failed": "Impossibile eliminare la rubrica", + "move_to": "Sposta in…", + "move_title": "Sposta in una rubrica", + "move_here": "Sposta qui" }, "detail": { "emails": "Indirizzi email", @@ -3248,7 +3255,8 @@ "sort_first_name": "Nome", "sort_last_name": "Cognome" }, - "open_categories": "Apri categorie" + "open_categories": "Apri categorie", + "row_actions": "Azioni per {name}" }, "calendar": { "title": "Calendario", diff --git a/locales/ja/common.json b/locales/ja/common.json index 038aa1981..83ce5dc7a 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "カテゴリ名", "category_renamed": "カテゴリの名前を変更しました", "category_rename_failed": "カテゴリの名前変更に失敗しました", + "delete_category": "カテゴリを削除", + "category_deleted": "カテゴリ「{name}」を削除しました", + "category_delete_failed": "カテゴリを削除できませんでした", + "undo": "元に戻す", "category_added": "{name} に連絡先を追加しました", "category_added_plural": "{count} 件の連絡先を {name} に追加しました", "empty_state": "連絡先がありません", @@ -2959,7 +2963,10 @@ "delete": "アドレス帳を削除", "confirm_delete": "「{name}」を削除しますか?このアドレス帳のすべての連絡先が削除されます。", "deleted": "アドレス帳を削除しました", - "delete_failed": "アドレス帳の削除に失敗しました" + "delete_failed": "アドレス帳の削除に失敗しました", + "move_to": "移動先…", + "move_title": "アドレス帳に移動", + "move_here": "ここに移動" }, "detail": { "emails": "メールアドレス", @@ -3248,7 +3255,8 @@ "sort_first_name": "名", "sort_last_name": "姓" }, - "open_categories": "カテゴリを開く" + "open_categories": "カテゴリを開く", + "row_actions": "{name} の操作" }, "calendar": { "title": "カレンダー", diff --git a/locales/ko/common.json b/locales/ko/common.json index b807693cf..91a8a23a9 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "카테고리 이름", "category_renamed": "카테고리 이름이 변경되었습니다", "category_rename_failed": "카테고리 이름 변경 실패", + "delete_category": "카테고리 삭제", + "category_deleted": "\"{name}\" 카테고리를 삭제했습니다", + "category_delete_failed": "카테고리를 삭제하지 못했습니다", + "undo": "실행 취소", "category_added": "{name}에 연락처가 추가되었어요", "category_added_plural": "{name}에 {count}개의 연락처가 추가되었어요", "empty_state": "아직 연락처가 없어요", @@ -2959,7 +2963,10 @@ "delete": "주소록 삭제", "confirm_delete": "\"{name}\"을(를) 삭제하시겠습니까? 이 주소록의 모든 연락처가 삭제됩니다.", "deleted": "주소록이 삭제되었습니다", - "delete_failed": "주소록 삭제에 실패했습니다" + "delete_failed": "주소록 삭제에 실패했습니다", + "move_to": "이동…", + "move_title": "주소록으로 이동", + "move_here": "여기로 이동" }, "detail": { "emails": "이메일", @@ -3248,7 +3255,8 @@ "sort_first_name": "이름", "sort_last_name": "성" }, - "open_categories": "카테고리 열기" + "open_categories": "카테고리 열기", + "row_actions": "{name} 작업" }, "calendar": { "title": "캘린더", diff --git a/locales/lv/common.json b/locales/lv/common.json index 7e2c95d00..291397958 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2955,7 +2955,10 @@ "delete": "Dzēst adrešu grāmatu", "confirm_delete": "Dzēst \"{name}\"? Visi kontakti šajā adrešu grāmatā tiks noņemti.", "deleted": "Adrešu grāmata dzēsta", - "delete_failed": "Neizdevās dzēst adrešu grāmatu" + "delete_failed": "Neizdevās dzēst adrešu grāmatu", + "move_to": "Pārvietot uz…", + "move_title": "Pārvietot uz adrešu grāmatu", + "move_here": "Pārvietot šeit" }, "detail": { "emails": "E-pasta adreses", @@ -3214,6 +3217,10 @@ "category_name_label": "Kategorijas nosaukums", "category_renamed": "Kategorija pārdēvēta", "category_rename_failed": "Neizdevās pārdēvēt kategoriju", + "delete_category": "Dzēst kategoriju", + "category_deleted": "Kategorija “{name}” dzēsta", + "category_delete_failed": "Neizdevās dzēst kategoriju", + "undo": "Atsaukt", "context_menu": { "open": "Atvērt", "edit": "Rediģēt", @@ -3248,7 +3255,8 @@ "sort_first_name": "Vārds", "sort_last_name": "Uzvārds" }, - "open_categories": "Atvērt kategorijas" + "open_categories": "Atvērt kategorijas", + "row_actions": "Darbības: {name}" }, "calendar": { "title": "Kalendārs", diff --git a/locales/mn/common.json b/locales/mn/common.json index bd26ef36a..c01e98584 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Ангиллын нэр", "category_renamed": "Ангиллын нэрийг өөрчилсөн", "category_rename_failed": "Ангиллын нэрийг өөрчилж чадсангүй", + "delete_category": "Ангиллыг устгах", + "category_deleted": "\"{name}\" ангиллыг устгалаа", + "category_delete_failed": "Ангиллыг устгаж чадсангүй", + "undo": "Буцаах", "category_added": "Харилцагчийг {name}", "category_added_plural": "{count} харилцагчдыг {name}", "empty_state": "Харилцагч байхгүй", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Харилцагч руу буцах", "back_to_email": "Имэйл рүү буцах", "open_categories": "Нээлттэй ангилалууд", + "row_actions": "{name}-н үйлдлүүд", "tabs": { "all": "Бүгд", "groups": "Бүлэг" @@ -2960,7 +2965,10 @@ "delete": "Хаягийн дэвтэр устгах", "confirm_delete": "\"{name}\"-г устгах уу? Энэ хаягийн дэвтэр дэх бүх харилцагчийг устгах болно.", "deleted": "Хаягийн дэвтэр устсан", - "delete_failed": "Хаягийн дэвтрийг устгаж чадсангүй" + "delete_failed": "Хаягийн дэвтрийг устгаж чадсангүй", + "move_to": "Зөөх…", + "move_title": "Хаягийн дэвтэр рүү зөөх", + "move_here": "Энд зөөх" }, "detail": { "emails": "Имэйл хаягууд", diff --git a/locales/nb/common.json b/locales/nb/common.json index b6b98dbf2..2b0f17e0f 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Kategorinavn", "category_renamed": "Kategorien har fått nytt navn", "category_rename_failed": "Kunne ikke gi kategorien nytt navn", + "delete_category": "Slett kategori", + "category_deleted": "Kategorien «{name}» er slettet", + "category_delete_failed": "Kunne ikke slette kategorien", + "undo": "Angre", "category_added": "Kontakt lagt til i {name}", "category_added_plural": "{count} kontakter lagt til i {name}", "empty_state": "Ingen kontakter ennå", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Tilbake til kontaktene", "back_to_email": "Tilbake til e-post", "open_categories": "Åpne kategorier", + "row_actions": "Handlinger for {name}", "tabs": { "all": "Alle", "groups": "Grupper" @@ -2960,7 +2965,10 @@ "delete": "Slett adressebok", "confirm_delete": "Vil du slette «{name}»? Alle kontaktene i denne adresseboken fjernes.", "deleted": "Adresseboken er slettet", - "delete_failed": "Kunne ikke slette adresseboken" + "delete_failed": "Kunne ikke slette adresseboken", + "move_to": "Flytt til…", + "move_title": "Flytt til adressebok", + "move_here": "Flytt hit" }, "detail": { "emails": "E-postadresser", diff --git a/locales/nl/common.json b/locales/nl/common.json index 76be4911a..3921ff9aa 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Categorienaam", "category_renamed": "Categorie hernoemd", "category_rename_failed": "Categorie hernoemen mislukt", + "delete_category": "Categorie verwijderen", + "category_deleted": "Categorie '{name}' verwijderd", + "category_delete_failed": "Kan de categorie niet verwijderen", + "undo": "Ongedaan maken", "category_added": "Contact toegevoegd aan {name}", "category_added_plural": "{count} contacten toegevoegd aan {name}", "empty_state": "Geen contacten", @@ -2959,7 +2963,10 @@ "delete": "Adresboek verwijderen", "confirm_delete": "\"{name}\" verwijderen? Alle contacten in dit adresboek worden verwijderd.", "deleted": "Adresboek verwijderd", - "delete_failed": "Adresboek kon niet worden verwijderd" + "delete_failed": "Adresboek kon niet worden verwijderd", + "move_to": "Verplaatsen naar…", + "move_title": "Naar adresboek verplaatsen", + "move_here": "Hierheen verplaatsen" }, "detail": { "emails": "E-mailadressen", @@ -3248,7 +3255,8 @@ "sort_first_name": "Voornaam", "sort_last_name": "Achternaam" }, - "open_categories": "Categorieën openen" + "open_categories": "Categorieën openen", + "row_actions": "Acties voor {name}" }, "calendar": { "title": "Agenda", diff --git a/locales/pl/common.json b/locales/pl/common.json index fdcd53fb9..6537e42d5 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Nazwa kategorii", "category_renamed": "Zmieniono nazwę kategorii", "category_rename_failed": "Nie udało się zmienić nazwy kategorii", + "delete_category": "Usuń kategorię", + "category_deleted": "Usunięto kategorię „{name}”", + "category_delete_failed": "Nie udało się usunąć kategorii", + "undo": "Cofnij", "category_added": "Kontakt dodany do {name}", "category_added_plural": "{count} kontaktów dodano do {name}", "empty_state": "Brak kontaktów", @@ -2959,7 +2963,10 @@ "delete": "Usuń książkę adresową", "confirm_delete": "Usunąć „{name}\"? Wszystkie kontakty w tej książce adresowej zostaną usunięte.", "deleted": "Książka adresowa usunięta", - "delete_failed": "Nie udało się usunąć książki adresowej" + "delete_failed": "Nie udało się usunąć książki adresowej", + "move_to": "Przenieś do…", + "move_title": "Przenieś do książki adresowej", + "move_here": "Przenieś tutaj" }, "detail": { "emails": "Adresy e-mail", @@ -3248,7 +3255,8 @@ "sort_first_name": "Imię", "sort_last_name": "Nazwisko" }, - "open_categories": "Otwórz kategorie" + "open_categories": "Otwórz kategorie", + "row_actions": "Działania dla {name}" }, "calendar": { "title": "Kalendarz", diff --git a/locales/pt/common.json b/locales/pt/common.json index 0654a6376..6f0c20bc9 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Nome da categoria", "category_renamed": "Categoria renomeada", "category_rename_failed": "Falha ao renomear a categoria", + "delete_category": "Excluir categoria", + "category_deleted": "Categoria \"{name}\" excluída", + "category_delete_failed": "Não foi possível excluir a categoria", + "undo": "Desfazer", "category_added": "Contato adicionado a {name}", "category_added_plural": "{count} contatos adicionados a {name}", "empty_state": "Nenhum contato", @@ -2959,7 +2963,10 @@ "delete": "Excluir lista de contatos", "confirm_delete": "Excluir \"{name}\"? Todos os contatos desta lista serão removidos.", "deleted": "Lista de contatos excluída", - "delete_failed": "Falha ao excluir a lista de contatos" + "delete_failed": "Falha ao excluir a lista de contatos", + "move_to": "Mover para…", + "move_title": "Mover para catálogo de endereços", + "move_here": "Mover para cá" }, "detail": { "emails": "Endereços de e-mail", @@ -3248,7 +3255,8 @@ "sort_first_name": "Nome", "sort_last_name": "Sobrenome" }, - "open_categories": "Abrir categorias" + "open_categories": "Abrir categorias", + "row_actions": "Ações para {name}" }, "calendar": { "title": "Calendário", diff --git a/locales/ro/common.json b/locales/ro/common.json index 7fa77d0c7..dcb1c8f5f 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Numele categoriei", "category_renamed": "Categorie redenumită", "category_rename_failed": "Nu s-a putut redenumi categoria", + "delete_category": "Ștergeți categoria", + "category_deleted": "Categoria „{name}” a fost ștearsă", + "category_delete_failed": "Nu s-a putut șterge categoria", + "undo": "Anulați", "category_added": "Contact adăugat la {name}", "category_added_plural": "{count} contacte adăugate la {name}", "empty_state": "Nu există încă contacte", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Înapoi la contacte", "back_to_email": "Înapoi la e-mail", "open_categories": "Deschideți categoriile", + "row_actions": "Acțiuni pentru {name}", "tabs": { "all": "Toate", "groups": "Grupuri" @@ -2960,7 +2965,10 @@ "delete": "Șterge agenda de adrese", "confirm_delete": "Doriți să ștergeți „{name}”? Toate contactele din această agendă vor fi eliminate.", "deleted": "Agenda de adrese a fost ștersă", - "delete_failed": "Nu s-a putut șterge agenda de adrese" + "delete_failed": "Nu s-a putut șterge agenda de adrese", + "move_to": "Mutați în…", + "move_title": "Mutați în agendă", + "move_here": "Mutați aici" }, "detail": { "emails": "Adrese de e-mail", diff --git a/locales/ru/common.json b/locales/ru/common.json index ba8bc9bff..ed611b23c 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Название категории", "category_renamed": "Категория переименована", "category_rename_failed": "Не удалось переименовать категорию", + "delete_category": "Удалить категорию", + "category_deleted": "Категория «{name}» удалена", + "category_delete_failed": "Не удалось удалить категорию", + "undo": "Отменить", "category_added": "Контакт добавлен в {name}", "category_added_plural": "{count} контактов добавлено в {name}", "empty_state": "Контактов пока нет", @@ -2959,7 +2963,10 @@ "delete": "Удалить адресную книгу", "confirm_delete": "Удалить «{name}»? Все контакты в этой адресной книге будут удалены.", "deleted": "Адресная книга удалена", - "delete_failed": "Не удалось удалить адресную книгу" + "delete_failed": "Не удалось удалить адресную книгу", + "move_to": "Переместить в…", + "move_title": "Переместить в адресную книгу", + "move_here": "Переместить сюда" }, "detail": { "emails": "Адреса электронной почты", @@ -3248,7 +3255,8 @@ "sort_first_name": "Имя", "sort_last_name": "Фамилия" }, - "open_categories": "Открыть категории" + "open_categories": "Открыть категории", + "row_actions": "Действия для {name}" }, "calendar": { "title": "Календарь", diff --git a/locales/sk/common.json b/locales/sk/common.json index 2f47c971e..7ef437d3c 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Názov kategórie", "category_renamed": "Kategória bola premenovaná", "category_rename_failed": "Premenovanie kategórie zlyhalo", + "delete_category": "Odstrániť kategóriu", + "category_deleted": "Kategória „{name}“ bola odstránená", + "category_delete_failed": "Kategóriu sa nepodarilo odstrániť", + "undo": "Späť", "category_added": "Kontakt pridaný do {name}", "category_added_plural": "{count} kontaktov bolo pridaných do {name}", "empty_state": "Žiadne kontakty", @@ -2928,6 +2932,7 @@ "back_to_contacts": "Späť na kontakty", "back_to_email": "Späť na e-mail", "open_categories": "Otvoriť kategórie", + "row_actions": "Akcie pre {name}", "tabs": { "all": "Všetky", "groups": "Skupiny" @@ -2960,7 +2965,10 @@ "delete": "Zmazať adresár", "confirm_delete": "Zmazať \"{name}\"? Všetky kontakty v tomto adresári budú odstránené.", "deleted": "Adresár zmazaný", - "delete_failed": "Adresár sa nepodarilo zmazať" + "delete_failed": "Adresár sa nepodarilo zmazať", + "move_to": "Presunúť do…", + "move_title": "Presunúť do adresára", + "move_here": "Presunúť sem" }, "detail": { "emails": "E-mailové adresy", diff --git a/locales/th/common.json b/locales/th/common.json index d2a991314..470e16194 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -2246,6 +2246,10 @@ "category_name_label": "ชื่อหมวดหมู่", "category_renamed": "เปลี่ยนชื่อหมวดหมู่แล้ว", "category_rename_failed": "เปลี่ยนชื่อหมวดหมู่ไม่สำเร็จ", + "delete_category": "ลบหมวดหมู่", + "category_deleted": "ลบหมวดหมู่ \"{name}\" แล้ว", + "category_delete_failed": "ลบหมวดหมู่ไม่ได้", + "undo": "เลิกทำ", "category_added": "เพิ่มผู้ติดต่อใน {name} แล้ว", "category_added_plural": "เพิ่มผู้ติดต่อ {count} รายการใน {name} แล้ว", "empty_state": "ยังไม่มีผู้ติดต่อ", @@ -2263,6 +2267,7 @@ "back_to_contacts": "กลับไปยังผู้ติดต่อ", "back_to_email": "กลับไปยังอีเมล", "open_categories": "เปิดหมวดหมู่", + "row_actions": "การทำงานสำหรับ {name}", "tabs": { "all": "ทั้งหมด", "groups": "กลุ่มผู้ติดต่อ" @@ -2295,7 +2300,10 @@ "delete": "ลบสมุดที่อยู่", "confirm_delete": "ลบ “{name}” หรือไม่? ผู้ติดต่อทั้งหมดในสมุดที่อยู่นี้จะถูกลบ", "deleted": "ลบสมุดที่อยู่แล้ว", - "delete_failed": "ลบสมุดที่อยู่ไม่สำเร็จ" + "delete_failed": "ลบสมุดที่อยู่ไม่สำเร็จ", + "move_to": "ย้ายไปที่…", + "move_title": "ย้ายไปยังสมุดที่อยู่", + "move_here": "ย้ายมาที่นี่" }, "detail": { "emails": "ที่อยู่อีเมล", diff --git a/locales/tr/common.json b/locales/tr/common.json index 7e2013450..8da701c31 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Kategori adı", "category_renamed": "Kategori yeniden adlandırıldı", "category_rename_failed": "Kategori yeniden adlandırılamadı", + "delete_category": "Kategoriyi sil", + "category_deleted": "\"{name}\" kategorisi silindi", + "category_delete_failed": "Kategori silinemedi", + "undo": "Geri al", "category_added": "{name} kategorisine kişi eklendi", "category_added_plural": "{name} kategorisine {count} kişi eklendi", "empty_state": "Henüz kişi yok", @@ -2959,7 +2963,10 @@ "delete": "Adres defterini sil", "confirm_delete": "\"{name}\" silinsin mi? Bu adres defterindeki tüm kişiler kaldırılacak.", "deleted": "Adres defteri silindi", - "delete_failed": "Adres defteri silinemedi" + "delete_failed": "Adres defteri silinemedi", + "move_to": "Taşı…", + "move_title": "Adres defterine taşı", + "move_here": "Buraya taşı" }, "detail": { "emails": "E-posta Adresleri", @@ -3248,7 +3255,8 @@ "sort_first_name": "Ad", "sort_last_name": "Soyad" }, - "open_categories": "Kategorileri aç" + "open_categories": "Kategorileri aç", + "row_actions": "{name} için işlemler" }, "calendar": { "title": "Takvim", diff --git a/locales/uk/common.json b/locales/uk/common.json index 1b22505de..e5899d901 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "Назва категорії", "category_renamed": "Категорію перейменовано", "category_rename_failed": "Не вдалося перейменувати категорію", + "delete_category": "Видалити категорію", + "category_deleted": "Категорію «{name}» видалено", + "category_delete_failed": "Не вдалося видалити категорію", + "undo": "Скасувати", "category_added": "Контакт додано до {name}", "category_added_plural": "{count} контактів додано до {name}", "empty_state": "Контактів ще немає", @@ -2959,7 +2963,10 @@ "delete": "Видалити адресну книгу", "confirm_delete": "Видалити «{name}»? Усі контакти в цій адресній книзі будуть видалені.", "deleted": "Адресну книгу видалено", - "delete_failed": "Не вдалося видалити адресну книгу" + "delete_failed": "Не вдалося видалити адресну книгу", + "move_to": "Перемістити до…", + "move_title": "Перемістити до адресної книги", + "move_here": "Перемістити сюди" }, "detail": { "emails": "Адреси електронної пошти", @@ -3248,7 +3255,8 @@ "sort_first_name": "Ім'я", "sort_last_name": "Прізвище" }, - "open_categories": "Відкрити категорії" + "open_categories": "Відкрити категорії", + "row_actions": "Дії для {name}" }, "calendar": { "title": "Календар", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 9dde6f0af..0f6d47e0d 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "類別名稱", "category_renamed": "類別已重新命名", "category_rename_failed": "重新命名類別失敗", + "delete_category": "刪除類別", + "category_deleted": "已刪除類別「{name}」", + "category_delete_failed": "無法刪除類別", + "undo": "復原", "category_added": "聯絡人已新增至 {name}", "category_added_plural": "{count} 位聯絡人已新增至 {name}", "empty_state": "尚無聯絡人", @@ -2928,6 +2932,7 @@ "back_to_contacts": "回到聯絡人", "back_to_email": "回到郵件", "open_categories": "開啟類別", + "row_actions": "{name} 的動作", "tabs": { "all": "全部", "groups": "群組" @@ -2960,7 +2965,10 @@ "delete": "刪除通訊錄", "confirm_delete": "要刪除「{name}」嗎?此通訊錄中的所有聯絡人都會移除。", "deleted": "通訊錄已刪除", - "delete_failed": "刪除通訊錄失敗" + "delete_failed": "刪除通訊錄失敗", + "move_to": "移至…", + "move_title": "移至通訊錄", + "move_here": "移到這裡" }, "detail": { "emails": "電子郵件地址", diff --git a/locales/zh/common.json b/locales/zh/common.json index 878283b1b..c268c00d8 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2911,6 +2911,10 @@ "category_name_label": "类别名称", "category_renamed": "类别已重命名", "category_rename_failed": "重命名类别失败", + "delete_category": "删除类别", + "category_deleted": "已删除类别“{name}”", + "category_delete_failed": "无法删除类别", + "undo": "撤销", "category_added": "联系人已添加至 {name}", "category_added_plural": "{count} 联系人已添加到 {name}", "empty_state": "还没有联系人", @@ -2959,7 +2963,10 @@ "delete": "删除通讯录", "confirm_delete": "删除「{name}」?此通讯录中的所有联系人将被移除。", "deleted": "通讯录已删除", - "delete_failed": "删除通讯录失败" + "delete_failed": "删除通讯录失败", + "move_to": "移动到…", + "move_title": "移动到通讯录", + "move_here": "移到此处" }, "detail": { "emails": "邮箱地址", @@ -3248,7 +3255,8 @@ "sort_first_name": "名", "sort_last_name": "姓" }, - "open_categories": "打开分类" + "open_categories": "打开分类", + "row_actions": "{name} 的操作" }, "calendar": { "title": "日历", diff --git a/stores/contact-store.ts b/stores/contact-store.ts index be503c30e..f9a550975 100644 --- a/stores/contact-store.ts +++ b/stores/contact-store.ts @@ -463,6 +463,10 @@ interface ContactStore { removeAddressBook: (client: IJMAPClient, addressBook: AddressBook) => Promise; shareAddressBook: (client: IJMAPClient, addressBook: AddressBook, principalId: string, rights: AddressBookRights | null) => Promise; renameKeyword: (client: IJMAPClient | null, oldKeyword: string, newKeyword: string) => Promise; + /** Take a category off every contact; returns the ids of the contacts that had it, for undo. */ + removeKeyword: (client: IJMAPClient | null, keyword: string) => Promise; + /** Put a category on contacts, e.g. to undo removing it. */ + addKeyword: (client: IJMAPClient | null, keyword: string, contactIds: string[]) => Promise; importContacts: (client: IJMAPClient | null, contacts: ContactCard[]) => Promise; @@ -1405,6 +1409,48 @@ export const useContactStore = create()( } }, + removeKeyword: async (client, keyword) => { + set({ error: null }); + const kw = keyword.trim(); + const { contacts, supportsSync } = get(); + const affected = contacts.filter(c => c.keywords?.[kw]); + const done: string[] = []; + for (const contact of affected) { + const { [kw]: _gone, ...rest } = contact.keywords || {}; + void _gone; + if (supportsSync && client) { + const originalId = contact.originalId || contact.id; + const accountId = contact.isShared ? contact.accountId : undefined; + // JMAP patch: only this keyword goes; the card's others stay. + await client.updateContact(originalId, { keywords: rest }, accountId); + } + set((state) => ({ + contacts: state.contacts.map(c => (c.id === contact.id ? { ...c, keywords: rest } : c)), + })); + done.push(contact.id); + } + return done; + }, + + addKeyword: async (client, keyword, contactIds) => { + set({ error: null }); + const kw = keyword.trim(); + const { contacts, supportsSync } = get(); + for (const id of contactIds) { + const contact = contacts.find(c => c.id === id); + if (!contact || contact.keywords?.[kw]) continue; + const keywords = { ...(contact.keywords || {}), [kw]: true }; + if (supportsSync && client) { + const originalId = contact.originalId || contact.id; + const accountId = contact.isShared ? contact.accountId : undefined; + await client.updateContact(originalId, { keywords }, accountId); + } + set((state) => ({ + contacts: state.contacts.map(c => (c.id === id ? { ...c, keywords } : c)), + })); + } + }, + loadRecentRecipients: async (client, sentMailboxId) => { if (sentMailboxId) set({ sentMailboxId }); if (get().recentRecipientsLoaded || !sentMailboxId) return;