diff --git a/app/(main)/[locale]/pro/page.tsx b/app/(main)/[locale]/pro/page.tsx index 6df1960c8..937ccaaea 100644 --- a/app/(main)/[locale]/pro/page.tsx +++ b/app/(main)/[locale]/pro/page.tsx @@ -16,6 +16,9 @@ import { PaneSizeContext } from "@/hooks/use-pane-size"; import { PaneIdContext, ProTabFocusContext } from "@/hooks/use-pane-context"; import { useDeepLinkUrl } from "@/hooks/use-deep-link-url"; import { appPath, buildMailPath } from "@/lib/deep-links"; +import { GlobalSearchPalette } from "@/components/global-search/global-search-palette"; +import { SearchTabBody } from "@/components/global-search/search-tab-body"; +import { useGlobalSearchStore } from "@/stores/global-search-store"; import { ProTabBar } from "@/components/pro/pro-tab-bar"; import { PRO_TAB_DRAG_MIME, @@ -62,6 +65,9 @@ function renderTabBody(tab: ProTab): React.ReactNode { if (tab.kind === 'folder' && tab.folderData) { return ; } + if (tab.kind === 'search' && tab.searchData) { + return ; + } const Component = APP_TAB_COMPONENTS[tab.kind]; return Component ? : null; } @@ -298,8 +304,10 @@ export default function ProHome() { return data.accountId ? `${path}?account=${encodeURIComponent(data.accountId)}` : path; } case 'compose': - // A draft has no permalink - fall back to the shell's own route so - // the bar never shows a message the user is no longer looking at. + case 'search': + // Neither a draft nor a search-results tab has a permalink - fall + // back to the shell's own route so the bar never shows a message the + // user is no longer looking at. return appPath('/pro'); default: return null; @@ -307,6 +315,24 @@ export default function ProHome() { }, [isAuthenticated, client, focusedActiveTab, selectedMailbox, selectedEmailId, mailboxes, accountMailboxes]); useDeepLinkUrl(reflectedPath); + // Global search shortcuts: Ctrl/⌘+K toggles the palette, Ctrl+Shift+F + // opens it (features doc §"Focus global search"). Window-level and keyed on + // event.code so they fire from inside inputs and on non-Latin layouts. + useEffect(() => { + const onSearchShortcut = (event: KeyboardEvent) => { + if (!(event.ctrlKey || event.metaKey) || event.altKey) return; + if (event.code === 'KeyK' && !event.shiftKey) { + event.preventDefault(); + useGlobalSearchStore.getState().togglePalette(); + } else if (event.code === 'KeyF' && event.shiftKey) { + event.preventDefault(); + useGlobalSearchStore.getState().openPalette(); + } + }; + window.addEventListener('keydown', onSearchShortcut); + return () => window.removeEventListener('keydown', onSearchShortcut); + }, []); + const handleRailNavigate = (itemId: 'mail' | 'calendar' | 'contacts' | 'files' | 'settings') => { openTab(itemId); return true; @@ -496,6 +522,7 @@ export default function ProHome() { activeAppId={inlineApp?.id ?? null} onNavigate={handleRailNavigate} activeItemId={railActiveItemId} + onOpenSearch={() => useGlobalSearchStore.getState().openPalette()} /> @@ -526,6 +553,8 @@ export default function ProHome() { )} + + setShowShortcutsModal(false)} diff --git a/components/global-search/__tests__/global-search-palette.test.tsx b/components/global-search/__tests__/global-search-palette.test.tsx new file mode 100644 index 000000000..9a6c2cf69 --- /dev/null +++ b/components/global-search/__tests__/global-search-palette.test.tsx @@ -0,0 +1,158 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { ContactHit, MailHit, SearchOutcome } from '@/lib/global-search/types'; +import { emptyOutcome } from '@/lib/global-search/types'; +import { parseSearchQuery } from '@/lib/global-search/query-parser'; +import { useGlobalSearchStore } from '@/stores/global-search-store'; +import { useProTabStore } from '@/stores/pro-tab-store'; +import { useSearchHistoryStore } from '@/stores/search-history-store'; + +const openHit = vi.fn(); +vi.mock('@/lib/global-search/open-hit', () => ({ openHit: (...args: unknown[]) => openHit(...args) })); + +const searchState: { + outcome: SearchOutcome; + parsed: ReturnType; + isSearching: boolean; + isEmpty: boolean; +} = { + outcome: emptyOutcome(), + parsed: parseSearchQuery(''), + isSearching: false, + isEmpty: true, +}; +vi.mock('@/hooks/use-global-search', () => ({ + useGlobalSearch: () => ({ ...searchState, rerun: vi.fn(), loadMoreMail: vi.fn(), mailLimit: 25 }), +})); + +vi.mock('@/stores/account-store', () => { + const state = { + accounts: [ + { id: 'login-a', label: 'Work', email: 'a@x', isConnected: true }, + { id: 'login-b', label: 'Home', email: 'b@x', isConnected: true }, + ], + }; + const hook = (sel?: (s: typeof state) => unknown) => (typeof sel === 'function' ? sel(state) : state); + hook.getState = () => state; + return { useAccountStore: hook }; +}); + +import { GlobalSearchPalette } from '../global-search-palette'; + +function mailHit(id: string, title: string): MailHit { + return { + kind: 'mail', localAccountId: 'login-a', jmapAccountId: 'j', id, accountLabel: 'Work', + title, subtitle: 'Inbox', date: '2026-01-01T00:00:00Z', source: 'remote', + email: { id } as MailHit['email'], snippet: null, + }; +} + +function contactHit(id: string, title: string): ContactHit { + return { + kind: 'contacts', localAccountId: 'login-a', jmapAccountId: 'j', id, accountLabel: 'Work', + title, subtitle: 'Personal', date: null, source: 'remote', + contact: { id } as ContactHit['contact'], storeId: `login-a::${id}`, + }; +} + +describe('GlobalSearchPalette', () => { + beforeEach(() => { + openHit.mockReset(); + useGlobalSearchStore.setState({ isOpen: true, query: '', scope: 'all', accountId: null }); + useSearchHistoryStore.setState({ recentSearches: [] }); + searchState.outcome = emptyOutcome(); + searchState.parsed = parseSearchQuery(''); + searchState.isSearching = false; + searchState.isEmpty = true; + }); + + it('renders nothing while closed', () => { + useGlobalSearchStore.setState({ isOpen: false }); + render(); + expect(screen.queryByRole('dialog')).toBeNull(); + }); + + it('shows recent searches for an empty query and re-fills the input from one', () => { + useSearchHistoryStore.setState({ recentSearches: ['zebra report'] }); + render(); + fireEvent.click(screen.getByText('zebra report')); + expect(useGlobalSearchStore.getState().query).toBe('zebra report'); + }); + + it('groups hits by kind and opens a clicked row', () => { + useGlobalSearchStore.setState({ query: 'zeb' }); + searchState.parsed = parseSearchQuery('zeb'); + searchState.isEmpty = false; + const outcome = emptyOutcome(); + outcome.hits.mail = [mailHit('m1', 'Zebra subject')]; + outcome.hits.contacts = [contactHit('c1', 'Zebra Person')]; + outcome.status.mail.status = 'done'; + outcome.status.contacts.status = 'done'; + searchState.outcome = outcome; + render(); + + expect(screen.getByText('Zebra subject')).toBeInTheDocument(); + expect(screen.getByText('Zebra Person')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Zebra Person')); + expect(openHit).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'contacts', id: 'c1', storeId: 'login-a::c1' }), + expect.objectContaining({ proShell: true }), + ); + expect(useGlobalSearchStore.getState().isOpen).toBe(false); + expect(useSearchHistoryStore.getState().recentSearches).toContain('zeb'); + }); + + it('shows a per-account error row', () => { + useGlobalSearchStore.setState({ query: 'zeb' }); + searchState.parsed = parseSearchQuery('zeb'); + searchState.isEmpty = false; + const outcome = emptyOutcome(); + outcome.status.mail.status = 'done'; + outcome.status.mail.errors = [{ localAccountId: 'login-b', accountLabel: 'Home', message: 'timeout' }]; + searchState.outcome = outcome; + render(); + expect(screen.getByText('account_error')).toBeInTheDocument(); + }); + + it('Enter in the input opens a search tab with the query', () => { + useGlobalSearchStore.setState({ query: 'zebra report', scope: 'mail' }); + searchState.parsed = parseSearchQuery('zebra report'); + searchState.isEmpty = false; + render(); + const input = screen.getByPlaceholderText('placeholder') as HTMLInputElement; + fireEvent.keyDown(input, { key: 'Enter' }); + const tab = useProTabStore.getState().tabs.find((t) => t.kind === 'search'); + expect(tab?.searchData).toMatchObject({ query: 'zebra report', scope: 'mail', accountId: null }); + expect(useGlobalSearchStore.getState().isOpen).toBe(false); + }); + + it('ArrowDown moves focus from the input into the first result row', () => { + useGlobalSearchStore.setState({ query: 'zeb' }); + searchState.parsed = parseSearchQuery('zeb'); + searchState.isEmpty = false; + const outcome = emptyOutcome(); + outcome.hits.mail = [mailHit('m1', 'Zebra subject'), mailHit('m2', 'Second')]; + outcome.status.mail.status = 'done'; + searchState.outcome = outcome; + render(); + const dialog = screen.getByRole('dialog'); + fireEvent.keyDown(dialog, { key: 'ArrowDown' }); + expect((document.activeElement as HTMLElement).textContent).toContain('Zebra subject'); + fireEvent.keyDown(dialog, { key: 'ArrowDown' }); + expect((document.activeElement as HTMLElement).textContent).toContain('Second'); + }); + + it('Escape closes the palette', () => { + render(); + fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' }); + expect(useGlobalSearchStore.getState().isOpen).toBe(false); + }); + + it('scope chips update the store scope', () => { + render(); + fireEvent.click(screen.getByRole('radio', { name: 'scope_files' })); + expect(useGlobalSearchStore.getState().scope).toBe('files'); + }); +}); diff --git a/components/global-search/global-search-palette.tsx b/components/global-search/global-search-palette.tsx new file mode 100644 index 000000000..8b7f9e029 --- /dev/null +++ b/components/global-search/global-search-palette.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { useEffect, useMemo, useRef } from "react"; +import { AlertTriangle, Clock3, CornerDownLeft, Loader2, Search } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "@/i18n/navigation"; +import { useGlobalSearch } from "@/hooks/use-global-search"; +import { useMenuNavigation } from "@/hooks/use-menu-navigation"; +import { openHit } from "@/lib/global-search/open-hit"; +import { SEARCH_KINDS, type GlobalSearchHit, type SearchKind } from "@/lib/global-search/types"; +import { useAccountStore } from "@/stores/account-store"; +import { useGlobalSearchStore } from "@/stores/global-search-store"; +import { useProTabStore } from "@/stores/pro-tab-store"; +import { useSearchHistoryStore } from "@/stores/search-history-store"; +import { PaletteResultRow } from "@/components/global-search/palette-result-row"; +import { SearchScopeChips } from "@/components/global-search/search-scope-chips"; + +export const PALETTE_LOCAL_LIMIT = 10; +export const PALETTE_REMOTE_LIMIT = 25; +const MAX_ROWS_PER_KIND = 5; + +/** + * The "search everything" quick palette (#641): one query across mail, + * contacts, calendar and files of every logged-in account. Local cache hits + * appear per keystroke; server hits merge in per kind as each login answers. + * Enter on a row opens it in the right account; Enter in the input opens the + * full search tab. + */ +export function GlobalSearchPalette({ proShell = true }: { proShell?: boolean }) { + const t = useTranslations('global_search'); + const router = useRouter(); + + const isOpen = useGlobalSearchStore((s) => s.isOpen); + const query = useGlobalSearchStore((s) => s.query); + const scope = useGlobalSearchStore((s) => s.scope); + const accountId = useGlobalSearchStore((s) => s.accountId); + const closePalette = useGlobalSearchStore((s) => s.closePalette); + const setQuery = useGlobalSearchStore((s) => s.setQuery); + const setScope = useGlobalSearchStore((s) => s.setScope); + const setAccountId = useGlobalSearchStore((s) => s.setAccountId); + + const accounts = useAccountStore((s) => s.accounts); + const accountOptions = useMemo( + () => accounts.filter((a) => a.isConnected).map((a) => ({ localAccountId: a.id, label: a.label || a.email })), + [accounts], + ); + + const recentSearches = useSearchHistoryStore((s) => s.recentSearches); + const addRecentSearch = useSearchHistoryStore((s) => s.addRecentSearch); + + const { outcome, parsed, isSearching } = useGlobalSearch({ + query, scope, accountId, + localLimit: PALETTE_LOCAL_LIMIT, + remoteLimit: PALETTE_REMOTE_LIMIT, + enabled: isOpen, + }); + + const inputRef = useRef(null); + const { menuRef, onKeyDown } = useMenuNavigation({ open: isOpen, onClose: closePalette, autoFocus: false }); + + useEffect(() => { + if (!isOpen) return; + const frame = requestAnimationFrame(() => inputRef.current?.focus()); + return () => cancelAnimationFrame(frame); + }, [isOpen]); + + if (!isOpen) return null; + + const trimmed = query.trim(); + + const submit = () => { + if (!trimmed) return; + addRecentSearch(trimmed); + if (proShell) { + useProTabStore.getState().openSearchTab({ query: trimmed, scope, accountId, title: trimmed }); + } + closePalette(); + }; + + const handleOpenHit = (hit: GlobalSearchHit) => { + if (trimmed) addRecentSearch(trimmed); + closePalette(); + void openHit(hit, { proShell, navigate: (path) => router.push(path) }); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + const inInput = event.target === inputRef.current; + if (inInput) { + if (event.key === 'Enter') { + event.preventDefault(); + submit(); + return; + } + // Leave text-cursor keys to the input; ArrowDown moves into the list. + if (event.key === 'Home' || event.key === 'End' || event.key === 'ArrowUp' || event.key === 'Tab') { + if (event.key === 'Tab') return; // allow tabbing to the chips + return; + } + } + onKeyDown(event as unknown as KeyboardEvent); + }; + + const kindsToShow = SEARCH_KINDS.filter((kind) => parsed.scope === 'all' || parsed.scope === kind); + const hasAnyHit = kindsToShow.some((kind) => outcome.hits[kind].length > 0); + const showRecent = !trimmed && recentSearches.length > 0; + + const renderGroup = (kind: SearchKind) => { + const hits = outcome.hits[kind]; + const status = outcome.status[kind]; + if (hits.length === 0 && status.errors.length === 0 && status.status !== 'loading') return null; + return ( +
+
+ {t(`scope_${kind}`)} + {status.status === 'loading' && } +
+ {hits.slice(0, MAX_ROWS_PER_KIND).map((hit) => ( + + ))} + {status.errors.map((error) => ( +
+ + {t('account_error', { account: error.accountLabel })} +
+ ))} +
+ ); + }; + + return ( +
{ if (e.target === e.currentTarget) closePalette(); }} + > +
+
+ + setQuery(e.target.value)} + placeholder={t('placeholder')} + aria-label={t('title')} + className="flex-1 min-w-0 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none" + /> + {isSearching && } +
+ +
+ +
+ +
+ {showRecent && ( +
+
{t('recent_searches')}
+ {recentSearches.map((term) => ( + + ))} +
+ )} + {!trimmed && !showRecent && ( +

{t('empty_hint')}

+ )} + {trimmed && kindsToShow.map(renderGroup)} + {trimmed && !hasAnyHit && !isSearching && ( +
+

{t('no_results', { query: trimmed })}

+

{t('server_hint')}

+
+ )} +
+ + {proShell && trimmed && ( + + )} +
+
+ ); +} diff --git a/components/global-search/palette-result-row.tsx b/components/global-search/palette-result-row.tsx new file mode 100644 index 000000000..d85b2b61d --- /dev/null +++ b/components/global-search/palette-result-row.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { BookUser, CalendarDays, FileText, Folder, Mail, Repeat } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { parseSearchSnippet, type SnippetSegment } from "@/lib/search-snippet"; +import type { GlobalSearchHit } from "@/lib/global-search/types"; +import { cn } from "@/lib/utils"; + +function HitIcon({ hit }: { hit: GlobalSearchHit }) { + const className = "w-4 h-4 shrink-0 text-muted-foreground"; + switch (hit.kind) { + case 'mail': return ; + case 'contacts': return ; + case 'calendar': return ; + case 'files': return hit.isFolder ? : ; + } +} + +function Snippet({ value }: { value: string }) { + const segments: SnippetSegment[] = parseSearchSnippet(value); + return ( + + {segments.map((segment, i) => segment.marked + ? {segment.text} + : {segment.text})} + + ); +} + +function formatHitDate(iso: string): string { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return ''; + const sameYear = date.getFullYear() === new Date().getFullYear(); + return new Intl.DateTimeFormat(undefined, sameYear ? { month: 'short', day: 'numeric' } : { year: 'numeric', month: 'short', day: 'numeric' }).format(date); +} + +export interface PaletteResultRowProps { + hit: GlobalSearchHit; + onOpen: (hit: GlobalSearchHit) => void; + className?: string; +} + +/** + * One palette hit: icon, title, account · context subtitle, date; the server + * snippet with its ``ed terms when mail search returned one. + */ +export function PaletteResultRow({ hit, onOpen, className }: PaletteResultRowProps) { + const t = useTranslations('global_search'); + const title = hit.title || (hit.kind === 'mail' ? t('no_subject') : hit.id); + const subtitle = [hit.accountLabel, hit.subtitle].filter(Boolean).join(' · '); + const snippet = hit.kind === 'mail' && hit.snippet?.preview ? hit.snippet.preview : null; + return ( + + ); +} diff --git a/components/global-search/search-facets.tsx b/components/global-search/search-facets.tsx new file mode 100644 index 000000000..a9ab2e9b5 --- /dev/null +++ b/components/global-search/search-facets.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import type { SearchScope } from "@/lib/global-search/query-parser"; +import { SEARCH_KINDS, type SearchKind } from "@/lib/global-search/types"; +import { cn } from "@/lib/utils"; + +export interface SearchFacetsProps { + scope: SearchScope; + onScopeChange: (scope: SearchScope) => void; + kindCounts: Record; + /** '+' suffix on the count (the server reported more than was fetched). */ + kindHasMore: Record; + accounts: Array<{ localAccountId: string; label: string; count: number }>; + accountId: string | null; + onAccountChange: (accountId: string | null) => void; + flat: boolean; + onFlatChange: (flat: boolean) => void; +} + +function FacetButton({ active, label, count, onClick }: { active: boolean; label: string; count?: string; onClick: () => void }) { + return ( + + ); +} + +/** Facet column of the search tab: result kind, account, grouped/flat order. */ +export function SearchFacets({ + scope, onScopeChange, kindCounts, kindHasMore, + accounts, accountId, onAccountChange, + flat, onFlatChange, +}: SearchFacetsProps) { + const t = useTranslations('global_search'); + const total = SEARCH_KINDS.reduce((sum, kind) => sum + kindCounts[kind], 0); + const anyMore = SEARCH_KINDS.some((kind) => kindHasMore[kind]); + return ( +
+
+

{t('scope_label')}

+ onScopeChange('all')} + /> + {SEARCH_KINDS.map((kind) => ( + onScopeChange(kind)} + /> + ))} +
+ + {accounts.length > 1 && ( +
+

{t('account_label')}

+ onAccountChange(null)} /> + {accounts.map((account) => ( + onAccountChange(account.localAccountId)} + /> + ))} +
+ )} + +
+ onFlatChange(false)} /> + onFlatChange(true)} /> +
+
+ ); +} diff --git a/components/global-search/search-preview.tsx b/components/global-search/search-preview.tsx new file mode 100644 index 000000000..1b176801d --- /dev/null +++ b/components/global-search/search-preview.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { BookUser, CalendarDays, ExternalLink, FileText, Folder, Repeat } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { ProEmailView } from "@/components/pro/pro-email-tab-body"; +import type { CalendarHit, ContactHit, FileHit, GlobalSearchHit } from "@/lib/global-search/types"; +import { getContactDisplayName } from "@/stores/contact-store"; +import { useAuthStore } from "@/stores/auth-store"; +import { formatFileSize } from "@/lib/utils"; + +function formatDateTime(iso: string | null): string { + if (!iso) return ''; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return iso; + return new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: iso.includes('T') ? 'short' : undefined }).format(date); +} + +function OpenButton({ hit, onOpen, label }: { hit: GlobalSearchHit; onOpen: (hit: GlobalSearchHit) => void; label: string }) { + return ( + + ); +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +function ContactPreview({ hit, onOpen, openLabel }: { hit: ContactHit; onOpen: (hit: GlobalSearchHit) => void; openLabel: string }) { + const contact = hit.contact; + const name = getContactDisplayName(contact) || hit.title; + const emails = Object.values(contact.emails ?? {}).map((e) => e.address).filter(Boolean); + const phones = Object.values(contact.phones ?? {}).map((p) => (p as { number?: string }).number).filter(Boolean); + const orgs = Object.values(contact.organizations ?? {}).map((o) => (o as { name?: string }).name).filter(Boolean); + return ( +
+
+
+ +
+

{name}

+

{[hit.accountLabel, hit.subtitle].filter(Boolean).join(' · ')}

+
+
+ +
+
+ {emails.map((email) => {email})} + {phones.map((phone) => {phone})} + {orgs.map((org) => {org})} +
+
+ ); +} + +function CalendarPreview({ hit, onOpen, openLabel, t }: { hit: CalendarHit; onOpen: (hit: GlobalSearchHit) => void; openLabel: string; t: ReturnType }) { + const event = hit.event; + const locations = Object.values(event.locations ?? {}).map((l) => l?.name).filter(Boolean); + const participants = Object.values(event.participants ?? {}) + .map((p) => (p as { name?: string | null; email?: string | null })) + .map((p) => p.name || p.email) + .filter(Boolean); + return ( +
+
+
+ +
+

+ {hit.title} + {hit.isRecurring && } +

+

{[hit.accountLabel, hit.subtitle].filter(Boolean).join(' · ')}

+
+
+ +
+
+ {formatDateTime(event.start)} + {locations.map((location) => {location})} + {event.description && {event.description}} + {participants.length > 0 && {participants.join(', ')}} +
+
+ ); +} + +function FilePreview({ hit, onOpen, openLabel }: { hit: FileHit; onOpen: (hit: GlobalSearchHit) => void; openLabel: string }) { + const Icon = hit.isFolder ? Folder : FileText; + return ( +
+
+
+ +
+

{hit.title}

+

{[hit.accountLabel, hit.folderPath].filter(Boolean).join(' · ')}

+
+
+ +
+
+ {!hit.isFolder && {formatFileSize(hit.node.size)}{hit.node.type ? ` · ${hit.node.type}` : ''}} + {hit.date && {formatDateTime(hit.date)}} +
+
+ ); +} + +export interface SearchPreviewProps { + hit: GlobalSearchHit | null; + onOpen: (hit: GlobalSearchHit) => void; +} + +/** Preview pane of the search tab: full mail view, summary card for the rest. */ +export function SearchPreview({ hit, onOpen }: SearchPreviewProps) { + const t = useTranslations('global_search'); + const getClientForAccount = useAuthStore((s) => s.getClientForAccount); + const activeAccountId = useAuthStore((s) => s.activeAccountId); + + if (!hit) { + return

{t('preview_empty')}

; + } + + if (hit.kind === 'mail') { + const clientOverride = hit.localAccountId !== activeAccountId ? getClientForAccount(hit.localAccountId) ?? undefined : undefined; + return ( + + ); + } + const openLabel = t('open_full'); + if (hit.kind === 'contacts') return ; + if (hit.kind === 'calendar') return ; + return ; +} diff --git a/components/global-search/search-result-list.tsx b/components/global-search/search-result-list.tsx new file mode 100644 index 000000000..69baf18d4 --- /dev/null +++ b/components/global-search/search-result-list.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { AlertTriangle, Loader2 } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { compareByRecency } from "@/lib/global-search/rank"; +import { hitKey, SEARCH_KINDS, type GlobalSearchHit, type SearchKind, type SearchOutcome } from "@/lib/global-search/types"; +import { cn } from "@/lib/utils"; +import { PaletteResultRow } from "@/components/global-search/palette-result-row"; + +export interface SearchResultListProps { + outcome: SearchOutcome; + kinds: readonly SearchKind[]; + /** Flat = one list, newest first; grouped = a section per kind. */ + flat: boolean; + selectedKey: string | null; + onSelect: (hit: GlobalSearchHit) => void; + /** Double-click: open the hit on its full surface. */ + onOpen: (hit: GlobalSearchHit) => void; + onLoadMoreMail: () => void; +} + +function Row({ hit, selected, onSelect, onOpen }: { + hit: GlobalSearchHit; + selected: boolean; + onSelect: (hit: GlobalSearchHit) => void; + onOpen: (hit: GlobalSearchHit) => void; +}) { + return ( +
onOpen(hit)}> + +
+ ); +} + +/** Result list of the search tab. Single click previews, double click opens. */ +export function SearchResultList({ outcome, kinds, flat, selectedKey, onSelect, onOpen, onLoadMoreMail }: SearchResultListProps) { + const t = useTranslations('global_search'); + + const errorRows = (kind: SearchKind) => outcome.status[kind].errors.map((error) => ( +
+ + {t('account_error', { account: error.accountLabel })} +
+ )); + + if (flat) { + const all = kinds.flatMap((kind) => outcome.hits[kind]).sort(compareByRecency); + return ( +
+ {all.map((hit) => ( + + ))} + {SEARCH_KINDS.filter((kind) => kinds.includes(kind)).flatMap((kind) => errorRows(kind))} + {outcome.status.mail.hasMore && kinds.includes('mail') && ( + + )} +
+ ); + } + + return ( +
+ {kinds.map((kind) => { + const hits = outcome.hits[kind]; + const status = outcome.status[kind]; + if (hits.length === 0 && status.errors.length === 0 && status.status !== 'loading') return null; + return ( +
+

+ {t(`scope_${kind}`)} + {hits.length}{status.hasMore ? '+' : ''} + {status.status === 'loading' && } +

+ {hits.map((hit) => ( + + ))} + {errorRows(kind)} + {kind === 'mail' && status.hasMore && ( + + )} +
+ ); + })} +
+ ); +} diff --git a/components/global-search/search-scope-chips.tsx b/components/global-search/search-scope-chips.tsx new file mode 100644 index 000000000..72a6f91a4 --- /dev/null +++ b/components/global-search/search-scope-chips.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import type { SearchScope } from "@/lib/global-search/query-parser"; +import { cn } from "@/lib/utils"; + +export interface ScopeAccountOption { + localAccountId: string; + label: string; +} + +export interface SearchScopeChipsProps { + scope: SearchScope; + onScopeChange: (scope: SearchScope) => void; + accounts: ScopeAccountOption[]; + accountId: string | null; + onAccountChange: (accountId: string | null) => void; +} + +const SCOPES: readonly SearchScope[] = ['all', 'mail', 'contacts', 'calendar', 'files']; + +/** + * Scope + account chips of the global search palette. They are sugar over the + * `in:` / `account:` query operators - an explicit operator in the query wins. + */ +export function SearchScopeChips({ scope, onScopeChange, accounts, accountId, onAccountChange }: SearchScopeChipsProps) { + const t = useTranslations('global_search'); + return ( +
+
+ {SCOPES.map((option) => ( + + ))} +
+ {accounts.length > 1 && ( + + )} +
+ ); +} diff --git a/components/global-search/search-tab-body.tsx b/components/global-search/search-tab-body.tsx new file mode 100644 index 000000000..94b97a46b --- /dev/null +++ b/components/global-search/search-tab-body.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Loader2, Search } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useGlobalSearch } from "@/hooks/use-global-search"; +import { openHit } from "@/lib/global-search/open-hit"; +import type { SearchScope } from "@/lib/global-search/query-parser"; +import { hitKey, SEARCH_KINDS, type GlobalSearchHit, type SearchKind } from "@/lib/global-search/types"; +import { useAccountStore } from "@/stores/account-store"; +import { useProTabStore, type ProSearchTabData } from "@/stores/pro-tab-store"; +import { useSearchHistoryStore } from "@/stores/search-history-store"; +import { SearchFacets } from "@/components/global-search/search-facets"; +import { SearchPreview } from "@/components/global-search/search-preview"; +import { SearchResultList } from "@/components/global-search/search-result-list"; + +export const SEARCH_TAB_REMOTE_LIMIT = 100; +export const SEARCH_TAB_LOCAL_LIMIT = 50; + +export interface SearchTabBodyProps { + tabId: string; + data: ProSearchTabData; +} + +/** + * The full search-results tab (plan §3.2): facets · grouped result list · + * preview pane. The submitted query lives on the tab (it survives reloads); + * editing it here re-runs the search and renames the tab. Folder navigation + * in a mail tab never touches this tab - the Pro-side answer to #923. + */ +export function SearchTabBody({ tabId, data }: SearchTabBodyProps) { + const t = useTranslations('global_search'); + const updateSearchTab = useProTabStore((s) => s.updateSearchTab); + const addRecentSearch = useSearchHistoryStore((s) => s.addRecentSearch); + + const [draft, setDraft] = useState(data.query); + useEffect(() => setDraft(data.query), [data.query]); + const [flat, setFlat] = useState(false); + const [selected, setSelected] = useState(null); + + const { outcome, parsed, isSearching, loadMoreMail } = useGlobalSearch({ + query: data.query, + scope: data.scope, + accountId: data.accountId, + localLimit: SEARCH_TAB_LOCAL_LIMIT, + remoteLimit: SEARCH_TAB_REMOTE_LIMIT, + debounceMs: 50, + }); + + // A new query invalidates the previewed hit. + useEffect(() => setSelected(null), [data.query, data.scope, data.accountId]); + + const accounts = useAccountStore((s) => s.accounts); + const accountFacets = useMemo(() => { + const counts = new Map(); + for (const kind of SEARCH_KINDS) { + for (const hit of outcome.hits[kind]) { + counts.set(hit.localAccountId, (counts.get(hit.localAccountId) ?? 0) + 1); + } + } + return accounts + .filter((a) => a.isConnected) + .map((a) => ({ localAccountId: a.id, label: a.label || a.email, count: counts.get(a.id) ?? 0 })); + }, [accounts, outcome]); + + const kindCounts = useMemo(() => { + const counts = {} as Record; + const more = {} as Record; + for (const kind of SEARCH_KINDS) { + counts[kind] = outcome.hits[kind].length; + more[kind] = outcome.status[kind].hasMore; + } + return { counts, more }; + }, [outcome]); + + const kinds = useMemo( + () => SEARCH_KINDS.filter((kind) => parsed.scope === 'all' || parsed.scope === kind), + [parsed.scope], + ); + + const submitDraft = () => { + const query = draft.trim(); + if (!query || query === data.query) return; + addRecentSearch(query); + updateSearchTab(tabId, { query, title: query }); + }; + + const setScope = (scope: SearchScope) => updateSearchTab(tabId, { scope }); + const setAccountId = (accountId: string | null) => updateSearchTab(tabId, { accountId }); + const handleOpen = (hit: GlobalSearchHit) => { void openHit(hit, { proShell: true }); }; + + return ( +
+ + +
+
+ + setDraft(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') submitDraft(); }} + onBlur={submitDraft} + aria-label={t('title')} + placeholder={t('placeholder')} + className="flex-1 min-w-0 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none" + /> + {isSearching && } +
+
+ + {kinds.every((kind) => outcome.hits[kind].length === 0) && !isSearching && ( +
+

{t('no_results', { query: data.query })}

+

{t('server_hint')}

+
+ )} +
+
+ + +
+ ); +} diff --git a/components/layout/navigation-rail.tsx b/components/layout/navigation-rail.tsx index 44ce83e0c..793d58362 100644 --- a/components/layout/navigation-rail.tsx +++ b/components/layout/navigation-rail.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useLayoutEffect, useCallback } from "react"; import { createPortal } from "react-dom"; -import { Mail, Calendar, BookUser, HardDrive, Settings, Keyboard, Plus, Shield, LogOut, Check } from "lucide-react"; +import { Mail, Calendar, BookUser, HardDrive, Settings, Keyboard, Plus, Shield, LogOut, Check, Search } from "lucide-react"; import { AccountSwitcher } from "./account-switcher"; import { icons as lucideIcons, type LucideIcon } from "lucide-react"; import { useConfig } from "@/hooks/use-config"; @@ -61,6 +61,11 @@ interface NavigationRailProps { * active app). */ activeItemId?: 'mail' | 'calendar' | 'contacts' | 'files' | 'settings' | null; + /** + * Pro shell only: renders a Search entry that opens the global search + * palette (#641) instead of navigating anywhere. + */ + onOpenSearch?: () => void; } function StorageQuotaCircle({ quota, usagePercent }: { quota: { used: number; total: number }; usagePercent: number }) { @@ -192,8 +197,10 @@ export function NavigationRail({ activeAppId, onNavigate, activeItemId, + onOpenSearch, }: NavigationRailProps) { const t = useTranslations("sidebar"); + const tGlobalSearch = useTranslations("global_search"); const pathname = usePathname(); const router = useRouter(); const { appLogoLightUrl, appLogoDarkUrl } = useConfig(); @@ -504,6 +511,24 @@ export function NavigationRail({ role="navigation" aria-label={t("nav_label")} > + {onOpenSearch && ( + + )} {visibleItems.map((item) => { const isActive = getIsActive(item.href, item.id); const Icon = item.icon; diff --git a/hooks/use-keyboard-shortcuts.ts b/hooks/use-keyboard-shortcuts.ts index a648a5113..7100d4868 100644 --- a/hooks/use-keyboard-shortcuts.ts +++ b/hooks/use-keyboard-shortcuts.ts @@ -295,6 +295,7 @@ export const KEYBOARD_SHORTCUTS = { global: [ { key: "c", description: "shortcuts.global.compose" }, { key: "/", description: "shortcuts.global.search" }, + { key: "Ctrl/⌘ + K", description: "shortcuts.global.global_search" }, { key: "?", description: "shortcuts.global.help" }, { key: "Shift + G", description: "shortcuts.global.refresh" }, { key: "Ctrl + A", description: "shortcuts.global.select_all" }, diff --git a/locales/ar/common.json b/locales/ar/common.json index 16370f78e..b07f4f798 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -2103,7 +2103,8 @@ "too_long": "يجب ألا يتجاوز اسم القالب 200 حرف" } }, - "files": { "display": { + "files": { + "display": { "title": "العرض", "description": "إعداد كيفية عرض الملفات والمجلدات" }, @@ -2275,7 +2276,8 @@ "search": "التركيز على البحث", "help": "إظهار الاختصارات", "refresh": "تحديث الرسائل", - "select_all": "تحديد الكل" + "select_all": "تحديد الكل", + "global_search": "البحث في كل شيء" }, "threads": { "expand_collapse": "توسيع/طي المحادثة" @@ -3265,10 +3267,10 @@ "hint": "انقر على أي رسالة على اليسار للبدء، أو خذ الجولة أدناه." }, "files": { - "office_edit": "تحرير المستند", - "office_loading": "جارٍ فتح محرر المستندات…", - "office_error": "تعذر فتح محرر المستندات.", - "office_close": "إغلاق المحرر", + "office_edit": "تحرير المستند", + "office_loading": "جارٍ فتح محرر المستندات…", + "office_error": "تعذر فتح محرر المستندات.", + "office_close": "إغلاق المحرر", "open_in_new_tab": "فتح في علامة تبويب جديدة", "title": "الملفات", "search_placeholder": "بحث في الملفات...", @@ -3555,5 +3557,31 @@ "missing_params": "رمز التفويض أو الحالة مفقود.", "delivery_failed": "تعذّر إرسال نتيجة التفويض إلى تبويب التطبيق. يرجى إغلاق هذه النافذة والمحاولة مرة أخرى." } + }, + "global_search": { + "title": "البحث في كل شيء", + "placeholder": "ابحث في البريد وجهات الاتصال والتقويم والملفات...", + "scope_label": "البحث في", + "account_label": "الحساب", + "scope_all": "الكل", + "scope_mail": "البريد", + "scope_contacts": "جهات الاتصال", + "scope_calendar": "التقويم", + "scope_files": "الملفات", + "all_accounts": "جميع الحسابات", + "show_all": "عرض كل النتائج", + "no_results": "لا توجد نتائج لـ “{query}”", + "server_hint": "الكلمات الكاملة تبحث في الخادم؛ أجزاء الكلمات تطابق العناصر المحمّلة بالفعل", + "searching": "جارٍ البحث...", + "account_error": "تعذر البحث في {account}", + "recent_searches": "عمليات البحث الأخيرة", + "recurring": "متكرر", + "no_subject": "(no subject)", + "empty_hint": "ابحث في البريد وجهات الاتصال والأحداث والملفات في جميع الحسابات. جرّب from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "مسطح حسب التاريخ", + "grouped": "مجمّع", + "load_more": "تحميل المزيد", + "preview_empty": "اختر نتيجة لمعاينتها", + "open_full": "فتح" } } diff --git a/locales/ca/common.json b/locales/ca/common.json index c857c44ab..a5d6318b8 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -2103,7 +2103,8 @@ "too_long": "El nom de la plantilla ha de tenir com a màxim 200 caràcters" } }, - "files": { "display": { + "files": { + "display": { "title": "Visualització", "description": "Configureu com es mostren els fitxers i les carpetes" }, @@ -2275,7 +2276,8 @@ "search": "Enfoca la cerca", "help": "Mostra les dreceres", "refresh": "Actualitza els correus", - "select_all": "Selecciona-ho tot" + "select_all": "Selecciona-ho tot", + "global_search": "Cerca-ho tot" }, "threads": { "expand_collapse": "Desplega/replega el fil" @@ -3265,10 +3267,10 @@ "hint": "Feu clic a qualsevol correu de l'esquerra per començar, o feu la visita guiada de sota." }, "files": { - "office_edit": "Edita el document", - "office_loading": "S'està obrint l'editor de documents…", - "office_error": "No s'ha pogut obrir l'editor de documents.", - "office_close": "Tanca l'editor", + "office_edit": "Edita el document", + "office_loading": "S'està obrint l'editor de documents…", + "office_error": "No s'ha pogut obrir l'editor de documents.", + "office_close": "Tanca l'editor", "open_in_new_tab": "Obre en una pestanya nova", "title": "Fitxers", "search_placeholder": "Cerca fitxers...", @@ -3555,5 +3557,31 @@ "missing_params": "Falta el codi dautorització o lestat.", "delivery_failed": "No sha pogut lliurar el resultat de lautorització a la pestanya de laplicació. Tanqueu aquesta finestra i torneu-ho a provar." } + }, + "global_search": { + "title": "Cerca-ho tot", + "placeholder": "Cerca al correu, contactes, calendari, fitxers...", + "scope_label": "Cerca a", + "account_label": "Compte", + "scope_all": "Tot", + "scope_mail": "Correu", + "scope_contacts": "Contactes", + "scope_calendar": "Calendari", + "scope_files": "Fitxers", + "all_accounts": "Tots els comptes", + "show_all": "Mostra tots els resultats", + "no_results": "Cap resultat per a “{query}”", + "server_hint": "Les paraules completes cerquen al servidor; les parcials coincideixen amb els elements ja carregats", + "searching": "Cercant...", + "account_error": "No s'ha pogut cercar a {account}", + "recent_searches": "Cerques recents", + "recurring": "Periòdic", + "no_subject": "(no subject)", + "empty_hint": "Cerca correu, contactes, esdeveniments i fitxers a tots els comptes. Prova from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Pla per data", + "grouped": "Agrupat", + "load_more": "Carrega'n més", + "preview_empty": "Seleccioneu un resultat per previsualitzar-lo", + "open_full": "Obre" } } diff --git a/locales/cs/common.json b/locales/cs/common.json index b06a34a6c..612070b3c 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2096,7 +2096,8 @@ "too_long": "Název šablony může mít maximálně 200 znaků" } }, - "files": { "display": { + "files": { + "display": { "title": "Zobrazení", "description": "Nakonfigurujte způsob zobrazení souborů a složek" }, @@ -2275,7 +2276,8 @@ "search": "Přejít do hledání", "help": "Zobrazit zkratky", "refresh": "Obnovit zprávy", - "select_all": "Vybrat vše" + "select_all": "Vybrat vše", + "global_search": "Hledat ve všem" }, "threads": { "expand_collapse": "Rozbalit/sbalit vlákno" @@ -3223,10 +3225,10 @@ "hint": "Klikněte na libovolnou zprávu vlevo pro začátek, nebo využijte průvodce níže." }, "files": { - "office_edit": "Upravit dokument", - "office_loading": "Otevírání editoru dokumentů…", - "office_error": "Editor dokumentů se nepodařilo otevřít.", - "office_close": "Zavřít editor", + "office_edit": "Upravit dokument", + "office_loading": "Otevírání editoru dokumentů…", + "office_error": "Editor dokumentů se nepodařilo otevřít.", + "office_close": "Zavřít editor", "open_in_new_tab": "Otevřít na nové kartě", "title": "Soubory", "search_placeholder": "Hledat soubory...", @@ -3555,5 +3557,31 @@ "missing_params": "Chybí autorizační kód nebo stav.", "delivery_failed": "Výsledek autorizace se nepodařilo předat kartě aplikace. Zavřete toto okno a zkuste to znovu." } + }, + "global_search": { + "title": "Hledat ve všem", + "placeholder": "Hledat v poště, kontaktech, kalendáři, souborech...", + "scope_label": "Hledat v", + "account_label": "Účet", + "scope_all": "Vše", + "scope_mail": "Pošta", + "scope_contacts": "Kontakty", + "scope_calendar": "Kalendář", + "scope_files": "Soubory", + "all_accounts": "Všechny účty", + "show_all": "Zobrazit všechny výsledky", + "no_results": "Žádné výsledky pro „{query}“", + "server_hint": "Celá slova prohledávají server; části slov odpovídají již načteným položkám", + "searching": "Hledání...", + "account_error": "Nepodařilo se prohledat {account}", + "recent_searches": "Nedávná hledání", + "recurring": "Opakovaná", + "no_subject": "(no subject)", + "empty_hint": "Hledejte poštu, kontakty, události a soubory ve všech účtech. Zkuste from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Ploše podle data", + "grouped": "Seskupené", + "load_more": "Načíst další", + "preview_empty": "Vyberte výsledek pro náhled", + "open_full": "Otevřít" } } diff --git a/locales/da/common.json b/locales/da/common.json index 283766271..9bbd93191 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2099,7 +2099,8 @@ "too_long": "Skabelonnavn skal være 200 tegn eller mindre" } }, - "files": { "display": { + "files": { + "display": { "title": "Visning", "description": "Konfigurér hvordan filer og mapper vises" }, @@ -2275,7 +2276,8 @@ "search": "Fokusér søgning", "help": "Vis genveje", "refresh": "Opdatér e-mails", - "select_all": "Vælg alle" + "select_all": "Vælg alle", + "global_search": "Søg i alt" }, "threads": { "expand_collapse": "Udvid/skjul tråd" @@ -3265,10 +3267,10 @@ "hint": "Klik på en e-mail til venstre for at komme i gang, eller tag rundvisningen nedenfor." }, "files": { - "office_edit": "Rediger dokument", - "office_loading": "Åbner dokumenteditor…", - "office_error": "Dokumenteditoren kunne ikke åbnes.", - "office_close": "Luk editor", + "office_edit": "Rediger dokument", + "office_loading": "Åbner dokumenteditor…", + "office_error": "Dokumenteditoren kunne ikke åbnes.", + "office_close": "Luk editor", "open_in_new_tab": "Åbn i ny fane", "title": "Filer", "search_placeholder": "Søg efter filer...", @@ -3555,5 +3557,31 @@ "missing_params": "Autorisationskode eller tilstand mangler.", "delivery_failed": "Resultatet af godkendelsen kunne ikke leveres til app-fanen. Luk dette vindue og prøv igen." } + }, + "global_search": { + "title": "Søg i alt", + "placeholder": "Søg i mail, kontakter, kalender, filer...", + "scope_label": "Søg i", + "account_label": "Konto", + "scope_all": "Alt", + "scope_mail": "Mail", + "scope_contacts": "Kontakter", + "scope_calendar": "Kalender", + "scope_files": "Filer", + "all_accounts": "Alle konti", + "show_all": "Vis alle resultater", + "no_results": "Ingen resultater for “{query}”", + "server_hint": "Hele ord søger på serveren; dele af ord matcher allerede indlæste elementer", + "searching": "Søger...", + "account_error": "Kunne ikke søge i {account}", + "recent_searches": "Seneste søgninger", + "recurring": "Tilbagevendende", + "no_subject": "(no subject)", + "empty_hint": "Søg i mail, kontakter, begivenheder og filer på alle konti. Prøv from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Fladt efter dato", + "grouped": "Grupperet", + "load_more": "Indlæs flere", + "preview_empty": "Vælg et resultat for at se det", + "open_full": "Åbn" } } diff --git a/locales/de/common.json b/locales/de/common.json index 9936bab2f..b24ae741a 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2096,7 +2096,8 @@ "too_long": "Vorlagenname darf maximal 200 Zeichen haben" } }, - "files": { "display": { + "files": { + "display": { "title": "Anzeige", "description": "Konfigurieren Sie die Anzeige von Dateien und Ordnern" }, @@ -2275,7 +2276,8 @@ "search": "Suche fokussieren", "help": "Tastaturkürzel anzeigen", "refresh": "E-Mails aktualisieren", - "select_all": "Alle auswählen" + "select_all": "Alle auswählen", + "global_search": "Alles durchsuchen" }, "threads": { "expand_collapse": "Unterhaltung erweitern/einklappen" @@ -3223,10 +3225,10 @@ "hint": "Klicken Sie links auf eine E-Mail, um loszulegen, oder starten Sie die Tour." }, "files": { - "office_edit": "Dokument bearbeiten", - "office_loading": "Dokumenteditor wird geöffnet…", - "office_error": "Der Dokumenteditor konnte nicht geöffnet werden.", - "office_close": "Editor schließen", + "office_edit": "Dokument bearbeiten", + "office_loading": "Dokumenteditor wird geöffnet…", + "office_error": "Der Dokumenteditor konnte nicht geöffnet werden.", + "office_close": "Editor schließen", "open_in_new_tab": "In neuem Tab öffnen", "title": "Dateien", "search_placeholder": "Dateien suchen...", @@ -3555,5 +3557,31 @@ "missing_params": "Autorisierungscode oder State fehlt.", "delivery_failed": "Das Autorisierungsergebnis konnte nicht an den App-Tab übermittelt werden. Bitte schließen Sie dieses Fenster und versuchen Sie es erneut." } + }, + "global_search": { + "title": "Alles durchsuchen", + "placeholder": "Mail, Kontakte, Kalender, Dateien durchsuchen...", + "scope_label": "Suchen in", + "account_label": "Konto", + "scope_all": "Alle", + "scope_mail": "E-Mail", + "scope_contacts": "Kontakte", + "scope_calendar": "Kalender", + "scope_files": "Dateien", + "all_accounts": "Alle Konten", + "show_all": "Alle Ergebnisse anzeigen", + "no_results": "Keine Ergebnisse für „{query}“", + "server_hint": "Ganze Wörter durchsuchen den Server; Wortteile treffen bereits geladene Einträge", + "searching": "Suche läuft...", + "account_error": "{account} konnte nicht durchsucht werden", + "recent_searches": "Letzte Suchen", + "recurring": "Wiederkehrend", + "no_subject": "(no subject)", + "empty_hint": "Mail, Kontakte, Termine und Dateien aller Konten durchsuchen. Zum Beispiel from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Flach nach Datum", + "grouped": "Gruppiert", + "load_more": "Mehr laden", + "preview_empty": "Ergebnis auswählen, um es anzuzeigen", + "open_full": "Öffnen" } } diff --git a/locales/en/common.json b/locales/en/common.json index 8fe1e59e5..8284f8244 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1845,7 +1845,7 @@ }, "refresh_cache": { "label": "Refresh cached data", - "description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.", + "description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions — fixes a stale or wrong view without signing out.", "button": "Refresh" }, "reset_settings": { @@ -2103,7 +2103,8 @@ "too_long": "Template name must be 200 characters or less" } }, - "files": { "display": { + "files": { + "display": { "title": "Display", "description": "Configure how files and folders are displayed" }, @@ -2275,7 +2276,8 @@ "search": "Focus search", "help": "Show shortcuts", "refresh": "Refresh emails", - "select_all": "Select all" + "select_all": "Select all", + "global_search": "Search everything" }, "threads": { "expand_collapse": "Expand/collapse thread" @@ -3265,10 +3267,10 @@ "hint": "Click any email on the left to get started, or take the tour below." }, "files": { - "office_edit": "Edit document", - "office_loading": "Opening document editor…", - "office_error": "The document editor could not be opened.", - "office_close": "Close editor", + "office_edit": "Edit document", + "office_loading": "Opening document editor…", + "office_error": "The document editor could not be opened.", + "office_close": "Close editor", "open_in_new_tab": "Open in new tab", "title": "Files", "search_placeholder": "Search files...", @@ -3555,5 +3557,31 @@ "missing_params": "Missing authorization code or state.", "delivery_failed": "The authorization result could not be delivered to the app tab. Please close this window and try again." } + }, + "global_search": { + "title": "Search everything", + "placeholder": "Search mail, contacts, calendar, files...", + "scope_label": "Search in", + "account_label": "Account", + "scope_all": "All", + "scope_mail": "Mail", + "scope_contacts": "Contacts", + "scope_calendar": "Calendar", + "scope_files": "Files", + "all_accounts": "All accounts", + "show_all": "Show all results", + "no_results": "No results for “{query}”", + "server_hint": "Complete words search the server; partial words match already-loaded items", + "searching": "Searching...", + "account_error": "Couldn't search {account}", + "recent_searches": "Recent searches", + "recurring": "Recurring", + "no_subject": "(no subject)", + "empty_hint": "Search mail, contacts, events and files across all accounts. Try from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Flat by date", + "grouped": "Grouped", + "load_more": "Load more", + "preview_empty": "Select a result to preview it", + "open_full": "Open" } } diff --git a/locales/es/common.json b/locales/es/common.json index af5683f08..391e8b032 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2096,7 +2096,8 @@ "too_long": "El nombre de la plantilla no debe superar los 200 caracteres" } }, - "files": { "display": { + "files": { + "display": { "title": "Visualización", "description": "Configura cómo se muestran los archivos y carpetas" }, @@ -2275,7 +2276,8 @@ "search": "Enfocar búsqueda", "help": "Mostrar atajos", "refresh": "Actualizar correos", - "select_all": "Seleccionar todo" + "select_all": "Seleccionar todo", + "global_search": "Buscar en todo" }, "threads": { "expand_collapse": "Expandir/contraer conversación" @@ -3223,10 +3225,10 @@ "hint": "Haz clic en un correo a la izquierda para empezar, o inicia el tour." }, "files": { - "office_edit": "Editar documento", - "office_loading": "Abriendo el editor de documentos…", - "office_error": "No se pudo abrir el editor de documentos.", - "office_close": "Cerrar editor", + "office_edit": "Editar documento", + "office_loading": "Abriendo el editor de documentos…", + "office_error": "No se pudo abrir el editor de documentos.", + "office_close": "Cerrar editor", "open_in_new_tab": "Abrir en una pestaña nueva", "title": "Archivos", "search_placeholder": "Buscar archivos...", @@ -3555,5 +3557,31 @@ "missing_params": "Falta el código de autorización o el estado.", "delivery_failed": "No se pudo entregar el resultado de la autorización a la pestaña de la aplicación. Cierre esta ventana e inténtelo de nuevo." } + }, + "global_search": { + "title": "Buscar en todo", + "placeholder": "Buscar correo, contactos, calendario, archivos...", + "scope_label": "Buscar en", + "account_label": "Cuenta", + "scope_all": "Todo", + "scope_mail": "Correo", + "scope_contacts": "Contactos", + "scope_calendar": "Calendario", + "scope_files": "Archivos", + "all_accounts": "Todas las cuentas", + "show_all": "Mostrar todos los resultados", + "no_results": "Sin resultados para “{query}”", + "server_hint": "Las palabras completas buscan en el servidor; las parciales coinciden con lo ya cargado", + "searching": "Buscando...", + "account_error": "No se pudo buscar en {account}", + "recent_searches": "Búsquedas recientes", + "recurring": "Periódico", + "no_subject": "(no subject)", + "empty_hint": "Busca correo, contactos, eventos y archivos en todas las cuentas. Prueba from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Plano por fecha", + "grouped": "Agrupado", + "load_more": "Cargar más", + "preview_empty": "Selecciona un resultado para previsualizarlo", + "open_full": "Abrir" } } diff --git a/locales/fa/common.json b/locales/fa/common.json index 2d717bdea..8527595f8 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -2103,7 +2103,8 @@ "too_long": "نام قالب باید ۲۰۰ کاراکتر یا کمتر باشد" } }, - "files": { "display": { + "files": { + "display": { "title": "نمایش", "description": "پیکربندی نحوه نمایش فایل‌ها و پوشه‌ها" }, @@ -2275,7 +2276,8 @@ "search": "تمرکز جستجو", "help": "نمایش میانبرها", "refresh": "بازنشانی ایمیل‌ها", - "select_all": "انتخاب همه" + "select_all": "انتخاب همه", + "global_search": "جستجو در همه جا" }, "threads": { "expand_collapse": "باز/بسته کردن رشته" @@ -3265,10 +3267,10 @@ "hint": "روی هر ایمیل در سمت چپ کلیک کنید یا تور را ببینید." }, "files": { - "office_edit": "ویرایش سند", - "office_loading": "در حال باز کردن ویرایشگر سند…", - "office_error": "ویرایشگر سند باز نشد.", - "office_close": "بستن ویرایشگر", + "office_edit": "ویرایش سند", + "office_loading": "در حال باز کردن ویرایشگر سند…", + "office_error": "ویرایشگر سند باز نشد.", + "office_close": "بستن ویرایشگر", "open_in_new_tab": "باز کردن در تب جدید", "title": "فایل‌ها", "search_placeholder": "جستجوی فایل‌ها...", @@ -3555,5 +3557,31 @@ "missing_params": "کد احراز هویت یا حالت وجود ندارد.", "delivery_failed": "نتیجه احراز هویت به برگه برنامه ارسال نشد. لطفاً این پنجره را ببندید و دوباره تلاش کنید." } + }, + "global_search": { + "title": "جستجو در همه جا", + "placeholder": "جستجو در ایمیل، مخاطبین، تقویم، فایل‌ها...", + "scope_label": "جستجو در", + "account_label": "حساب", + "scope_all": "همه", + "scope_mail": "ایمیل", + "scope_contacts": "مخاطبین", + "scope_calendar": "تقویم", + "scope_files": "فایل‌ها", + "all_accounts": "همه حساب‌ها", + "show_all": "نمایش همه نتایج", + "no_results": "نتیجه‌ای برای “{query}” یافت نشد", + "server_hint": "کلمات کامل در سرور جستجو می‌شوند؛ بخشی از کلمات با موارد بارگذاری‌شده مطابقت دارد", + "searching": "در حال جستجو...", + "account_error": "جستجو در {account} ممکن نشد", + "recent_searches": "جستجوهای اخیر", + "recurring": "تکرارشونده", + "no_subject": "(no subject)", + "empty_hint": "در ایمیل، مخاطبین، رویدادها و فایل‌های همه حساب‌ها جستجو کنید. امتحان کنید: from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "تخت بر اساس تاریخ", + "grouped": "گروه‌بندی‌شده", + "load_more": "بارگذاری بیشتر", + "preview_empty": "برای پیش‌نمایش یک نتیجه انتخاب کنید", + "open_full": "باز کردن" } } diff --git a/locales/fr/common.json b/locales/fr/common.json index 38c32b640..4b93a37d6 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2096,7 +2096,8 @@ "too_long": "Le nom du modèle ne doit pas dépasser 200 caractères" } }, - "files": { "display": { + "files": { + "display": { "title": "Affichage", "description": "Configurez l'affichage des fichiers et dossiers" }, @@ -2275,7 +2276,8 @@ "search": "Rechercher", "help": "Afficher les raccourcis", "refresh": "Actualiser les emails", - "select_all": "Tout sélectionner" + "select_all": "Tout sélectionner", + "global_search": "Tout rechercher" }, "threads": { "expand_collapse": "Développer/réduire la conversation" @@ -3223,10 +3225,10 @@ "hint": "Cliquez sur un e-mail à gauche pour commencer, ou lancez la visite." }, "files": { - "office_edit": "Modifier le document", - "office_loading": "Ouverture de l'éditeur de documents…", - "office_error": "L'éditeur de documents n'a pas pu être ouvert.", - "office_close": "Fermer l'éditeur", + "office_edit": "Modifier le document", + "office_loading": "Ouverture de l'éditeur de documents…", + "office_error": "L'éditeur de documents n'a pas pu être ouvert.", + "office_close": "Fermer l'éditeur", "open_in_new_tab": "Ouvrir dans un nouvel onglet", "title": "Fichiers", "search_placeholder": "Rechercher des fichiers...", @@ -3555,5 +3557,31 @@ "missing_params": "Code dautorisation ou état manquant.", "delivery_failed": "Le résultat de lautorisation na pas pu être transmis à longlet de lapplication. Fermez cette fenêtre et réessayez." } + }, + "global_search": { + "title": "Tout rechercher", + "placeholder": "Rechercher courriels, contacts, calendrier, fichiers...", + "scope_label": "Rechercher dans", + "account_label": "Compte", + "scope_all": "Tout", + "scope_mail": "Messagerie", + "scope_contacts": "Contacts", + "scope_calendar": "Calendrier", + "scope_files": "Fichiers", + "all_accounts": "Tous les comptes", + "show_all": "Afficher tous les résultats", + "no_results": "Aucun résultat pour « {query} »", + "server_hint": "Les mots complets interrogent le serveur ; les mots partiels correspondent aux éléments déjà chargés", + "searching": "Recherche...", + "account_error": "Impossible de rechercher dans {account}", + "recent_searches": "Recherches récentes", + "recurring": "Récurrent", + "no_subject": "(no subject)", + "empty_hint": "Recherchez courriels, contacts, événements et fichiers sur tous les comptes. Essayez from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "À plat par date", + "grouped": "Groupé", + "load_more": "Charger plus", + "preview_empty": "Sélectionnez un résultat pour l'aperçu", + "open_full": "Ouvrir" } } diff --git a/locales/he/common.json b/locales/he/common.json index ef6a4b414..f459d8a35 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -2018,7 +2018,8 @@ "too_long": "שם התבנית חייב להכיל 200 תווים או פחות" } }, - "files": { "display": { + "files": { + "display": { "title": "תצוגה", "description": "קבע את אופן הצגת הקבצים והתיקיות" }, @@ -2199,7 +2200,8 @@ "search": "התמקד בחיפוש", "help": "הצג קיצורי דרך", "refresh": "רענן מיילים", - "select_all": "בחר הכל" + "select_all": "בחר הכל", + "global_search": "חיפוש בכל מקום" }, "threads": { "expand_collapse": "הרחב/כווץ את השרשור" @@ -3147,10 +3149,10 @@ "hint": "לחץ על אימייל כלשהו מימין כדי להתחיל, או צא לסיור למטה." }, "files": { - "office_edit": "עריכת מסמך", - "office_loading": "פותח את עורך המסמכים…", - "office_error": "לא ניתן היה לפתוח את עורך המסמכים.", - "office_close": "סגירת העורך", + "office_edit": "עריכת מסמך", + "office_loading": "פותח את עורך המסמכים…", + "office_error": "לא ניתן היה לפתוח את עורך המסמכים.", + "office_close": "סגירת העורך", "title": "קבצים", "search_placeholder": "חפש קבצים...", "empty_state_title": "אין עדיין קבצים", @@ -3556,5 +3558,31 @@ "missing_params": "חסר קוד אימות או מצב.", "delivery_failed": "לא ניתן היה למסור את תוצאת האימות לכרטיסיית האפליקציה. סגרו חלון זה ונסו שוב." } + }, + "global_search": { + "title": "חיפוש בכל מקום", + "placeholder": "חיפוש בדואר, אנשי קשר, יומן, קבצים...", + "scope_label": "חיפוש ב", + "account_label": "חשבון", + "scope_all": "הכל", + "scope_mail": "דוֹאַר", + "scope_contacts": "אנשי קשר", + "scope_calendar": "לוּחַ שָׁנָה", + "scope_files": "קבצים", + "all_accounts": "כל החשבונות", + "show_all": "הצגת כל התוצאות", + "no_results": "אין תוצאות עבור “{query}”", + "server_hint": "מילים שלמות מחפשות בשרת; חלקי מילים תואמים פריטים שכבר נטענו", + "searching": "מחפש...", + "account_error": "לא ניתן היה לחפש ב-{account}", + "recent_searches": "חיפושים אחרונים", + "recurring": "חוזר", + "no_subject": "(no subject)", + "empty_hint": "חפשו דואר, אנשי קשר, אירועים וקבצים בכל החשבונות. נסו: from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "שטוח לפי תאריך", + "grouped": "מקובץ", + "load_more": "טען עוד", + "preview_empty": "בחרו תוצאה לתצוגה מקדימה", + "open_full": "פתיחה" } } diff --git a/locales/hu/common.json b/locales/hu/common.json index 548ed06e3..3d1bf206e 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -2099,7 +2099,8 @@ "too_long": "A sablon neve legfeljebb 200 karakter lehet" } }, - "files": { "display": { + "files": { + "display": { "title": "Megjelenítés", "description": "Fájlok és mappák megjelenésének beállítása" }, @@ -2275,7 +2276,8 @@ "search": "Keresés fókusz", "help": "Parancsok mutatása", "refresh": "E-mailek frissítése", - "select_all": "Összes kijelölése" + "select_all": "Összes kijelölése", + "global_search": "Keresés mindenben" }, "threads": { "expand_collapse": "Beszélgetés kibontása/összecsukása" @@ -3265,10 +3267,10 @@ "hint": "Kattints bármely e-mailre a bal oldalon a kezdéshez, vagy nézd meg az alábbi bemutatót." }, "files": { - "office_edit": "Dokumentum szerkesztése", - "office_loading": "Dokumentumszerkesztő megnyitása…", - "office_error": "A dokumentumszerkesztőt nem sikerült megnyitni.", - "office_close": "Szerkesztő bezárása", + "office_edit": "Dokumentum szerkesztése", + "office_loading": "Dokumentumszerkesztő megnyitása…", + "office_error": "A dokumentumszerkesztőt nem sikerült megnyitni.", + "office_close": "Szerkesztő bezárása", "open_in_new_tab": "Megnyitás új lapon", "title": "Fájlok", "search_placeholder": "Fájlok keresése...", @@ -3555,5 +3557,31 @@ "missing_params": "Hiányzik a hitelesítési kód vagy az állapot.", "delivery_failed": "A hitelesítés eredményét nem sikerült átadni az alkalmazás lapjának. Zárja be ezt az ablakot, és próbálja újra." } + }, + "global_search": { + "title": "Keresés mindenben", + "placeholder": "Keresés a levélben, névjegyekben, naptárban, fájlokban...", + "scope_label": "Keresés itt", + "account_label": "Fiók", + "scope_all": "Minden", + "scope_mail": "Levelek", + "scope_contacts": "Névjegyek", + "scope_calendar": "Naptár", + "scope_files": "Fájlok", + "all_accounts": "Minden fiók", + "show_all": "Összes találat megjelenítése", + "no_results": "Nincs találat erre: „{query}”", + "server_hint": "A teljes szavak a szerveren keresnek; a szórészletek a már betöltött elemekre illeszkednek", + "searching": "Keresés...", + "account_error": "Nem sikerült keresni itt: {account}", + "recent_searches": "Legutóbbi keresések", + "recurring": "Ismétlődő", + "no_subject": "(no subject)", + "empty_hint": "Keressen levelet, névjegyet, eseményt és fájlt minden fiókban. Próbálja ki: from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Lapos, dátum szerint", + "grouped": "Csoportosított", + "load_more": "Továbbiak betöltése", + "preview_empty": "Válasszon egy találatot az előnézethez", + "open_full": "Megnyitás" } } diff --git a/locales/it/common.json b/locales/it/common.json index 11ae65cf1..7ee5f3145 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2096,7 +2096,8 @@ "too_long": "Il nome del modello non deve superare i 200 caratteri" } }, - "files": { "display": { + "files": { + "display": { "title": "Visualizzazione", "description": "Configura come vengono visualizzati file e cartelle" }, @@ -2275,7 +2276,8 @@ "search": "Cerca", "help": "Mostra scorciatoie", "refresh": "Aggiorna messaggi", - "select_all": "Seleziona tutto" + "select_all": "Seleziona tutto", + "global_search": "Cerca ovunque" }, "threads": { "expand_collapse": "Espandi/comprimi conversazione" @@ -3223,10 +3225,10 @@ "hint": "Clicca su un'email a sinistra per iniziare, oppure fai il tour." }, "files": { - "office_edit": "Modifica documento", - "office_loading": "Apertura dell'editor di documenti…", - "office_error": "Impossibile aprire l'editor di documenti.", - "office_close": "Chiudi editor", + "office_edit": "Modifica documento", + "office_loading": "Apertura dell'editor di documenti…", + "office_error": "Impossibile aprire l'editor di documenti.", + "office_close": "Chiudi editor", "open_in_new_tab": "Apri in una nuova scheda", "title": "File", "search_placeholder": "Cerca file...", @@ -3555,5 +3557,31 @@ "missing_params": "Codice di autorizzazione o stato mancante.", "delivery_failed": "Impossibile consegnare il risultato dellautorizzazione alla scheda dellapp. Chiudi questa finestra e riprova." } + }, + "global_search": { + "title": "Cerca ovunque", + "placeholder": "Cerca email, contatti, calendario, file...", + "scope_label": "Cerca in", + "account_label": "Account", + "scope_all": "Tutto", + "scope_mail": "Posta", + "scope_contacts": "Contatti", + "scope_calendar": "Calendario", + "scope_files": "File", + "all_accounts": "Tutti gli account", + "show_all": "Mostra tutti i risultati", + "no_results": "Nessun risultato per “{query}”", + "server_hint": "Le parole complete cercano sul server; quelle parziali corrispondono agli elementi già caricati", + "searching": "Ricerca in corso...", + "account_error": "Impossibile cercare in {account}", + "recent_searches": "Ricerche recenti", + "recurring": "Ricorrente", + "no_subject": "(no subject)", + "empty_hint": "Cerca email, contatti, eventi e file in tutti gli account. Prova from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Piatto per data", + "grouped": "Raggruppato", + "load_more": "Carica altri", + "preview_empty": "Seleziona un risultato per l'anteprima", + "open_full": "Apri" } } diff --git a/locales/ja/common.json b/locales/ja/common.json index e68bd2af5..481d50d8c 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2096,7 +2096,8 @@ "too_long": "テンプレート名は200文字以内にしてください" } }, - "files": { "display": { + "files": { + "display": { "title": "表示", "description": "ファイルとフォルダーの表示方法を設定します" }, @@ -2275,7 +2276,8 @@ "search": "検索にフォーカス", "help": "ショートカットを表示", "refresh": "メールを更新", - "select_all": "すべて選択" + "select_all": "すべて選択", + "global_search": "すべてを検索" }, "threads": { "expand_collapse": "スレッドの展開/折りたたみ" @@ -3223,10 +3225,10 @@ "hint": "左のメールをクリックして始めるか、ツアーを開始してください。" }, "files": { - "office_edit": "ドキュメントを編集", - "office_loading": "ドキュメントエディターを開いています…", - "office_error": "ドキュメントエディターを開けませんでした。", - "office_close": "エディターを閉じる", + "office_edit": "ドキュメントを編集", + "office_loading": "ドキュメントエディターを開いています…", + "office_error": "ドキュメントエディターを開けませんでした。", + "office_close": "エディターを閉じる", "open_in_new_tab": "新しいタブで開く", "title": "ファイル", "search_placeholder": "ファイルを検索...", @@ -3555,5 +3557,31 @@ "missing_params": "認証コードまたはステートがありません。", "delivery_failed": "認証結果をアプリのタブに渡せませんでした。このウィンドウを閉じて、もう一度お試しください。" } + }, + "global_search": { + "title": "すべてを検索", + "placeholder": "メール、連絡先、カレンダー、ファイルを検索...", + "scope_label": "検索対象", + "account_label": "アカウント", + "scope_all": "すべて", + "scope_mail": "メール", + "scope_contacts": "連絡先", + "scope_calendar": "カレンダー", + "scope_files": "ファイル", + "all_accounts": "すべてのアカウント", + "show_all": "すべての結果を表示", + "no_results": "「{query}」に一致する結果はありません", + "server_hint": "完全な単語はサーバーを検索します。部分一致は読み込み済みの項目に適用されます", + "searching": "検索中...", + "account_error": "{account} を検索できませんでした", + "recent_searches": "最近の検索", + "recurring": "繰り返し", + "no_subject": "(no subject)", + "empty_hint": "すべてのアカウントのメール、連絡先、予定、ファイルを検索できます。from:、subject:、has:attachment、after:2026-01-01 なども試せます。", + "flat_by_date": "日付順のフラット表示", + "grouped": "グループ表示", + "load_more": "さらに読み込む", + "preview_empty": "プレビューする結果を選択してください", + "open_full": "開く" } } diff --git a/locales/ko/common.json b/locales/ko/common.json index 7ecd3b418..3644d578a 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2096,7 +2096,8 @@ "too_long": "템플릿 이름은 200자 이하여야 해요" } }, - "files": { "display": { + "files": { + "display": { "title": "화면 표시", "description": "파일과 폴더를 보여주는 방식을 설정해요" }, @@ -2275,7 +2276,8 @@ "search": "검색창 이동", "help": "단축키 보기", "refresh": "메일 새로고침", - "select_all": "모두 선택" + "select_all": "모두 선택", + "global_search": "전체 검색" }, "threads": { "expand_collapse": "대화 펼치기/접기" @@ -3223,10 +3225,10 @@ "hint": "왼쪽에서 메일을 클릭해서 시작하거나, 아래의 둘러보기를 클릭해 보세요." }, "files": { - "office_edit": "문서 편집", - "office_loading": "문서 편집기를 여는 중…", - "office_error": "문서 편집기를 열 수 없습니다.", - "office_close": "편집기 닫기", + "office_edit": "문서 편집", + "office_loading": "문서 편집기를 여는 중…", + "office_error": "문서 편집기를 열 수 없습니다.", + "office_close": "편집기 닫기", "open_in_new_tab": "새 탭에서 열기", "title": "파일", "search_placeholder": "파일 검색...", @@ -3555,5 +3557,31 @@ "missing_params": "인증 코드 또는 상태가 누락되었습니다.", "delivery_failed": "인증 결과를 앱 탭에 전달하지 못했습니다. 이 창을 닫고 다시 시도해 주세요." } + }, + "global_search": { + "title": "전체 검색", + "placeholder": "메일, 연락처, 캘린더, 파일 검색...", + "scope_label": "검색 범위", + "account_label": "계정", + "scope_all": "전체", + "scope_mail": "메일", + "scope_contacts": "연락처", + "scope_calendar": "캘린더", + "scope_files": "파일", + "all_accounts": "모든 계정", + "show_all": "모든 결과 보기", + "no_results": "“{query}”에 대한 결과가 없습니다", + "server_hint": "완전한 단어는 서버에서 검색되며, 단어 일부는 이미 불러온 항목과 일치합니다", + "searching": "검색 중...", + "account_error": "{account}을(를) 검색할 수 없습니다", + "recent_searches": "최근 검색", + "recurring": "반복", + "no_subject": "(no subject)", + "empty_hint": "모든 계정의 메일, 연락처, 일정, 파일을 검색하세요. from:, subject:, has:attachment, after:2026-01-01 도 사용할 수 있습니다.", + "flat_by_date": "날짜순 평뱀 보기", + "grouped": "그룹 보기", + "load_more": "더 불러오기", + "preview_empty": "미리 볼 결과를 선택하세요", + "open_full": "열기" } } diff --git a/locales/lv/common.json b/locales/lv/common.json index 00579946f..5f537cd71 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2096,7 +2096,8 @@ "too_long": "Veidnes nosaukums nedrīkst pārsniegt 200 rakstzīmes" } }, - "files": { "display": { + "files": { + "display": { "title": "Attēlošana", "description": "Pielāgojiet failu un mapju attēlošanu" }, @@ -2275,7 +2276,8 @@ "search": "Iet uz meklēšanu", "help": "Rādīt īsinājumtaustiņus", "refresh": "Atsvaidzināt vēstules", - "select_all": "Atlasīt visas" + "select_all": "Atlasīt visas", + "global_search": "Meklēt visā" }, "threads": { "expand_collapse": "Izvērst/sairt sarunu" @@ -3223,10 +3225,10 @@ "hint": "Noklikšķiniet uz jebkuru vēstuli kreisajā pusē, lai sāktu." }, "files": { - "office_edit": "Rediģēt dokumentu", - "office_loading": "Atver dokumentu redaktoru…", - "office_error": "Neizdevās atvērt dokumentu redaktoru.", - "office_close": "Aizvērt redaktoru", + "office_edit": "Rediģēt dokumentu", + "office_loading": "Atver dokumentu redaktoru…", + "office_error": "Neizdevās atvērt dokumentu redaktoru.", + "office_close": "Aizvērt redaktoru", "open_in_new_tab": "Atvērt jaunā cilnē", "title": "Faili", "search_placeholder": "Meklēt failus...", @@ -3555,5 +3557,31 @@ "missing_params": "Trūkst autorizācijas kods vai stāvoklis.", "delivery_failed": "Autorizācijas rezultātu neizdevās nodot lietotnes cilnei. Aizveriet šo logu un mēģiniet vēlreiz." } + }, + "global_search": { + "title": "Meklēt visā", + "placeholder": "Meklēt pastā, kontaktos, kalendārā, failos...", + "scope_label": "Meklēt", + "account_label": "Konts", + "scope_all": "Viss", + "scope_mail": "Pasts", + "scope_contacts": "Kontakti", + "scope_calendar": "Kalendārs", + "scope_files": "Faili", + "all_accounts": "Visi konti", + "show_all": "Rādīt visus rezultātus", + "no_results": "Nav rezultātu vaicājumam “{query}”", + "server_hint": "Pilni vārdi meklē serverī; vārdu daļas atbilst jau ielādētajiem vienumiem", + "searching": "Meklē...", + "account_error": "Neizdevās meklēt kontā {account}", + "recent_searches": "Pēdējie meklējumi", + "recurring": "Period isks", + "no_subject": "(no subject)", + "empty_hint": "Meklējiet pastu, kontaktus, notikumus un failus visos kontos. Izmēģiniet from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Plakani pēc datuma", + "grouped": "Grupēts", + "load_more": "Ielādēt vairāk", + "preview_empty": "Izvēlieties rezultātu, lai to priekšskatītu", + "open_full": "Atvērt" } } diff --git a/locales/mn/common.json b/locales/mn/common.json index fc20da957..9b1e9ad66 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -2103,7 +2103,8 @@ "too_long": "Загварын нэр 200 ба түүнээс бага тэмдэгт байх ёстой" } }, - "files": { "display": { + "files": { + "display": { "title": "Дэлгэц", "description": "Файл, фолдерыг хэрхэн харуулахыг тохируулах" }, @@ -2275,7 +2276,8 @@ "search": "Хайлт руу шилжих", "help": "Товчлол харуулах", "refresh": "Имэйл сэргээх", - "select_all": "Бүгдийг сонго" + "select_all": "Бүгдийг сонго", + "global_search": "Бүгдийг хайх" }, "threads": { "expand_collapse": "Хэлхээсийг дэлгэх/хумих" @@ -3265,10 +3267,10 @@ "hint": "Эхлэхийн тулд зүүн талд байгаа дурын имэйл дээр дарж эсвэл доорх аялалд хамрагдаарай." }, "files": { - "office_edit": "Баримт засах", - "office_loading": "Баримтын засварлагчийг нээж байна…", - "office_error": "Баримтын засварлагчийг нээж чадсангүй.", - "office_close": "Засварлагчийг хаах", + "office_edit": "Баримт засах", + "office_loading": "Баримтын засварлагчийг нээж байна…", + "office_error": "Баримтын засварлагчийг нээж чадсангүй.", + "office_close": "Засварлагчийг хаах", "open_in_new_tab": "Шинэ табаар нээх", "title": "Файл", "search_placeholder": "Файл хайх...", @@ -3555,5 +3557,31 @@ "missing_params": "Зөвшөөрлийн код эсвэл төлөв байхгүй байна.", "delivery_failed": "Зөвшөөрлийн үр дүнг аппын таб руу дамжуулж чадсангүй. Энэ цонхыг хаагаад дахин оролдоно уу." } + }, + "global_search": { + "title": "Бүгдийг хайх", + "placeholder": "Шуудан, харилцагч, хуанли, файлаас хайх...", + "scope_label": "Хайх хүрээ", + "account_label": "Бүртгэл", + "scope_all": "Бүгд", + "scope_mail": "Мэйл", + "scope_contacts": "Харилцах утас", + "scope_calendar": "Хуанли", + "scope_files": "Файл", + "all_accounts": "Бүх бүртгэл", + "show_all": "Бүх үр дүнг харуулах", + "no_results": "“{query}”-д үр дүн олдсонгүй", + "server_hint": "Бүтэн үгс серверээс хайна; үгийн хэсэг ачаалагдсан зүйлстэй таарна", + "searching": "Хайж байна...", + "account_error": "{account}-аас хайж чадсангүй", + "recent_searches": "Сүүлийн хайлтууд", + "recurring": "Давтагдах", + "no_subject": "(no subject)", + "empty_hint": "Бүх бүртгэлийн шуудан, харилцагч, үйл явдал, файлаас хайгаарай. from:, subject:, has:attachment, after:2026-01-01 гэж туршиж үзээрэй.", + "flat_by_date": "Огноогоор жагсаах", + "grouped": "Бүлэглэсэн", + "load_more": "Цааш ачаалах", + "preview_empty": "Урьдчилан харах үр дүнгээ сонгоно уу", + "open_full": "Нээх" } } diff --git a/locales/nb/common.json b/locales/nb/common.json index 255fc55b8..fb71ae585 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -2103,7 +2103,8 @@ "too_long": "Malnavnet kan inneholde høyst 200 tegn" } }, - "files": { "display": { + "files": { + "display": { "title": "Visning", "description": "Velg hvordan filer og mapper skal vises" }, @@ -2275,7 +2276,8 @@ "search": "Aktiver søkefeltet", "help": "Vis tastatursnarveier", "refresh": "Oppdater e-poster", - "select_all": "Velg alle" + "select_all": "Velg alle", + "global_search": "Søk i alt" }, "threads": { "expand_collapse": "Utvid eller fold sammen tråden" @@ -3265,10 +3267,10 @@ "hint": "Klikk på en av e-postene til venstre for å komme i gang, eller start omvisningen nedenfor." }, "files": { - "office_edit": "Rediger dokument", - "office_loading": "Åpner dokumentredigering…", - "office_error": "Dokumentredigeringen kunne ikke åpnes.", - "office_close": "Lukk redigering", + "office_edit": "Rediger dokument", + "office_loading": "Åpner dokumentredigering…", + "office_error": "Dokumentredigeringen kunne ikke åpnes.", + "office_close": "Lukk redigering", "open_in_new_tab": "Åpne i ny fane", "title": "Filer", "search_placeholder": "Søk etter filer...", @@ -3555,5 +3557,31 @@ "missing_params": "Mangler autorisasjonskode eller tilstand.", "delivery_failed": "Autoriseringsresultatet kunne ikke leveres til app-fanen. Lukk dette vinduet og prøv igjen." } + }, + "global_search": { + "title": "Søk i alt", + "placeholder": "Søk i e-post, kontakter, kalender, filer...", + "scope_label": "Søk i", + "account_label": "Konto", + "scope_all": "Alt", + "scope_mail": "E-post", + "scope_contacts": "Kontakter", + "scope_calendar": "Kalender", + "scope_files": "Filer", + "all_accounts": "Alle kontoer", + "show_all": "Vis alle resultater", + "no_results": "Ingen resultater for “{query}”", + "server_hint": "Hele ord søker på serveren; deler av ord treffer allerede innlastede elementer", + "searching": "Søker...", + "account_error": "Kunne ikke søke i {account}", + "recent_searches": "Nylige søk", + "recurring": "Gjentakende", + "no_subject": "(no subject)", + "empty_hint": "Søk i e-post, kontakter, hendelser og filer på alle kontoer. Prøv from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Flatt etter dato", + "grouped": "Gruppert", + "load_more": "Last inn flere", + "preview_empty": "Velg et resultat for å forhåndsvise det", + "open_full": "Åpne" } } diff --git a/locales/nl/common.json b/locales/nl/common.json index 96cbfa48b..d931a6b61 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2096,7 +2096,8 @@ "too_long": "Sjabloonnaam mag maximaal 200 tekens zijn" } }, - "files": { "display": { + "files": { + "display": { "title": "Weergave", "description": "Configureer hoe bestanden en mappen worden weergegeven" }, @@ -2275,7 +2276,8 @@ "search": "Focus op zoeken", "help": "Sneltoetsen tonen", "refresh": "E-mails vernieuwen", - "select_all": "Alles selecteren" + "select_all": "Alles selecteren", + "global_search": "Alles doorzoeken" }, "threads": { "expand_collapse": "Gesprek uitklappen/inklappen" @@ -3223,10 +3225,10 @@ "hint": "Klik links op een e-mail om te beginnen, of start de tour." }, "files": { - "office_edit": "Document bewerken", - "office_loading": "Documenteditor wordt geopend…", - "office_error": "De documenteditor kon niet worden geopend.", - "office_close": "Editor sluiten", + "office_edit": "Document bewerken", + "office_loading": "Documenteditor wordt geopend…", + "office_error": "De documenteditor kon niet worden geopend.", + "office_close": "Editor sluiten", "open_in_new_tab": "Openen in nieuw tabblad", "title": "Bestanden", "search_placeholder": "Bestanden zoeken...", @@ -3555,5 +3557,31 @@ "missing_params": "Autorisatiecode of status ontbreekt.", "delivery_failed": "Het autorisatieresultaat kon niet worden afgeleverd bij het app-tabblad. Sluit dit venster en probeer het opnieuw." } + }, + "global_search": { + "title": "Alles doorzoeken", + "placeholder": "Zoek in mail, contacten, agenda, bestanden...", + "scope_label": "Zoeken in", + "account_label": "Account", + "scope_all": "Alles", + "scope_mail": "E-mail", + "scope_contacts": "Contacten", + "scope_calendar": "Agenda", + "scope_files": "Bestanden", + "all_accounts": "Alle accounts", + "show_all": "Alle resultaten tonen", + "no_results": "Geen resultaten voor “{query}”", + "server_hint": "Volledige woorden doorzoeken de server; woorddelen vinden al geladen items", + "searching": "Zoeken...", + "account_error": "Kon niet zoeken in {account}", + "recent_searches": "Recente zoekopdrachten", + "recurring": "Terugkerend", + "no_subject": "(no subject)", + "empty_hint": "Doorzoek mail, contacten, afspraken en bestanden van alle accounts. Probeer from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Plat op datum", + "grouped": "Gegroepeerd", + "load_more": "Meer laden", + "preview_empty": "Selecteer een resultaat voor een voorbeeld", + "open_full": "Openen" } } diff --git a/locales/pl/common.json b/locales/pl/common.json index 4c2226cf6..fff2f0210 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2096,7 +2096,8 @@ "too_long": "Nazwa szablonu musi mieć maksymalnie 200 znaków" } }, - "files": { "display": { + "files": { + "display": { "title": "Wyświetlanie", "description": "Skonfiguruj sposób wyświetlania plików i folderów" }, @@ -2275,7 +2276,8 @@ "search": "Przejdź do wyszukiwania", "help": "Pokaż skróty", "refresh": "Odśwież wiadomości", - "select_all": "Zaznacz wszystko" + "select_all": "Zaznacz wszystko", + "global_search": "Szukaj wszędzie" }, "threads": { "expand_collapse": "Rozwiń/zwiń wątek" @@ -3223,10 +3225,10 @@ "hint": "Kliknij dowolną wiadomość po lewej, aby zacząć, albo skorzystaj z przewodnika poniżej." }, "files": { - "office_edit": "Edytuj dokument", - "office_loading": "Otwieranie edytora dokumentów…", - "office_error": "Nie udało się otworzyć edytora dokumentów.", - "office_close": "Zamknij edytor", + "office_edit": "Edytuj dokument", + "office_loading": "Otwieranie edytora dokumentów…", + "office_error": "Nie udało się otworzyć edytora dokumentów.", + "office_close": "Zamknij edytor", "open_in_new_tab": "Otwórz w nowej karcie", "title": "Pliki", "search_placeholder": "Szukaj plików...", @@ -3555,5 +3557,31 @@ "missing_params": "Brak kodu autoryzacji lub stanu.", "delivery_failed": "Nie udało się przekazać wyniku autoryzacji do karty aplikacji. Zamknij to okno i spróbuj ponownie." } + }, + "global_search": { + "title": "Szukaj wszędzie", + "placeholder": "Szukaj w poczcie, kontaktach, kalendarzu, plikach...", + "scope_label": "Szukaj w", + "account_label": "Konto", + "scope_all": "Wszystko", + "scope_mail": "Poczta", + "scope_contacts": "Kontakty", + "scope_calendar": "Kalendarz", + "scope_files": "Pliki", + "all_accounts": "Wszystkie konta", + "show_all": "Pokaż wszystkie wyniki", + "no_results": "Brak wyników dla „{query}”", + "server_hint": "Całe słowa przeszukują serwer; części słów pasują do już wczytanych elementów", + "searching": "Szukanie...", + "account_error": "Nie udało się przeszukać {account}", + "recent_searches": "Ostatnie wyszukiwania", + "recurring": "Cykliczne", + "no_subject": "(no subject)", + "empty_hint": "Szukaj poczty, kontaktów, wydarzeń i plików na wszystkich kontach. Wypróbuj from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Płasko według daty", + "grouped": "Pogrupowane", + "load_more": "Wczytaj więcej", + "preview_empty": "Wybierz wynik, aby zobaczyć podgląd", + "open_full": "Otwórz" } } diff --git a/locales/pt/common.json b/locales/pt/common.json index a49688a0e..cadee83c2 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2096,7 +2096,8 @@ "too_long": "O nome do modelo não pode exceder 200 caracteres" } }, - "files": { "display": { + "files": { + "display": { "title": "Exibição", "description": "Configure como arquivos e pastas são exibidos" }, @@ -2275,7 +2276,8 @@ "search": "Focar busca", "help": "Mostrar atalhos", "refresh": "Atualizar e-mails", - "select_all": "Selecionar todos" + "select_all": "Selecionar todos", + "global_search": "Pesquisar em tudo" }, "threads": { "expand_collapse": "Expandir/recolher conversa" @@ -3223,10 +3225,10 @@ "hint": "Clique num e-mail à esquerda para começar, ou inicie o tour." }, "files": { - "office_edit": "Editar documento", - "office_loading": "A abrir o editor de documentos…", - "office_error": "Não foi possível abrir o editor de documentos.", - "office_close": "Fechar editor", + "office_edit": "Editar documento", + "office_loading": "A abrir o editor de documentos…", + "office_error": "Não foi possível abrir o editor de documentos.", + "office_close": "Fechar editor", "open_in_new_tab": "Abrir num novo separador", "title": "Arquivos", "search_placeholder": "Pesquisar arquivos...", @@ -3555,5 +3557,31 @@ "missing_params": "Falta o código de autorização ou o estado.", "delivery_failed": "Não foi possível entregar o resultado da autorização ao separador da aplicação. Feche esta janela e tente novamente." } + }, + "global_search": { + "title": "Pesquisar em tudo", + "placeholder": "Pesquisar e-mail, contactos, calendário, ficheiros...", + "scope_label": "Pesquisar em", + "account_label": "Conta", + "scope_all": "Tudo", + "scope_mail": "E-mail", + "scope_contacts": "Contatos", + "scope_calendar": "Calendário", + "scope_files": "Arquivos", + "all_accounts": "Todas as contas", + "show_all": "Mostrar todos os resultados", + "no_results": "Sem resultados para “{query}”", + "server_hint": "Palavras completas pesquisam no servidor; palavras parciais correspondem ao que já está carregado", + "searching": "A pesquisar...", + "account_error": "Não foi possível pesquisar em {account}", + "recent_searches": "Pesquisas recentes", + "recurring": "Recorrente", + "no_subject": "(no subject)", + "empty_hint": "Pesquise e-mail, contactos, eventos e ficheiros em todas as contas. Experimente from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Plano por data", + "grouped": "Agrupado", + "load_more": "Carregar mais", + "preview_empty": "Selecione um resultado para pré-visualizar", + "open_full": "Abrir" } } diff --git a/locales/ro/common.json b/locales/ro/common.json index 3283b70de..bfe279d6a 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -2103,7 +2103,8 @@ "too_long": "Numele șablonului trebuie să aibă cel mult 200 de caractere" } }, - "files": { "display": { + "files": { + "display": { "title": "Afișare", "description": "Configurați modul de afișare a fișierelor și folderelor" }, @@ -2275,7 +2276,8 @@ "search": "Căutare focalizată", "help": "Afișați comenzile rapide", "refresh": "Reîmprospătați e-mailurile", - "select_all": "Selectează tot" + "select_all": "Selectează tot", + "global_search": "Caută în tot" }, "threads": { "expand_collapse": "Extindeți/reduceți firul de discuție" @@ -3265,10 +3267,10 @@ "hint": "Faceți clic pe orice e-mail din partea stângă pentru a începe sau parcurgeți turul de mai jos." }, "files": { - "office_edit": "Editează documentul", - "office_loading": "Se deschide editorul de documente…", - "office_error": "Editorul de documente nu a putut fi deschis.", - "office_close": "Închide editorul", + "office_edit": "Editează documentul", + "office_loading": "Se deschide editorul de documente…", + "office_error": "Editorul de documente nu a putut fi deschis.", + "office_close": "Închide editorul", "open_in_new_tab": "Deschide într-o filă nouă", "title": "Fișiere", "search_placeholder": "Căutare fișiere...", @@ -3555,5 +3557,31 @@ "missing_params": "Lipsește codul de autorizare sau starea.", "delivery_failed": "Rezultatul autorizării nu a putut fi transmis filei aplicației. Închideți această fereastră și încercați din nou." } + }, + "global_search": { + "title": "Caută în tot", + "placeholder": "Caută în mail, contacte, calendar, fișiere...", + "scope_label": "Caută în", + "account_label": "Cont", + "scope_all": "Tot", + "scope_mail": "E-mail", + "scope_contacts": "Contacte", + "scope_calendar": "Calendar", + "scope_files": "Fișiere", + "all_accounts": "Toate conturile", + "show_all": "Afișează toate rezultatele", + "no_results": "Niciun rezultat pentru „{query}”", + "server_hint": "Cuvintele complete caută pe server; fragmentele de cuvinte se potrivesc cu elementele deja încărcate", + "searching": "Se caută...", + "account_error": "Nu s-a putut căuta în {account}", + "recent_searches": "Căutări recente", + "recurring": "Recurent", + "no_subject": "(no subject)", + "empty_hint": "Caută mail, contacte, evenimente și fișiere în toate conturile. Încearcă from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Plat după dată", + "grouped": "Grupat", + "load_more": "Încarcă mai multe", + "preview_empty": "Selectează un rezultat pentru previzualizare", + "open_full": "Deschide" } } diff --git a/locales/ru/common.json b/locales/ru/common.json index 31eadddfb..e6b6e5a05 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2096,7 +2096,8 @@ "too_long": "Название шаблона не должно превышать 200 символов" } }, - "files": { "display": { + "files": { + "display": { "title": "Отображение", "description": "Настройте отображение файлов и папок" }, @@ -2275,7 +2276,8 @@ "search": "Перейти к поиску", "help": "Показать сочетания клавиш", "refresh": "Обновить письма", - "select_all": "Выбрать все" + "select_all": "Выбрать все", + "global_search": "Искать везде" }, "threads": { "expand_collapse": "Развернуть/свернуть цепочку" @@ -3223,10 +3225,10 @@ "hint": "Нажмите на любое письмо слева, чтобы начать, или пройдите тур ниже." }, "files": { - "office_edit": "Редактировать документ", - "office_loading": "Открытие редактора документов…", - "office_error": "Не удалось открыть редактор документов.", - "office_close": "Закрыть редактор", + "office_edit": "Редактировать документ", + "office_loading": "Открытие редактора документов…", + "office_error": "Не удалось открыть редактор документов.", + "office_close": "Закрыть редактор", "open_in_new_tab": "Открыть в новой вкладке", "title": "Файлы", "search_placeholder": "Поиск файлов...", @@ -3555,5 +3557,31 @@ "missing_params": "Отсутствует код авторизации или состояние.", "delivery_failed": "Не удалось передать результат авторизации на вкладку приложения. Закройте это окно и попробуйте снова." } + }, + "global_search": { + "title": "Искать везде", + "placeholder": "Поиск по почте, контактам, календарю, файлам...", + "scope_label": "Искать в", + "account_label": "Аккаунт", + "scope_all": "Всё", + "scope_mail": "Почта", + "scope_contacts": "Контакты", + "scope_calendar": "Календарь", + "scope_files": "Файлы", + "all_accounts": "Все аккаунты", + "show_all": "Показать все результаты", + "no_results": "Ничего не найдено по запросу «{query}»", + "server_hint": "Целые слова ищутся на сервере; части слов — среди уже загруженного", + "searching": "Поиск...", + "account_error": "Не удалось выполнить поиск в {account}", + "recent_searches": "Недавние запросы", + "recurring": "Повторяющееся", + "no_subject": "(no subject)", + "empty_hint": "Ищите почту, контакты, события и файлы во всех аккаунтах. Попробуйте from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Списком по дате", + "grouped": "По группам", + "load_more": "Загрузить ещё", + "preview_empty": "Выберите результат для предпросмотра", + "open_full": "Открыть" } } diff --git a/locales/sk/common.json b/locales/sk/common.json index a51766880..506fffaf3 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -2103,7 +2103,8 @@ "too_long": "Názov šablóny môže mať maximálne 200 znakov" } }, - "files": { "display": { + "files": { + "display": { "title": "Zobrazenie", "description": "Nakonfigurujte spôsob zobrazenia súborov a priečinkov" }, @@ -2275,7 +2276,8 @@ "search": "Prejsť na hľadanie", "help": "Zobraziť skratky", "refresh": "Obnoviť e-maily", - "select_all": "Vybrať všetko" + "select_all": "Vybrať všetko", + "global_search": "Hľadať všade" }, "threads": { "expand_collapse": "Rozbaliť/zbaliť vlákno" @@ -3265,10 +3267,10 @@ "hint": "Kliknite na ľubovoľný e-mail vľavo na začiatok alebo využite sprievodcu nižšie." }, "files": { - "office_edit": "Upraviť dokument", - "office_loading": "Otvára sa editor dokumentov…", - "office_error": "Editor dokumentov sa nepodarilo otvoriť.", - "office_close": "Zavrieť editor", + "office_edit": "Upraviť dokument", + "office_loading": "Otvára sa editor dokumentov…", + "office_error": "Editor dokumentov sa nepodarilo otvoriť.", + "office_close": "Zavrieť editor", "open_in_new_tab": "Otvoriť v novej karte", "title": "Súbory", "search_placeholder": "Hľadať súbory...", @@ -3555,5 +3557,31 @@ "missing_params": "Chýba autorizačný kód alebo stav.", "delivery_failed": "Výsledok autorizácie sa nepodarilo odovzdať karte aplikácie. Zavrite toto okno a skúste to znova." } + }, + "global_search": { + "title": "Hľadať všade", + "placeholder": "Hľadať v pošte, kontaktoch, kalendári, súboroch...", + "scope_label": "Hľadať v", + "account_label": "Účet", + "scope_all": "Všetko", + "scope_mail": "Pošta", + "scope_contacts": "Kontakty", + "scope_calendar": "Kalendár", + "scope_files": "Súbory", + "all_accounts": "Všetky účty", + "show_all": "Zobraziť všetky výsledky", + "no_results": "Žiadne výsledky pre „{query}“", + "server_hint": "Celé slová prehľadávajú server; časti slov zodpovedajú už načítaným položkám", + "searching": "Hľadá sa...", + "account_error": "Nepodarilo sa prehľadať {account}", + "recent_searches": "Nedávne hľadania", + "recurring": "Opakovaná", + "no_subject": "(no subject)", + "empty_hint": "Hľadajte poštu, kontakty, udalosti a súbory vo všetkých účtoch. Skúste from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Plocho podľa dátumu", + "grouped": "Zoskupené", + "load_more": "Načítať ďalšie", + "preview_empty": "Vyberte výsledok pre náhľad", + "open_full": "Otvoriť" } } diff --git a/locales/tr/common.json b/locales/tr/common.json index 71fdd24be..69cf04d38 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2096,7 +2096,8 @@ "too_long": "Şablon adı en fazla 200 karakter olmalıdır" } }, - "files": { "display": { + "files": { + "display": { "title": "Görüntüleme", "description": "Dosya ve klasörlerin nasıl görüntüleneceğini yapılandırın" }, @@ -2275,7 +2276,8 @@ "search": "Aramaya odaklan", "help": "Kısayolları göster", "refresh": "E-postaları yenile", - "select_all": "Tümünü seç" + "select_all": "Tümünü seç", + "global_search": "Her yerde ara" }, "threads": { "expand_collapse": "İleti dizisini genişlet/daralt" @@ -3265,10 +3267,10 @@ "hint": "Başlamak için soldaki herhangi bir e-postaya tıklayın veya aşağıdaki turu yapın." }, "files": { - "office_edit": "Belgeyi düzenle", - "office_loading": "Belge düzenleyici açılıyor…", - "office_error": "Belge düzenleyici açılamadı.", - "office_close": "Düzenleyiciyi kapat", + "office_edit": "Belgeyi düzenle", + "office_loading": "Belge düzenleyici açılıyor…", + "office_error": "Belge düzenleyici açılamadı.", + "office_close": "Düzenleyiciyi kapat", "open_in_new_tab": "Yeni sekmede aç", "title": "Dosyalar", "search_placeholder": "Dosyalarda ara...", @@ -3555,5 +3557,31 @@ "missing_params": "Yetkilendirme kodu veya durum eksik.", "delivery_failed": "Yetkilendirme sonucu uygulama sekmesine iletilemedi. Lütfen bu pencereyi kapatıp tekrar deneyin." } + }, + "global_search": { + "title": "Her yerde ara", + "placeholder": "Posta, kişiler, takvim ve dosyalarda ara...", + "scope_label": "Şuralarda ara", + "account_label": "Hesap", + "scope_all": "Tümü", + "scope_mail": "Posta", + "scope_contacts": "Kişiler", + "scope_calendar": "Takvim", + "scope_files": "Dosyalar", + "all_accounts": "Tüm hesaplar", + "show_all": "Tüm sonuçları göster", + "no_results": "“{query}” için sonuç yok", + "server_hint": "Tam kelimeler sunucuda arar; kelime parçaları yüklenmiş öğelerle eşleşir", + "searching": "Aranıyor...", + "account_error": "{account} içinde arama yapılamadı", + "recent_searches": "Son aramalar", + "recurring": "Yinelenen", + "no_subject": "(no subject)", + "empty_hint": "Tüm hesaplarda posta, kişi, etkinlik ve dosya arayın. Şunları deneyin: from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Tarihe göre düz", + "grouped": "Gruplandırılmış", + "load_more": "Daha fazla yükle", + "preview_empty": "Önizlemek için bir sonuç seçin", + "open_full": "Aç" } } diff --git a/locales/uk/common.json b/locales/uk/common.json index a47cc271a..b8701f2ac 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2096,7 +2096,8 @@ "too_long": "Назва шаблону має містити не більше 200 символів" } }, - "files": { "display": { + "files": { + "display": { "title": "Дисплей", "description": "Налаштувати спосіб відображення файлів і папок" }, @@ -2275,7 +2276,8 @@ "search": "Пошук по фокусу", "help": "Показати ярлики", "refresh": "Оновити електронні листи", - "select_all": "Вибрати все" + "select_all": "Вибрати все", + "global_search": "Шукати скрізь" }, "threads": { "expand_collapse": "Розгорнути/згорнути ланцюжок" @@ -3223,10 +3225,10 @@ "hint": "Натисніть будь-яку електронну пошту ліворуч, щоб розпочати, або перегляньте огляд нижче." }, "files": { - "office_edit": "Редагувати документ", - "office_loading": "Відкриття редактора документів…", - "office_error": "Не вдалося відкрити редактор документів.", - "office_close": "Закрити редактор", + "office_edit": "Редагувати документ", + "office_loading": "Відкриття редактора документів…", + "office_error": "Не вдалося відкрити редактор документів.", + "office_close": "Закрити редактор", "open_in_new_tab": "Відкрити в новій вкладці", "title": "Файли", "search_placeholder": "Пошук файлів...", @@ -3555,5 +3557,31 @@ "missing_params": "Відсутній код авторизації або стан.", "delivery_failed": "Не вдалося передати результат авторизації на вкладку програми. Закрийте це вікно та спробуйте ще раз." } + }, + "global_search": { + "title": "Шукати скрізь", + "placeholder": "Пошук у пошті, контактах, календарі, файлах...", + "scope_label": "Шукати в", + "account_label": "Обліковий запис", + "scope_all": "Усе", + "scope_mail": "Пошта", + "scope_contacts": "Контакти", + "scope_calendar": "Календар", + "scope_files": "Файли", + "all_accounts": "Усі облікові записи", + "show_all": "Показати всі результати", + "no_results": "Нічого не знайдено за запитом «{query}»", + "server_hint": "Цілі слова шукаються на сервері; частини слів — серед уже завантаженого", + "searching": "Пошук...", + "account_error": "Не вдалося виконати пошук у {account}", + "recent_searches": "Останні запити", + "recurring": "Повторювана", + "no_subject": "(no subject)", + "empty_hint": "Шукайте пошту, контакти, події та файли в усіх облікових записах. Спробуйте from:, subject:, has:attachment, after:2026-01-01.", + "flat_by_date": "Списком за датою", + "grouped": "За групами", + "load_more": "Завантажити ще", + "preview_empty": "Виберіть результат для перегляду", + "open_full": "Відкрити" } } diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 939a8598c..65d458179 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -2098,7 +2098,8 @@ "too_long": "範本名稱不得超過 200 個字元" } }, - "files": { "display": { + "files": { + "display": { "title": "顯示", "description": "設定檔案和資料夾的顯示方式" }, @@ -2270,7 +2271,8 @@ "search": "將焦點移至搜尋框", "help": "顯示快速鍵", "refresh": "重新整理郵件", - "select_all": "全部選取" + "select_all": "全部選取", + "global_search": "全域搜尋" }, "threads": { "expand_collapse": "展開/收合對話串" @@ -3260,10 +3262,10 @@ "hint": "點擊左側任一郵件即可開始,或進行下方的功能導覽。" }, "files": { - "office_edit": "編輯文件", - "office_loading": "正在開啟文件編輯器…", - "office_error": "無法開啟文件編輯器。", - "office_close": "關閉編輯器", + "office_edit": "編輯文件", + "office_loading": "正在開啟文件編輯器…", + "office_error": "無法開啟文件編輯器。", + "office_close": "關閉編輯器", "open_in_new_tab": "在新分頁開啟", "title": "檔案", "search_placeholder": "搜尋檔案…", @@ -3550,5 +3552,31 @@ "missing_params": "缺少授權碼或狀態。", "delivery_failed": "無法將授權結果傳遞至應用程式分頁。請關閉此視窗並再試一次。" } + }, + "global_search": { + "title": "全域搜尋", + "placeholder": "搜尋郵件、聯絡人、行事曆、檔案...", + "scope_label": "搜尋範圍", + "account_label": "帳戶", + "scope_all": "全部", + "scope_mail": "郵件", + "scope_contacts": "聯絡人", + "scope_calendar": "行事曆", + "scope_files": "檔案", + "all_accounts": "所有帳戶", + "show_all": "顯示所有結果", + "no_results": "找不到「{query}」的結果", + "server_hint": "完整單字會搜尋伺服器;部分單字則比對已載入的項目", + "searching": "搜尋中...", + "account_error": "無法搜尋 {account}", + "recent_searches": "最近搜尋", + "recurring": "重複", + "no_subject": "(no subject)", + "empty_hint": "在所有帳戶中搜尋郵件、聯絡人、行事和檔案。可試試 from:、subject:、has:attachment、after:2026-01-01。", + "flat_by_date": "依日期平列", + "grouped": "分組", + "load_more": "載入更多", + "preview_empty": "選擇結果以預覽", + "open_full": "開啟" } } diff --git a/locales/zh/common.json b/locales/zh/common.json index 3a2ccd8c5..e77910547 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2096,7 +2096,8 @@ "too_long": "模板名称不得超过 200 个字符" } }, - "files": { "display": { + "files": { + "display": { "title": "显示", "description": "配置文件和文件夹的显示方式" }, @@ -2275,7 +2276,8 @@ "search": "聚焦搜索框", "help": "显示快捷方式", "refresh": "刷新邮件", - "select_all": "全选" + "select_all": "全选", + "global_search": "全局搜索" }, "threads": { "expand_collapse": "展开/折叠会话" @@ -3223,10 +3225,10 @@ "hint": "点击左侧的任意邮件即可开始,或参加下方的导览。" }, "files": { - "office_edit": "编辑文档", - "office_loading": "正在打开文档编辑器…", - "office_error": "无法打开文档编辑器。", - "office_close": "关闭编辑器", + "office_edit": "编辑文档", + "office_loading": "正在打开文档编辑器…", + "office_error": "无法打开文档编辑器。", + "office_close": "关闭编辑器", "open_in_new_tab": "在新标签页中打开", "title": "文件", "search_placeholder": "搜索文件...", @@ -3555,5 +3557,31 @@ "missing_params": "缺少授权码或状态。", "delivery_failed": "无法将授权结果传递到应用标签页。请关闭此窗口并重试。" } + }, + "global_search": { + "title": "全局搜索", + "placeholder": "搜索邮件、联系人、日历、文件...", + "scope_label": "搜索范围", + "account_label": "账户", + "scope_all": "全部", + "scope_mail": "邮件", + "scope_contacts": "联系人", + "scope_calendar": "日历", + "scope_files": "文件", + "all_accounts": "所有账户", + "show_all": "显示全部结果", + "no_results": "未找到“{query}”的结果", + "server_hint": "完整单词在服务器上搜索;部分单词匹配已加载的内容", + "searching": "搜索中...", + "account_error": "无法搜索 {account}", + "recent_searches": "最近搜索", + "recurring": "重复", + "no_subject": "(no subject)", + "empty_hint": "在所有账户中搜索邮件、联系人、日程和文件。可尝试 from:、subject:、has:attachment、after:2026-01-01。", + "flat_by_date": "按日期平铺", + "grouped": "分组", + "load_more": "加载更多", + "preview_empty": "选择一个结果以预览", + "open_full": "打开" } }