feat: global search palette and search tab in the Pro shell #641

This commit is contained in:
Linus Rath
2026-09-01 20:55:13 +02:00
parent 0c7437f67a
commit c075b79cbb
38 changed files with 1977 additions and 166 deletions
+31 -2
View File
@@ -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 <ProFolderTabBody tabId={tab.id} data={tab.folderData} />;
}
if (tab.kind === 'search' && tab.searchData) {
return <SearchTabBody tabId={tab.id} data={tab.searchData} />;
}
const Component = APP_TAB_COMPONENTS[tab.kind];
return Component ? <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()}
/>
</div>
@@ -526,6 +553,8 @@ export default function ProHome() {
)}
</div>
<GlobalSearchPalette />
<KeyboardShortcutsModal
isOpen={showShortcutsModal}
onClose={() => setShowShortcutsModal(false)}
@@ -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<typeof parseSearchQuery>;
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(<GlobalSearchPalette />);
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(<GlobalSearchPalette />);
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(<GlobalSearchPalette />);
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(<GlobalSearchPalette />);
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(<GlobalSearchPalette />);
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(<GlobalSearchPalette />);
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(<GlobalSearchPalette />);
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
expect(useGlobalSearchStore.getState().isOpen).toBe(false);
});
it('scope chips update the store scope', () => {
render(<GlobalSearchPalette />);
fireEvent.click(screen.getByRole('radio', { name: 'scope_files' }));
expect(useGlobalSearchStore.getState().scope).toBe('files');
});
});
@@ -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<HTMLInputElement>(null);
const { menuRef, onKeyDown } = useMenuNavigation<HTMLDivElement>({ 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 (
<div key={kind} className="py-1">
<div className="flex items-center gap-2 px-3 py-1 text-xs font-medium text-muted-foreground uppercase tracking-wide">
{t(`scope_${kind}`)}
{status.status === 'loading' && <Loader2 className="w-3 h-3 animate-spin" aria-label={t('searching')} />}
</div>
{hits.slice(0, MAX_ROWS_PER_KIND).map((hit) => (
<PaletteResultRow key={`${hit.localAccountId}-${hit.id}`} hit={hit} onOpen={handleOpenHit} />
))}
{status.errors.map((error) => (
<div key={error.localAccountId} className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground">
<AlertTriangle className="w-3.5 h-3.5 shrink-0 text-amber-500" />
{t('account_error', { account: error.accountLabel })}
</div>
))}
</div>
);
};
return (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-[1px] z-[60] flex items-start justify-center pt-[12vh] p-4 animate-in fade-in duration-150"
onMouseDown={(e) => { if (e.target === e.currentTarget) closePalette(); }}
>
<div
ref={menuRef}
role="dialog"
aria-modal="true"
aria-label={t('title')}
onKeyDown={handleKeyDown}
className="bg-background border border-border rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[70vh] animate-in zoom-in-95 duration-150"
>
<div className="flex items-center gap-2.5 px-3 py-2.5 border-b border-border">
<Search className="w-4 h-4 shrink-0 text-muted-foreground" />
<input
ref={inputRef}
data-global-search-input
type="text"
value={query}
onChange={(e) => 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 && <Loader2 className="w-4 h-4 shrink-0 animate-spin text-muted-foreground" aria-label={t('searching')} />}
</div>
<div className="px-3 py-2 border-b border-border">
<SearchScopeChips
scope={scope}
onScopeChange={setScope}
accounts={accountOptions}
accountId={accountId}
onAccountChange={setAccountId}
/>
</div>
<div className="flex-1 min-h-0 overflow-y-auto py-1">
{showRecent && (
<div className="py-1">
<div className="px-3 py-1 text-xs font-medium text-muted-foreground uppercase tracking-wide">{t('recent_searches')}</div>
{recentSearches.map((term) => (
<button
key={term}
type="button"
role="option"
aria-selected={false}
onClick={() => setQuery(term)}
className="w-full flex items-center gap-2.5 px-3 py-1.5 text-left text-sm text-foreground rounded-md hover:bg-muted focus:bg-muted focus:outline-none"
>
<Clock3 className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{term}</span>
</button>
))}
</div>
)}
{!trimmed && !showRecent && (
<p className="px-3 py-6 text-sm text-muted-foreground text-center">{t('empty_hint')}</p>
)}
{trimmed && kindsToShow.map(renderGroup)}
{trimmed && !hasAnyHit && !isSearching && (
<div className="px-3 py-6 text-center">
<p className="text-sm text-foreground">{t('no_results', { query: trimmed })}</p>
<p className="mt-1 text-xs text-muted-foreground">{t('server_hint')}</p>
</div>
)}
</div>
{proShell && trimmed && (
<button
type="button"
role="option"
aria-selected={false}
onClick={submit}
className="flex items-center gap-2 px-3 py-2 border-t border-border text-sm text-muted-foreground hover:bg-muted hover:text-foreground focus:bg-muted focus:outline-none cursor-pointer"
>
<CornerDownLeft className="w-3.5 h-3.5" />
{t('show_all')}
</button>
)}
</div>
</div>
);
}
@@ -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 <Mail className={className} />;
case 'contacts': return <BookUser className={className} />;
case 'calendar': return <CalendarDays className={className} />;
case 'files': return hit.isFolder ? <Folder className={className} /> : <FileText className={className} />;
}
}
function Snippet({ value }: { value: string }) {
const segments: SnippetSegment[] = parseSearchSnippet(value);
return (
<span className="truncate">
{segments.map((segment, i) => segment.marked
? <mark key={i} className="bg-primary/20 text-foreground rounded-sm px-px">{segment.text}</mark>
: <span key={i}>{segment.text}</span>)}
</span>
);
}
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 `<mark>`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 (
<button
type="button"
role="option"
aria-selected={false}
onClick={() => onOpen(hit)}
data-hit-kind={hit.kind}
className={cn(
"w-full flex items-start gap-2.5 px-3 py-2 text-left rounded-md",
"hover:bg-muted focus:bg-muted focus:outline-none",
className,
)}
>
<span className="mt-0.5"><HitIcon hit={hit} /></span>
<span className="flex-1 min-w-0">
<span className="flex items-center gap-1.5 min-w-0">
<span className="truncate text-sm text-foreground">
{hit.kind === 'mail' && hit.snippet?.subject ? <Snippet value={hit.snippet.subject} /> : title}
</span>
{hit.kind === 'calendar' && hit.isRecurring && (
<Repeat aria-label={t('recurring')} className="w-3 h-3 shrink-0 text-muted-foreground" />
)}
</span>
<span className="block truncate text-xs text-muted-foreground">{subtitle}</span>
{snippet && (
<span className="block truncate text-xs text-muted-foreground"><Snippet value={snippet} /></span>
)}
</span>
{hit.date && (
<span className="shrink-0 text-xs text-muted-foreground tabular-nums mt-0.5">{formatHitDate(hit.date)}</span>
)}
</button>
);
}
@@ -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<SearchKind, number>;
/** '+' suffix on the count (the server reported more than was fetched). */
kindHasMore: Record<SearchKind, boolean>;
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 (
<button
type="button"
onClick={onClick}
aria-pressed={active}
className={cn(
"w-full flex items-center justify-between gap-2 px-2 py-1 rounded-md text-sm text-left cursor-pointer",
active ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
<span className="truncate">{label}</span>
{count !== undefined && <span className="shrink-0 text-xs tabular-nums">{count}</span>}
</button>
);
}
/** 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 (
<div className="flex flex-col gap-4 p-3 overflow-y-auto">
<section>
<h3 className="px-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wide">{t('scope_label')}</h3>
<FacetButton
active={scope === 'all'}
label={t('scope_all')}
count={`${total}${anyMore ? '+' : ''}`}
onClick={() => onScopeChange('all')}
/>
{SEARCH_KINDS.map((kind) => (
<FacetButton
key={kind}
active={scope === kind}
label={t(`scope_${kind}`)}
count={`${kindCounts[kind]}${kindHasMore[kind] ? '+' : ''}`}
onClick={() => onScopeChange(kind)}
/>
))}
</section>
{accounts.length > 1 && (
<section>
<h3 className="px-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wide">{t('account_label')}</h3>
<FacetButton active={accountId === null} label={t('all_accounts')} onClick={() => onAccountChange(null)} />
{accounts.map((account) => (
<FacetButton
key={account.localAccountId}
active={accountId === account.localAccountId}
label={account.label}
count={String(account.count)}
onClick={() => onAccountChange(account.localAccountId)}
/>
))}
</section>
)}
<section>
<FacetButton active={!flat} label={t('grouped')} onClick={() => onFlatChange(false)} />
<FacetButton active={flat} label={t('flat_by_date')} onClick={() => onFlatChange(true)} />
</section>
</div>
);
}
+152
View File
@@ -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 (
<button
type="button"
onClick={() => onOpen(hit)}
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border border-border text-xs text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer"
>
<ExternalLink className="w-3.5 h-3.5" />
{label}
</button>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="text-sm">
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className="text-foreground break-words">{children}</dd>
</div>
);
}
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 (
<div className="p-4 flex flex-col gap-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3 min-w-0">
<BookUser className="w-8 h-8 shrink-0 text-muted-foreground" />
<div className="min-w-0">
<h2 className="text-base font-semibold text-foreground truncate">{name}</h2>
<p className="text-xs text-muted-foreground truncate">{[hit.accountLabel, hit.subtitle].filter(Boolean).join(' · ')}</p>
</div>
</div>
<OpenButton hit={hit} onOpen={onOpen} label={openLabel} />
</div>
<dl className="flex flex-col gap-2">
{emails.map((email) => <Field key={email} label="@">{email}</Field>)}
{phones.map((phone) => <Field key={phone} label="#">{phone}</Field>)}
{orgs.map((org) => <Field key={org} label="">{org}</Field>)}
</dl>
</div>
);
}
function CalendarPreview({ hit, onOpen, openLabel, t }: { hit: CalendarHit; onOpen: (hit: GlobalSearchHit) => void; openLabel: string; t: ReturnType<typeof useTranslations> }) {
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 (
<div className="p-4 flex flex-col gap-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3 min-w-0">
<CalendarDays className="w-8 h-8 shrink-0 text-muted-foreground" />
<div className="min-w-0">
<h2 className="flex items-center gap-1.5 text-base font-semibold text-foreground">
<span className="truncate">{hit.title}</span>
{hit.isRecurring && <Repeat aria-label={t('recurring')} className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />}
</h2>
<p className="text-xs text-muted-foreground truncate">{[hit.accountLabel, hit.subtitle].filter(Boolean).join(' · ')}</p>
</div>
</div>
<OpenButton hit={hit} onOpen={onOpen} label={openLabel} />
</div>
<dl className="flex flex-col gap-2">
<Field label="">{formatDateTime(event.start)}</Field>
{locations.map((location) => <Field key={location} label="">{location}</Field>)}
{event.description && <Field label="">{event.description}</Field>}
{participants.length > 0 && <Field label="">{participants.join(', ')}</Field>}
</dl>
</div>
);
}
function FilePreview({ hit, onOpen, openLabel }: { hit: FileHit; onOpen: (hit: GlobalSearchHit) => void; openLabel: string }) {
const Icon = hit.isFolder ? Folder : FileText;
return (
<div className="p-4 flex flex-col gap-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3 min-w-0">
<Icon className="w-8 h-8 shrink-0 text-muted-foreground" />
<div className="min-w-0">
<h2 className="text-base font-semibold text-foreground truncate">{hit.title}</h2>
<p className="text-xs text-muted-foreground truncate">{[hit.accountLabel, hit.folderPath].filter(Boolean).join(' · ')}</p>
</div>
</div>
<OpenButton hit={hit} onOpen={onOpen} label={openLabel} />
</div>
<dl className="flex flex-col gap-2">
{!hit.isFolder && <Field label="">{formatFileSize(hit.node.size)}{hit.node.type ? ` · ${hit.node.type}` : ''}</Field>}
{hit.date && <Field label="">{formatDateTime(hit.date)}</Field>}
</dl>
</div>
);
}
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 <p className="p-6 text-sm text-muted-foreground text-center">{t('preview_empty')}</p>;
}
if (hit.kind === 'mail') {
const clientOverride = hit.localAccountId !== activeAccountId ? getClientForAccount(hit.localAccountId) ?? undefined : undefined;
return (
<ProEmailView
key={`${hit.localAccountId}-${hit.id}`}
emailId={hit.id}
client={clientOverride}
accountId={hit.jmapAccountId}
className="w-full"
/>
);
}
const openLabel = t('open_full');
if (hit.kind === 'contacts') return <ContactPreview hit={hit} onOpen={onOpen} openLabel={openLabel} />;
if (hit.kind === 'calendar') return <CalendarPreview hit={hit} onOpen={onOpen} openLabel={openLabel} t={t} />;
return <FilePreview hit={hit} onOpen={onOpen} openLabel={openLabel} />;
}
@@ -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 (
<div onDoubleClick={() => onOpen(hit)}>
<PaletteResultRow hit={hit} onOpen={onSelect} className={cn(selected && "bg-muted")} />
</div>
);
}
/** 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) => (
<div key={`${kind}-${error.localAccountId}`} className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground">
<AlertTriangle className="w-3.5 h-3.5 shrink-0 text-amber-500" />
{t('account_error', { account: error.accountLabel })}
</div>
));
if (flat) {
const all = kinds.flatMap((kind) => outcome.hits[kind]).sort(compareByRecency);
return (
<div className="py-1">
{all.map((hit) => (
<Row key={hitKey(hit)} hit={hit} selected={selectedKey === hitKey(hit)} onSelect={onSelect} onOpen={onOpen} />
))}
{SEARCH_KINDS.filter((kind) => kinds.includes(kind)).flatMap((kind) => errorRows(kind))}
{outcome.status.mail.hasMore && kinds.includes('mail') && (
<button type="button" onClick={onLoadMoreMail} className="w-full px-3 py-2 text-sm text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer">
{t('load_more')}
</button>
)}
</div>
);
}
return (
<div className="py-1">
{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 (
<section key={kind} className="pb-2">
<h3 className="flex items-center gap-2 px-3 py-1 text-xs font-medium text-muted-foreground uppercase tracking-wide sticky top-0 bg-background">
{t(`scope_${kind}`)}
<span className="tabular-nums normal-case">{hits.length}{status.hasMore ? '+' : ''}</span>
{status.status === 'loading' && <Loader2 className="w-3 h-3 animate-spin" aria-label={t('searching')} />}
</h3>
{hits.map((hit) => (
<Row key={hitKey(hit)} hit={hit} selected={selectedKey === hitKey(hit)} onSelect={onSelect} onOpen={onOpen} />
))}
{errorRows(kind)}
{kind === 'mail' && status.hasMore && (
<button type="button" onClick={onLoadMoreMail} className="w-full px-3 py-2 text-sm text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer">
{t('load_more')}
</button>
)}
</section>
);
})}
</div>
);
}
@@ -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 (
<div className="flex items-center gap-1.5 flex-wrap">
<div role="radiogroup" aria-label={t('scope_label')} className="flex items-center gap-1">
{SCOPES.map((option) => (
<button
key={option}
type="button"
role="radio"
aria-checked={scope === option}
onClick={() => onScopeChange(option)}
className={cn(
"px-2 py-0.5 rounded-full text-xs border transition-colors cursor-pointer",
scope === option
? "bg-primary/10 border-primary/40 text-primary font-medium"
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
{t(`scope_${option}`)}
</button>
))}
</div>
{accounts.length > 1 && (
<select
aria-label={t('account_label')}
value={accountId ?? ''}
onChange={(e) => onAccountChange(e.target.value || null)}
className="ml-auto max-w-[14rem] truncate text-xs bg-transparent border border-border rounded-full px-2 py-0.5 text-muted-foreground cursor-pointer focus:outline-none focus:border-primary/40"
>
<option value="">{t('all_accounts')}</option>
{accounts.map((account) => (
<option key={account.localAccountId} value={account.localAccountId}>{account.label}</option>
))}
</select>
)}
</div>
);
}
@@ -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<GlobalSearchHit | null>(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<string, number>();
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<SearchKind, number>;
const more = {} as Record<SearchKind, boolean>;
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 (
<div className="flex h-full min-h-0 w-full">
<aside className="w-48 shrink-0 border-r border-border overflow-y-auto">
<SearchFacets
scope={data.scope}
onScopeChange={setScope}
kindCounts={kindCounts.counts}
kindHasMore={kindCounts.more}
accounts={accountFacets}
accountId={data.accountId}
onAccountChange={setAccountId}
flat={flat}
onFlatChange={setFlat}
/>
</aside>
<section className="flex-1 min-w-0 flex flex-col border-r border-border">
<div className="flex items-center gap-2 px-3 py-2 border-b border-border">
<Search className="w-4 h-4 shrink-0 text-muted-foreground" />
<input
type="text"
value={draft}
onChange={(e) => 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 && <Loader2 className="w-4 h-4 shrink-0 animate-spin text-muted-foreground" aria-label={t('searching')} />}
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
<SearchResultList
outcome={outcome}
kinds={kinds}
flat={flat}
selectedKey={selected ? hitKey(selected) : null}
onSelect={setSelected}
onOpen={handleOpen}
onLoadMoreMail={loadMoreMail}
/>
{kinds.every((kind) => outcome.hits[kind].length === 0) && !isSearching && (
<div className="px-3 py-8 text-center">
<p className="text-sm text-foreground">{t('no_results', { query: data.query })}</p>
<p className="mt-1 text-xs text-muted-foreground">{t('server_hint')}</p>
</div>
)}
</div>
</section>
<aside className="hidden lg:block flex-1 min-w-0 overflow-y-auto">
<SearchPreview hit={selected} onOpen={handleOpen} />
</aside>
</div>
);
}
+26 -1
View File
@@ -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 && (
<button
type="button"
onClick={onOpenSearch}
data-tour="nav-search"
className={cn(
"relative flex items-center gap-2.5 rounded-md transition-colors duration-150 cursor-pointer",
collapsed ? "justify-center w-10 h-10" : "px-2.5 text-sm",
"max-lg:min-h-[44px]",
"text-muted-foreground hover:bg-muted hover:text-foreground"
)}
title={collapsed ? tGlobalSearch("title") : undefined}
style={collapsed ? undefined : { paddingBlock: 'var(--density-sidebar-py)' }}
>
<Search className="w-[18px] h-[18px] flex-shrink-0" />
{!collapsed && <span className="truncate">{tGlobalSearch("title")}</span>}
</button>
)}
{visibleItems.map((item) => {
const isActive = getIsActive(item.href, item.id);
const Icon = item.icon;
+1
View File
@@ -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" },
+34 -6
View File
@@ -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": "فتح"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+35 -7
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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": "باز کردن"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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": "פתיחה"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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": "開く"
}
}
+34 -6
View File
@@ -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": "열기"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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": "Нээх"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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"
}
}
+34 -6
View File
@@ -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": "Открыть"
}
}
+34 -6
View File
@@ -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ť"
}
}
+34 -6
View File
@@ -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ç"
}
}
+34 -6
View File
@@ -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": "Відкрити"
}
}
+34 -6
View File
@@ -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": "開啟"
}
}
+34 -6
View File
@@ -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": "打开"
}
}