mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: global search palette and search tab in the Pro shell #641
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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": "Відкрити"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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": "打开"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user