From 3a2be9a7d45cdb20ef45d0d57b9facec73cc03b3 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:38:38 +0100 Subject: [PATCH] fix: edit the name recipients see on the Account page and stop cutting account addresses Settings > Account had a "Display name" that edited Stalwart's account description, while mail went out with the identity's name, so changing it changed nothing people saw. The page now shows "Your name", the primary address's name, and saves it to that identity. The account switcher and avatars take the same name; the principal description is only used for an account without an identity name. The address editor calls the field "Your name" too, and "identities" are "addresses" in the Account section and its dialog, said once instead of twice. Logged-in accounts show the server only for an account in trouble or one sharing its address with another, with no drag hint or arrows for a single account. The switcher wraps addresses instead of cutting them and has no remove button per row; an account you're not in is removed from Settings > Account. Changed tests: auth-store-display-name now expects the identity name to win over the principal; account-switcher-remove expects no remove button on the rows; settings-editors-unsaved uses the renamed "Add address" button and the "Your name" label, which has no placeholder. --- components/identity/identity-form.tsx | 10 +- .../identity/identity-manager-modal.tsx | 27 ++-- .../account-switcher-remove.test.tsx | 42 +++--- components/layout/account-switcher.tsx | 54 ++----- ...ccount-settings-name-and-accounts.test.tsx | 112 ++++++++++++++ .../__tests__/identity-settings.test.tsx | 14 ++ .../settings-editors-unsaved.test.tsx | 5 +- .../settings/account-security-settings.tsx | 54 ------- components/settings/account-settings.tsx | 138 ++++++++++-------- components/settings/identity-settings.tsx | 20 +-- components/settings/your-name-row.tsx | 76 ++++++++++ .../settings-search-addresses.test.ts | 34 +++++ lib/account-utils.ts | 14 ++ lib/settings-search.ts | 6 +- locales/ar/common.json | 33 +++++ locales/ca/common.json | 33 +++++ locales/cs/common.json | 33 +++++ locales/da/common.json | 33 +++++ locales/de/common.json | 33 +++++ locales/en/common.json | 33 +++++ locales/es/common.json | 33 +++++ locales/et/common.json | 33 +++++ locales/fa/common.json | 33 +++++ locales/fr/common.json | 33 +++++ locales/he/common.json | 33 +++++ locales/hu/common.json | 33 +++++ locales/it/common.json | 33 +++++ locales/ja/common.json | 33 +++++ locales/ko/common.json | 33 +++++ locales/lv/common.json | 33 +++++ locales/mn/common.json | 33 +++++ locales/nb/common.json | 33 +++++ locales/nl/common.json | 33 +++++ locales/pl/common.json | 33 +++++ locales/pt/common.json | 33 +++++ locales/ro/common.json | 33 +++++ locales/ru/common.json | 33 +++++ locales/sk/common.json | 33 +++++ locales/th/common.json | 33 +++++ locales/tr/common.json | 33 +++++ locales/uk/common.json | 33 +++++ locales/vi/common.json | 33 +++++ locales/zh-TW/common.json | 33 +++++ locales/zh/common.json | 33 +++++ .../__tests__/auth-store-display-name.test.ts | 59 +++++--- stores/auth-store.ts | 19 ++- stores/identity-store.ts | 18 ++- 47 files changed, 1451 insertions(+), 241 deletions(-) create mode 100644 components/settings/__tests__/account-settings-name-and-accounts.test.tsx create mode 100644 components/settings/your-name-row.tsx create mode 100644 lib/__tests__/settings-search-addresses.test.ts diff --git a/components/identity/identity-form.tsx b/components/identity/identity-form.tsx index e48c83d54..b1717b352 100644 --- a/components/identity/identity-form.tsx +++ b/components/identity/identity-form.tsx @@ -34,6 +34,7 @@ export function IdentityForm({ identity, onSave, onCancel, onDirtyChange }: Iden const t = useTranslations('identities.form'); const tValidation = useTranslations('identities.validation_errors'); const tOrg = useTranslations('org_signature'); + const tName = useTranslations('uxs2_ident.your_name'); const isEditing = !!identity; // Composing > Plain text only: the signature is plain text too. const plainTextMode = useSettingsStore((s) => s.plainTextMode); @@ -156,8 +157,9 @@ export function IdentityForm({ identity, onSave, onCancel, onDirtyChange }: Iden
{/* Name */}
+ {/* "Your name", like the Account page: the one name recipients see. */} setFormData({ ...formData, name: e.target.value })} - placeholder={t('name_placeholder')} disabled={isSubmitting} className={errors.name ? 'border-destructive' : ''} - aria-describedby={errors.name ? 'name-error' : undefined} + aria-describedby={errors.name ? 'name-error' : 'identity-name-hint'} aria-invalid={!!errors.name} /> +

{tName('description')}

{errors.name && (
-

{t('create_new')}

+

{tAddr('add')}

void cancelForm()} @@ -247,7 +248,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr className="mb-6 w-full sm:w-auto" > - {t('create_new')} + {tAddr('add')} )} @@ -261,7 +262,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr {editingId === identity.id ? (

- {t('edit_identity')} + {tAddr('edit')}

)} setEditingId(identity.id)} disabled={!!editingId || isCreating} > @@ -358,7 +359,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr {identities.length === 0 && !isCreating && (
-

{t('no_identities')}

+

{tAddr('count', { count: 0 })}

)}
diff --git a/components/layout/__tests__/account-switcher-remove.test.tsx b/components/layout/__tests__/account-switcher-remove.test.tsx index f287edb60..a93fdcacd 100644 --- a/components/layout/__tests__/account-switcher-remove.test.tsx +++ b/components/layout/__tests__/account-switcher-remove.test.tsx @@ -1,9 +1,8 @@ -import { act, fireEvent, render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it } from 'vitest'; -// Removing an account from the popover: the app's confirm dialog instead of -// the browser's, a control a touch screen can reach, and sign-out rows in -// the menu's plain text colour. +// The popover's rows: no remove button (settings-p2-11), whole addresses, +// and sign-out rows in the menu's plain text colour. const { AccountSwitcher } = await import('../account-switcher'); const { useAccountStore } = await import('@/stores/account-store'); @@ -18,35 +17,34 @@ function account(id: string) { }; } -const removeAccount = vi.fn(); - beforeEach(() => { - removeAccount.mockClear(); useAccountStore.setState({ accounts: [account('a'), account('b')] } as never); - useAuthStore.setState({ activeAccountId: 'a', removeAccount } as never); + useAuthStore.setState({ activeAccountId: 'a' } as never); useSettingsStore.setState({ interfaceLayout: 'default' } as never); }); describe('removing an account from the popover', () => { - it('asks with the app dialog, not the browser, and removes on confirm', async () => { - const browserConfirm = vi.spyOn(window, 'confirm').mockReturnValue(true); + // settings-p2-11: each row had a hidden X next to "Sign out of …" below. + // An account you're not in is removed under Settings > Account, where + // "Manage accounts…" leads. + it('has no remove button on the rows', () => { render(); fireEvent.click(screen.getByTestId('account-switcher')); - fireEvent.click(screen.getByRole('menuitem', { name: 'remove_account' })); - - expect(browserConfirm).not.toHaveBeenCalled(); - const dialog = screen.getByRole('alertdialog', { name: 'remove_account_title' }); - expect(removeAccount).not.toHaveBeenCalled(); - await act(async () => { fireEvent.click(screen.getByRole('button', { name: 'remove_account' })); }); - expect(removeAccount).toHaveBeenCalledWith('b'); - expect(dialog).not.toBeInTheDocument(); - browserConfirm.mockRestore(); + expect(screen.queryByRole('menuitem', { name: 'remove_account' })).toBeNull(); + expect(screen.getAllByTestId('account-option')).toHaveLength(2); }); - it('keeps the remove control visible on a touch screen', () => { + // The address is what tells two same-named accounts apart, and it was cut + // ("dso-ux-settings@example…"). + it('shows the whole address, wrapped, with the full text as a tooltip', () => { + const long = (id: string, email: string) => ({ ...account(id), email, username: email, label: 'Sam', displayName: 'Sam' }); + useAccountStore.setState({ accounts: [long('a', 'dso-ux-settings@example.org'), long('b', 'dso-ux-settings-b@example.org')] } as never); render(); fireEvent.click(screen.getByTestId('account-switcher')); - expect(screen.getByRole('menuitem', { name: 'remove_account' }).className).toContain('pointer-coarse:opacity-100'); + const title = screen.getByText('dso-ux-settings-b@example.org'); + expect(title.className).not.toContain('truncate'); + expect(title.className).toContain('wrap-anywhere'); + expect(title.getAttribute('title')).toBe('dso-ux-settings-b@example.org'); }); it('draws "sign out of all accounts" like the other rows, start-aligned', () => { diff --git a/components/layout/account-switcher.tsx b/components/layout/account-switcher.tsx index 57e4df9a9..aa1292cde 100644 --- a/components/layout/account-switcher.tsx +++ b/components/layout/account-switcher.tsx @@ -12,7 +12,7 @@ import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { useSettingsStore } from "@/stores/settings-store"; import { HelpMenuItems } from "@/components/layout/help-links"; -import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds, accountRowText, accountAvatarName } from "@/lib/account-utils"; +import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds, accountRowText, accountAvatarName, accountSharesAddress } from "@/lib/account-utils"; import { inboxUnread, totalInboxUnread } from "@/lib/account-inbox-unread"; import { isDocumentRTL } from "@/i18n/direction"; import { cn, formatFileSize } from "@/lib/utils"; @@ -20,9 +20,6 @@ import { useRouter } from "@/i18n/navigation"; import { onRefusedLoginsChanged, siblingWaits, waitDate } from "@/lib/devices/wipe-state"; import { Avatar } from "@/components/ui/avatar"; import { useMenuNavigation } from "@/hooks/use-menu-navigation"; -import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; -import { ConfirmDialog } from "@/components/ui/confirm-dialog"; -import { IconButton } from "@/components/ui/icon-button"; import { toUnicodeDomain, toUnicodeEmail } from "@/lib/idn"; import { buildSettingsPath } from "@/lib/deep-links"; @@ -118,9 +115,7 @@ export function AccountSwitcher({ const activeAccount = accounts.find((a) => a.id === activeAccountId); const switchAccount = useAuthStore((s) => s.switchAccount); const logout = useAuthStore((s) => s.logout); - const removeAccount = useAuthStore((s) => s.removeAccount); const logoutAll = useAuthStore((s) => s.logoutAll); - const { dialogProps: confirmDialogProps, confirm } = useConfirmDialog(); const tWipe = useTranslations("offlineWipe"); // An account that signed out by itself beside this one: its data waits. const [, noteWaitsChanged] = useState(0); @@ -218,20 +213,6 @@ export function AccountSwitcher({ router.push(buildSettingsPath("account", "browser") as never); }; - const handleRemove = async (e: React.MouseEvent, account: AccountEntry) => { - e.stopPropagation(); - const label = toUnicodeEmail(account.email || account.username); - // The popover closes first: the dialog is the only thing asking now. - setOpen(false); - const ok = await confirm({ - title: t("remove_account_title"), - message: t("remove_account_confirm", { account: label }), - confirmText: t("remove_account"), - variant: "destructive", - }); - if (ok) removeAccount(account.id); - }; - const handleLogout = () => { setOpen(false); logout(); @@ -316,9 +297,7 @@ export function AccountSwitcher({ const unreadFor = (account: AccountEntry) => mvPopover || severalAccounts ? inboxUnread(unreadByAccount[account.id]) : undefined; // The server is only worth a line when it is what tells two rows apart. - const addressKey = (account: AccountEntry) => toUnicodeEmail(account.email || account.username).trim().toLowerCase(); - const sharesAddress = (account: AccountEntry) => - accounts.some((other) => other.id !== account.id && addressKey(other) === addressKey(account)); + const sharesAddress = (account: AccountEntry) => accountSharesAddress(account, accounts); const connectedIds = accounts.filter((account) => account.isConnected).map((account) => account.id); const allInboxesUnread = totalInboxUnread(connectedIds, unreadByAccount); // What waits in the accounts not on screen: a dot on the avatar says so. @@ -341,7 +320,7 @@ export function AccountSwitcher({ : "w-full px-2 py-1.5 hover:bg-muted text-start min-w-0", className )} - title={variant !== "expanded" ? [displayName, othersUnreadText].filter(Boolean).join("\n") : undefined} + title={[displayName, displaySubtitle, variant !== "expanded" ? othersUnreadText : null].filter(Boolean).join("\n")} aria-label={othersUnreadText ? `${t("switch_account")}, ${othersUnreadText}` : t("switch_account")} aria-expanded={open} aria-haspopup="menu" @@ -414,8 +393,8 @@ export function AccountSwitcher({
-

{displayName}

- {displaySubtitle &&

{displaySubtitle}

} +

{displayName}

+ {displaySubtitle &&

{displaySubtitle}

}
)} @@ -479,7 +458,7 @@ export function AccountSwitcher({ className={cn( "w-full flex items-start gap-3 px-3 py-2.5 text-start transition-colors", isActive ? "bg-accent/50" : "hover:bg-muted", - (!isActive && !account.isDefault) ? (isDraggable ? "pe-16" : "pe-10") : (isDraggable && "pe-7") + isDraggable && "pe-7" )} role="menuitem" disabled={isActive} @@ -494,7 +473,7 @@ export function AccountSwitcher({
- + {toUnicodeEmail(text.title)} {account.isDefault && accounts.length > 1 && ( @@ -505,7 +484,7 @@ export function AccountSwitcher({
{text.subtitle && ( -

+

{toUnicodeEmail(text.subtitle)}

)} @@ -524,25 +503,11 @@ export function AccountSwitcher({ {isDraggable && ( )} - {!isActive && !account.isDefault && ( - // Shown on hover and focus, and always on a touch screen (R7). - void handleRemove(e, account)} - role="menuitem" - label={t("remove_account")} - className="absolute end-1.5 top-1/2 -translate-y-1/2 text-muted-foreground opacity-0 transition-opacity group-hover/acct:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100 hover:bg-destructive/10 hover:text-destructive" - > - - - )}
); })} @@ -718,7 +683,6 @@ export function AccountSwitcher({
, document.body )} - ); } diff --git a/components/settings/__tests__/account-settings-name-and-accounts.test.tsx b/components/settings/__tests__/account-settings-name-and-accounts.test.tsx new file mode 100644 index 000000000..0aca17da6 --- /dev/null +++ b/components/settings/__tests__/account-settings-name-and-accounts.test.tsx @@ -0,0 +1,112 @@ +import { act, fireEvent, render, screen, within } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AccountSettings } from '../account-settings'; +import { useAccountStore } from '@/stores/account-store'; +import { useAuthStore } from '@/stores/auth-store'; +import { useIdentityStore } from '@/stores/identity-store'; +import type { Identity } from '@/lib/jmap/types'; + +// settings-p2-2: Account had "Display name" (Stalwart's account description, +// "DSO UX audit settings (throwaway…)") while mail went out as the identity's +// "Sam Settings". There is one name now: the address's, "Your name". +// settings-p2-11: the server host stood under every account, and the drag +// hint showed with one account. + +vi.mock('next-intl', () => { + const t = (key: string, values?: Record) => (values ? `${key} ${JSON.stringify(values)}` : key); + return { useTranslations: () => t, useLocale: () => 'en' }; +}); +vi.mock('@/lib/stalwart/principal', () => ({ fetchPrincipalDisplayName: vi.fn(async () => 'Admin note (throwaway)') })); +vi.mock('../identity-settings', () => ({ EmailAddressesSection: () => null })); +vi.mock('../layout-settings', () => ({ MultipleAccountsSection: () => null })); +vi.mock('../sign-out-row', () => ({ SignOutRow: () => null })); +vi.mock('../account-archive-settings', () => ({ AccountArchiveSettings: () => null })); + +const sam: Identity = { + id: 'i1', name: 'Sam Settings', email: 'sam@x.test', textSignature: '', htmlSignature: '', mayDelete: false, +}; + +function entry(id: string, email: string, serverUrl = 'https://mail.x.test') { + return { + id, label: 'Admin note (throwaway)', displayName: 'Admin note (throwaway)', email, username: email, serverUrl, + authMode: 'basic' as const, rememberMe: true, cookieSlot: 0, isConnected: true, hasError: false, lastLoginAt: 1, + avatarColor: '#2563eb', isDefault: id === 'a', + }; +} + +const updateIdentity = vi.fn(); +const removeAccount = vi.fn(); + +function signIn(accounts: ReturnType[]) { + useAccountStore.setState({ accounts: accounts as never, activeAccountId: 'a', defaultAccountId: 'a' }); + useIdentityStore.setState({ identities: [sam] }); + useAuthStore.setState({ + username: 'sam@x.test', serverUrl: 'https://mail.x.test', isDemoMode: false, authMode: 'basic', + client: { updateIdentity, getSharedAccounts: () => [] } as never, + activeAccountId: 'a', identities: [sam], primaryIdentity: sam, removeAccount, + } as never); +} + +beforeEach(() => { + updateIdentity.mockReset().mockResolvedValue(undefined); + removeAccount.mockReset(); +}); + +describe('Account: your name', () => { + it("shows the address's name, not the server's account description, and saves it to the address", async () => { + signIn([entry('a', 'sam@x.test')]); + render(); + + const row = screen.getByText('label').closest('[data-search-label]') as HTMLElement; + const input = within(row).getByRole('textbox'); + expect(input).toHaveValue('Sam Settings'); + const profile = document.querySelector('[data-settings-section="profile"]') as HTMLElement; + expect(within(profile).queryByText('Admin note (throwaway)')).toBeNull(); + expect(within(profile).queryByDisplayValue('Admin note (throwaway)')).toBeNull(); + expect(screen.queryByText('name_label')).toBeNull(); + + fireEvent.change(input, { target: { value: 'Sam Lee' } }); + await act(async () => { fireEvent.submit(input.closest('form')!); }); + + expect(updateIdentity).toHaveBeenCalledWith('i1', { name: 'Sam Lee' }); + expect(useAuthStore.getState().primaryIdentity?.name).toBe('Sam Lee'); + // The switcher and the avatar follow the name people see. + await vi.waitFor(() => expect(useAccountStore.getState().getAccountById('a')?.displayName).toBe('Sam Lee')); + }); +}); + +describe('Account: signed-in accounts', () => { + it('shows no server line and no drag hint for one account', () => { + signIn([entry('a', 'sam@x.test')]); + render(); + expect(screen.queryByTestId('account-server')).toBeNull(); + expect(screen.queryByText('mail.x.test')).toBeNull(); + expect(screen.queryByText('accounts.description')).toBeNull(); + expect(screen.queryByRole('button', { name: 'accounts.move_up' })).toBeNull(); + }); + + it('shows the server only when two accounts share an address', () => { + signIn([entry('a', 'sam@x.test'), entry('b', 'sam@y.test', 'https://mail.y.test')]); + const { unmount } = render(); + expect(screen.queryByTestId('account-server')).toBeNull(); + expect(screen.getByText('accounts.description')).toBeTruthy(); + unmount(); + + signIn([entry('a', 'sam@x.test'), entry('b', 'sam@x.test', 'https://mail.y.test')]); + render(); + expect(screen.getAllByTestId('account-server').map((el) => el.textContent)).toEqual(['mail.x.test', 'mail.y.test']); + }); + + it('removes an account you are not in after asking', async () => { + signIn([entry('a', 'sam@x.test'), entry('b', 'sam@y.test')]); + render(); + // Only the other account has a remove button. + const buttons = screen.getAllByRole('button', { name: 'remove_account' }); + expect(buttons).toHaveLength(1); + fireEvent.click(buttons[0]); + const dialog = await screen.findByRole('alertdialog'); + expect(removeAccount).not.toHaveBeenCalled(); + await act(async () => { fireEvent.click(within(dialog).getByRole('button', { name: 'remove_account' })); }); + expect(removeAccount).toHaveBeenCalledWith('b'); + }); +}); diff --git a/components/settings/__tests__/identity-settings.test.tsx b/components/settings/__tests__/identity-settings.test.tsx index 21847d0a9..2908c8973 100644 --- a/components/settings/__tests__/identity-settings.test.tsx +++ b/components/settings/__tests__/identity-settings.test.tsx @@ -1,5 +1,6 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; +import en from '@/locales/en/common.json'; import { EmailAddressesSection } from '../identity-settings'; // SET-19: "Learn more" next to Sub-Addressing opened the identity manager, @@ -21,4 +22,17 @@ describe('Email addresses section', () => { fireEvent.click(screen.getByRole('button', { name: 'manage' })); expect(screen.getByRole('dialog')).toBeTruthy(); }); + + // settings-p2-18: "Email addresses: The email addresses you can send from" + // over "Your sender addresses: Addresses you can send mail from". The + // section says it once; the row only counts them, without "identity". + it('says what the addresses are once, and counts them as addresses', () => { + render(); + const row = screen.getByText('label').closest('[data-search-label]') as HTMLElement; + expect(row.querySelector('p')).toBeNull(); + expect(within(row).getByText('count')).toBeTruthy(); + expect(screen.queryByText('identities_count.description')).toBeNull(); + expect(en.uxs2_ident.addresses.count).not.toMatch(/identit/i); + expect(en.uxs2_ident.addresses.manage).toBe('Edit addresses…'); + }); }); diff --git a/components/settings/__tests__/settings-editors-unsaved.test.tsx b/components/settings/__tests__/settings-editors-unsaved.test.tsx index 89af29a1c..aff1fbda7 100644 --- a/components/settings/__tests__/settings-editors-unsaved.test.tsx +++ b/components/settings/__tests__/settings-editors-unsaved.test.tsx @@ -102,8 +102,9 @@ describe('identity dialog', () => { // In a portal on body, not inside the settings page. expect(screen.getByRole('dialog').parentElement?.parentElement).toBe(document.body); - fireEvent.click(screen.getByRole('button', { name: 'create_new' })); - fireEvent.change(screen.getByPlaceholderText('name_placeholder'), { target: { value: 'Work' } }); + // "Add address" and "Your name" (settings-p2-2, p2-3), no placeholder. + fireEvent.click(screen.getByRole('button', { name: 'add' })); + fireEvent.change(screen.getByLabelText('label'), { target: { value: 'Work' } }); clickOutside(); expect(onClose).not.toHaveBeenCalled(); diff --git a/components/settings/account-security-settings.tsx b/components/settings/account-security-settings.tsx index ec1194a30..1391ef407 100644 --- a/components/settings/account-security-settings.tsx +++ b/components/settings/account-security-settings.tsx @@ -174,60 +174,6 @@ function PasswordChangeSection() { ); } -/** - * Editable display name (the Stalwart principal's name). Rendered by the - * Account page's Profile section; the Security page no longer repeats it. - */ -export function DisplayNameSection() { - const t = useTranslations('settings.security'); - const { displayName, updateDisplayName, isSaving, isLoadingPrincipal } = useAccountSecurityStore(); - const [name, setName] = useState(displayName); - const [saved, setSaved] = useState(false); - - useEffect(() => { - setName(displayName); - }, [displayName]); - - const handleSave = async () => { - try { - await updateDisplayName(name); - setSaved(true); - setTimeout(() => setSaved(false), 2000); - toast.success(t('display_name.success')); - } catch (err) { - toast.error(t('display_name.error'), err instanceof Error ? err.message : undefined); - } - }; - - if (isLoadingPrincipal) { - return ( - - - - ); - } - - return ( - -
- setName(e.target.value)} - placeholder={displayName || t('display_name.placeholder')} - className="w-48" - /> - -
-
- ); -} - function generateTotp(accountLabel: string, issuer: string): { totp: OTPAuth.TOTP; url: string } { const totp = new OTPAuth.TOTP({ issuer, diff --git a/components/settings/account-settings.tsx b/components/settings/account-settings.tsx index 1914fa4d8..cea4ed09b 100644 --- a/components/settings/account-settings.tsx +++ b/components/settings/account-settings.tsx @@ -1,8 +1,8 @@ "use client"; -import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; +import { useState, useRef, useCallback, useMemo } from 'react'; import { useTranslations } from 'next-intl'; -import { Check, GripVertical, Plus, Star, AlertCircle, ChevronRight, ChevronUp, ChevronDown } from '@/components/icons'; +import { Check, GripVertical, Plus, Star, AlertCircle, ChevronRight, ChevronUp, ChevronDown, X } from '@/components/icons'; import { useAuthStore } from '@/stores/auth-store'; import { useEmailStore } from '@/stores/email-store'; import { useAccountStore, type AccountEntry } from '@/stores/account-store'; @@ -12,18 +12,18 @@ import { SettingsSection, SettingItem } from './settings-section'; import { Avatar } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { useRouter } from '@/i18n/navigation'; -import { getMaxAccounts, accountRowText } from '@/lib/account-utils'; +import { getMaxAccounts, accountRowText, accountSharesAddress } from '@/lib/account-utils'; import { formatFileSize, cn } from '@/lib/utils'; import { toUnicodeDomain, toUnicodeEmail } from '@/lib/idn'; import { IS_LITE } from '@/lib/lite'; -import { isPasswordSession } from '@/lib/auth/password-session'; import { usePolicyStore } from '@/stores/policy-store'; import { AccountArchiveSettings } from './account-archive-settings'; import { SignOutRow } from './sign-out-row'; -import { DisplayNameSection } from './account-security-settings'; +import { YourNameRow } from './your-name-row'; import { EmailAddressesSection } from './identity-settings'; import { MultipleAccountsSection } from './layout-settings'; -import { useAccountSecurityStore } from '@/stores/account-security-store'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; +import { useConfirmDialog } from '@/hooks/use-confirm-dialog'; function hostnameOf(serverUrl: string): string { try { return toUnicodeDomain(new URL(serverUrl).hostname); } catch { return serverUrl; } @@ -42,30 +42,15 @@ function firstScopedTab(caps: SharedAccount['capabilities']): string | null { export function AccountSettings() { const t = useTranslations('settings.account'); const tCommon = useTranslations('common'); + const tSidebar = useTranslations('sidebar'); const router = useRouter(); + const removeAccount = useAuthStore((s) => s.removeAccount); + const { dialogProps: confirmDialogProps, confirm } = useConfirmDialog(); const { username, serverUrl, isDemoMode, primaryIdentity, authMode, client } = useAuthStore(); const activeAccountId = useAuthStore((s) => s.activeAccountId); const switchAccount = useAuthStore((s) => s.switchAccount); const setManagedAccount = useManagedAccountStore((s) => s.setManagedAccount); const archiveEnabled = usePolicyStore((s) => s.isFeatureEnabled('accountArchiveEnabled')) && !IS_LITE && !isDemoMode; - const isAuthenticated = useAuthStore((s) => s.isAuthenticated); - // The display name is editable on Stalwart (the principal's name) for - // password sign-ins; identity-provider and token sessions cannot manage - // the principal. - const { isStalwart, probe, fetchPrincipal } = useAccountSecurityStore(); - const activeAccount = useAccountStore((s) => (activeAccountId ? s.getAccountById(activeAccountId) : undefined)); - const canEditDisplayName = isStalwart === true && isPasswordSession(authMode, activeAccount); - // Wait for `client` before probing, as the Security page does: on reload - // the persisted session flips authenticated before the client reconnects. - useEffect(() => { - if (!isAuthenticated || !client) return; - if (isStalwart === null) { - void probe().then((detected) => { if (detected) void fetchPrincipal(); }); - } else if (isStalwart) { - void fetchPrincipal(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAuthenticated, client, isStalwart]); // Shared/group accounts delegated to this session (excludes the user's own // primary account). These can be drilled into for scoped settings editing. @@ -83,10 +68,8 @@ export function AccountSettings() { const draggedIndexRef = useRef(null); const quotaPercentage = quota && quota.total > 0 ? Math.min(Math.round((quota.used / quota.total) * 100), 100) : 0; - // `account.displayName` is refreshed from the server on every login/restore - // (Stalwart principal "Full name" when available - #900); the identity name - // is only a fallback until that entry exists. - const displayName = account?.displayName || primaryIdentity?.name || (isDemoMode ? 'Nora Lindqvist' : undefined); + // Shown when there is no identity to edit: the name the app knows. + const displayName = primaryIdentity?.name || account?.displayName || (isDemoMode ? 'Nora Lindqvist' : undefined); const email = primaryIdentity?.email || account?.email || username; const max = getMaxAccounts(); @@ -131,6 +114,18 @@ export function AccountSettings() { void switchAccount(id); }, [activeAccountId, switchAccount]); + // The switcher has no remove button on its rows; an account you're not in + // is removed here ("Manage accounts…" leads to this list). + const handleRemove = useCallback(async (target: AccountEntry) => { + const ok = await confirm({ + title: tSidebar('remove_account_title'), + message: tSidebar('remove_account_confirm', { account: toUnicodeEmail(target.email || target.username) }), + confirmText: tSidebar('remove_account'), + variant: 'destructive', + }); + if (ok) removeAccount(target.id); + }, [confirm, removeAccount, tSidebar]); + const handleAddAccount = useCallback(() => { router.push(`/login?mode=add-account` as never); }, [router]); @@ -148,14 +143,9 @@ export function AccountSettings() { return (
- {/* Display Name: editable where the server lets us, read-only otherwise. */} - {canEditDisplayName ? ( - - ) : ( - - {displayName || tCommon('unknown')} - - )} + {/* One name: the primary address's, what recipients see (settings-p2-2). + Stalwart's principal description is an admin's field and isn't shown. */} + {/* Email Address */} @@ -225,13 +215,20 @@ export function AccountSettings() { {/* Logged-in accounts list */} {accounts.length > 0 && ( - + 1 ? t('accounts.description') : undefined} + >
{accounts.map((a, index) => ( 1} index={index} isActive={a.id === activeAccountId} isFirst={index === 0} @@ -245,6 +242,7 @@ export function AccountSettings() { onMoveDown={() => moveAccount(index, index + 1)} onSwitch={() => handleSwitch(a.id)} onSetDefault={() => setDefaultAccount(a.id)} + onRemove={() => void handleRemove(a)} labels={{ active: t('accounts.active'), default: t('accounts.default_badge'), @@ -253,6 +251,7 @@ export function AccountSettings() { moveUp: t('accounts.move_up'), moveDown: t('accounts.move_down'), dragHandle: t('accounts.drag_handle'), + remove: tSidebar('remove_account'), }} /> ))} @@ -272,6 +271,8 @@ export function AccountSettings() { )} + + {archiveEnabled && } @@ -320,6 +321,10 @@ export function AccountSettings() { interface AccountRowProps { account: AccountEntry; text: { title: string; subtitle: string | null }; + /** The server line: only for an account in trouble or a shared address. */ + showServer: boolean; + /** Dragging and the arrows: only with two accounts or more. */ + canReorder: boolean; index: number; isActive: boolean; isFirst: boolean; @@ -333,6 +338,7 @@ interface AccountRowProps { onMoveDown: () => void; onSwitch: () => void; onSetDefault: () => void; + onRemove: () => void; labels: { active: string; default: string; @@ -341,12 +347,15 @@ interface AccountRowProps { moveUp: string; moveDown: string; dragHandle: string; + remove: string; }; } function AccountRow({ account, text, + showServer, + canReorder, index, isActive, isFirst, @@ -360,11 +369,12 @@ function AccountRow({ onMoveDown, onSwitch, onSetDefault, + onRemove, labels, }: AccountRowProps) { return (
onDragStart(e, index)} onDragOver={(e) => onDragOver(e, index)} onDrop={(e) => onDrop(e, index)} @@ -378,12 +388,14 @@ function AccountRow({ : 'border-border hover:bg-muted/50' )} > -
- -
+ {canReorder && ( +
+ +
+ )}
- + {toUnicodeEmail(text.title)} {account.isDefault && ( @@ -420,23 +432,18 @@ function AccountRow({ )}
{text.subtitle && ( -

+

{toUnicodeEmail(text.subtitle)}

)} -
- {account.hasError ? ( - - ) : ( - - )} - - {hostnameOf(account.serverUrl)} - -
+ {showServer && ( +
+ {account.hasError && } + + {hostnameOf(account.serverUrl)} + +
+ )}
@@ -451,6 +458,7 @@ function AccountRow({ )} + {canReorder && (<> + )} + {!isActive && ( + + )}
); diff --git a/components/settings/identity-settings.tsx b/components/settings/identity-settings.tsx index 3601bf851..68e21be11 100644 --- a/components/settings/identity-settings.tsx +++ b/components/settings/identity-settings.tsx @@ -27,29 +27,23 @@ export function EmailAddressesSection() { const t = useTranslations('settings.identities'); const tAccount = useTranslations('settings.account'); const tEmail = useTranslations('settings.email_behavior'); + const tAddr = useTranslations('uxs2_ident.addresses'); const { identities } = useIdentityStore(); const { subAddressDelimiter, updateSetting } = useSettingsStore(); const [showModal, setShowModal] = useState(false); return ( <> - - {/* Identity Count */} - + {/* The section says what the addresses are; the row under it doesn't + say it again (settings-p2-18). No "identity": a developer word. */} + +
- {identities.length === 0 - ? t('identities_count.count_zero') - : identities.length === 1 - ? t('identities_count.count_one') - : t('identities_count.count_other', { count: identities.length })} + {tAddr('count', { count: identities.length })}
diff --git a/components/settings/your-name-row.tsx b/components/settings/your-name-row.tsx new file mode 100644 index 000000000..97a817159 --- /dev/null +++ b/components/settings/your-name-row.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { Check, Loader2 } from '@/components/icons'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { SettingItem } from './settings-section'; +import { useAuthStore } from '@/stores/auth-store'; +import { saveIdentityChanges } from '@/stores/identity-store'; +import { toast } from '@/stores/toast-store'; + +/** + * "Your name" on the Account page: the name on the primary sending address, + * which is what recipients see next to it. It saves to that identity, so the + * Account page and the address editor change the same name (settings-p2-2). + * Without an identity there is nothing to save to, and the row only shows + * the name the app knows. + */ +export function YourNameRow({ fallbackName }: { fallbackName?: string }) { + const t = useTranslations('uxs2_ident.your_name'); + const tForm = useTranslations('identities.form'); + const tCommon = useTranslations('common'); + const client = useAuthStore((s) => s.client); + const identity = useAuthStore((s) => s.primaryIdentity); + const current = identity?.name ?? ''; + const [name, setName] = useState(current); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + + useEffect(() => { setName(current); }, [current]); + + if (!identity || !client) { + return ( + + {fallbackName || tCommon('unknown')} + + ); + } + + const save = async () => { + setSaving(true); + try { + if (!(await saveIdentityChanges(client, identity, { name: name.trim() }))) return; + useAuthStore.getState().syncIdentities(); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + toast.success(t('saved')); + } catch (error) { + toast.error(t('error'), error instanceof Error ? error.message : undefined); + } finally { + setSaving(false); + } + }; + + return ( + + { e.preventDefault(); void save(); }} + > + setName(e.target.value)} + maxLength={256} + autoComplete="name" + className="w-48" + disabled={saving} + /> + + + + ); +} diff --git a/lib/__tests__/settings-search-addresses.test.ts b/lib/__tests__/settings-search-addresses.test.ts new file mode 100644 index 000000000..47f1f1af1 --- /dev/null +++ b/lib/__tests__/settings-search-addresses.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import en from '@/locales/en/common.json'; +import de from '@/locales/de/common.json'; +import { + type SettingsSearchTab, + matchesAllWords, + settingsSubResults, + subResultText, + tabSearchEntries, +} from '../settings-search'; + +// settings-p2-3: "identity", "alias", "sender" and "from address" found +// nothing, and "signature" led to a row that only linked elsewhere. The +// signatures now sit on Composing, and these words find them. +const everywhere = (messages: unknown, query: string) => (Object.keys(tabSearchEntries) as SettingsSearchTab[]).flatMap((tab) => + settingsSubResults(messages, tab, 'Bulwark') + .filter((r) => matchesAllWords(subResultText(r), query)) + .map((r) => `${tab}: ${r.label}`)); + +describe('settings search: addresses and your name', () => { + it('finds the sending addresses on Account for identity and alias', () => { + for (const query of ['identity', 'alias', 'sender address']) { + expect(everywhere(en, query), query).toContain('account: Email addresses'); + } + }); + + it('finds "Your name" for display name', () => { + expect(everywhere(en, 'display name')).toContain('account: Your name'); + }); + + it('finds them in German too', () => { + expect(everywhere(de, 'Identität')).toContain('account: E-Mail-Adressen'); + }); +}); diff --git a/lib/account-utils.ts b/lib/account-utils.ts index 2081326cf..00e537362 100644 --- a/lib/account-utils.ts +++ b/lib/account-utils.ts @@ -1,4 +1,5 @@ import { IS_DESKTOP_BUILD } from '@/lib/desktop/flag'; +import { toUnicodeEmail } from '@/lib/idn'; /** * Utilities for multi-account support: @@ -174,6 +175,19 @@ export function accountRowText( return { title: name, subtitle: address }; } +/** + * Whether another signed-in account has the same address (one mailbox on two + * servers). Only then is the server worth a line under an account: in every + * other case it says nothing the person needs (settings-14, settings-p2-11). + */ +export function accountSharesAddress( + account: NamedAccount, + accounts: NamedAccount[], +): boolean { + const key = (a: NamedAccount) => toUnicodeEmail(a.email || a.username).trim().toLowerCase(); + return accounts.some((other) => other.id !== account.id && key(other) === key(account)); +} + /** * The name an account's avatar takes its initials from. Usually the account's * name. When another account has the same name, both would show the same two diff --git a/lib/settings-search.ts b/lib/settings-search.ts index a6d7cd9f2..ec46be264 100644 --- a/lib/settings-search.ts +++ b/lib/settings-search.ts @@ -64,12 +64,12 @@ const syn = (id: string) => `uxs_structure.search.${id}`; // the label (see descriptionKeyFor) and searched too. export const tabSearchEntries: Record = { account: [ - { label: 'settings.account.name_label' }, + { label: 'uxs2_ident.your_name.label', synonyms: 'uxs2_ident.search.your_name' }, { label: 'settings.account.email.label' }, { label: 'settings.account.auth_method_label' }, { label: 'settings.account.server.label', advanced: true }, - { label: 'settings.account.sections.addresses', synonyms: syn('signature') }, - { label: 'settings.identities.identities_count.label' }, + { label: 'settings.account.sections.addresses', synonyms: 'uxs2_ident.search.addresses' }, + { label: 'uxs2_ident.addresses.label' }, { label: 'settings.identities.sub_addressing.label' }, { label: 'settings.email_behavior.sub_address_delimiter.label', advanced: true }, { label: 'settings.account.accounts.title', when: 'multiAccount' }, diff --git a/locales/ar/common.json b/locales/ar/common.json index 999cafaf0..aa0e9d63c 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, zero {# ساعة} one {ساعة واحدة} two {ساعتان} few {# ساعات} many {# ساعة} other {# ساعة}}", "desktop_attention_title": "على هذا الكمبيوتر" }, + "uxs2_ident": { + "your_name": { + "label": "اسمك", + "description": "يرى الناس هذا الاسم بجانب عنوانك عندما ترسل إليهم رسالة.", + "saved": "تم حفظ اسمك.", + "error": "تعذّر حفظ اسمك. حاول مرة أخرى." + }, + "addresses": { + "description": "العناوين التي يمكنك الإرسال منها، لكل منها اسمه وتوقيعه.", + "label": "عناوينك", + "count": "{count, plural, =0 {لا توجد عناوين بعد} zero {# عنوان} one {عنوان واحد} two {عنوانان} few {# عناوين} many {# عنوانًا} other {# عنوان}}", + "manage": "تعديل العناوين…", + "modal_title": "عناوين بريدك الإلكتروني", + "add": "إضافة عنوان", + "edit": "تعديل العنوان", + "added": "تمت إضافة العنوان.", + "saved": "تم حفظ العنوان.", + "deleted": "تم حذف العنوان.", + "delete_title": "حذف هذا العنوان؟" + }, + "signatures": { + "title": "التوقيعات", + "description": "يُضاف في نهاية الرسائل التي تكتبها. لكل عنوان توقيعه الخاص.", + "saved": "تم حفظ التوقيع.", + "error": "تعذّر حفظ التوقيع. حاول مرة أخرى.", + "no_addresses": "لا يوجد عنوان للإرسال منه بعد." + }, + "search": { + "addresses": "هوية هويات اسم مستعار مرسل عنوان المرسل عنوان من identity alias sender", + "signatures": "توقيع توقيع البريد تذييل هوية مرسل عنوان من signature", + "your_name": "الاسم المعروض الاسم الكامل اسم المرسل display name" + } + }, "desktop_sign_in": { "server_needed": "أدخل عنوان الخادم أولًا.", "server_declined": "لم تسمح بهذا الخادم، لذا لم يُرسل إليه أي شيء.", diff --git a/locales/ca/common.json b/locales/ca/common.json index 091cd601c..08ad74d20 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hora} other {# hores}}", "desktop_attention_title": "En aquest ordinador" }, + "uxs2_ident": { + "your_name": { + "label": "El vostre nom", + "description": "La gent veu aquest nom al costat de la vostra adreça quan els envieu correu.", + "saved": "S'ha desat el vostre nom.", + "error": "No s'ha pogut desar el vostre nom. Torneu-ho a provar." + }, + "addresses": { + "description": "Les adreces des de les quals podeu enviar, cadascuna amb el seu nom i la seva signatura.", + "label": "Les vostres adreces", + "count": "{count, plural, =0 {Encara no hi ha adreces} one {# adreça} other {# adreces}}", + "manage": "Edita les adreces…", + "modal_title": "Les vostres adreces de correu", + "add": "Afegeix una adreça", + "edit": "Edita l'adreça", + "added": "S'ha afegit l'adreça.", + "saved": "S'ha desat l'adreça.", + "deleted": "S'ha suprimit l'adreça.", + "delete_title": "Voleu suprimir aquesta adreça?" + }, + "signatures": { + "title": "Signatures", + "description": "S'afegeix al final dels missatges que escriviu. Cada adreça té la seva.", + "saved": "S'ha desat la signatura.", + "error": "No s'ha pogut desar la signatura. Torneu-ho a provar.", + "no_addresses": "Encara no teniu cap adreça per enviar." + }, + "search": { + "addresses": "identitat identitats àlies remitent adreça del remitent adreça de identity alias sender", + "signatures": "signatura signatura de correu peu identitat remitent adreça de signature", + "your_name": "nom visible nom complet nom del remitent display name" + } + }, "desktop_sign_in": { "server_needed": "Primer escriu l'adreça del servidor.", "server_declined": "No has permès aquest servidor, així que no s'hi ha enviat res.", diff --git a/locales/cs/common.json b/locales/cs/common.json index b2ae541cf..2a0cb7035 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}", "desktop_attention_title": "Na tomto počítači" }, + "uxs2_ident": { + "your_name": { + "label": "Vaše jméno", + "description": "Toto jméno lidé vidí vedle vaší adresy, když jim pošlete e-mail.", + "saved": "Vaše jméno je uloženo.", + "error": "Vaše jméno se nepodařilo uložit. Zkuste to znovu." + }, + "addresses": { + "description": "Adresy, ze kterých můžete odesílat, každá s vlastním jménem a podpisem.", + "label": "Vaše adresy", + "count": "{count, plural, =0 {Zatím žádné adresy} one {# adresa} few {# adresy} many {# adresy} other {# adres}}", + "manage": "Upravit adresy…", + "modal_title": "Vaše e-mailové adresy", + "add": "Přidat adresu", + "edit": "Upravit adresu", + "added": "Adresa přidána.", + "saved": "Adresa uložena.", + "deleted": "Adresa smazána.", + "delete_title": "Smazat tuto adresu?" + }, + "signatures": { + "title": "Podpisy", + "description": "Přidá se na konec zpráv, které píšete. Každá adresa má svůj vlastní.", + "saved": "Podpis uložen.", + "error": "Podpis se nepodařilo uložit. Zkuste to znovu.", + "no_addresses": "Zatím nemáte adresu, ze které lze odesílat." + }, + "search": { + "addresses": "identita identity alias aliasy odesílatel adresa odesílatele adresa od identity alias sender", + "signatures": "podpis e-mailový podpis zápatí identita odesílatel adresa od signature", + "your_name": "zobrazované jméno celé jméno jméno odesílatele display name" + } + }, "desktop_sign_in": { "server_needed": "Nejdřív zadejte adresu serveru.", "server_declined": "Tento server jste nepovolili, proto se na něj nic neodeslalo.", diff --git a/locales/da/common.json b/locales/da/common.json index 7d48f4b15..7fddd6fd1 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# time} other {# timer}}", "desktop_attention_title": "På denne computer" }, + "uxs2_ident": { + "your_name": { + "label": "Dit navn", + "description": "Folk ser dette navn ved siden af din adresse, når du sender dem mail.", + "saved": "Dit navn er gemt.", + "error": "Dit navn kunne ikke gemmes. Prøv igen." + }, + "addresses": { + "description": "De adresser, du kan sende fra, hver med sit eget navn og sin egen signatur.", + "label": "Dine adresser", + "count": "{count, plural, =0 {Ingen adresser endnu} one {# adresse} other {# adresser}}", + "manage": "Rediger adresser…", + "modal_title": "Dine e-mailadresser", + "add": "Tilføj adresse", + "edit": "Rediger adresse", + "added": "Adressen er tilføjet.", + "saved": "Adressen er gemt.", + "deleted": "Adressen er slettet.", + "delete_title": "Slet denne adresse?" + }, + "signatures": { + "title": "Signaturer", + "description": "Tilføjes i slutningen af de beskeder, du skriver. Hver adresse har sin egen.", + "saved": "Signaturen er gemt.", + "error": "Signaturen kunne ikke gemmes. Prøv igen.", + "no_addresses": "Der er endnu ingen adresse at sende fra." + }, + "search": { + "addresses": "identitet identiteter alias afsender afsenderadresse fra-adresse identity alias sender", + "signatures": "signatur e-mailsignatur sidefod identitet afsender fra-adresse signature", + "your_name": "visningsnavn fulde navn afsendernavn display name" + } + }, "desktop_sign_in": { "server_needed": "Angiv først serverens adresse.", "server_declined": "Du tillod ikke denne server, så der blev ikke sendt noget til den.", diff --git a/locales/de/common.json b/locales/de/common.json index e1e33c934..caa48891b 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# Stunde} other {# Stunden}}", "desktop_attention_title": "Auf diesem Computer" }, + "uxs2_ident": { + "your_name": { + "label": "Ihr Name", + "description": "Diesen Namen sehen andere neben Ihrer Adresse, wenn Sie ihnen schreiben.", + "saved": "Ihr Name ist gespeichert.", + "error": "Ihr Name konnte nicht gespeichert werden. Versuchen Sie es noch einmal." + }, + "addresses": { + "description": "Die Adressen, von denen Sie senden können, jede mit eigenem Namen und eigener Signatur.", + "label": "Ihre Adressen", + "count": "{count, plural, =0 {Noch keine Adressen} one {# Adresse} other {# Adressen}}", + "manage": "Adressen bearbeiten…", + "modal_title": "Ihre E-Mail-Adressen", + "add": "Adresse hinzufügen", + "edit": "Adresse bearbeiten", + "added": "Adresse hinzugefügt.", + "saved": "Adresse gespeichert.", + "deleted": "Adresse gelöscht.", + "delete_title": "Diese Adresse löschen?" + }, + "signatures": { + "title": "Signaturen", + "description": "Steht am Ende der Nachrichten, die Sie schreiben. Jede Adresse hat ihre eigene.", + "saved": "Signatur gespeichert.", + "error": "Die Signatur konnte nicht gespeichert werden. Versuchen Sie es noch einmal.", + "no_addresses": "Es gibt noch keine Adresse zum Senden." + }, + "search": { + "addresses": "Identität Identitäten Alias Aliasse Absender Absenderadresse Von-Adresse Absendername identity alias sender", + "signatures": "Signatur E-Mail-Signatur Grußformel Fußzeile Identität Absender Von-Adresse signature", + "your_name": "Anzeigename vollständiger Name Absendername display name" + } + }, "desktop_sign_in": { "server_needed": "Geben Sie zuerst die Adresse Ihres Servers ein.", "server_declined": "Sie haben diesen Server nicht zugelassen, daher wurde nichts an ihn gesendet.", diff --git a/locales/en/common.json b/locales/en/common.json index ae08611b3..01e55b7a3 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hour} other {# hours}}", "desktop_attention_title": "On this computer" }, + "uxs2_ident": { + "your_name": { + "label": "Your name", + "description": "People see this name next to your address when you send them mail.", + "saved": "Your name is saved.", + "error": "Your name could not be saved. Try again." + }, + "addresses": { + "description": "The addresses you can send from, each with its own name and signature.", + "label": "Your addresses", + "count": "{count, plural, =0 {No addresses yet} one {# address} other {# addresses}}", + "manage": "Edit addresses…", + "modal_title": "Your email addresses", + "add": "Add address", + "edit": "Edit address", + "added": "Address added.", + "saved": "Address saved.", + "deleted": "Address deleted.", + "delete_title": "Delete this address?" + }, + "signatures": { + "title": "Signatures", + "description": "Added at the end of the messages you write. Each address has its own.", + "saved": "Signature saved.", + "error": "The signature could not be saved. Try again.", + "no_addresses": "There is no address to send from yet." + }, + "search": { + "addresses": "identity identities alias aliases sender sender address from address from name", + "signatures": "signature email signature sign-off footer identity sender from address", + "your_name": "display name full name sender name from name" + } + }, "desktop_sign_in": { "server_needed": "Enter your server's address first.", "server_declined": "You didn't allow this server, so nothing was sent to it.", diff --git a/locales/es/common.json b/locales/es/common.json index e4c6f39e9..7db263487 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hora} other {# horas}}", "desktop_attention_title": "En este ordenador" }, + "uxs2_ident": { + "your_name": { + "label": "Tu nombre", + "description": "La gente ve este nombre junto a tu dirección cuando les envías un correo.", + "saved": "Tu nombre se ha guardado.", + "error": "No se ha podido guardar tu nombre. Inténtalo de nuevo." + }, + "addresses": { + "description": "Las direcciones desde las que puedes enviar, cada una con su propio nombre y firma.", + "label": "Tus direcciones", + "count": "{count, plural, =0 {Todavía no hay direcciones} one {# dirección} other {# direcciones}}", + "manage": "Editar direcciones…", + "modal_title": "Tus direcciones de correo", + "add": "Añadir dirección", + "edit": "Editar dirección", + "added": "Dirección añadida.", + "saved": "Dirección guardada.", + "deleted": "Dirección eliminada.", + "delete_title": "¿Eliminar esta dirección?" + }, + "signatures": { + "title": "Firmas", + "description": "Se añade al final de los mensajes que escribes. Cada dirección tiene la suya.", + "saved": "Firma guardada.", + "error": "No se ha podido guardar la firma. Inténtalo de nuevo.", + "no_addresses": "Todavía no hay ninguna dirección desde la que enviar." + }, + "search": { + "addresses": "identidad identidades alias remitente dirección del remitente dirección de envío identity alias sender", + "signatures": "firma firma de correo pie identidad remitente dirección de envío signature", + "your_name": "nombre visible nombre completo nombre del remitente display name" + } + }, "desktop_sign_in": { "server_needed": "Primero escribe la dirección del servidor.", "server_declined": "No permitiste este servidor, así que no se le envió nada.", diff --git a/locales/et/common.json b/locales/et/common.json index 30ac1ee96..b37427db9 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# tund} other {# tundi}}", "desktop_attention_title": "Selles arvutis" }, + "uxs2_ident": { + "your_name": { + "label": "Teie nimi", + "description": "Seda nime näevad inimesed teie aadressi kõrval, kui saadate neile kirja.", + "saved": "Teie nimi on salvestatud.", + "error": "Teie nime ei õnnestunud salvestada. Proovige uuesti." + }, + "addresses": { + "description": "Aadressid, millelt saate saata, igaühel oma nimi ja allkiri.", + "label": "Teie aadressid", + "count": "{count, plural, =0 {Aadresse veel pole} one {# aadress} other {# aadressi}}", + "manage": "Muuda aadresse…", + "modal_title": "Teie e-posti aadressid", + "add": "Lisa aadress", + "edit": "Muuda aadressi", + "added": "Aadress lisatud.", + "saved": "Aadress salvestatud.", + "deleted": "Aadress kustutatud.", + "delete_title": "Kas kustutada see aadress?" + }, + "signatures": { + "title": "Allkirjad", + "description": "Lisatakse teie kirjutatud kirjade lõppu. Igal aadressil on oma.", + "saved": "Allkiri salvestatud.", + "error": "Allkirja ei õnnestunud salvestada. Proovige uuesti.", + "no_addresses": "Teil pole veel aadressi, millelt saata." + }, + "search": { + "addresses": "identiteet identiteedid alias saatja saatja aadress saatja nimi identity alias sender", + "signatures": "allkiri e-kirja allkiri jalus identiteet saatja saatja aadress signature", + "your_name": "kuvatav nimi täisnimi saatja nimi display name" + } + }, "desktop_sign_in": { "server_needed": "Sisesta kõigepealt serveri aadress.", "server_declined": "Sa ei lubanud seda serverit, nii et sellele ei saadetud midagi.", diff --git a/locales/fa/common.json b/locales/fa/common.json index 117abe12f..72b21f212 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# ساعت} other {# ساعت}}", "desktop_attention_title": "روی این رایانه" }, + "uxs2_ident": { + "your_name": { + "label": "نام شما", + "description": "وقتی برای دیگران نامه می‌فرستید، این نام را کنار نشانی شما می‌بینند.", + "saved": "نام شما ذخیره شد.", + "error": "نام شما ذخیره نشد. دوباره امتحان کنید." + }, + "addresses": { + "description": "نشانی‌هایی که می‌توانید از آن‌ها نامه بفرستید، هر کدام با نام و امضای خودش.", + "label": "نشانی‌های شما", + "count": "{count, plural, =0 {هنوز نشانی‌ای نیست} one {# نشانی} other {# نشانی}}", + "manage": "ویرایش نشانی‌ها…", + "modal_title": "نشانی‌های ایمیل شما", + "add": "افزودن نشانی", + "edit": "ویرایش نشانی", + "added": "نشانی افزوده شد.", + "saved": "نشانی ذخیره شد.", + "deleted": "نشانی حذف شد.", + "delete_title": "این نشانی حذف شود؟" + }, + "signatures": { + "title": "امضاها", + "description": "به انتهای پیام‌هایی که می‌نویسید افزوده می‌شود. هر نشانی امضای خودش را دارد.", + "saved": "امضا ذخیره شد.", + "error": "امضا ذخیره نشد. دوباره امتحان کنید.", + "no_addresses": "هنوز نشانی‌ای برای فرستادن ندارید." + }, + "search": { + "addresses": "هویت هویت‌ها نام مستعار فرستنده نشانی فرستنده نشانی از identity alias sender", + "signatures": "امضا امضای ایمیل پانویس هویت فرستنده نشانی از signature", + "your_name": "نام نمایشی نام کامل نام فرستنده display name" + } + }, "desktop_sign_in": { "server_needed": "ابتدا نشانی سرور را وارد کنید.", "server_declined": "شما این سرور را مجاز نکردید، پس چیزی به آن فرستاده نشد.", diff --git a/locales/fr/common.json b/locales/fr/common.json index 920bc103e..fd00f005c 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# heure} other {# heures}}", "desktop_attention_title": "Sur cet ordinateur" }, + "uxs2_ident": { + "your_name": { + "label": "Votre nom", + "description": "Les gens voient ce nom à côté de votre adresse quand vous leur écrivez.", + "saved": "Votre nom est enregistré.", + "error": "Votre nom n'a pas pu être enregistré. Réessayez." + }, + "addresses": { + "description": "Les adresses depuis lesquelles vous pouvez envoyer, chacune avec son nom et sa signature.", + "label": "Vos adresses", + "count": "{count, plural, =0 {Pas encore d'adresse} one {# adresse} other {# adresses}}", + "manage": "Modifier les adresses…", + "modal_title": "Vos adresses e-mail", + "add": "Ajouter une adresse", + "edit": "Modifier l'adresse", + "added": "Adresse ajoutée.", + "saved": "Adresse enregistrée.", + "deleted": "Adresse supprimée.", + "delete_title": "Supprimer cette adresse ?" + }, + "signatures": { + "title": "Signatures", + "description": "Ajoutée à la fin des messages que vous écrivez. Chaque adresse a la sienne.", + "saved": "Signature enregistrée.", + "error": "La signature n'a pas pu être enregistrée. Réessayez.", + "no_addresses": "Vous n'avez encore aucune adresse d'envoi." + }, + "search": { + "addresses": "identité identités alias expéditeur adresse d'expédition adresse de l'expéditeur identity alias sender", + "signatures": "signature signature e-mail pied de page identité expéditeur adresse d'expédition", + "your_name": "nom affiché nom complet nom de l'expéditeur display name" + } + }, "desktop_sign_in": { "server_needed": "Saisissez d'abord l'adresse du serveur.", "server_declined": "Vous n'avez pas autorisé ce serveur, donc rien ne lui a été envoyé.", diff --git a/locales/he/common.json b/locales/he/common.json index 2ddbebc18..56e1df697 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -7147,6 +7147,39 @@ "app_lock_timeout_hours": "{count, plural, one {שעה אחת} two {שעתיים} other {# שעות}}", "desktop_attention_title": "במחשב הזה" }, + "uxs2_ident": { + "your_name": { + "label": "השם שלכם", + "description": "אנשים רואים את השם הזה ליד הכתובת שלכם כשאתם שולחים להם דואר.", + "saved": "השם שלכם נשמר.", + "error": "לא ניתן היה לשמור את השם שלכם. נסו שוב." + }, + "addresses": { + "description": "הכתובות שמהן אפשר לשלוח, לכל אחת שם וחתימה משלה.", + "label": "הכתובות שלכם", + "count": "{count, plural, =0 {עדיין אין כתובות} one {כתובת אחת} two {# כתובות} other {# כתובות}}", + "manage": "עריכת כתובות…", + "modal_title": "כתובות הדואר שלכם", + "add": "הוספת כתובת", + "edit": "עריכת כתובת", + "added": "הכתובת נוספה.", + "saved": "הכתובת נשמרה.", + "deleted": "הכתובת נמחקה.", + "delete_title": "למחוק את הכתובת הזו?" + }, + "signatures": { + "title": "חתימות", + "description": "נוספת בסוף ההודעות שאתם כותבים. לכל כתובת חתימה משלה.", + "saved": "החתימה נשמרה.", + "error": "לא ניתן היה לשמור את החתימה. נסו שוב.", + "no_addresses": "עדיין אין כתובת לשליחה." + }, + "search": { + "addresses": "זהות זהויות כינוי שולח כתובת השולח כתובת מאת identity alias sender", + "signatures": "חתימה חתימת דואר כותרת תחתונה זהות שולח כתובת מאת signature", + "your_name": "שם תצוגה שם מלא שם השולח display name" + } + }, "desktop_sign_in": { "server_needed": "קודם צריך להזין את כתובת השרת.", "server_declined": "לא אישרת את השרת הזה, לכן לא נשלח אליו דבר.", diff --git a/locales/hu/common.json b/locales/hu/common.json index 40440385b..870f35bd4 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# óra} other {# óra}}", "desktop_attention_title": "Ezen a számítógépen" }, + "uxs2_ident": { + "your_name": { + "label": "Az Ön neve", + "description": "Ezt a nevet látják mások a címe mellett, amikor levelet küld nekik.", + "saved": "A neve mentve.", + "error": "A nevét nem sikerült menteni. Próbálja újra." + }, + "addresses": { + "description": "A címek, amelyekről küldhet, mindegyik saját névvel és aláírással.", + "label": "Az Ön címei", + "count": "{count, plural, =0 {Még nincs cím} one {# cím} other {# cím}}", + "manage": "Címek szerkesztése…", + "modal_title": "Az Ön e-mail-címei", + "add": "Cím hozzáadása", + "edit": "Cím szerkesztése", + "added": "Cím hozzáadva.", + "saved": "Cím mentve.", + "deleted": "Cím törölve.", + "delete_title": "Törli ezt a címet?" + }, + "signatures": { + "title": "Aláírások", + "description": "Az Ön által írt üzenetek végére kerül. Minden címnek saját aláírása van.", + "saved": "Aláírás mentve.", + "error": "Az aláírást nem sikerült menteni. Próbálja újra.", + "no_addresses": "Még nincs cím, amelyről küldhetne." + }, + "search": { + "addresses": "identitás identitások álnév alias feladó feladó címe feladócím identity alias sender", + "signatures": "aláírás e-mail aláírás lábléc identitás feladó feladócím signature", + "your_name": "megjelenített név teljes név feladó neve display name" + } + }, "desktop_sign_in": { "server_needed": "Először adja meg a szerver címét.", "server_declined": "Nem engedélyezte ezt a szervert, ezért semmit sem küldtünk neki.", diff --git a/locales/it/common.json b/locales/it/common.json index 4c928e274..7a9ec1c94 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# ora} other {# ore}}", "desktop_attention_title": "Su questo computer" }, + "uxs2_ident": { + "your_name": { + "label": "Il tuo nome", + "description": "Le persone vedono questo nome accanto al tuo indirizzo quando scrivi loro.", + "saved": "Il tuo nome è stato salvato.", + "error": "Non è stato possibile salvare il tuo nome. Riprova." + }, + "addresses": { + "description": "Gli indirizzi da cui puoi inviare, ognuno con il proprio nome e la propria firma.", + "label": "I tuoi indirizzi", + "count": "{count, plural, =0 {Ancora nessun indirizzo} one {# indirizzo} other {# indirizzi}}", + "manage": "Modifica indirizzi…", + "modal_title": "I tuoi indirizzi email", + "add": "Aggiungi indirizzo", + "edit": "Modifica indirizzo", + "added": "Indirizzo aggiunto.", + "saved": "Indirizzo salvato.", + "deleted": "Indirizzo eliminato.", + "delete_title": "Eliminare questo indirizzo?" + }, + "signatures": { + "title": "Firme", + "description": "Aggiunta alla fine dei messaggi che scrivi. Ogni indirizzo ha la sua.", + "saved": "Firma salvata.", + "error": "Non è stato possibile salvare la firma. Riprova.", + "no_addresses": "Non c'è ancora un indirizzo da cui inviare." + }, + "search": { + "addresses": "identità alias mittente indirizzo del mittente indirizzo da identity sender", + "signatures": "firma firma email piè di pagina identità mittente indirizzo da signature", + "your_name": "nome visualizzato nome completo nome del mittente display name" + } + }, "desktop_sign_in": { "server_needed": "Inserisci prima l'indirizzo del server.", "server_declined": "Non hai autorizzato questo server, quindi non gli è stato inviato nulla.", diff --git a/locales/ja/common.json b/locales/ja/common.json index a4788a73e..242f2d0f0 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {#時間}}", "desktop_attention_title": "このコンピューター" }, + "uxs2_ident": { + "your_name": { + "label": "あなたの名前", + "description": "メールを送ると、相手にはこの名前がアドレスの横に表示されます。", + "saved": "名前を保存しました。", + "error": "名前を保存できませんでした。もう一度お試しください。" + }, + "addresses": { + "description": "送信に使えるアドレスです。アドレスごとに名前と署名があります。", + "label": "あなたのアドレス", + "count": "{count, plural, =0 {まだアドレスはありません} other {# 件のアドレス}}", + "manage": "アドレスを編集…", + "modal_title": "あなたのメールアドレス", + "add": "アドレスを追加", + "edit": "アドレスを編集", + "added": "アドレスを追加しました。", + "saved": "アドレスを保存しました。", + "deleted": "アドレスを削除しました。", + "delete_title": "このアドレスを削除しますか?" + }, + "signatures": { + "title": "署名", + "description": "書いたメッセージの最後に付きます。アドレスごとに別の署名を使えます。", + "saved": "署名を保存しました。", + "error": "署名を保存できませんでした。もう一度お試しください。", + "no_addresses": "送信に使えるアドレスがまだありません。" + }, + "search": { + "addresses": "ID アイデンティティ エイリアス 差出人 差出人アドレス 送信元アドレス identity alias sender", + "signatures": "署名 メール署名 フッター アイデンティティ 差出人 送信元アドレス signature", + "your_name": "表示名 氏名 差出人名 display name" + } + }, "desktop_sign_in": { "server_needed": "先にサーバーのアドレスを入力してください。", "server_declined": "このサーバーを許可しなかったため、何も送信していません。", diff --git a/locales/ko/common.json b/locales/ko/common.json index 8692e53c6..f0ccdb32b 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {#시간}}", "desktop_attention_title": "이 컴퓨터에서" }, + "uxs2_ident": { + "your_name": { + "label": "내 이름", + "description": "메일을 보내면 받는 사람에게 주소 옆에 이 이름이 보입니다.", + "saved": "이름을 저장했습니다.", + "error": "이름을 저장하지 못했습니다. 다시 시도하세요." + }, + "addresses": { + "description": "보낼 때 쓸 수 있는 주소입니다. 주소마다 이름과 서명이 따로 있습니다.", + "label": "내 주소", + "count": "{count, plural, =0 {아직 주소가 없습니다} other {주소 #개}}", + "manage": "주소 편집…", + "modal_title": "내 이메일 주소", + "add": "주소 추가", + "edit": "주소 편집", + "added": "주소를 추가했습니다.", + "saved": "주소를 저장했습니다.", + "deleted": "주소를 삭제했습니다.", + "delete_title": "이 주소를 삭제할까요?" + }, + "signatures": { + "title": "서명", + "description": "작성한 메시지 끝에 붙습니다. 주소마다 서명이 따로 있습니다.", + "saved": "서명을 저장했습니다.", + "error": "서명을 저장하지 못했습니다. 다시 시도하세요.", + "no_addresses": "아직 보낼 수 있는 주소가 없습니다." + }, + "search": { + "addresses": "ID 아이덴티티 별칭 보낸 사람 보낸 사람 주소 발신 주소 identity alias sender", + "signatures": "서명 이메일 서명 바닥글 아이덴티티 보낸 사람 발신 주소 signature", + "your_name": "표시 이름 전체 이름 보낸 사람 이름 display name" + } + }, "desktop_sign_in": { "server_needed": "먼저 서버 주소를 입력하세요.", "server_declined": "이 서버를 허용하지 않아 아무것도 보내지 않았습니다.", diff --git a/locales/lv/common.json b/locales/lv/common.json index 30ae51e41..c4445ae9f 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, zero {# stundu} one {# stunda} other {# stundas}}", "desktop_attention_title": "Šajā datorā" }, + "uxs2_ident": { + "your_name": { + "label": "Jūsu vārds", + "description": "Šo vārdu cilvēki redz blakus jūsu adresei, kad sūtāt viņiem vēstuli.", + "saved": "Jūsu vārds ir saglabāts.", + "error": "Jūsu vārdu neizdevās saglabāt. Mēģiniet vēlreiz." + }, + "addresses": { + "description": "Adreses, no kurām varat sūtīt, katrai savs vārds un paraksts.", + "label": "Jūsu adreses", + "count": "{count, plural, =0 {Vēl nav adrešu} zero {# adrešu} one {# adrese} other {# adreses}}", + "manage": "Rediģēt adreses…", + "modal_title": "Jūsu e-pasta adreses", + "add": "Pievienot adresi", + "edit": "Rediģēt adresi", + "added": "Adrese pievienota.", + "saved": "Adrese saglabāta.", + "deleted": "Adrese izdzēsta.", + "delete_title": "Vai dzēst šo adresi?" + }, + "signatures": { + "title": "Paraksti", + "description": "Tiek pievienots jūsu rakstīto ziņojumu beigās. Katrai adresei ir savs.", + "saved": "Paraksts saglabāts.", + "error": "Parakstu neizdevās saglabāt. Mēģiniet vēlreiz.", + "no_addresses": "Vēl nav adreses, no kuras sūtīt." + }, + "search": { + "addresses": "identitāte identitātes aizstājvārds sūtītājs sūtītāja adrese no adrese identity alias sender", + "signatures": "paraksts e-pasta paraksts kājene identitāte sūtītājs no adrese signature", + "your_name": "parādāmais vārds pilns vārds sūtītāja vārds display name" + } + }, "desktop_sign_in": { "server_needed": "Vispirms ievadiet servera adresi.", "server_declined": "Jūs neatļāvāt šo serveri, tāpēc tam nekas netika nosūtīts.", diff --git a/locales/mn/common.json b/locales/mn/common.json index 677689faa..516a36c98 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# цаг} other {# цаг}}", "desktop_attention_title": "Энэ компьютер дээр" }, + "uxs2_ident": { + "your_name": { + "label": "Таны нэр", + "description": "Та хүмүүст захидал илгээхэд энэ нэр таны хаягийн хажууд харагдана.", + "saved": "Таны нэр хадгалагдлаа.", + "error": "Таны нэрийг хадгалж чадсангүй. Дахин оролдоно уу." + }, + "addresses": { + "description": "Таны илгээж болох хаягууд, тус бүр өөрийн нэр, гарын үсэгтэй.", + "label": "Таны хаягууд", + "count": "{count, plural, =0 {Одоогоор хаяг алга} one {# хаяг} other {# хаяг}}", + "manage": "Хаягуудыг засах…", + "modal_title": "Таны имэйл хаягууд", + "add": "Хаяг нэмэх", + "edit": "Хаягийг засах", + "added": "Хаяг нэмэгдлээ.", + "saved": "Хаяг хадгалагдлаа.", + "deleted": "Хаяг устгагдлаа.", + "delete_title": "Энэ хаягийг устгах уу?" + }, + "signatures": { + "title": "Гарын үсгүүд", + "description": "Таны бичсэн захидлын төгсгөлд нэмэгдэнэ. Хаяг бүр өөрийн гарын үсэгтэй.", + "saved": "Гарын үсэг хадгалагдлаа.", + "error": "Гарын үсгийг хадгалж чадсангүй. Дахин оролдоно уу.", + "no_addresses": "Илгээх хаяг одоогоор алга." + }, + "search": { + "addresses": "хувийн мэдээлэл таних тэмдэг өөр нэр илгээгч илгээгчийн хаяг хэнээс хаяг identity alias sender", + "signatures": "гарын үсэг имэйлийн гарын үсэг хөл хэсэг илгээгч хэнээс хаяг signature", + "your_name": "харагдах нэр бүтэн нэр илгээгчийн нэр display name" + } + }, "desktop_sign_in": { "server_needed": "Эхлээд серверийн хаягаа оруулна уу.", "server_declined": "Та энэ серверийг зөвшөөрөөгүй тул юу ч илгээгээгүй.", diff --git a/locales/nb/common.json b/locales/nb/common.json index bc849a939..c6c2e8bf7 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# time} other {# timer}}", "desktop_attention_title": "På denne datamaskinen" }, + "uxs2_ident": { + "your_name": { + "label": "Navnet ditt", + "description": "Folk ser dette navnet ved siden av adressen din når du sender dem e-post.", + "saved": "Navnet ditt er lagret.", + "error": "Navnet ditt kunne ikke lagres. Prøv igjen." + }, + "addresses": { + "description": "Adressene du kan sende fra, hver med sitt eget navn og sin egen signatur.", + "label": "Adressene dine", + "count": "{count, plural, =0 {Ingen adresser ennå} one {# adresse} other {# adresser}}", + "manage": "Rediger adresser…", + "modal_title": "E-postadressene dine", + "add": "Legg til adresse", + "edit": "Rediger adresse", + "added": "Adressen er lagt til.", + "saved": "Adressen er lagret.", + "deleted": "Adressen er slettet.", + "delete_title": "Vil du slette denne adressen?" + }, + "signatures": { + "title": "Signaturer", + "description": "Legges til på slutten av meldingene du skriver. Hver adresse har sin egen.", + "saved": "Signaturen er lagret.", + "error": "Signaturen kunne ikke lagres. Prøv igjen.", + "no_addresses": "Det finnes ingen adresse å sende fra ennå." + }, + "search": { + "addresses": "identitet identiteter alias avsender avsenderadresse fra-adresse identity sender", + "signatures": "signatur e-postsignatur bunntekst identitet avsender fra-adresse signature", + "your_name": "visningsnavn fullt navn avsendernavn display name" + } + }, "desktop_sign_in": { "server_needed": "Skriv inn serveradressen først.", "server_declined": "Du tillot ikke denne serveren, så ingenting ble sendt til den.", diff --git a/locales/nl/common.json b/locales/nl/common.json index 40c5b7aba..3cbcd086b 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# uur} other {# uur}}", "desktop_attention_title": "Op deze computer" }, + "uxs2_ident": { + "your_name": { + "label": "Uw naam", + "description": "Mensen zien deze naam naast uw adres als u ze mailt.", + "saved": "Uw naam is opgeslagen.", + "error": "Uw naam kon niet worden opgeslagen. Probeer het opnieuw." + }, + "addresses": { + "description": "De adressen waarvanaf u kunt verzenden, elk met een eigen naam en handtekening.", + "label": "Uw adressen", + "count": "{count, plural, =0 {Nog geen adressen} one {# adres} other {# adressen}}", + "manage": "Adressen bewerken…", + "modal_title": "Uw e-mailadressen", + "add": "Adres toevoegen", + "edit": "Adres bewerken", + "added": "Adres toegevoegd.", + "saved": "Adres opgeslagen.", + "deleted": "Adres verwijderd.", + "delete_title": "Dit adres verwijderen?" + }, + "signatures": { + "title": "Handtekeningen", + "description": "Komt onder de berichten die u schrijft. Elk adres heeft een eigen handtekening.", + "saved": "Handtekening opgeslagen.", + "error": "De handtekening kon niet worden opgeslagen. Probeer het opnieuw.", + "no_addresses": "Er is nog geen adres om vanaf te verzenden." + }, + "search": { + "addresses": "identiteit identiteiten alias afzender afzenderadres van-adres identity sender", + "signatures": "handtekening e-mailhandtekening voettekst identiteit afzender van-adres signature", + "your_name": "weergavenaam volledige naam afzendernaam display name" + } + }, "desktop_sign_in": { "server_needed": "Vul eerst het adres van de server in.", "server_declined": "Je hebt deze server niet toegestaan, dus er is niets naartoe gestuurd.", diff --git a/locales/pl/common.json b/locales/pl/common.json index 977c2f348..5f7fd6fd0 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# godzina} few {# godziny} many {# godzin} other {# godziny}}", "desktop_attention_title": "Na tym komputerze" }, + "uxs2_ident": { + "your_name": { + "label": "Twoje imię i nazwisko", + "description": "Ludzie widzą tę nazwę obok Twojego adresu, gdy wysyłasz im wiadomość.", + "saved": "Zapisano Twoje imię i nazwisko.", + "error": "Nie udało się zapisać Twojego imienia i nazwiska. Spróbuj ponownie." + }, + "addresses": { + "description": "Adresy, z których możesz wysyłać, każdy z własną nazwą i podpisem.", + "label": "Twoje adresy", + "count": "{count, plural, =0 {Jeszcze brak adresów} one {# adres} few {# adresy} many {# adresów} other {# adresu}}", + "manage": "Edytuj adresy…", + "modal_title": "Twoje adresy e-mail", + "add": "Dodaj adres", + "edit": "Edytuj adres", + "added": "Dodano adres.", + "saved": "Zapisano adres.", + "deleted": "Usunięto adres.", + "delete_title": "Usunąć ten adres?" + }, + "signatures": { + "title": "Podpisy", + "description": "Dodawany na końcu pisanych wiadomości. Każdy adres ma własny.", + "saved": "Zapisano podpis.", + "error": "Nie udało się zapisać podpisu. Spróbuj ponownie.", + "no_addresses": "Nie masz jeszcze adresu, z którego można wysyłać." + }, + "search": { + "addresses": "tożsamość tożsamości alias nadawca adres nadawcy adres od identity sender", + "signatures": "podpis podpis e-mail stopka tożsamość nadawca adres od signature", + "your_name": "nazwa wyświetlana imię i nazwisko nazwa nadawcy display name" + } + }, "desktop_sign_in": { "server_needed": "Najpierw wpisz adres serwera.", "server_declined": "Ten serwer nie został dozwolony, więc nic do niego nie wysłano.", diff --git a/locales/pt/common.json b/locales/pt/common.json index 4b3b27d99..4b8048722 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hora} other {# horas}}", "desktop_attention_title": "Neste computador" }, + "uxs2_ident": { + "your_name": { + "label": "O seu nome", + "description": "As pessoas veem este nome ao lado do seu endereço quando lhes envia e-mail.", + "saved": "O seu nome foi guardado.", + "error": "Não foi possível guardar o seu nome. Tente novamente." + }, + "addresses": { + "description": "Os endereços a partir dos quais pode enviar, cada um com o seu nome e assinatura.", + "label": "Os seus endereços", + "count": "{count, plural, =0 {Ainda sem endereços} one {# endereço} other {# endereços}}", + "manage": "Editar endereços…", + "modal_title": "Os seus endereços de e-mail", + "add": "Adicionar endereço", + "edit": "Editar endereço", + "added": "Endereço adicionado.", + "saved": "Endereço guardado.", + "deleted": "Endereço eliminado.", + "delete_title": "Eliminar este endereço?" + }, + "signatures": { + "title": "Assinaturas", + "description": "É acrescentada no fim das mensagens que escreve. Cada endereço tem a sua.", + "saved": "Assinatura guardada.", + "error": "Não foi possível guardar a assinatura. Tente novamente.", + "no_addresses": "Ainda não há nenhum endereço a partir do qual enviar." + }, + "search": { + "addresses": "identidade identidades alias remetente endereço do remetente endereço de envio identity sender", + "signatures": "assinatura assinatura de e-mail rodapé identidade remetente endereço de envio signature", + "your_name": "nome a mostrar nome completo nome do remetente display name" + } + }, "desktop_sign_in": { "server_needed": "Digite primeiro o endereço do servidor.", "server_declined": "Você não permitiu este servidor, então nada foi enviado a ele.", diff --git a/locales/ro/common.json b/locales/ro/common.json index 60c77c966..ebbc9ad4e 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# oră} few {# ore} other {# de ore}}", "desktop_attention_title": "Pe acest computer" }, + "uxs2_ident": { + "your_name": { + "label": "Numele dvs.", + "description": "Oamenii văd acest nume lângă adresa dvs. când le trimiteți e-mail.", + "saved": "Numele dvs. a fost salvat.", + "error": "Numele dvs. nu a putut fi salvat. Încercați din nou." + }, + "addresses": { + "description": "Adresele de pe care puteți trimite, fiecare cu numele și semnătura ei.", + "label": "Adresele dvs.", + "count": "{count, plural, =0 {Încă nicio adresă} one {# adresă} few {# adrese} other {# de adrese}}", + "manage": "Editați adresele…", + "modal_title": "Adresele dvs. de e-mail", + "add": "Adăugați o adresă", + "edit": "Editați adresa", + "added": "Adresa a fost adăugată.", + "saved": "Adresa a fost salvată.", + "deleted": "Adresa a fost ștearsă.", + "delete_title": "Ștergeți această adresă?" + }, + "signatures": { + "title": "Semnături", + "description": "Se adaugă la sfârșitul mesajelor pe care le scrieți. Fiecare adresă are semnătura ei.", + "saved": "Semnătura a fost salvată.", + "error": "Semnătura nu a putut fi salvată. Încercați din nou.", + "no_addresses": "Nu există încă nicio adresă de pe care să trimiteți." + }, + "search": { + "addresses": "identitate identități alias expeditor adresa expeditorului adresa de la identity sender", + "signatures": "semnătură semnătură e-mail subsol identitate expeditor adresa de la signature", + "your_name": "nume afișat nume complet numele expeditorului display name" + } + }, "desktop_sign_in": { "server_needed": "Introdu mai întâi adresa serverului.", "server_declined": "Nu ai permis acest server, așa că nu i s-a trimis nimic.", diff --git a/locales/ru/common.json b/locales/ru/common.json index 3a0c9fcec..954c8992e 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# час} few {# часа} many {# часов} other {# часа}}", "desktop_attention_title": "На этом компьютере" }, + "uxs2_ident": { + "your_name": { + "label": "Ваше имя", + "description": "Это имя люди видят рядом с вашим адресом, когда вы им пишете.", + "saved": "Ваше имя сохранено.", + "error": "Не удалось сохранить ваше имя. Попробуйте ещё раз." + }, + "addresses": { + "description": "Адреса, с которых вы можете отправлять почту, у каждого своё имя и своя подпись.", + "label": "Ваши адреса", + "count": "{count, plural, =0 {Адресов пока нет} one {# адрес} few {# адреса} many {# адресов} other {# адреса}}", + "manage": "Изменить адреса…", + "modal_title": "Ваши адреса электронной почты", + "add": "Добавить адрес", + "edit": "Изменить адрес", + "added": "Адрес добавлен.", + "saved": "Адрес сохранён.", + "deleted": "Адрес удалён.", + "delete_title": "Удалить этот адрес?" + }, + "signatures": { + "title": "Подписи", + "description": "Добавляется в конце ваших писем. У каждого адреса своя.", + "saved": "Подпись сохранена.", + "error": "Не удалось сохранить подпись. Попробуйте ещё раз.", + "no_addresses": "Пока нет адреса, с которого можно отправлять." + }, + "search": { + "addresses": "идентичность личность псевдоним алиас отправитель адрес отправителя адрес от identity alias sender", + "signatures": "подпись подпись письма нижний колонтитул отправитель адрес от signature", + "your_name": "отображаемое имя полное имя имя отправителя display name" + } + }, "desktop_sign_in": { "server_needed": "Сначала введите адрес сервера.", "server_declined": "Вы не разрешили этот сервер, поэтому ему ничего не отправлено.", diff --git a/locales/sk/common.json b/locales/sk/common.json index 502164d6d..a90922544 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodín}}", "desktop_attention_title": "Na tomto počítači" }, + "uxs2_ident": { + "your_name": { + "label": "Vaše meno", + "description": "Toto meno ľudia vidia vedľa vašej adresy, keď im pošlete e-mail.", + "saved": "Vaše meno je uložené.", + "error": "Vaše meno sa nepodarilo uložiť. Skúste to znova." + }, + "addresses": { + "description": "Adresy, z ktorých môžete odosielať, každá s vlastným menom a podpisom.", + "label": "Vaše adresy", + "count": "{count, plural, =0 {Zatiaľ žiadne adresy} one {# adresa} few {# adresy} many {# adresy} other {# adries}}", + "manage": "Upraviť adresy…", + "modal_title": "Vaše e-mailové adresy", + "add": "Pridať adresu", + "edit": "Upraviť adresu", + "added": "Adresa pridaná.", + "saved": "Adresa uložená.", + "deleted": "Adresa odstránená.", + "delete_title": "Odstrániť túto adresu?" + }, + "signatures": { + "title": "Podpisy", + "description": "Pridá sa na koniec správ, ktoré píšete. Každá adresa má vlastný.", + "saved": "Podpis uložený.", + "error": "Podpis sa nepodarilo uložiť. Skúste to znova.", + "no_addresses": "Zatiaľ nemáte adresu, z ktorej sa dá odosielať." + }, + "search": { + "addresses": "identita identity alias odosielateľ adresa odosielateľa adresa od identity sender", + "signatures": "podpis e-mailový podpis päta identita odosielateľ adresa od signature", + "your_name": "zobrazované meno celé meno meno odosielateľa display name" + } + }, "desktop_sign_in": { "server_needed": "Najprv zadajte adresu servera.", "server_declined": "Tento server ste nepovolili, preto sa naň nič neodoslalo.", diff --git a/locales/th/common.json b/locales/th/common.json index 852be7f21..87a04b350 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -7154,6 +7154,39 @@ "app_lock_timeout_hours": "{count, plural, other {# ชั่วโมง}}", "desktop_attention_title": "บนคอมพิวเตอร์เครื่องนี้" }, + "uxs2_ident": { + "your_name": { + "label": "ชื่อของคุณ", + "description": "ผู้คนจะเห็นชื่อนี้ข้างที่อยู่ของคุณเมื่อคุณส่งอีเมลถึงพวกเขา", + "saved": "บันทึกชื่อของคุณแล้ว", + "error": "บันทึกชื่อของคุณไม่ได้ ลองอีกครั้ง" + }, + "addresses": { + "description": "ที่อยู่ที่คุณใช้ส่งได้ แต่ละที่อยู่มีชื่อและลายเซ็นของตัวเอง", + "label": "ที่อยู่ของคุณ", + "count": "{count, plural, =0 {ยังไม่มีที่อยู่} other {# ที่อยู่}}", + "manage": "แก้ไขที่อยู่…", + "modal_title": "ที่อยู่อีเมลของคุณ", + "add": "เพิ่มที่อยู่", + "edit": "แก้ไขที่อยู่", + "added": "เพิ่มที่อยู่แล้ว", + "saved": "บันทึกที่อยู่แล้ว", + "deleted": "ลบที่อยู่แล้ว", + "delete_title": "ลบที่อยู่นี้หรือไม่" + }, + "signatures": { + "title": "ลายเซ็น", + "description": "ใส่ไว้ท้ายข้อความที่คุณเขียน แต่ละที่อยู่มีลายเซ็นของตัวเอง", + "saved": "บันทึกลายเซ็นแล้ว", + "error": "บันทึกลายเซ็นไม่ได้ ลองอีกครั้ง", + "no_addresses": "ยังไม่มีที่อยู่สำหรับส่ง" + }, + "search": { + "addresses": "ข้อมูลประจำตัว นามแฝง ผู้ส่ง ที่อยู่ผู้ส่ง ที่อยู่จาก identity alias sender", + "signatures": "ลายเซ็น ลายเซ็นอีเมล ท้ายกระดาษ ผู้ส่ง ที่อยู่จาก signature", + "your_name": "ชื่อที่แสดง ชื่อเต็ม ชื่อผู้ส่ง display name" + } + }, "desktop_sign_in": { "server_needed": "ป้อนที่อยู่เซิร์ฟเวอร์ก่อน", "server_declined": "คุณไม่ได้อนุญาตเซิร์ฟเวอร์นี้ จึงไม่มีการส่งข้อมูลใดไป", diff --git a/locales/tr/common.json b/locales/tr/common.json index 8e246b38f..5d5c5a157 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# saat} other {# saat}}", "desktop_attention_title": "Bu bilgisayarda" }, + "uxs2_ident": { + "your_name": { + "label": "Adınız", + "description": "Birine e-posta gönderdiğinizde bu adı adresinizin yanında görür.", + "saved": "Adınız kaydedildi.", + "error": "Adınız kaydedilemedi. Yeniden deneyin." + }, + "addresses": { + "description": "Gönderebileceğiniz adresler; her birinin kendi adı ve imzası var.", + "label": "Adresleriniz", + "count": "{count, plural, =0 {Henüz adres yok} one {# adres} other {# adres}}", + "manage": "Adresleri düzenle…", + "modal_title": "E-posta adresleriniz", + "add": "Adres ekle", + "edit": "Adresi düzenle", + "added": "Adres eklendi.", + "saved": "Adres kaydedildi.", + "deleted": "Adres silindi.", + "delete_title": "Bu adres silinsin mi?" + }, + "signatures": { + "title": "İmzalar", + "description": "Yazdığınız iletilerin sonuna eklenir. Her adresin kendi imzası vardır.", + "saved": "İmza kaydedildi.", + "error": "İmza kaydedilemedi. Yeniden deneyin.", + "no_addresses": "Henüz gönderebileceğiniz bir adres yok." + }, + "search": { + "addresses": "kimlik kimlikler takma ad gönderen gönderen adresi kimden adresi identity alias sender", + "signatures": "imza e-posta imzası alt bilgi kimlik gönderen kimden adresi signature", + "your_name": "görünen ad tam ad gönderen adı display name" + } + }, "desktop_sign_in": { "server_needed": "Önce sunucu adresini girin.", "server_declined": "Bu sunucuya izin vermediniz, bu yüzden ona hiçbir şey gönderilmedi.", diff --git a/locales/uk/common.json b/locales/uk/common.json index 52bf3725a..2028ab3b8 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# година} few {# години} many {# годин} other {# години}}", "desktop_attention_title": "На цьому комп'ютері" }, + "uxs2_ident": { + "your_name": { + "label": "Ваше ім'я", + "description": "Це ім'я люди бачать поруч із вашою адресою, коли ви їм пишете.", + "saved": "Ваше ім'я збережено.", + "error": "Не вдалося зберегти ваше ім'я. Спробуйте ще раз." + }, + "addresses": { + "description": "Адреси, з яких ви можете надсилати пошту, кожна зі своїм ім'ям і підписом.", + "label": "Ваші адреси", + "count": "{count, plural, =0 {Адрес поки немає} one {# адреса} few {# адреси} many {# адрес} other {# адреси}}", + "manage": "Змінити адреси…", + "modal_title": "Ваші адреси електронної пошти", + "add": "Додати адресу", + "edit": "Змінити адресу", + "added": "Адресу додано.", + "saved": "Адресу збережено.", + "deleted": "Адресу видалено.", + "delete_title": "Видалити цю адресу?" + }, + "signatures": { + "title": "Підписи", + "description": "Додається в кінці ваших листів. Кожна адреса має свій.", + "saved": "Підпис збережено.", + "error": "Не вдалося зберегти підпис. Спробуйте ще раз.", + "no_addresses": "Поки немає адреси, з якої можна надсилати." + }, + "search": { + "addresses": "ідентичність особа псевдонім аліас відправник адреса відправника адреса від identity alias sender", + "signatures": "підпис підпис листа нижній колонтитул відправник адреса від signature", + "your_name": "відображуване ім'я повне ім'я ім'я відправника display name" + } + }, "desktop_sign_in": { "server_needed": "Спочатку введіть адресу сервера.", "server_declined": "Ви не дозволили цей сервер, тому йому нічого не надіслано.", diff --git a/locales/vi/common.json b/locales/vi/common.json index a02cf42d1..f9d2ca0f0 100644 --- a/locales/vi/common.json +++ b/locales/vi/common.json @@ -7158,6 +7158,39 @@ "app_lock_timeout_hours": "{count, plural, other {# giờ}}", "desktop_attention_title": "Trên máy tính này" }, + "uxs2_ident": { + "your_name": { + "label": "Tên của bạn", + "description": "Mọi người thấy tên này cạnh địa chỉ của bạn khi bạn gửi thư cho họ.", + "saved": "Đã lưu tên của bạn.", + "error": "Không lưu được tên của bạn. Hãy thử lại." + }, + "addresses": { + "description": "Các địa chỉ bạn có thể gửi thư, mỗi địa chỉ có tên và chữ ký riêng.", + "label": "Địa chỉ của bạn", + "count": "{count, plural, =0 {Chưa có địa chỉ nào} other {# địa chỉ}}", + "manage": "Sửa địa chỉ…", + "modal_title": "Địa chỉ email của bạn", + "add": "Thêm địa chỉ", + "edit": "Sửa địa chỉ", + "added": "Đã thêm địa chỉ.", + "saved": "Đã lưu địa chỉ.", + "deleted": "Đã xóa địa chỉ.", + "delete_title": "Xóa địa chỉ này?" + }, + "signatures": { + "title": "Chữ ký", + "description": "Được thêm vào cuối thư bạn viết. Mỗi địa chỉ có chữ ký riêng.", + "saved": "Đã lưu chữ ký.", + "error": "Không lưu được chữ ký. Hãy thử lại.", + "no_addresses": "Chưa có địa chỉ nào để gửi." + }, + "search": { + "addresses": "danh tính bí danh người gửi địa chỉ người gửi địa chỉ gửi từ identity alias sender", + "signatures": "chữ ký chữ ký email chân trang danh tính người gửi địa chỉ gửi từ signature", + "your_name": "tên hiển thị họ tên tên người gửi display name" + } + }, "desktop_sign_in": { "server_needed": "Trước tiên hãy nhập địa chỉ máy chủ.", "server_declined": "Bạn không cho phép máy chủ này nên không có gì được gửi đến đó.", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index afe32e3ed..960b63520 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {# 小時}}", "desktop_attention_title": "在這台電腦上" }, + "uxs2_ident": { + "your_name": { + "label": "你的名稱", + "description": "你寄信給別人時,對方會在你的地址旁看到這個名稱。", + "saved": "你的名稱已儲存。", + "error": "無法儲存你的名稱。請再試一次。" + }, + "addresses": { + "description": "你可以用來寄信的地址,每個地址都有自己的名稱和簽名。", + "label": "你的地址", + "count": "{count, plural, =0 {還沒有地址} other {# 個地址}}", + "manage": "編輯地址…", + "modal_title": "你的電子郵件地址", + "add": "新增地址", + "edit": "編輯地址", + "added": "已新增地址。", + "saved": "已儲存地址。", + "deleted": "已刪除地址。", + "delete_title": "要刪除這個地址嗎?" + }, + "signatures": { + "title": "簽名", + "description": "加在你所寫郵件的結尾。每個地址都有自己的簽名。", + "saved": "已儲存簽名。", + "error": "無法儲存簽名。請再試一次。", + "no_addresses": "還沒有可以寄信的地址。" + }, + "search": { + "addresses": "身分 別名 寄件者 寄件者地址 寄件地址 identity alias sender", + "signatures": "簽名 郵件簽名 頁尾 身分 寄件者 寄件地址 signature", + "your_name": "顯示名稱 全名 寄件者名稱 display name" + } + }, "desktop_sign_in": { "server_needed": "請先輸入伺服器位址。", "server_declined": "你沒有允許此伺服器,因此沒有傳送任何內容給它。", diff --git a/locales/zh/common.json b/locales/zh/common.json index a5fd459b2..ac15e6f16 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {# 小时}}", "desktop_attention_title": "在这台电脑上" }, + "uxs2_ident": { + "your_name": { + "label": "你的名字", + "description": "你给别人发邮件时,对方会在你的地址旁看到这个名字。", + "saved": "你的名字已保存。", + "error": "无法保存你的名字。请重试。" + }, + "addresses": { + "description": "你可以用来发信的地址,每个地址都有自己的名字和签名。", + "label": "你的地址", + "count": "{count, plural, =0 {还没有地址} other {# 个地址}}", + "manage": "编辑地址…", + "modal_title": "你的电子邮件地址", + "add": "添加地址", + "edit": "编辑地址", + "added": "地址已添加。", + "saved": "地址已保存。", + "deleted": "地址已删除。", + "delete_title": "删除这个地址?" + }, + "signatures": { + "title": "签名", + "description": "添加在你所写邮件的末尾。每个地址都有自己的签名。", + "saved": "签名已保存。", + "error": "无法保存签名。请重试。", + "no_addresses": "还没有可以发信的地址。" + }, + "search": { + "addresses": "身份 别名 发件人 发件人地址 发件地址 identity alias sender", + "signatures": "签名 邮件签名 页脚 身份 发件人 发件地址 signature", + "your_name": "显示名称 全名 发件人姓名 display name" + } + }, "desktop_sign_in": { "server_needed": "请先输入服务器地址。", "server_declined": "你没有允许此服务器,因此没有向它发送任何内容。", diff --git a/stores/__tests__/auth-store-display-name.test.ts b/stores/__tests__/auth-store-display-name.test.ts index 40aef4126..36eab0668 100644 --- a/stores/__tests__/auth-store-display-name.test.ts +++ b/stores/__tests__/auth-store-display-name.test.ts @@ -32,35 +32,39 @@ function seedAccount(displayName = 'Old Name', label = displayName): string { } // Issue #900: the registry entry is written once by addAccount and never -// refreshed, so a Stalwart admin renaming the user ("Full name") was never -// reflected in the account switcher / settings header. -describe('syncAccountDisplayName (#900)', () => { +// refreshed, so a renamed user kept the old name in the account switcher. +// settings-p2-2: the name that counts is the primary identity's, the one +// recipients see and Settings > Account edits. Stalwart's principal +// description is only the fallback for an account without one. +describe('syncAccountDisplayName (#900, settings-p2-2)', () => { beforeEach(() => { mockedPrincipal.mockReset(); useAccountStore.setState({ accounts: [], activeAccountId: null, defaultAccountId: null }); }); - it('replaces the cached name with the live Stalwart principal name', async () => { + it('takes the identity name over the Stalwart principal description', async () => { + const id = seedAccount('Old Name'); + mockedPrincipal.mockResolvedValue('Admin note (throwaway)'); + + await syncAccountDisplayName(id, client, 'Sam Settings'); + + const account = useAccountStore.getState().getAccountById(id)!; + expect(account.displayName).toBe('Sam Settings'); + expect(account.label).toBe('Sam Settings'); + expect(mockedPrincipal).not.toHaveBeenCalled(); + }); + + it('falls back to the principal name when the identity has no name', async () => { const id = seedAccount('Old Name'); mockedPrincipal.mockResolvedValueOnce('New Name'); - await syncAccountDisplayName(id, client, 'Old Name'); + await syncAccountDisplayName(id, client, ' '); const account = useAccountStore.getState().getAccountById(id)!; expect(account.displayName).toBe('New Name'); - expect(account.label).toBe('New Name'); expect(mockedPrincipal).toHaveBeenCalledWith(client, account.cookieSlot); }); - it('falls back to the identity name when the principal is unavailable', async () => { - const id = seedAccount('Old Name'); - mockedPrincipal.mockResolvedValueOnce(null); - - await syncAccountDisplayName(id, client, 'Identity Name'); - - expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('Identity Name'); - }); - it('keeps the cached name when neither source yields one', async () => { const id = seedAccount('Old Name'); mockedPrincipal.mockResolvedValueOnce(null); @@ -70,12 +74,30 @@ describe('syncAccountDisplayName (#900)', () => { expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('Old Name'); }); - it('leaves a diverged label alone', async () => { - const id = seedAccount('Old Name', 'Work'); + it('keeps the name when the caller does not know the identities', async () => { + const id = seedAccount('Sam Settings'); + mockedPrincipal.mockResolvedValue('Admin note (throwaway)'); + + await syncAccountDisplayName(id, client); + + expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('Sam Settings'); + expect(mockedPrincipal).not.toHaveBeenCalled(); + }); + + it('names an unnamed account from the principal when the identities are unknown', async () => { + const id = seedAccount(''); mockedPrincipal.mockResolvedValueOnce('New Name'); await syncAccountDisplayName(id, client); + expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('New Name'); + }); + + it('leaves a diverged label alone', async () => { + const id = seedAccount('Old Name', 'Work'); + + await syncAccountDisplayName(id, client, 'New Name'); + const account = useAccountStore.getState().getAccountById(id)!; expect(account.displayName).toBe('New Name'); expect(account.label).toBe('Work'); @@ -86,9 +108,8 @@ describe('syncAccountDisplayName (#900)', () => { expect(mockedPrincipal).not.toHaveBeenCalled(); const id = seedAccount('Same'); - mockedPrincipal.mockResolvedValueOnce('Same'); const before = useAccountStore.getState().getAccountById(id); - await syncAccountDisplayName(id, client); + await syncAccountDisplayName(id, client, 'Same'); expect(useAccountStore.getState().getAccountById(id)).toBe(before); }); }); diff --git a/stores/auth-store.ts b/stores/auth-store.ts index 4fb3b9f6c..88483e511 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -295,9 +295,9 @@ function getClientRateLimitState(client: IJMAPClient | null): Pick Account edits. The Stalwart principal name is only the + * fallback for an account without one. * * Best-effort and never throws: a failed lookup keeps the cached name. */ @@ -309,9 +309,16 @@ export async function syncAccountDisplayName( const account = useAccountStore.getState().getAccountById(accountId); if (!account) return; - const name = (await fetchPrincipalDisplayName(client, account.cookieSlot)) - || fallbackName?.trim() - || ''; + // The name people see on sent mail is the sending identity's, and that is + // the one name Settings lets them change (settings-p2-2). Stalwart's + // principal description ("Full name") is often an admin's note, so it only + // names an account that has no identity name. A caller that doesn't know + // the identities keeps the name the account already has. + let name = fallbackName?.trim() || ''; + if (!name) { + if (fallbackName === undefined && account.displayName) return; + name = (await fetchPrincipalDisplayName(client, account.cookieSlot)) || ''; + } if (!name) return; // Re-read: the entry may have changed (or been removed) during the request. diff --git a/stores/identity-store.ts b/stores/identity-store.ts index 3f22669b6..6dbb80f3e 100644 --- a/stores/identity-store.ts +++ b/stores/identity-store.ts @@ -182,7 +182,7 @@ export async function createIdentityWithHooks(client: IJMAPClient, data: Identit export async function updateIdentityWithHooks( client: IJMAPClient, identity: Identity, - updates: Omit, + updates: Partial>, ): Promise { const next = identityToView({ ...identity, ...updates } as Identity); if (!(await identityHooks.onBeforeIdentityChange.intercept({ identityId: identity.id, change: 'update', identity: next }))) { @@ -193,6 +193,22 @@ export async function updateIdentityWithHooks( return true; } +/** + * Change some fields of one identity (the Account page's name, a signature + * on Composing) and keep the local list in step, without reloading every + * identity. Callers then run the auth store's syncIdentities so the From + * line and the account switcher follow. + */ +export async function saveIdentityChanges( + client: IJMAPClient, + identity: Identity, + updates: Partial>, +): Promise { + if (!(await updateIdentityWithHooks(client, identity, updates))) return false; + useIdentityStore.getState().updateIdentityLocal(identity.id, updates as Partial); + return true; +} + export async function deleteIdentityWithHooks(client: IJMAPClient, identity: Identity): Promise { if (!(await identityHooks.onBeforeIdentityChange.intercept({ identityId: identity.id, change: 'delete', identity: identityToView(identity),