diff --git a/app/(main)/[locale]/contacts/page.tsx b/app/(main)/[locale]/contacts/page.tsx index 6deb6a47..2f1b7520 100644 --- a/app/(main)/[locale]/contacts/page.tsx +++ b/app/(main)/[locale]/contacts/page.tsx @@ -18,7 +18,9 @@ import { ContactImportDialog } from "@/components/contacts/contact-import-dialog import { RenameDialog } from "@/components/files/rename-dialog"; import { exportContacts } from "@/components/contacts/contact-export"; import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot"; -import { useContactStore, getContactDisplayName } from "@/stores/contact-store"; +import { useContactStore, getContactDisplayName, getContactPrimaryEmail } from "@/stores/contact-store"; +import { savePendingMailto } from "@/lib/protocol-handlers/session"; +import { formatRecipient } from "@/lib/email-composer-utils"; import { useAuthStore, redirectToLogin } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { usePolicyStore } from "@/stores/policy-store"; @@ -461,6 +463,51 @@ export default function ContactsPage() { setView("group-edit"); }, []); + // Open the in-app composer in the current session rather than routing through + // a mailto: URL. `window.location='mailto:'` hands off to the OS handler + // (which may open a different mail app), and the mailto protocol round-trip + // reloads the app - dropping the in-memory per-account JMAP clients of a + // multi-account session, which reads as a logout. Stashing the recipients and + // doing a client-side router.push keeps the session and the active account + // intact; the main route consumes the pending compose and opens the composer + // (see consumePendingMailto in page.tsx). + const openComposeInApp = useCallback((recipients: string[], field: "to" | "cc" | "bcc") => { + savePendingMailto({ + to: field === "to" ? recipients : [], + cc: field === "cc" ? recipients : [], + bcc: field === "bcc" ? recipients : [], + subject: "", + body: "", + }); + router.push("/"); + }, [router]); + + const handleComposeGroupFromSidebar = useCallback((groupId: string, field: "to" | "cc" | "bcc") => { + // Format each member as "Name " so the composer keeps the display + // name (round-trips via formatRecipient -> parseRecipientList). Dedupe by + // email, case-insensitively; members without an email are skipped. + const seen = new Set(); + const recipients: string[] = []; + for (const member of getGroupMembers(groupId)) { + const email = getContactPrimaryEmail(member).trim(); + const key = email.toLowerCase(); + if (!email || seen.has(key)) continue; + seen.add(key); + recipients.push(formatRecipient(getContactDisplayName(member), email)); + } + if (recipients.length === 0) { + toast.error(t("groups.no_member_emails")); + return; + } + openComposeInApp(recipients, field); + }, [getGroupMembers, t, openComposeInApp]); + + const handleComposeContact = useCallback((contact: ContactCard) => { + const email = getContactPrimaryEmail(contact).trim(); + if (!email) return; + openComposeInApp([formatRecipient(getContactDisplayName(contact), email)], "to"); + }, [openComposeInApp]); + const handleDeleteGroupFromSidebar = useCallback(async (groupId: string) => { const confirmed = await confirmDialog({ title: t("groups.delete_confirm_title"), @@ -625,6 +672,7 @@ export default function ContactsPage() { onEdit={handleEditGroup} onDelete={handleDeleteGroup} onRemoveMember={handleRemoveGroupMember} + onComposeGroup={(field) => handleComposeGroupFromSidebar(selectedGroup.id, field)} isMobile={isMobile} onSelectMember={(id) => { setSelectedContact(id); @@ -702,6 +750,11 @@ export default function ContactsPage() { contact={selectedContact} onEdit={handleEdit} onDelete={handleDelete} + onCompose={ + selectedContact + ? () => handleComposeContact(selectedContact) + : undefined + } onAddToGroup={ selectedContact ? () => handleAddContactToGroup(selectedContact.id) @@ -807,6 +860,7 @@ export default function ContactsPage() { onImport={() => setShowImportDialog(true)} onEditGroup={handleEditGroupFromSidebar} onDeleteGroup={handleDeleteGroupFromSidebar} + onComposeGroup={handleComposeGroupFromSidebar} onDropContacts={handleDropContacts} onDropContactsToCategory={handleDropContactsToCategory} onRenameAddressBook={client ? (book) => setRenamingAddressBook(book) : undefined} diff --git a/components/contacts/__tests__/contacts-sidebar.test.tsx b/components/contacts/__tests__/contacts-sidebar.test.tsx new file mode 100644 index 00000000..a47534d0 --- /dev/null +++ b/components/contacts/__tests__/contacts-sidebar.test.tsx @@ -0,0 +1,65 @@ +import { render, screen } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ContactsSidebar } from '../contacts-sidebar'; +import type { ContactCard } from '@/lib/jmap/types'; + +// next-intl + next/navigation are mocked globally in vitest.setup (t returns the key). +vi.mock('@/stores/account-store', () => { + const state = { accounts: [], activeAccountId: null }; + const hook = (sel?: (s: typeof state) => unknown) => + typeof sel === 'function' ? sel(state) : state; + hook.getState = () => state; + return { useAccountStore: hook }; +}); + +const group = { + id: 'g1', + kind: 'group', + name: { full: 'Team' }, + members: { '1': true }, +} as unknown as ContactCard; + +function renderSidebar(onComposeGroup = vi.fn()) { + render( + , + ); + return onComposeGroup; +} + +describe('ContactsSidebar — compose to group', () => { + beforeEach(() => vi.clearAllMocks()); + + it('shows a "Send email to group" submenu in the group context menu', () => { + renderSidebar(); + fireEvent.contextMenu(screen.getByText('Team')); + expect(screen.getByText('groups.send_email')).toBeInTheDocument(); + // and the existing Edit/Delete entries still render + expect(screen.getByText('groups.edit')).toBeInTheDocument(); + expect(screen.getByText('form.delete')).toBeInTheDocument(); + }); + + it('calls onComposeGroup(groupId, field) when a To/Cc/Bcc item is clicked', () => { + const onComposeGroup = renderSidebar(); + fireEvent.contextMenu(screen.getByText('Team')); + + // Open the submenu (hover) then click "Cc". + const trigger = screen.getByText('groups.send_email').closest('.relative')!; + fireEvent.mouseOver(trigger); + fireEvent.mouseEnter(trigger); + + fireEvent.click(screen.getByText('groups.send_email_cc')); + expect(onComposeGroup).toHaveBeenCalledWith('g1', 'cc'); + }); +}); diff --git a/components/contacts/contact-detail.tsx b/components/contacts/contact-detail.tsx index f1f3382b..b2f489d5 100644 --- a/components/contacts/contact-detail.tsx +++ b/components/contacts/contact-detail.tsx @@ -32,6 +32,8 @@ interface ContactDetailProps { onDelete: () => void; onAddToGroup?: () => void; onDuplicate?: () => void; + /** Compose an email to this contact in the app (no OS mailto handoff). */ + onCompose?: () => void; isMobile?: boolean; className?: string; } @@ -115,7 +117,7 @@ function formatDate(dateInput: AnniversaryDate): string { return dateStr; } -export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDuplicate, isMobile, className }: ContactDetailProps) { +export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDuplicate, onCompose, isMobile, className }: ContactDetailProps) { const t = useTranslations("contacts"); const smimeStore = useSmimeStore(); const [parsedCerts, setParsedCerts] = useState>(new Map()); @@ -260,14 +262,16 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli
- {email && ( - {t("detail.compose_email")} - + )} {phone && ( void; onRemoveMember: (memberId: string) => void; onSelectMember: (id: string) => void; + /** Compose an email to every member with the recipients placed in `field`. */ + onComposeGroup?: (field: "to" | "cc" | "bcc") => void; isMobile?: boolean; className?: string; } @@ -26,11 +28,13 @@ export function ContactGroupDetail({ onDelete, onRemoveMember, onSelectMember, + onComposeGroup, isMobile, className, }: ContactGroupDetailProps) { const t = useTranslations("contacts"); const groupName = getContactDisplayName(group); + const hasEmailMembers = members.some((m) => getContactPrimaryEmail(m).trim()); return (
@@ -62,6 +66,25 @@ export function ContactGroupDetail({
+ {onComposeGroup && hasEmailMembers && ( +
+ + {t("groups.send_email")} +
+ {(["to", "cc", "bcc"] as const).map((field) => ( + + ))} +
+
+ )}
diff --git a/components/contacts/contacts-sidebar.tsx b/components/contacts/contacts-sidebar.tsx index 99d4ce26..44dd1c7c 100644 --- a/components/contacts/contacts-sidebar.tsx +++ b/components/contacts/contacts-sidebar.tsx @@ -2,10 +2,10 @@ import { useMemo, useState, useCallback, useEffect, useRef, type DragEvent } from "react"; import { useTranslations } from "next-intl"; -import { BookUser, User, Users, Plus, Share2, Book, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2, Settings } from "lucide-react"; +import { BookUser, User, Users, Plus, Share2, Book, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2, Settings, Mail } from "lucide-react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; -import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu"; +import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu"; import { useContextMenu } from "@/hooks/use-context-menu"; import { cn } from "@/lib/utils"; import type { ContactCard, AddressBook } from "@/lib/jmap/types"; @@ -25,6 +25,7 @@ interface ContactsSidebarProps { onImport?: () => void; onEditGroup?: (groupId: string) => void; onDeleteGroup?: (groupId: string) => void; + onComposeGroup?: (groupId: string, field: "to" | "cc" | "bcc") => void; onDropContacts?: (contactIds: string[], addressBook: AddressBook) => void; onDropContactsToCategory?: (contactIds: string[], keyword: string) => void; onRenameAddressBook?: (addressBook: AddressBook) => void; @@ -93,6 +94,7 @@ export function ContactsSidebar({ onImport, onEditGroup, onDeleteGroup, + onComposeGroup, onDropContacts, onDropContactsToCategory, onRenameAddressBook, @@ -684,6 +686,21 @@ export function ContactsSidebar({ onEditGroup?.(groupContextMenu.data!.id); }} /> + {onComposeGroup && ( + + {(["to", "cc", "bcc"] as const).map((field) => ( + { + const groupId = groupContextMenu.data!.id; + closeGroupContextMenu(); + onComposeGroup(groupId, field); + }} + /> + ))} + + )} encodeURIComponent(r)).join(","); + return field === "to" ? `mailto:${encoded}` : `mailto:?${field}=${encoded}`; +} + +describe("parseMailto display-name handling", () => { + it("preserves display names through the mailto round-trip", () => { + const recipients = [ + formatRecipient("Alice Smith", "alice@x.com"), + formatRecipient("Bob", "bob@y.com"), + ]; + const parsed = parseMailto(encodeMailto(recipients, "to")); + expect(parseRecipientList(parsed!.to.join(", "))).toEqual([ + { name: "Alice Smith", email: "alice@x.com" }, + { name: "Bob", email: "bob@y.com" }, + ]); + }); + + it("keeps a display name containing a comma intact (quote-aware split)", () => { + const recipients = [ + formatRecipient("Doe, John", "john@doe.org"), // -> "Doe, John" + "alice@x.com", + ]; + const parsed = parseMailto(encodeMailto(recipients, "cc")); + expect(parsed!.cc).toEqual(['"Doe, John" ', "alice@x.com"]); + expect(parseRecipientList(parsed!.cc.join(", "))).toEqual([ + { name: "Doe, John", email: "john@doe.org" }, + { email: "alice@x.com" }, + ]); + }); +}); diff --git a/lib/protocol-handlers/mailto.ts b/lib/protocol-handlers/mailto.ts index bc3e1ba8..c05c79ce 100644 --- a/lib/protocol-handlers/mailto.ts +++ b/lib/protocol-handlers/mailto.ts @@ -1,3 +1,5 @@ +import { splitRecipients as splitRecipientString } from "@/lib/email-composer-utils"; + export interface ParsedMailto { to: string[]; cc: string[]; @@ -25,10 +27,9 @@ function stripBodyControlChars(value: string): string { } function splitRecipients(value: string): string[] { - return stripControlChars(value) - .split(",") - .map((recipient) => recipient.trim()) - .filter(Boolean); + // Quote/angle-aware split so a `"Doe, John" ` display name with + // an embedded comma stays a single recipient instead of being torn in two. + return splitRecipientString(stripControlChars(value)); } type QueryParam = { diff --git a/locales/cs/common.json b/locales/cs/common.json index 63a36fc0..01ca20f9 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2359,7 +2359,12 @@ "members_label": "Členové", "search_members": "Hledat kontakty k přidání...", "no_members": "Tato skupina nemá žádné členy", - "member_count": "{count, plural, =0 {Žádní členové} one {1 člen} few {# členové} other {# členů}}" + "member_count": "{count, plural, =0 {Žádní členové} one {1 člen} few {# členové} other {# členů}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Importovat kontakty", diff --git a/locales/da/common.json b/locales/da/common.json index 192e6bf1..4365f880 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2359,7 +2359,12 @@ "members_label": "Medlemmer", "search_members": "Søg efter kontakter at tilføje...", "no_members": "Ingen medlemmer i denne gruppe", - "member_count": "{count, plural, =0 {Ingen medlemmer} one {1 medlem} other {# medlemmer}}" + "member_count": "{count, plural, =0 {Ingen medlemmer} one {1 medlem} other {# medlemmer}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Importér kontakter", diff --git a/locales/de/common.json b/locales/de/common.json index f64d6269..a0f81efc 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2359,7 +2359,12 @@ "members_label": "Mitglieder", "search_members": "Kontakte zum Hinzufügen suchen...", "no_members": "Keine Mitglieder in dieser Gruppe", - "member_count": "{count, plural, =0 {Keine Mitglieder} one {1 Mitglied} other {# Mitglieder}}" + "member_count": "{count, plural, =0 {Keine Mitglieder} one {1 Mitglied} other {# Mitglieder}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Kontakte importieren", diff --git a/locales/en/common.json b/locales/en/common.json index 23a25a60..ca1d0f8d 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2360,7 +2360,12 @@ "members_label": "Members", "search_members": "Search contacts to add...", "no_members": "No members in this group", - "member_count": "{count, plural, =0 {No members} one {1 member} other {# members}}" + "member_count": "{count, plural, =0 {No members} one {1 member} other {# members}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Import Contacts", diff --git a/locales/es/common.json b/locales/es/common.json index 88448bd9..b88ffa1c 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2359,7 +2359,12 @@ "members_label": "Miembros", "search_members": "Buscar contactos para agregar...", "no_members": "No hay miembros en este grupo", - "member_count": "{count, plural, =0 {Sin miembros} one {1 miembro} other {# miembros}}" + "member_count": "{count, plural, =0 {Sin miembros} one {1 miembro} other {# miembros}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Importar contactos", diff --git a/locales/fr/common.json b/locales/fr/common.json index 75c840df..95c74a1e 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2359,7 +2359,12 @@ "members_label": "Membres", "search_members": "Rechercher des contacts à ajouter...", "no_members": "Aucun membre dans ce groupe", - "member_count": "{count, plural, =0 {Aucun membre} one {1 membre} other {# membres}}" + "member_count": "{count, plural, =0 {Aucun membre} one {1 membre} other {# membres}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Importer des contacts", diff --git a/locales/hu/common.json b/locales/hu/common.json index f7abc249..1a5aed9f 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -2360,7 +2360,12 @@ "members_label": "Tagok", "search_members": "Névjegyek keresése a hozzáadáshoz...", "no_members": "Nincs tag ebben a csoportban", - "member_count": "{count, plural, =0 {Nincs tag} one {1 tag} other {# tag}}" + "member_count": "{count, plural, =0 {Nincs tag} one {1 tag} other {# tag}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Névjegyek importálása", diff --git a/locales/it/common.json b/locales/it/common.json index d1de5459..0566a8a0 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2359,7 +2359,12 @@ "members_label": "Membri", "search_members": "Cerca contatti da aggiungere...", "no_members": "Nessun membro in questo gruppo", - "member_count": "{count, plural, =0 {Nessun membro} one {1 membro} other {# membri}}" + "member_count": "{count, plural, =0 {Nessun membro} one {1 membro} other {# membri}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Importa contatti", diff --git a/locales/ja/common.json b/locales/ja/common.json index f508c373..91c67b66 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2359,7 +2359,12 @@ "members_label": "メンバー", "search_members": "追加する連絡先を検索...", "no_members": "このグループにメンバーがいません", - "member_count": "{count, plural, =0 {メンバーなし} other {#人のメンバー}}" + "member_count": "{count, plural, =0 {メンバーなし} other {#人のメンバー}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "連絡先をインポート", diff --git a/locales/ko/common.json b/locales/ko/common.json index 749710b9..bbe92263 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2359,7 +2359,12 @@ "members_label": "멤버", "search_members": "추가할 연락처 검색...", "no_members": "이 그룹에는 멤버가 없어요", - "member_count": "멤버 {count}명" + "member_count": "멤버 {count}명", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "연락처 가져오기", diff --git a/locales/lv/common.json b/locales/lv/common.json index 030dab89..be64fde0 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2355,7 +2355,12 @@ "members_label": "Dalībnieki", "search_members": "Meklēt kontaktus, ko pievienot...", "no_members": "Šajā grupā nav dalībnieku", - "member_count": "{count, plural, =0 {Nav dalībnieku} one {1 dalībnieks} other {# dalībnieki}}" + "member_count": "{count, plural, =0 {Nav dalībnieku} one {1 dalībnieks} other {# dalībnieki}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Kontaktu imports", diff --git a/locales/nl/common.json b/locales/nl/common.json index c442c882..b05bba1d 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2359,7 +2359,12 @@ "members_label": "Leden", "search_members": "Contacten zoeken om toe te voegen...", "no_members": "Geen leden in deze groep", - "member_count": "{count, plural, =0 {Geen leden} one {1 lid} other {# leden}}" + "member_count": "{count, plural, =0 {Geen leden} one {1 lid} other {# leden}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Contacten importeren", diff --git a/locales/pl/common.json b/locales/pl/common.json index ebaf193e..6c18886b 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2359,7 +2359,12 @@ "members_label": "Członkowie", "search_members": "Szukaj kontaktów do dodania...", "no_members": "Brak członków w tej grupie", - "member_count": "{count, plural, =0 {Brak członków} one {1 członek} other {# członków}}" + "member_count": "{count, plural, =0 {Brak członków} one {1 członek} other {# członków}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Importuj kontakty", diff --git a/locales/pt/common.json b/locales/pt/common.json index 79a18875..1245e271 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2359,7 +2359,12 @@ "members_label": "Membros", "search_members": "Pesquisar contatos para adicionar...", "no_members": "Nenhum membro neste grupo", - "member_count": "{count, plural, =0 {Nenhum membro} one {1 membro} other {# membros}}" + "member_count": "{count, plural, =0 {Nenhum membro} one {1 membro} other {# membros}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Importar contatos", diff --git a/locales/ru/common.json b/locales/ru/common.json index 0a534fda..8adc1710 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2359,7 +2359,12 @@ "members_label": "Участники", "search_members": "Поиск контактов для добавления...", "no_members": "В этой группе нет участников", - "member_count": "{count, plural, =0 {Нет участников} one {1 участник} other {# участников}}" + "member_count": "{count, plural, =0 {Нет участников} one {1 участник} other {# участников}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Импорт контактов", diff --git a/locales/tr/common.json b/locales/tr/common.json index 8ad57c42..7b312ee0 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2359,7 +2359,12 @@ "members_label": "Üyeler", "search_members": "Eklemek için kişileri arayın...", "no_members": "Bu grupta üye yok", - "member_count": "{count, plural, =0 {Üye yok} one {1 üye} other {# üye}}" + "member_count": "{count, plural, =0 {Üye yok} one {1 üye} other {# üye}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Kişileri İçe Aktar", diff --git a/locales/uk/common.json b/locales/uk/common.json index 0a364713..ab746338 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2359,7 +2359,12 @@ "members_label": "Члени", "search_members": "Пошук контактів для додавання...", "no_members": "У цій групі немає учасників", - "member_count": "{count, plural, =0 {Немає учасників} one {1 учасник} few {# учасники} many {# учасників} other {# учасників}}" + "member_count": "{count, plural, =0 {Немає учасників} one {1 учасник} few {# учасники} many {# учасників} other {# учасників}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "Імпортувати контакти", diff --git a/locales/zh/common.json b/locales/zh/common.json index 718afd4e..fc6228d7 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2359,7 +2359,12 @@ "members_label": "成员", "search_members": "搜索联系人以添加...", "no_members": "该群组中没有成员", - "member_count": "{count, plural, =0 {无成员} one {1 位成员} other {# 位成员}}" + "member_count": "{count, plural, =0 {无成员} one {1 位成员} other {# 位成员}}", + "send_email": "Send email to group", + "send_email_to": "To", + "send_email_cc": "Cc", + "send_email_bcc": "Bcc", + "no_member_emails": "This group has no members with an email address." }, "import": { "title": "导入联系人",