diff --git a/components/email/__tests__/email-context-menu-shortcuts.test.tsx b/components/email/__tests__/email-context-menu-shortcuts.test.tsx index 062e60278..cc3c8f8f5 100644 --- a/components/email/__tests__/email-context-menu-shortcuts.test.tsx +++ b/components/email/__tests__/email-context-menu-shortcuts.test.tsx @@ -58,10 +58,10 @@ describe('message menu key hints', () => { it('shows the keys the list runs each action with', () => { renderMenu(); expect(within(row('reply')).getByText('R')).toBeInTheDocument(); - expect(within(row('reply_all')).getByText('Shift+R')).toBeInTheDocument(); + expect(within(row('reply_all')).getByText('Shift + R')).toBeInTheDocument(); expect(within(row('forward')).getByText('F')).toBeInTheDocument(); expect(within(row('star')).getByText('S')).toBeInTheDocument(); - expect(within(row('mark_read')).getByText('Shift+I')).toBeInTheDocument(); + expect(within(row('mark_read')).getByText('Shift + I')).toBeInTheDocument(); expect(within(row('mark_as_spam')).getByText('!')).toBeInTheDocument(); expect(within(screen.getByTestId('ctx-delete')).getByText('Del')).toBeInTheDocument(); }); @@ -77,13 +77,38 @@ describe('message menu key hints', () => { it('shows them on a selection too', () => { renderMenu({ isMultiSelect: true, selectedCount: 2, onBatchDelete: vi.fn(), onBatchMarkAsRead: vi.fn(), onBatchMarkAsSpam: vi.fn() }); expect(within(screen.getByTestId('ctx-delete')).getByText('Del')).toBeInTheDocument(); - expect(within(screen.getByTestId('ctx-mark-read')).getByText('Shift+I')).toBeInTheDocument(); + expect(within(screen.getByTestId('ctx-mark-read')).getByText('Shift + I')).toBeInTheDocument(); expect(within(screen.getByTestId('ctx-spam')).getByText('!')).toBeInTheDocument(); }); it('matches the read state: U marks a read message unread', () => { expect(messageActionShortcut('read', { unread: false, singleKeys: true })).toBe('U'); - expect(messageActionShortcut('read', { unread: true, singleKeys: true })).toBe('Shift+I'); + expect(messageActionShortcut('read', { unread: true, singleKeys: true })).toBe('Shift + I'); expect(messageActionShortcut('print', { singleKeys: true })).toBeUndefined(); }); }); + +describe('message menu offline', () => { + afterEach(async () => { + const { useOfflineStore } = await import('@/lib/offline/state'); + useOfflineStore.setState({ browserOffline: false, enabled: [] }); + }); + + it('greys out changes that would fail without a connection, and says why', async () => { + const { useOfflineStore } = await import('@/lib/offline/state'); + useOfflineStore.setState({ browserOffline: true, enabled: [] }); + renderMenu(); + expect(screen.getByText('offline_header')).toBeInTheDocument(); + expect((screen.getByTestId('ctx-delete') as HTMLButtonElement).disabled).toBe(true); + expect((screen.getByTestId('ctx-star') as HTMLButtonElement).disabled).toBe(true); + expect((screen.getByTestId('ctx-mark-read') as HTMLButtonElement).disabled).toBe(true); + // Writing a reply still works: the composer keeps the draft. + expect(row('reply')).not.toBeDisabled(); + }); + + it('leaves them on while online', () => { + renderMenu(); + expect(screen.queryByText('offline_header')).toBeNull(); + expect((screen.getByTestId('ctx-delete') as HTMLButtonElement).disabled).toBe(false); + }); +}); diff --git a/components/email/email-context-menu.tsx b/components/email/email-context-menu.tsx index deaf6e91e..e264dafca 100644 --- a/components/email/email-context-menu.tsx +++ b/components/email/email-context-menu.tsx @@ -31,6 +31,7 @@ import { } from "@/components/icons"; import { useMessageActionList, type MessageActionEntry, type MessageActionKey } from "./message-actions"; import { useMessageActionShortcut } from "./message-action-shortcuts"; +import { useConnectionState } from "@/hooks/use-connection-state"; import { buildMailboxTree, MailboxNode } from "@/lib/utils"; import { buildMoveTargets, resolveMoveOwnerAccountId } from "@/lib/move-targets"; import { localizeMailboxName } from "@/lib/mailbox-label"; @@ -142,6 +143,12 @@ interface EmailContextMenuProps { label?: string; } +/** Entries that change the message on the server, or queue the change offline. */ +const CHANGES_MAIL = new Set([ + "archive", "delete", "read", "remove_from_folder", "star", "pin", "spam", + "reschedule", "cancel_send", "cancel_and_edit", +]); + export function EmailContextMenu({ email, position, @@ -200,6 +207,11 @@ export function EmailContextMenu({ }: EmailContextMenuProps) { const t = useTranslations("context_menu"); const shortcutOf = useMessageActionShortcut(); + // Offline, a change to a message waits in the outbox only where the account keeps + // its mail on this device; otherwise it would fail, so it's greyed out. Rules and + // copies to another account always need the server. + const { offline, canChangeMail } = useConnectionState(); + const tMenu = useTranslations("menuAudit"); const archiveActionEnabled = useArchiveActionEnabled(); const mountainView = useSettingsStore((state) => state.interfaceLayout === "mountain-view"); const tSidebar = useTranslations("sidebar"); @@ -360,7 +372,7 @@ export function EmailContextMenu({ label={t("archive")} shortcut={shortcutOf("archive")} onClick={() => handleAction(onBatchArchive!)} - disabled={!onBatchArchive} + disabled={!onBatchArchive || !canChangeMail} /> ); @@ -373,7 +385,7 @@ export function EmailContextMenu({ testId="ctx-delete" shortcut={shortcutOf("delete")} onClick={() => handleAction(onBatchDelete!)} - disabled={!onBatchDelete} + disabled={!onBatchDelete || !canChangeMail} destructive={!mountainView} /> @@ -386,6 +398,7 @@ export function EmailContextMenu({ label={isUnread ? t("mark_read") : t("mark_unread")} testId={isUnread ? "ctx-mark-read" : "ctx-mark-unread"} shortcut={shortcutOf("read", isUnread)} + disabled={!canChangeMail} onClick={() => handleAction(() => onBatchMarkAsRead?.(isUnread))} /> @@ -394,7 +407,7 @@ export function EmailContextMenu({ <> {/* Move to submenu */} {moveTree.length > 0 && ( - + {renderMailboxNodes(moveTree, moveTargetIds, "move-to:", (mailboxId) => onBatchMoveToMailbox?.(mailboxId))} )} @@ -403,6 +416,7 @@ export function EmailContextMenu({ icon={FolderX} label={t("remove_from_folder")} testId="ctx-remove-from-folder" + disabled={!canChangeMail} onClick={() => handleAction(onBatchRemoveFromFolder!)} /> )} @@ -422,7 +436,7 @@ export function EmailContextMenu({ <> {/* Copy to another connected account, keeping the originals */} {onCopyToAccount && copyTrees.length > 0 && ( - + {copyTargetsContent} )} @@ -431,14 +445,14 @@ export function EmailContextMenu({ const rulesItem = ( <> {/* Rules: filter rules made from the sender(s), saved as Sieve */} - + ); const tagItem = ( <> {/* Set tag submenu - the whole selection's tags (#1077) */} {onBatchToggleTag && ( - +
handleAction(isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!)} - disabled={isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam} + disabled={(isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) || !canChangeMail} destructive={!isInJunkFolder && !mountainView} /> {batchPhishing.available && ( @@ -483,26 +497,26 @@ export function EmailContextMenu({ switch (entry.key) { case "move": return ( - + {renderMailboxNodes(moveTree, moveTargetIds, "move-to:", (mailboxId) => onMoveToMailbox?.(mailboxId))} ); case "copy_to_account": return ( - + {copyTargetsContent} ); case "tag": return ( - +
onSetTag?.(tagId)} scope={tagScope} />
); case "rules": - return ; + return ; } }; @@ -515,7 +529,7 @@ export function EmailContextMenu({ icon={entry.icon} label={entry.label} testId={entry.testId} - disabled={entry.disabled} + disabled={entry.disabled || (!canChangeMail && CHANGES_MAIL.has(entry.key))} destructive={entry.destructive} shortcut={shortcutOf(entry.key, isUnread)} onClick={() => handleAction(entry.run)} @@ -531,6 +545,7 @@ export function EmailContextMenu({ onClose={onClose} label={label} > + {offline && {tMenu("offline_header")}} {showBatchActions ? ( <> diff --git a/components/email/message-action-shortcuts.ts b/components/email/message-action-shortcuts.ts index ce9efe28c..944488657 100644 --- a/components/email/message-action-shortcuts.ts +++ b/components/email/message-action-shortcuts.ts @@ -12,7 +12,7 @@ import type { MessageActionKey } from "./message-actions"; */ const KEYS: Partial> = { reply: { key: "R", single: true }, - reply_all: { key: "Shift+R", single: true }, + reply_all: { key: "Shift + R", single: true }, forward: { key: "F", single: true }, archive: { key: "E", single: true }, delete: { key: "Del", single: false }, @@ -25,7 +25,7 @@ export function messageActionShortcut( options: { unread?: boolean; singleKeys: boolean }, ): string | undefined { const entry = key === "read" - ? { key: options.unread ? "Shift+I" : "U", single: true } + ? { key: options.unread ? "Shift + I" : "U", single: true } : KEYS[key]; if (!entry) return undefined; if (entry.single && !options.singleKeys) return undefined; diff --git a/components/email/rules-menu.tsx b/components/email/rules-menu.tsx index 643d11376..e8ffbe6af 100644 --- a/components/email/rules-menu.tsx +++ b/components/email/rules-menu.tsx @@ -407,10 +407,13 @@ export function RulesContextSubMenu({ anchor, senderEmails, onClose, + disabled = false, }: { anchor: Email; senderEmails: Email[]; onClose: () => void; + /** Rules are saved on the server: greyed out offline. */ + disabled?: boolean; }) { const t = useTranslations("context_menu"); const accountEmails = useMemo( @@ -424,7 +427,7 @@ export function RulesContextSubMenu({ icon={Filter} label={t("rules.title")} testId="ctx-rules" - disabled={availability === "cross_account"} + disabled={disabled || availability === "cross_account"} hint={availability === "cross_account" ? t("rules.cross_account") : undefined} > {target && } @@ -801,7 +804,7 @@ function FolderRulesContent({ * saved rules over the mail already in the folder (#1166). Hidden for shared * folders and accounts without Sieve. */ -export function FolderRulesSubMenu({ mailbox, onClose }: { mailbox: Mailbox; onClose: () => void }) { +export function FolderRulesSubMenu({ mailbox, onClose, disabled = false }: { mailbox: Mailbox; onClose: () => void; disabled?: boolean }) { const t = useTranslations("context_menu"); const client = useAuthStore((s) => s.client); const target = useMemo( @@ -810,7 +813,7 @@ export function FolderRulesSubMenu({ mailbox, onClose }: { mailbox: Mailbox; onC ); if (!target || !target.supportsSieve) return null; return ( - + ); diff --git a/components/layout/__tests__/mailbox-context-menu-states.test.tsx b/components/layout/__tests__/mailbox-context-menu-states.test.tsx new file mode 100644 index 000000000..5f6447986 --- /dev/null +++ b/components/layout/__tests__/mailbox-context-menu-states.test.tsx @@ -0,0 +1,81 @@ +import { render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { Mailbox } from '@/lib/jmap/types'; + +// A folder menu greys out what can't work: nothing to mark read, or no connection +// for what needs the server. + +vi.mock('@/components/email/rules-menu', () => ({ FolderRulesSubMenu: () => null })); +vi.mock('@/lib/offline/offline-mode', () => ({ offlineAllowed: () => true })); + +const { MailboxContextMenu, unreadInTree } = await import('../mailbox-context-menu'); +const { useOfflineStore } = await import('@/lib/offline/state'); +const { useAuthStore } = await import('@/stores/auth-store'); + +const box = (id: string, unread: number, parentId: string | null = null) => + ({ id, name: id, parentId, sortOrder: 1, totalEmails: 5, unreadEmails: unread, myRights: {} }) as unknown as Mailbox; + +function renderMenu(mailbox: Mailbox, mailboxes: Mailbox[], hasChildren = false) { + render( + , + ); +} + +const item = (name: string) => screen.getByRole('menuitem', { name }) as HTMLButtonElement; + +describe('folder menu states', () => { + afterEach(() => { + useOfflineStore.setState({ browserOffline: false, downServers: [], enabled: [] }); + }); + + it('greys out Mark folder as read when nothing in it is unread', () => { + const read = box('read', 0); + renderMenu(read, [read]); + expect(item('mark_folder_read').disabled).toBe(true); + }); + + it('keeps it for a folder with unread mail, and the tree entry for unread mail below', () => { + const parent = box('parent', 0); + const child = box('child', 3, 'parent'); + renderMenu(parent, [parent, child], true); + expect(item('mark_folder_read').disabled).toBe(true); + expect(item('mark_folder_tree_read').disabled).toBe(false); + expect(unreadInTree(parent, [parent, child])).toBe(3); + }); + + it('offline, greys out what needs the server and says why', () => { + useOfflineStore.setState({ browserOffline: true }); + const folder = box('f', 2); + renderMenu(folder, [folder]); + expect(screen.getByText('offline_header')).toBeInTheDocument(); + for (const name of ['new_subfolder', 'rename', 'menu_import', 'refresh', 'empty_folder_generic', 'delete_folder']) { + expect(item(name).disabled).toBe(true); + } + // No mail kept on this device: marking read would fail too. + expect(item('mark_folder_read').disabled).toBe(true); + }); + + it('offline with the mail kept on this device, marking read still works', () => { + useAuthStore.setState({ activeAccountId: 'acc' } as never); + useOfflineStore.setState({ browserOffline: true, enabled: ['acc'] }); + const folder = box('f', 2); + renderMenu(folder, [folder]); + expect(item('mark_folder_read').disabled).toBe(false); + expect(item('rename').disabled).toBe(true); + }); +}); diff --git a/components/layout/mailbox-context-menu.tsx b/components/layout/mailbox-context-menu.tsx index 827d1d592..0e2f7d40d 100644 --- a/components/layout/mailbox-context-menu.tsx +++ b/components/layout/mailbox-context-menu.tsx @@ -31,6 +31,7 @@ import { import { FolderRulesSubMenu } from "@/components/email/rules-menu"; import { openMailExport } from "@/stores/mail-transfer-store"; import { useDevicePolicy } from "@/hooks/use-device-policy"; +import { useConnectionState } from "@/hooks/use-connection-state"; interface Position { x: number; @@ -45,6 +46,20 @@ export type MailboxContextTarget = | { kind: "folders-section"; accountId?: string }; const PATH_SEPARATOR = " › "; + +/** Unread messages in a folder and the folders under it; undefined where a count isn't known. */ +export function unreadInTree(mailbox: Mailbox, mailboxes: Mailbox[]): number | undefined { + let total = 0; + const seen = new Set(); + const visit = (box: Mailbox): boolean => { + if (seen.has(box.id)) return true; + seen.add(box.id); + if (typeof box.unreadEmails !== "number") return false; + total += box.unreadEmails; + return mailboxes.filter((child) => child.parentId === box.id).every(visit); + }; + return visit(mailbox) ? total : undefined; +} const MAX_PATH_LENGTH = 40; const MAX_SEGMENT_LENGTH = 16; @@ -147,6 +162,11 @@ export function MailboxContextMenu({ const tExport = useTranslations("mail_export"); // Organisation > Devices > No export hides it; import stays. const exportOff = useDevicePolicy().noExport; + // Folders, rules and sharing need the server. Marking read waits in the outbox + // where the account keeps its mail on this device. + const { offline, canChangeMail } = useConnectionState(); + const tMenu = useTranslations("menuAudit"); + const offlineHeader = offline ? {tMenu("offline_header")} : null; const handleAction = (action: () => void) => { action(); @@ -198,13 +218,14 @@ export function MailboxContextMenu({ if (target.kind === "folders-section") { return ( + {offlineHeader} {!target.accountId && ( <> handleAction(onMarkAllFoldersRead!)} - disabled={!onMarkAllFoldersRead} + disabled={!onMarkAllFoldersRead || !canChangeMail} /> @@ -213,14 +234,14 @@ export function MailboxContextMenu({ icon={FolderPlus} label={t("new_folder")} onClick={() => handleAction(() => onCreateFolder?.(target.accountId))} - disabled={!onCreateFolder} + disabled={!onCreateFolder || offline} testId="mailbox-new-folder" /> handleAction(onRefresh!)} - disabled={!onRefresh} + disabled={!onRefresh || offline} /> {accountItems(target.accountId)} @@ -238,6 +259,8 @@ export function MailboxContextMenu({ const canSetSeen = mailbox.myRights?.maySetSeen !== false; const canRemoveItems = mailbox.myRights?.mayRemoveItems !== false; const canAddItems = mailbox.myRights?.mayAddItems !== false; + const nothingUnread = mailbox.unreadEmails === 0; + const nothingUnreadBelow = unreadInTree(mailbox, mailboxes) === 0; // Sharing needs the mail:share extension (handler present) and, on a folder // someone else shared with us, their permission to re-share it. const canShare = !!onShareFolder && (!mailbox.isShared || mailbox.myRights?.mayShare === true); @@ -252,19 +275,20 @@ export function MailboxContextMenu({ {renderShortenedPath(fullPath)} + {offlineHeader} handleAction(() => onMarkFolderRead?.(mailbox.id))} - disabled={!onMarkFolderRead || !canSetSeen} + disabled={!onMarkFolderRead || !canSetSeen || !canChangeMail || nothingUnread} /> {target.hasChildren && ( handleAction(() => onMarkFolderTreeRead?.(mailbox.id))} - disabled={!onMarkFolderTreeRead || !canSetSeen} + disabled={!onMarkFolderTreeRead || !canSetSeen || !canChangeMail || nothingUnreadBelow} /> )} @@ -274,14 +298,14 @@ export function MailboxContextMenu({ icon={FolderPlus} label={t("new_subfolder")} onClick={() => handleAction(() => onCreateSubfolder?.(mailbox.id))} - disabled={!onCreateSubfolder || !canCreateChild} + disabled={!onCreateSubfolder || !canCreateChild || offline} testId="mailbox-new-subfolder" /> handleAction(() => onRenameFolder?.(mailbox.id))} - disabled={!onRenameFolder || !canRename} + disabled={!onRenameFolder || !canRename || offline} testId="mailbox-rename" /> {onShareFolder && ( @@ -289,11 +313,11 @@ export function MailboxContextMenu({ icon={Users} label={t("share")} onClick={() => handleAction(() => onShareFolder(mailbox.id))} - disabled={!canShare} + disabled={!canShare || offline} testId="mailbox-share" /> )} - + {notifications && (() => { const muted = notifications.isMailboxMuted(mailbox); return ( @@ -313,7 +337,7 @@ export function MailboxContextMenu({ icon={FolderPlus} label={t("new_folder")} onClick={() => handleAction(() => onCreateFolder?.(target.sharedAccountId))} - disabled={!onCreateFolder} + disabled={!onCreateFolder || offline} testId="mailbox-new-folder" /> {accountItems(target.sharedAccountId)} @@ -326,7 +350,7 @@ export function MailboxContextMenu({ icon={Upload} label={tImport("menu_import")} onClick={() => handleAction(() => onImportEmail?.(mailbox.id))} - disabled={!onImportEmail || !canAddItems} + disabled={!onImportEmail || !canAddItems || offline} /> {!exportOff && ( handleAction(onRefresh!)} - disabled={!onRefresh} + disabled={!onRefresh || offline} /> {/* Destructive entries last (R6). */} @@ -351,14 +375,14 @@ export function MailboxContextMenu({ icon={FolderX} label={isTrashOrJunk ? t("empty_folder") : t("empty_folder_generic")} onClick={() => handleAction(() => onEmptyFolder?.(mailbox.id))} - disabled={!onEmptyFolder || mailbox.totalEmails === 0 || !canRemoveItems} + disabled={!onEmptyFolder || mailbox.totalEmails === 0 || !canRemoveItems || offline} destructive /> handleAction(() => onDeleteFolder?.(mailbox.id))} - disabled={!onDeleteFolder || !canDelete} + disabled={!onDeleteFolder || !canDelete || offline} destructive testId="mailbox-delete" /> diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index e22795c00..ccace0962 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -2335,6 +2335,8 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { empty: () => { if (selectedMailbox) void handleEmptyFolderFromContextMenu(selectedMailbox); }, }; const folderMenuMarkRead = useCallback(() => folderMenuLatest.current.markRead(), []); + // "Mark folder as read" is greyed out with nothing unread in the open folder. + const selectedFolderHasUnread = mailboxes.find((m) => m.id === selectedMailbox)?.unreadEmails !== 0; const folderMenuMarkAllRead = useCallback(() => folderMenuLatest.current.markAllRead(), []); const folderMenuEmpty = useCallback(() => folderMenuLatest.current.empty(), []); useEffect(() => { @@ -5541,7 +5543,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { onClick={() => { close(); void handleManualRefresh(); }} /> handleMarkFolderRead(selectedMailbox) : undefined} + onMarkFolderRead={selectedMailbox && selectedFolderHasUnread ? () => handleMarkFolderRead(selectedMailbox) : undefined} onMarkAllFoldersRead={handleMarkAllFoldersRead} onEmptyFolder={folderIsEmptyable && selectedMailbox ? () => void handleEmptyFolderFromContextMenu(selectedMailbox) : undefined} onPicked={close} @@ -5581,7 +5583,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { totalCount={searchQuery || !isFilterEmpty(searchFilters) ? undefined : mailboxes.find((mb) => mb.id === selectedMailbox)?.totalEmails} onRefresh={handleManualRefresh} isRefreshing={isManualRefreshing} - onMarkFolderRead={selectedMailbox ? () => handleMarkFolderRead(selectedMailbox) : undefined} + onMarkFolderRead={selectedMailbox && selectedFolderHasUnread ? () => handleMarkFolderRead(selectedMailbox) : undefined} onMarkAllFoldersRead={handleMarkAllFoldersRead} onEmptyFolder={selectedMailbox ? () => void handleEmptyFolderFromContextMenu(selectedMailbox) : undefined} onCreateFolder={() => handleCreateFolderFromContextMenu()} @@ -5651,7 +5653,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { folderRole={conversationRole} onRefresh={handleManualRefresh} refreshDisabled={!client || isManualRefreshing} - onMarkFolderRead={selectedMailbox ? folderMenuMarkRead : undefined} + onMarkFolderRead={selectedMailbox && selectedFolderHasUnread ? folderMenuMarkRead : undefined} onMarkAllFoldersRead={folderMenuMarkAllRead} onEmptyFolder={folderIsEmptyable && selectedMailbox ? folderMenuEmpty : undefined} /> diff --git a/hooks/use-connection-state.ts b/hooks/use-connection-state.ts new file mode 100644 index 000000000..e870307e5 --- /dev/null +++ b/hooks/use-connection-state.ts @@ -0,0 +1,41 @@ +"use client"; + +import { useSyncExternalStore } from "react"; +import { serverKey, useOfflineStore } from "@/lib/offline/state"; +import { offlineAllowed } from "@/lib/offline/offline-mode"; +import { useAuthStore } from "@/stores/auth-store"; + +function subscribeOnline(callback: () => void): () => void { + window.addEventListener("online", callback); + window.addEventListener("offline", callback); + return () => { + window.removeEventListener("online", callback); + window.removeEventListener("offline", callback); + }; +} + +export interface ConnectionState { + /** No network, or the open account's mail server can't be reached. */ + offline: boolean; + /** + * Changes to messages still work: online, or the account keeps its mail on this + * device and the change waits for the connection. Folders, rules and sharing + * always need the server. + */ + canChangeMail: boolean; +} + +/** What the menus go by to grey out what can't work without a connection. */ +export function useConnectionState(): ConnectionState { + const account = useAuthStore((s) => s.activeAccountId); + const server = useAuthStore((s) => s.serverUrl); + const navigatorOffline = useSyncExternalStore( + subscribeOnline, + () => navigator.onLine === false, + () => false, + ); + const storeOffline = useOfflineStore((s) => s.browserOffline || (!!server && s.downServers.includes(serverKey(server)))); + const keepsMail = useOfflineStore((s) => !!account && s.enabled.includes(account)); + const offline = navigatorOffline || storeOffline; + return { offline, canChangeMail: !offline || (keepsMail && offlineAllowed(account)) }; +} diff --git a/locales/ar/common.json b/locales/ar/common.json index ed9cbecb0..76899d3a2 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -6455,6 +6455,19 @@ "group_account": "الحساب", "switch_to": "التبديل إلى {account}" }, + "menuAudit": { + "offline_header": "أنت غير متصل. بعض الإجراءات تحتاج إلى اتصال.", + "files_new_folder": "مجلد جديد…", + "files_new_file": "ملف جديد…", + "files_new_text_file": "ملف نصي جديد…", + "files_upload_files": "رفع الملفات…", + "files_upload_folder": "رفع مجلد…", + "files_share": "مشاركة…", + "files_share_button": "مشاركة", + "contacts_add_to_group": "إضافة إلى مجموعة…", + "contacts_bulk_add_to_group": "إضافة إلى مجموعة…", + "schedule_send": "جدولة الإرسال…" + }, "notification_center": { "title": "الإشعارات", "mark_all_read": "تعليم الكل كمقروء", diff --git a/locales/ca/common.json b/locales/ca/common.json index 5e3d4916d..1522a3a4c 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -6455,6 +6455,19 @@ "group_account": "Compte", "switch_to": "Canvia a {account}" }, + "menuAudit": { + "offline_header": "No teniu connexió. Algunes accions la necessiten.", + "files_new_folder": "Carpeta nova…", + "files_new_file": "Fitxer nou…", + "files_new_text_file": "Fitxer de text nou…", + "files_upload_files": "Puja fitxers…", + "files_upload_folder": "Puja una carpeta…", + "files_share": "Comparteix…", + "files_share_button": "Comparteix", + "contacts_add_to_group": "Afegeix al grup…", + "contacts_bulk_add_to_group": "Afegeix al grup…", + "schedule_send": "Programa l'enviament…" + }, "notification_center": { "title": "Notificacions", "mark_all_read": "Marca-ho tot com a llegit", diff --git a/locales/cs/common.json b/locales/cs/common.json index ace3e4105..96f28a71a 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -6455,6 +6455,19 @@ "group_account": "Účet", "switch_to": "Přepnout na {account}" }, + "menuAudit": { + "offline_header": "Jste offline. Některé akce potřebují připojení.", + "files_new_folder": "Nová složka…", + "files_new_file": "Nový soubor…", + "files_new_text_file": "Nový textový soubor…", + "files_upload_files": "Nahrát soubory…", + "files_upload_folder": "Nahrát složku…", + "files_share": "Sdílet…", + "files_share_button": "Sdílet", + "contacts_add_to_group": "Přidat do skupiny…", + "contacts_bulk_add_to_group": "Přidat do skupiny…", + "schedule_send": "Naplánovat odeslání…" + }, "notification_center": { "title": "Oznámení", "mark_all_read": "Označit vše jako přečtené", diff --git a/locales/da/common.json b/locales/da/common.json index b473c8379..21b96cc43 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -6455,6 +6455,19 @@ "group_account": "Konto", "switch_to": "Skift til {account}" }, + "menuAudit": { + "offline_header": "Du er offline. Nogle handlinger kræver forbindelse.", + "files_new_folder": "Ny mappe…", + "files_new_file": "Ny fil…", + "files_new_text_file": "Ny tekstfil…", + "files_upload_files": "Upload filer…", + "files_upload_folder": "Upload mappe…", + "files_share": "Del…", + "files_share_button": "Del", + "contacts_add_to_group": "Tilføj til gruppe…", + "contacts_bulk_add_to_group": "Tilføj til gruppe…", + "schedule_send": "Planlæg afsendelse…" + }, "notification_center": { "title": "Notifikationer", "mark_all_read": "Markér alle som læst", diff --git a/locales/de/common.json b/locales/de/common.json index 9cb2097f7..a7a30ba85 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -6455,6 +6455,19 @@ "group_account": "Konto", "switch_to": "Zu {account} wechseln" }, + "menuAudit": { + "offline_header": "Sie sind offline. Manche Aktionen brauchen eine Verbindung.", + "files_new_folder": "Neuer Ordner…", + "files_new_file": "Neue Datei…", + "files_new_text_file": "Neue Textdatei…", + "files_upload_files": "Dateien hochladen…", + "files_upload_folder": "Ordner hochladen…", + "files_share": "Teilen…", + "files_share_button": "Teilen", + "contacts_add_to_group": "Zur Gruppe hinzufügen…", + "contacts_bulk_add_to_group": "Zur Gruppe hinzufügen…", + "schedule_send": "Senden planen…" + }, "notification_center": { "title": "Benachrichtigungen", "mark_all_read": "Alle als gelesen markieren", diff --git a/locales/en/common.json b/locales/en/common.json index 84f9389a7..5db5a5d24 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -6455,6 +6455,19 @@ "group_account": "Account", "switch_to": "Switch to {account}" }, + "menuAudit": { + "offline_header": "You're offline. Some actions need a connection.", + "files_new_folder": "New folder…", + "files_new_file": "New file…", + "files_new_text_file": "New text file…", + "files_upload_files": "Upload files…", + "files_upload_folder": "Upload folder…", + "files_share": "Share…", + "files_share_button": "Share", + "contacts_add_to_group": "Add to group…", + "contacts_bulk_add_to_group": "Add to group…", + "schedule_send": "Schedule send…" + }, "notification_center": { "title": "Notifications", "mark_all_read": "Mark all read", diff --git a/locales/es/common.json b/locales/es/common.json index a44fce868..60d8f6b6c 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -6455,6 +6455,19 @@ "group_account": "Cuenta", "switch_to": "Cambiar a {account}" }, + "menuAudit": { + "offline_header": "No tiene conexión. Algunas acciones la necesitan.", + "files_new_folder": "Nueva carpeta…", + "files_new_file": "Nuevo archivo…", + "files_new_text_file": "Nuevo archivo de texto…", + "files_upload_files": "Subir archivos…", + "files_upload_folder": "Subir carpeta…", + "files_share": "Compartir…", + "files_share_button": "Compartir", + "contacts_add_to_group": "Añadir al grupo…", + "contacts_bulk_add_to_group": "Agregar al grupo…", + "schedule_send": "Programar envío…" + }, "notification_center": { "title": "Notificaciones", "mark_all_read": "Marcar todo como leído", diff --git a/locales/et/common.json b/locales/et/common.json index ace51f3b5..32331fd35 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -6455,6 +6455,19 @@ "group_account": "Konto", "switch_to": "Lülitu kontole {account}" }, + "menuAudit": { + "offline_header": "Olete võrguühenduseta. Mõni toiming vajab ühendust.", + "files_new_folder": "Uus kaust…", + "files_new_file": "Uus fail…", + "files_new_text_file": "Uus tekstifail…", + "files_upload_files": "Laadi failid üles…", + "files_upload_folder": "Laadi kaust üles…", + "files_share": "Jaga…", + "files_share_button": "Jaga", + "contacts_add_to_group": "Lisa gruppi…", + "contacts_bulk_add_to_group": "Lisa gruppi…", + "schedule_send": "Ajasta saatmine…" + }, "notification_center": { "title": "Märguanded", "mark_all_read": "Märgi kõik loetuks", diff --git a/locales/fa/common.json b/locales/fa/common.json index 9961aab01..84e115b67 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -6455,6 +6455,19 @@ "group_account": "حساب", "switch_to": "رفتن به {account}" }, + "menuAudit": { + "offline_header": "آفلاین هستید. برخی کارها به اتصال نیاز دارند.", + "files_new_folder": "پوشه جدید…", + "files_new_file": "فایل جدید…", + "files_new_text_file": "فایل متنی جدید…", + "files_upload_files": "بارگذاری فایل‌ها…", + "files_upload_folder": "بارگذاری پوشه…", + "files_share": "اشتراک‌گذاری…", + "files_share_button": "اشتراک‌گذاری", + "contacts_add_to_group": "افزودن به گروه…", + "contacts_bulk_add_to_group": "افزودن به گروه…", + "schedule_send": "زمان‌بندی ارسال…" + }, "notification_center": { "title": "اعلان‌ها", "mark_all_read": "علامت‌گذاری همه به‌عنوان خوانده‌شده", diff --git a/locales/fr/common.json b/locales/fr/common.json index 750e84417..912d19a66 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -6455,6 +6455,19 @@ "group_account": "Compte", "switch_to": "Passer à {account}" }, + "menuAudit": { + "offline_header": "Vous êtes hors ligne. Certaines actions ont besoin d’une connexion.", + "files_new_folder": "Nouveau dossier…", + "files_new_file": "Nouveau fichier…", + "files_new_text_file": "Nouveau fichier texte…", + "files_upload_files": "Téléverser des fichiers…", + "files_upload_folder": "Téléverser un dossier…", + "files_share": "Partager…", + "files_share_button": "Partager", + "contacts_add_to_group": "Ajouter au groupe…", + "contacts_bulk_add_to_group": "Ajouter au groupe…", + "schedule_send": "Planifier l’envoi…" + }, "notification_center": { "title": "Notifications", "mark_all_read": "Tout marquer comme lu", diff --git a/locales/he/common.json b/locales/he/common.json index 23cb11fa8..63e69fec9 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -6456,6 +6456,19 @@ "group_account": "חשבון", "switch_to": "מעבר אל {account}" }, + "menuAudit": { + "offline_header": "אין חיבור. חלק מהפעולות צריכות חיבור.", + "files_new_folder": "תיקיה חדשה…", + "files_new_file": "קובץ חדש…", + "files_new_text_file": "קובץ טקסט חדש…", + "files_upload_files": "העלה קבצים…", + "files_upload_folder": "העלה תיקייה…", + "files_share": "שתף…", + "files_share_button": "שתף", + "contacts_add_to_group": "הוסף לקבוצה…", + "contacts_bulk_add_to_group": "הוסף לקבוצה…", + "schedule_send": "תזמן שליחה…" + }, "notification_center": { "title": "התראות", "mark_all_read": "סימון הכול כנקרא", diff --git a/locales/hu/common.json b/locales/hu/common.json index 172fa6df6..8b6bf498e 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -6455,6 +6455,19 @@ "group_account": "Fiók", "switch_to": "Váltás erre: {account}" }, + "menuAudit": { + "offline_header": "Nincs kapcsolat. Néhány művelethez kapcsolat kell.", + "files_new_folder": "Új mappa…", + "files_new_file": "Új fájl…", + "files_new_text_file": "Új szöveges fájl…", + "files_upload_files": "Fájlok feltöltése…", + "files_upload_folder": "Mappa feltöltése…", + "files_share": "Megosztás…", + "files_share_button": "Megosztás", + "contacts_add_to_group": "Hozzáadás a csoporthoz…", + "contacts_bulk_add_to_group": "Hozzáadás a csoporthoz…", + "schedule_send": "Késleltetett küldés…" + }, "notification_center": { "title": "Értesítések", "mark_all_read": "Összes megjelölése olvasottként", diff --git a/locales/it/common.json b/locales/it/common.json index 71f13cca0..27ad83884 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -6455,6 +6455,19 @@ "group_account": "Account", "switch_to": "Passa a {account}" }, + "menuAudit": { + "offline_header": "Sei offline. Alcune azioni richiedono una connessione.", + "files_new_folder": "Nuova cartella…", + "files_new_file": "Nuovo file…", + "files_new_text_file": "Nuovo file di testo…", + "files_upload_files": "Carica file…", + "files_upload_folder": "Carica cartella…", + "files_share": "Condividi…", + "files_share_button": "Condividi", + "contacts_add_to_group": "Aggiungi al gruppo…", + "contacts_bulk_add_to_group": "Aggiungi al gruppo…", + "schedule_send": "Programma invio…" + }, "notification_center": { "title": "Notifiche", "mark_all_read": "Segna tutte come lette", diff --git a/locales/ja/common.json b/locales/ja/common.json index 7d382e843..117732b7c 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -6455,6 +6455,19 @@ "group_account": "アカウント", "switch_to": "{account} に切り替え" }, + "menuAudit": { + "offline_header": "オフラインです。一部の操作には接続が必要です。", + "files_new_folder": "新しいフォルダー…", + "files_new_file": "新規ファイル…", + "files_new_text_file": "新規テキストファイル…", + "files_upload_files": "ファイルをアップロード…", + "files_upload_folder": "フォルダをアップロード…", + "files_share": "共有…", + "files_share_button": "共有", + "contacts_add_to_group": "グループに追加…", + "contacts_bulk_add_to_group": "グループに追加…", + "schedule_send": "送信を予約…" + }, "notification_center": { "title": "通知", "mark_all_read": "すべて既読にする", diff --git a/locales/ko/common.json b/locales/ko/common.json index 9f9643ad0..1cf33acbd 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -6455,6 +6455,19 @@ "group_account": "계정", "switch_to": "{account}(으)로 전환" }, + "menuAudit": { + "offline_header": "오프라인 상태입니다. 일부 작업에는 연결이 필요합니다.", + "files_new_folder": "새 폴더…", + "files_new_file": "새 파일…", + "files_new_text_file": "새 텍스트 파일…", + "files_upload_files": "파일 업로드…", + "files_upload_folder": "폴더 업로드…", + "files_share": "공유…", + "files_share_button": "공유", + "contacts_add_to_group": "그룹에 추가…", + "contacts_bulk_add_to_group": "그룹에 추가…", + "schedule_send": "보내기 예약…" + }, "notification_center": { "title": "알림", "mark_all_read": "모두 읽음으로 표시", diff --git a/locales/lv/common.json b/locales/lv/common.json index a4bc8ef83..47903d6f7 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -6455,6 +6455,19 @@ "group_account": "Konts", "switch_to": "Pārslēgties uz {account}" }, + "menuAudit": { + "offline_header": "Jūs esat bezsaistē. Dažām darbībām vajag savienojumu.", + "files_new_folder": "Jauna mape…", + "files_new_file": "Jauns fails…", + "files_new_text_file": "Jauns teksta fails…", + "files_upload_files": "Augšupielādēt failus…", + "files_upload_folder": "Augšupielādēt mapi…", + "files_share": "Kopīgot…", + "files_share_button": "Kopīgot", + "contacts_add_to_group": "Pievienot grupai…", + "contacts_bulk_add_to_group": "Pievienot grupai…", + "schedule_send": "Ieplānot sūtīšanu…" + }, "notification_center": { "title": "Paziņojumi", "mark_all_read": "Atzīmēt visus kā lasītus", diff --git a/locales/mn/common.json b/locales/mn/common.json index 152574c4e..4bf1e40a8 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -6455,6 +6455,19 @@ "group_account": "Бүртгэл", "switch_to": "{account} руу шилжих" }, + "menuAudit": { + "offline_header": "Та офлайн байна. Зарим үйлдэлд холболт хэрэгтэй.", + "files_new_folder": "Шинэ хавтас…", + "files_new_file": "Шинэ файл…", + "files_new_text_file": "Шинэ текст файл…", + "files_upload_files": "Файл байршуулах…", + "files_upload_folder": "Хавтас байршуулах…", + "files_share": "Хуваалцах…", + "files_share_button": "Хуваалцах", + "contacts_add_to_group": "Бүлэгт нэмэх…", + "contacts_bulk_add_to_group": "Бүлэгт нэмэх…", + "schedule_send": "Хуваарь илгээх…" + }, "notification_center": { "title": "Мэдэгдэл", "mark_all_read": "Бүгдийг уншсан болгох", diff --git a/locales/nb/common.json b/locales/nb/common.json index 9a6c14f76..1cadf8136 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -6455,6 +6455,19 @@ "group_account": "Konto", "switch_to": "Bytt til {account}" }, + "menuAudit": { + "offline_header": "Du er frakoblet. Noen handlinger trenger tilkobling.", + "files_new_folder": "Ny mappe…", + "files_new_file": "Ny fil…", + "files_new_text_file": "Ny tekstfil…", + "files_upload_files": "Last opp filer…", + "files_upload_folder": "Last opp mappe…", + "files_share": "Del…", + "files_share_button": "Del", + "contacts_add_to_group": "Legg til i gruppe…", + "contacts_bulk_add_to_group": "Legg til i gruppe…", + "schedule_send": "Planlegg sending…" + }, "notification_center": { "title": "Varsler", "mark_all_read": "Merk alle som lest", diff --git a/locales/nl/common.json b/locales/nl/common.json index 3c57a4007..04129a79b 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -6455,6 +6455,19 @@ "group_account": "Account", "switch_to": "Overschakelen naar {account}" }, + "menuAudit": { + "offline_header": "U bent offline. Sommige acties hebben een verbinding nodig.", + "files_new_folder": "Nieuwe map…", + "files_new_file": "Nieuw bestand…", + "files_new_text_file": "Nieuw tekstbestand…", + "files_upload_files": "Bestanden uploaden…", + "files_upload_folder": "Map uploaden…", + "files_share": "Delen…", + "files_share_button": "Delen", + "contacts_add_to_group": "Aan groep toevoegen…", + "contacts_bulk_add_to_group": "Aan groep toevoegen…", + "schedule_send": "Verzenden plannen…" + }, "notification_center": { "title": "Meldingen", "mark_all_read": "Alles als gelezen markeren", diff --git a/locales/pl/common.json b/locales/pl/common.json index 20bd079ae..50277339d 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -6455,6 +6455,19 @@ "group_account": "Konto", "switch_to": "Przełącz na {account}" }, + "menuAudit": { + "offline_header": "Jesteś offline. Niektóre działania wymagają połączenia.", + "files_new_folder": "Nowy folder…", + "files_new_file": "Nowy plik…", + "files_new_text_file": "Nowy plik tekstowy…", + "files_upload_files": "Prześlij pliki…", + "files_upload_folder": "Prześlij folder…", + "files_share": "Udostępnij…", + "files_share_button": "Udostępnij", + "contacts_add_to_group": "Dodaj do grupy…", + "contacts_bulk_add_to_group": "Dodaj do grupy…", + "schedule_send": "Zaplanuj wysyłkę…" + }, "notification_center": { "title": "Powiadomienia", "mark_all_read": "Oznacz wszystkie jako przeczytane", diff --git a/locales/pt/common.json b/locales/pt/common.json index 2452edf4a..d53551a93 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -6455,6 +6455,19 @@ "group_account": "Conta", "switch_to": "Mudar para {account}" }, + "menuAudit": { + "offline_header": "Está offline. Algumas ações precisam de ligação.", + "files_new_folder": "Nova pasta…", + "files_new_file": "Novo arquivo…", + "files_new_text_file": "Novo arquivo de texto…", + "files_upload_files": "Carregar arquivos…", + "files_upload_folder": "Carregar pasta…", + "files_share": "Compartilhar…", + "files_share_button": "Compartilhar", + "contacts_add_to_group": "Adicionar ao grupo…", + "contacts_bulk_add_to_group": "Adicionar ao grupo…", + "schedule_send": "Agendar envio…" + }, "notification_center": { "title": "Notificações", "mark_all_read": "Marcar todas como lidas", diff --git a/locales/ro/common.json b/locales/ro/common.json index 03bfef66a..f85219224 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -6455,6 +6455,19 @@ "group_account": "Cont", "switch_to": "Comută la {account}" }, + "menuAudit": { + "offline_header": "Sunteți offline. Unele acțiuni au nevoie de conexiune.", + "files_new_folder": "Dosar nou…", + "files_new_file": "Fișier nou…", + "files_new_text_file": "Fișier text nou…", + "files_upload_files": "Încărcați fișiere…", + "files_upload_folder": "Încărcare folder…", + "files_share": "Distribuie…", + "files_share_button": "Distribuie", + "contacts_add_to_group": "Adaugă la grup…", + "contacts_bulk_add_to_group": "Adaugă la grup…", + "schedule_send": "Programează trimiterea…" + }, "notification_center": { "title": "Notificări", "mark_all_read": "Marchează toate ca citite", diff --git a/locales/ru/common.json b/locales/ru/common.json index ebb25a96f..466ad5068 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -6455,6 +6455,19 @@ "group_account": "Аккаунт", "switch_to": "Переключиться на {account}" }, + "menuAudit": { + "offline_header": "Нет подключения. Для некоторых действий нужна сеть.", + "files_new_folder": "Новая папка…", + "files_new_file": "Новый файл…", + "files_new_text_file": "Новый текстовый файл…", + "files_upload_files": "Загрузить файлы…", + "files_upload_folder": "Загрузить папку…", + "files_share": "Поделиться…", + "files_share_button": "Поделиться", + "contacts_add_to_group": "Добавить в группу…", + "contacts_bulk_add_to_group": "Добавить в группу…", + "schedule_send": "Запланировать отправку…" + }, "notification_center": { "title": "Уведомления", "mark_all_read": "Отметить все как прочитанные", diff --git a/locales/sk/common.json b/locales/sk/common.json index 43343c99e..bccc84b78 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -6455,6 +6455,19 @@ "group_account": "Účet", "switch_to": "Prepnúť na {account}" }, + "menuAudit": { + "offline_header": "Ste offline. Niektoré akcie potrebujú pripojenie.", + "files_new_folder": "Nový priečinok…", + "files_new_file": "Nový súbor…", + "files_new_text_file": "Nový textový súbor…", + "files_upload_files": "Nahrať súbory…", + "files_upload_folder": "Nahrať priečinok…", + "files_share": "Zdieľať…", + "files_share_button": "Zdieľať", + "contacts_add_to_group": "Pridať do skupiny…", + "contacts_bulk_add_to_group": "Pridať do skupiny…", + "schedule_send": "Naplánovať odoslanie…" + }, "notification_center": { "title": "Oznámenia", "mark_all_read": "Označiť všetko ako prečítané", diff --git a/locales/th/common.json b/locales/th/common.json index d4aef2af2..2032b6105 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -98,6 +98,19 @@ "group_account": "บัญชี", "switch_to": "สลับไปที่ {account}" }, + "menuAudit": { + "offline_header": "คุณออฟไลน์อยู่ บางการทำงานต้องใช้การเชื่อมต่อ", + "files_new_folder": "โฟลเดอร์ใหม่…", + "files_new_file": "ไฟล์ใหม่…", + "files_new_text_file": "ไฟล์ข้อความใหม่…", + "files_upload_files": "อัปโหลดไฟล์…", + "files_upload_folder": "อัปโหลดโฟลเดอร์…", + "files_share": "แชร์…", + "files_share_button": "แชร์", + "contacts_add_to_group": "เพิ่มในกลุ่ม…", + "contacts_bulk_add_to_group": "เพิ่มในกลุ่ม…", + "schedule_send": "กำหนดเวลาส่ง…" + }, "notification_center": { "title": "การแจ้งเตือน", "mark_all_read": "ทำเครื่องหมายว่าอ่านแล้วทั้งหมด", diff --git a/locales/tr/common.json b/locales/tr/common.json index 03393f7f0..c7ef9d9b8 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -6455,6 +6455,19 @@ "group_account": "Hesap", "switch_to": "{account} hesabına geç" }, + "menuAudit": { + "offline_header": "Çevrimdışısınız. Bazı işlemler bağlantı gerektirir.", + "files_new_folder": "Yeni Klasör…", + "files_new_file": "Yeni Dosya…", + "files_new_text_file": "Yeni Metin Dosyası…", + "files_upload_files": "Dosya Yükle…", + "files_upload_folder": "Klasör Yükle…", + "files_share": "Paylaş…", + "files_share_button": "Paylaş", + "contacts_add_to_group": "Gruba ekle…", + "contacts_bulk_add_to_group": "Gruba ekle…", + "schedule_send": "Göndermeyi zamanla…" + }, "notification_center": { "title": "Bildirimler", "mark_all_read": "Tümünü okundu işaretle", diff --git a/locales/uk/common.json b/locales/uk/common.json index f033cd1ee..da741e92d 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -6455,6 +6455,19 @@ "group_account": "Обліковий запис", "switch_to": "Перейти до {account}" }, + "menuAudit": { + "offline_header": "Немає з’єднання. Для деяких дій потрібна мережа.", + "files_new_folder": "Нова папка…", + "files_new_file": "Новий файл…", + "files_new_text_file": "Новий текстовий файл…", + "files_upload_files": "Завантажити файли…", + "files_upload_folder": "Завантажити папку…", + "files_share": "Поділитися…", + "files_share_button": "Поділитися", + "contacts_add_to_group": "Додати до групи…", + "contacts_bulk_add_to_group": "Додати до групи…", + "schedule_send": "Запланувати надсилання…" + }, "notification_center": { "title": "Сповіщення", "mark_all_read": "Позначити всі як прочитані", diff --git a/locales/vi/common.json b/locales/vi/common.json index 9faa09bde..6c7be2f27 100644 --- a/locales/vi/common.json +++ b/locales/vi/common.json @@ -6467,6 +6467,19 @@ "group_account": "Tài khoản", "switch_to": "Chuyển sang {account}" }, + "menuAudit": { + "offline_header": "Bạn đang ngoại tuyến. Một số thao tác cần kết nối.", + "files_new_folder": "Thư mục mới…", + "files_new_file": "Tệp mới…", + "files_new_text_file": "Tệp văn bản mới…", + "files_upload_files": "Tải tệp lên…", + "files_upload_folder": "Tải lên thư mục…", + "files_share": "Chia sẻ…", + "files_share_button": "Chia sẻ", + "contacts_add_to_group": "Thêm vào nhóm…", + "contacts_bulk_add_to_group": "Thêm vào nhóm…", + "schedule_send": "Lên lịch gửi…" + }, "notification_center": { "title": "Thông báo", "mark_all_read": "Đánh dấu tất cả đã đọc", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index eed67bf21..74371a3e6 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -6455,6 +6455,19 @@ "group_account": "帳戶", "switch_to": "切換到 {account}" }, + "menuAudit": { + "offline_header": "您已離線。部分操作需要連線。", + "files_new_folder": "新增資料夾…", + "files_new_file": "新增檔案…", + "files_new_text_file": "新增文字檔案…", + "files_upload_files": "上傳檔案…", + "files_upload_folder": "上傳資料夾…", + "files_share": "分享…", + "files_share_button": "分享", + "contacts_add_to_group": "新增至群組…", + "contacts_bulk_add_to_group": "新增至群組…", + "schedule_send": "排程寄送…" + }, "notification_center": { "title": "通知", "mark_all_read": "全部標示為已讀", diff --git a/locales/zh/common.json b/locales/zh/common.json index 0edeb8c81..412f76ce8 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -6455,6 +6455,19 @@ "group_account": "账户", "switch_to": "切换到 {account}" }, + "menuAudit": { + "offline_header": "您已离线。部分操作需要联网。", + "files_new_folder": "新建文件夹…", + "files_new_file": "新建文件…", + "files_new_text_file": "新建文本文件…", + "files_upload_files": "上传文件…", + "files_upload_folder": "上传文件夹…", + "files_share": "共享…", + "files_share_button": "共享", + "contacts_add_to_group": "加入群组…", + "contacts_bulk_add_to_group": "添加到群组…", + "schedule_send": "计划发送…" + }, "notification_center": { "title": "通知", "mark_all_read": "全部标为已读",