This commit is contained in:
rustmailer
2026-01-20 00:53:09 +08:00
parent 30a8d856c1
commit d0e4cac229
33 changed files with 1742 additions and 814 deletions
+46 -40
View File
@@ -90,7 +90,7 @@ export function AccountPopover() {
)}
>
<AtSign className="h-4 w-4" />
Account
{t('search_accounts.label')}
{selectedIds.length > 0 && (
<Badge
variant="secondary"
@@ -108,7 +108,7 @@ export function AccountPopover() {
<Input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder={t('search.searchAccount')}
placeholder={t('search_accounts.search_placeholder')}
className="h-8 text-sm"
/>
</div>
@@ -124,7 +124,7 @@ export function AccountPopover() {
<X className="h-3.5 w-3.5" />
</div>
<span className="flex-1 text-left">
{t('search.clearAccounts')}
{t('search_accounts.clear_accounts')}
</span>
<span className="text-[10px] opacity-60 font-mono">
({selectedIds.length})
@@ -134,46 +134,52 @@ export function AccountPopover() {
</div>
)}
<ScrollArea className="h-96 p-1">
{filtered.map(account => {
const checked = selectedIds.includes(account.id)
const id = `account-${account.id}`
{filtered.length === 0 ? (
<p className="px-3 py-2 text-xs text-muted-foreground">
{t('search_accounts.no_accounts_found')}
</p>
) : (
filtered.map(account => {
const checked = selectedIds.includes(account.id)
const id = `account-${account.id}`
return (
<div
key={account.id}
onClick={() => toggleAccount(account.id)}
className={cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer',
'hover:bg-accent transition-colors'
)}
>
<Checkbox
id={id}
checked={checked}
onCheckedChange={() =>
toggleAccount(account.id)
}
onClick={e => e.stopPropagation()}
/>
<Label
htmlFor={id}
className="flex-1 truncate text-xs cursor-pointer"
return (
<div
key={account.id}
onClick={() => toggleAccount(account.id)}
className={cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer',
'hover:bg-accent transition-colors'
)}
>
<div className="flex items-center gap-2">
<span className="truncate">
{account.email}
</span>
<span className="text-[10px] text-muted-foreground">
#{account.id}
</span>
</div>
</Label>
</div>
)
})}
<Checkbox
id={id}
checked={checked}
onCheckedChange={() =>
toggleAccount(account.id)
}
onClick={e => e.stopPropagation()}
/>
<Label
htmlFor={id}
className="flex-1 truncate text-xs cursor-pointer"
>
<div className="flex items-center gap-2">
<span className="truncate">
{account.email}
</span>
<span className="text-[10px] text-muted-foreground">
#{account.id}
</span>
</div>
</Label>
</div>
)
})
)}
</ScrollArea>
</PopoverContent>
</Popover>
)
}
}
+21 -9
View File
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"
import { Check, ChevronDown, Mail, X } from "lucide-react"
import React from "react"
import { useContacts } from "@/hooks/use-contacts"
import { useTranslation } from 'react-i18next'
import {
Command,
CommandEmpty,
@@ -15,6 +16,7 @@ import {
} from "@/components/ui/command"
export function MailFilterPopover() {
const { t } = useTranslation()
const { filter, setFilter } = useSearchContext()
const fields = ['from', 'to', 'cc', 'bcc'] as const
@@ -47,7 +49,11 @@ export function MailFilterPopover() {
)}
>
<Mail className="h-3.5 w-3.5 opacity-60" />
<span>{activeCount > 0 ? `Participants (${activeCount})` : 'Participants'}</span>
<span>
{activeCount > 0
? t('search_contacts.label_with_count', { count: activeCount })
: t('search_contacts.label')}
</span>
<ChevronDown className="h-3.5 w-3.5 opacity-60" />
</Button>
</PopoverTrigger>
@@ -69,14 +75,19 @@ export function MailFilterPopover() {
</div>
{activeCount > 0 && (
<div className="p-2 flex justify-end bg-background">
<div className="px-1 pb-2">
<Button
variant="ghost"
size="sm"
className="h-7 px-3 text-xs font-medium text-muted-foreground hover:text-destructive transition-colors"
onClick={resetAll}
className="flex h-8 w-full items-center justify-start gap-2 px-2 text-xs font-medium text-destructive hover:bg-destructive/10 hover:text-destructive transition-colors"
>
Reset All Participants
<div className="flex h-4 w-4 items-center justify-center">
<X className="h-3.5 w-3.5" />
</div>
<span className="flex-1 text-left">
{t('search_contacts.reset_all')}
</span>
</Button>
</div>
)}
@@ -96,6 +107,7 @@ function ContactSelectorField({
onSelect: (email: string | undefined) => void
onReset: () => void
}) {
const { t } = useTranslation()
const [searchTerm, setSearchTerm] = React.useState("")
const { contacts, isLoading } = useContacts(searchTerm)
@@ -129,7 +141,7 @@ function ContactSelectorField({
: "text-xs text-muted-foreground/90"
)}
>
{value || 'Any'}
{value || t('search_contacts.any')}
</span>
</div>
@@ -161,16 +173,16 @@ function ContactSelectorField({
>
<Command shouldFilter={false}>
<CommandInput
placeholder={`Search ${label}...`}
placeholder={t('search_contacts.search_placeholder', { field: label })}
className="h-9"
value={searchTerm}
onValueChange={setSearchTerm}
/>
<CommandList className="max-h-[360px]">
{isLoading && (
<div className="p-4 text-xs text-center opacity-50">Loading...</div>
<div className="p-4 text-xs text-center opacity-50">{t('search_contacts.loading')}</div>
)}
<CommandEmpty>No contact found.</CommandEmpty>
<CommandEmpty>{t('search_contacts.no_contact_found')}</CommandEmpty>
<CommandGroup>
{contacts.slice(0, 100).map((email) => (
<CommandItem
@@ -193,7 +205,7 @@ function ContactSelectorField({
))}
{contacts.length > 100 && (
<div className="px-3 py-2 text-[10px] text-center text-muted-foreground border-t border-border/50">
Showing top 100 results {contacts.length} total
{t('search_contacts.showing_limit', { total: contacts.length })}
</div>
)}
</CommandGroup>
+1 -1
View File
@@ -21,7 +21,7 @@ import React from 'react'
import { EmailEnvelope } from '@/api'
import { SortingState } from '@tanstack/react-table'
export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'search-form' | 'restore'
export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'restore'
interface SearchContextType {
open: SearchDialogType | null
+5 -2
View File
@@ -2,11 +2,13 @@ import { X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useSearchContext } from "./context"
import { cn } from "@/lib/utils"
import { useTranslation } from "react-i18next";
export function FilterResetButton() {
const { filter, setFilter } = useSearchContext();
const { t } = useTranslation()
const { q, ...restFilters } = filter;
const activeFiltersCount = Object.keys(restFilters).filter(key => {
const value = restFilters[key];
if (Array.isArray(value)) return value.length > 0;
@@ -24,8 +26,9 @@ export function FilterResetButton() {
"h-8 px-2 text-xs gap-1.5 font-normal",
"text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
)}
title={t('search_reset.tooltip')}
>
<span>Reset</span>
<span>{t('search_reset.label')}</span>
<div className="flex items-center justify-center w-4 h-4 rounded-full bg-muted-foreground/20 text-[10px]">
{activeFiltersCount}
</div>
-32
View File
@@ -21,7 +21,6 @@ import { Card, CardContent } from '@/components/ui/card';
import { FixedHeader } from '@/components/layout/fixed-header';
import { Main } from '@/components/layout/main';
import { useSearchMessages } from '@/hooks/use-search-messages';
import { SearchFormDialog } from './search-form';
import { EnvelopeListPagination } from '@/components/pagination';
import React from 'react';
import { EmailEnvelope } from '@/api';
@@ -49,15 +48,12 @@ export default function Search() {
total,
totalPages,
isLoading,
isFetching,
page,
pageSize,
setPage,
setPageSize,
setSortBy,
setSortOrder,
onSubmit,
reset,
filter,
setFilter
} = useSearchMessages();
@@ -67,12 +63,6 @@ export default function Search() {
setPageSize(pageSize)
}
const handleReset = () => {
reset();
setSelectedTags([]);
};
const handleTagToggle = (tag: string) => {
setSelectedTags(prev =>
prev.includes(tag)
@@ -125,21 +115,6 @@ export default function Search() {
</Card>
)}
{/* {!isLoading && total === 0 && <div className="flex h-[750px] shrink-0 items-center justify-center rounded-md border border-dashed">
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
<img
src={Logo}
className="max-h-[100px] w-auto opacity-20 saturate-0 transition-all duration-300 hover:opacity-100 hover:saturate-100 object-contain"
alt="Bichon Logo"
/>
<h3 className="mt-4 text-lg font-semibold">{t('search.noEmailsFound')}</h3>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
{Object.keys(filter).length === 0
? t('search.startSearching')
: t('search.adjustSearch')}
</p>
</div>
</div>} */}
<MailListTable
isLoading={isLoading}
items={emails}
@@ -180,13 +155,6 @@ export default function Search() {
onOpenChange={() => setOpen('edit-tags')}
/>
<SearchFormDialog
key='search-form-dialog'
onSubmit={onSubmit} isLoading={isLoading || isFetching} reset={handleReset}
open={open === 'search-form'}
onOpenChange={() => setOpen('search-form')}
/>
<RestoreMessageDialog
key='restore-mail-dialog'
open={open === 'restore'}
+8 -6
View File
@@ -1,6 +1,7 @@
import * as React from 'react'
import { ChevronDown, Folders, X } from 'lucide-react'
import { useQueries } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import {
Popover,
@@ -31,6 +32,7 @@ import useMinimalAccountList from '@/hooks/use-minimal-account-list'
import { useSearchContext } from './context'
export function MailboxPopover() {
const { t } = useTranslation()
const { filter, setFilter } = useSearchContext()
const { minimalList = [] } = useMinimalAccountList()
@@ -123,7 +125,7 @@ export function MailboxPopover() {
)}
>
<Folders className="h-4 w-4" />
Mailbox
{t('search_mailbox.label')}
{selectedMailboxIds.length > 0 && (
<span className="ml-1 text-xs opacity-70">
{selectedMailboxIds.length}
@@ -138,7 +140,7 @@ export function MailboxPopover() {
<Input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search mailbox"
placeholder={t('search_mailbox.search_placeholder')}
className="h-8 text-sm"
/>
</div>
@@ -151,14 +153,14 @@ export function MailboxPopover() {
className="h-7 w-full justify-start text-xs text-muted-foreground hover:text-destructive transition-colors"
>
<X className="mr-2 h-3.5 w-3.5" />
Clear Mailboxes ({selectedMailboxIds.length})
{t('search_mailbox.clear_mailboxes')} ({selectedMailboxIds.length})
</Button>
</div>
)}
<ScrollArea className="h-96 p-1">
{disabled ? (
<p className="px-3 py-2 text-xs text-muted-foreground">
Please select account first
{t('search_mailbox.select_account_first')}
</p>
) : isLoading ? (
<div className="space-y-2 p-2">
@@ -171,7 +173,7 @@ export function MailboxPopover() {
</div>
) : grouped.length === 0 ? (
<p className="px-3 py-2 text-xs text-muted-foreground">
No mailbox found
{t('search_mailbox.no_mailbox_found')}
</p>
) : (
<Accordion
@@ -258,4 +260,4 @@ export function MailboxPopover() {
</PopoverContent>
</Popover>
)
}
}
@@ -94,7 +94,7 @@ export function MoreFiltersPopover() {
)}
>
<ListFilter className="h-3.5 w-3.5" />
<span className="text-xs">Advanced</span>
<span className="text-xs">{t('search_more.trigger_label')}</span>
{activeCount > 0 && (
<Badge className="ml-1 h-4 px-1 text-[10px] bg-primary text-primary-foreground border-none rounded-sm">
{activeCount}
@@ -105,7 +105,7 @@ export function MoreFiltersPopover() {
<PopoverContent align="end" className="w-72 p-4 flex flex-col gap-4">
<div className="flex items-center justify-between">
<h4 className="text-xs font-medium">Advanced Filters</h4>
<h4 className="text-xs font-medium">{t('search_more.title')}</h4>
{activeCount > 0 && (
<Button
variant="ghost"
@@ -123,7 +123,7 @@ export function MoreFiltersPopover() {
setOpen(false);
}}
>
Reset
{t('search_more.reset')}
</Button>
)}
</div>
@@ -140,22 +140,22 @@ export function MoreFiltersPopover() {
htmlFor="has_attachment"
className="text-xs font-normal cursor-pointer select-none"
>
Has Attachments
{t('search_more.has_attachment')}
</Label>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Attachment Name</Label>
<Label className="text-xs text-muted-foreground">{t('search_more.attachment_name_label')}</Label>
<Input
className="h-8 text-xs"
value={localState.attachment_name}
onChange={(e) => setLocalState(prev => ({ ...prev, attachment_name: e.target.value }))}
placeholder="e.g. invoice.pdf"
placeholder={t('search_more.attachment_name_placeholder')}
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Message Size</Label>
<Label className="text-xs text-muted-foreground">{t('search_more.message_size_label')}</Label>
<Select
value={localState.size_preset}
onValueChange={(v) => setLocalState(prev => ({ ...prev, size_preset: v }))}
@@ -164,30 +164,29 @@ export function MoreFiltersPopover() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem className="text-xs" value="any">{t('search.any')}</SelectItem>
<SelectItem className="text-xs" value="tiny">{t('search.tiny')}</SelectItem>
<SelectItem className="text-xs" value="small">{t('search.small')}</SelectItem>
<SelectItem className="text-xs" value="medium">{t('search.medium')}</SelectItem>
<SelectItem className="text-xs" value="large">{t('search.large')}</SelectItem>
<SelectItem className="text-xs" value="huge">{t('search.huge')}</SelectItem>
{Object.keys(SIZES).concat('any').map((key) => (
<SelectItem key={key} className="text-xs" value={key}>
{t(`search_more.size_presets.${key}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Original Message ID</Label>
<Label className="text-xs text-muted-foreground">{t('search_more.message_id_label')}</Label>
<Input
className="h-8 text-xs"
value={localState.message_id}
onChange={(e) => setLocalState(prev => ({ ...prev, message_id: e.target.value }))}
/>
<p className="text-[10px] text-muted-foreground opacity-70 leading-tight">
{t('search.originalMessageIdHeader')}
{t('search_more.message_id_description')}
</p>
</div>
<Button size="sm" className="w-full h-8 text-xs mt-2" onClick={handleApply}>
Apply Filters
{t('search_more.apply')}
</Button>
</PopoverContent>
</Popover>
-506
View File
@@ -1,506 +0,0 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { DatePicker } from "@/components/date-picker";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ChevronDown, ChevronUp, Filter, RotateCcw } from "lucide-react";
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from "react";
import { useForm } from "react-hook-form";
import { VirtualizedSelect } from "@/components/virtualized-select";
import useMinimalAccountList from "@/hooks/use-minimal-account-list";
import { useNavigate } from "@tanstack/react-router";
import { list_mailboxes, MailboxData } from "@/api/mailbox/api";
import { useQueries } from "@tanstack/react-query";
import { useSearchContext } from "./context";
import { toast } from "@/hooks/use-toast";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { useTranslation } from "react-i18next";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
const searchFilterSchema = z.object({
text: z.string().optional().or(z.literal("")),
from: z
.string()
.optional()
.or(z.literal("")),
to: z
.string()
.optional()
.or(z.literal("")),
cc: z
.string()
.optional()
.or(z.literal("")),
bcc: z
.string()
.optional()
.or(z.literal("")),
has_attachment: z.boolean().optional(),
attachment_name: z.string().optional().or(z.literal("")),
since: z.date().optional(),
before: z.date().optional(),
account_ids: z.array(z.number()),
mailbox_ids: z.array(z.number()),
size_preset: z.enum(['any', 'tiny', 'small', 'medium', 'large', 'huge']).optional(),
message_id: z.string().optional().or(z.literal("")),
});
type SearchFilterForm = z.infer<typeof searchFilterSchema>;
interface Props {
onSubmit: (values: Record<string, any>) => void,
isLoading: boolean,
reset: () => void,
open: boolean,
onOpenChange: (open: boolean) => void;
}
const isEmptyValue = (value: any): boolean => {
if (value === null || value === undefined) return true;
if (value === '') return true;
if (typeof value === 'string' && value.trim() === '') return true;
if (typeof value === 'number' && isNaN(value)) return true;
if (value === false) return true;
if (value === 0) return true;
if (Array.isArray(value) && value.length === 0) return true;
return false;
};
const cleanEmpty = <T extends Record<string, any>>(obj: T): Partial<T> => {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => !isEmptyValue(value))
) as Partial<T>;
};
function withSizePreset(values: Record<string, any>) {
const { size_preset, ...rest } = values;
switch (size_preset) {
case 'tiny':
return { ...rest, max_size: 15 * 1024 };
case 'small':
return { ...rest, max_size: 2 * 1024 * 1024 };
case 'medium':
return { ...rest, min_size: 2 * 1024 * 1024, max_size: 10 * 1024 * 1024 };
case 'large':
return { ...rest, min_size: 10 * 1024 * 1024, max_size: 20 * 1024 * 1024 };
case 'huge':
return { ...rest, min_size: 20 * 1024 * 1024 };
default:
return rest;
}
}
export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChange }: Props) {
const { t } = useTranslation()
const [showAdvanced, setShowAdvanced] = useState(false);
const [selectedAccountIds, setSelectedAccountIds] = useState<number[]>([]);
const { accountsOptions, isLoading: accountsIsLoading } = useMinimalAccountList();
const { selectedTags } = useSearchContext();
const form = useForm<SearchFilterForm>({
resolver: zodResolver(searchFilterSchema),
defaultValues: {
text: "",
from: "",
to: "",
cc: "",
bcc: "",
attachment_name: "",
message_id: "",
size_preset: 'any',
has_attachment: false,
since: undefined,
before: undefined,
account_ids: [],
mailbox_ids: [],
},
mode: "onChange",
});
const navigate = useNavigate();
const { mailboxes, isMailboxesLoading } = useQueries({
queries: selectedAccountIds.map((id) => ({
queryKey: ['search-account-mailboxes', id],
queryFn: () => list_mailboxes(id!, false),
})),
combine: (results) => ({
mailboxes: results.flatMap((result) => result.data?.sort((a, b) => a.name.localeCompare(b.name))),
isMailboxesLoading: results.some((result) => result.isLoading),
}),
})
const mailboxesOptions = mailboxes?.filter((item) => !!item).map((mailbox: MailboxData) => ({
value: mailbox.id.toString(),
label: mailbox.name,
description: accountsOptions.find((item) => Number(item.value) === mailbox.account_id)!.label
})) || [];
const handleSubmit = (values: Record<string, any>) => {
let cleaned = cleanEmpty(values);
const payload = withSizePreset(cleaned);
const finalPayload =
selectedTags.length > 0
? { ...payload, tags: selectedTags }
: payload;
if (Object.keys(finalPayload).length > 0) {
onSubmit(finalPayload);
} else {
toast({
title: t('search.pleaseSelectAtLeastOne'),
});
}
}
const handleClear = () => {
form.reset({
text: "",
from: "",
to: "",
cc: "",
bcc: "",
has_attachment: false,
attachment_name: "",
since: undefined,
before: undefined,
account_ids: [],
mailbox_ids: [],
size_preset: 'any',
message_id: "",
});
setSelectedAccountIds([]);
}
return (<Sheet
open={open}
onOpenChange={onOpenChange}
>
<SheetContent className='w-full md:max-w-4xl mx-auto'>
<SheetHeader className="p-4 pb-3 border-b shrink-0">
<div className="flex items-center justify-between">
<SheetTitle className="flex items-center gap-2">
{t('search.searchArchivedEmails')}
</SheetTitle>
</div>
</SheetHeader>
<SheetDescription>
{t('search.fullTextMultiAccount')}
</SheetDescription>
<Form {...form}>
<form id="email-search-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<FormField
control={form.control}
name="account_ids"
render={({ field }) => (
<FormItem className="min-w-[180px]">
<div className="flex flex-col gap-2">
<FormLabel className="text-xs whitespace-nowrap">{t('search.account')}</FormLabel>
<FormControl className="flex-1">
<VirtualizedSelect
options={accountsOptions}
isLoading={accountsIsLoading}
onSelectOption={(values) => {
const ids = values.map((id) => parseInt(id, 10)).sort()
setSelectedAccountIds(ids)
field.onChange(ids)
}}
value={field.value.map(String)}
placeholder={t('search.selectAccount')}
className="h-10 w-full"
noItemsComponent={
<div className="p-2">
<p className="text-xs">{t('search.noActiveEmailAccount')}</p>
<Button
variant="outline"
size="sm"
onClick={() => navigate({ to: "/accounts" })}
>
{t('search.addEmailAccount')}
</Button>
</div>
}
multiple
/>
</FormControl>
</div>
<FormMessage className="text-xs" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="mailbox_ids"
render={({ field }) => (
<FormItem className="min-w-[180px]">
<div className="flex flex-col gap-2">
<FormLabel className="text-xs whitespace-nowrap">{t('search.mailbox')}</FormLabel>
<FormControl className="flex-1">
<VirtualizedSelect
options={mailboxesOptions}
isLoading={isMailboxesLoading}
onSelectOption={(values) => {
field.onChange(values.map((id) => parseInt(id, 10)))
}}
value={field.value.map(String)}
placeholder={t('search.selectMailbox')}
className="h-10 w-full"
noItemsComponent={
<div className="p-2">
<p className="text-xs">
{t('search.noMailboxSelectAccount')}
</p>
</div>
}
multiple
/>
</FormControl>
</div>
<FormMessage className="text-xs" />
</FormItem>
)}
/>
</div>
<div className="flex flex-col sm:flex-row gap-3 items-stretch">
<FormField
control={form.control}
name="text"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input
placeholder={t('search.searchInSubjectBody')}
className="h-11 text-base"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-2 sm:ml-auto sm:self-center">
<Button type="submit" className="h-11 px-6" disabled={isLoading}>
{isLoading ? t('search.searchingButton') : <>{t('search.searchButton')}</>}
</Button>
<Button
type="button"
variant="outline"
className="h-11"
onClick={() => setShowAdvanced(!showAdvanced)}
>
<Filter className="w-4 h-4 mr-1" />
{t('search.advanced')}
{showAdvanced ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
</Button>
<Button
type="button"
variant="outline"
className="h-11"
onClick={() => { handleClear(); reset(); }}
>
<RotateCcw className="w-4 h-4 mr-1" />
{t('search.clear')}
</Button>
</div>
</div>
<div className="flex flex-wrap items-center gap-4 text-sm">
<FormField
control={form.control}
name="since"
render={({ field }) => (
<FormItem className="flex items-center gap-2">
<FormLabel className="text-xs whitespace-nowrap">{t('search.since')}</FormLabel>
<FormControl>
<DatePicker
placeholder={t('search.selectDate')}
selected={field.value}
onSelect={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="before"
render={({ field }) => (
<FormItem className="flex items-center gap-2">
<FormLabel className="text-xs whitespace-nowrap">{t('search.before')}</FormLabel>
<FormControl>
<DatePicker
placeholder={t('search.selectDate')}
selected={field.value}
onSelect={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="has_attachment"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<Checkbox
id="attach"
checked={field.value}
onCheckedChange={field.onChange}
/>
<FormLabel htmlFor="attach" className="cursor-pointer text-sm font-normal">
{t('search.hasAttachment')}
</FormLabel>
</FormItem>
)}
/>
</div>
</div>
<div>
{showAdvanced && <Accordion type="multiple" className="space-y-3">
<AccordionItem value="people">
<AccordionTrigger className="text-sm">
{t('search.sender')} / {t('search.recipient')}
</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 pt-2">
{(['from', 'to', 'cc', 'bcc'] as const).map((key) => (
<FormField
key={key}
control={form.control}
name={key}
render={({ field }) => (
<FormItem>
<FormLabel className="capitalize text-xs">
{key === 'from' ? t('search.from') : key === 'to' ? t('search.to') : key === 'cc' ? t('search.cc') : t('search.bcc')}:
</FormLabel>
<FormControl>
<Input
placeholder={`${key}@example.com`}
className="h-9"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="attachment">
<AccordionTrigger className="text-sm">
{t('search.attachmentsSize')}
</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 pt-2">
<FormField
control={form.control}
name="attachment_name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs">{t('search.attachmentName')}:</FormLabel>
<FormControl>
<Input placeholder="invoice.pdf" className="h-9" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="size_preset"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs">
{t('search.size')}
</FormLabel>
<Select
value={field.value}
onValueChange={(value) => field.onChange(value)}
>
<FormControl>
<SelectTrigger className="h-9">
<SelectValue placeholder={t('search.any')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="any">{t('search.any')}</SelectItem>
<SelectItem value="tiny">{t('search.tiny')}</SelectItem>
<SelectItem value="small">{t('search.small')}</SelectItem>
<SelectItem value="medium">{t('search.medium')}</SelectItem>
<SelectItem value="large">{t('search.large')}</SelectItem>
<SelectItem value="huge">{t('search.huge')}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
<FormDescription className="text-xs">
{t('search.sizeDescription')}
</FormDescription>
</FormItem>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="ids">
<AccordionTrigger className="text-sm">
{t('search.messageId')}
</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 pt-2">
<FormField
control={form.control}
name="message_id"
render={({ field }) => (
<FormItem className="col-span-full">
<FormControl>
<Input placeholder="<abc123@example.com>" className="h-9" {...field} />
</FormControl>
<FormDescription className="text-xs">
{t('search.originalMessageIdHeader')}
</FormDescription>
</FormItem>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>}
</div>
</form>
</Form>
</SheetContent>
</Sheet>);
}
+1 -1
View File
@@ -98,7 +98,7 @@ export function SearchTable({ columns, data, onRowClick, setSortBy, setSortOrder
return (
<div className="flex flex-1 flex-col gap-0.5">
{children && (<>{children(table)}</>)}
<ScrollArea className='h-[calc(100vh-13rem)] rounded-md border' orientation='both'>
<ScrollArea className='h-[calc(100vh-15rem)] rounded-md border' orientation='both'>
<ShadcnTable>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -22,7 +22,6 @@ const defaultColumns = (t: (key: string) => string) => [
{ label: t('search.from'), value: "from" },
{ label: t('search.to'), value: "to" },
{ label: t('search.subject'), value: "subject" },
{ label: t('mail.attachments'), value: "attachments" },
{ label: t('search.size'), value: "size" },
{ label: t('search.date'), value: "date" },
]
@@ -54,11 +53,11 @@ export function DataTableViewOptions<TData>({
className='ms-auto hidden h-8 lg:flex rounded-none'
>
<MixerHorizontalIcon className='size-4' />
View
{t('search_view.button_label')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[150px]'>
<DropdownMenuLabel className='text-xs'>Toggle columns</DropdownMenuLabel>
<DropdownMenuLabel className='text-xs'>{t('search_view.menu_title')}</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
-127
View File
@@ -1,127 +0,0 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ChevronDown, ChevronUp, Tag } from 'lucide-react';
import React from 'react';
import { useAvailableTags } from '@/hooks/use-available-tags';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Label } from '@/components/ui/label';
import { useTranslation } from 'react-i18next';
interface EnvelopeTagsProps {
selectedTags: string[];
onTagToggle: (tag: string) => void;
}
export function EnvelopeTags({ selectedTags, onTagToggle }: EnvelopeTagsProps) {
const { t } = useTranslation()
const [open, setOpen] = React.useState(true);
const {
tagsCount: tagsCount = [],
isLoading: tagsIsLoading,
} = useAvailableTags();
const sortedTags = React.useMemo(() => {
return [...tagsCount].sort((a, b) => b.count - a.count);
}, [tagsCount]);
if (tagsIsLoading) {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="h-4 w-32 bg-muted animate-pulse rounded" />
</div>
<div className="space-y-1.5">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex items-center gap-3 px-2 py-1.5">
<div className="h-4 w-4 bg-muted animate-pulse rounded" />
<div className="h-4 flex-1 bg-muted animate-pulse rounded" />
<div className="h-5 w-10 bg-muted animate-pulse rounded" />
</div>
))}
</div>
</div>
);
}
return (
<Collapsible open={open} onOpenChange={setOpen} className="space-y-2">
<CollapsibleTrigger className="flex w-full items-center justify-between text-sm font-medium hover:text-primary transition-colors">
<div className="flex items-center gap-2">
<Tag className="w-4 h-4" />
{t('mail.tags')}
{selectedTags.length > 0 && (
<Badge variant="secondary" className="ml-1.5 h-5 px-1.5 text-xs">
{selectedTags.length}
</Badge>
)}
</div>
{open ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</CollapsibleTrigger>
<CollapsibleContent className="space-y-0">
{sortedTags.length === 0 ? (
<p className="py-2 pl-2 text-sm text-muted-foreground">{t('mail.noTagsYet')}</p>
) : (
<ScrollArea className="h-[calc(100vh-12rem)] w-full pr-4 -mr-4">
{sortedTags.map(({ tag: facet, count }) => {
const checked = selectedTags.includes(facet);
const id = `tag-${facet}`;
return (
<div
key={facet}
className="flex items-center gap-3 px-2 py-0.5 hover:bg-accent/80 rounded-md transition-colors cursor-pointer group"
onClick={() => onTagToggle(facet)}
>
<Checkbox
id={id}
checked={checked}
onCheckedChange={() => onTagToggle(facet)}
onClick={(e) => e.stopPropagation()}
className="h-4 w-4"
/>
<Label
htmlFor={id}
className="flex-1 max-w-[140px] lg:max-w-[120px] cursor-pointer truncate text-sm font-medium"
title={facet}
>
{facet}
</Label>
<div className="shrink-0 ml-2">
<Badge
variant="secondary"
className="h-5 px-1.5 text-xs font-medium min-w-[1.75rem] text-center"
>
{count}
</Badge>
</div>
</div>
);
})}
</ScrollArea>
)}
</CollapsibleContent>
</Collapsible>
);
}
@@ -86,7 +86,7 @@ export function TagFilterPopover() {
)}
>
<Tag className="h-4 w-4" />
{t('mail.tags')}
{t('tag.label')}
{selectedTags.length > 0 && (
<Badge
variant="secondary"
@@ -107,7 +107,7 @@ export function TagFilterPopover() {
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('mail.searchTags')}
placeholder={t('tag.search_placeholder')}
className="h-8 text-sm"
autoFocus
/>
@@ -123,7 +123,7 @@ export function TagFilterPopover() {
<X className="h-3 w-3" />
</div>
<span className="flex-1 text-xs font-medium">
{t('common.clear_all_tags')}
{t('tag.clear_all')}
</span>
<span className="text-[10px] opacity-60">({selectedTags.length})</span>
</div>
@@ -141,7 +141,7 @@ export function TagFilterPopover() {
</div>
) : filteredTags.length === 0 ? (
<p className="px-3 py-2 text-xs text-muted-foreground">
{t('mail.noTagsFound')}
{t('tag.no_tags_found')}
</p>
) : (
filteredTags.map(({ tag, count }) => {
@@ -190,4 +190,4 @@ export function TagFilterPopover() {
</PopoverContent>
</Popover>
)
}
}
+52 -38
View File
@@ -1,14 +1,16 @@
import React, { useState, useEffect, useRef } from "react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Search, X, Clock, Trash2 } from "lucide-react"
import { Search, X, Clock, Trash2, Info } from "lucide-react"
import { cn } from "@/lib/utils"
import { useSearchContext } from "./context"
import { useTranslation } from "react-i18next"
const STORAGE_KEY = "mail_search_history"
const MAX_HISTORY = 20
export function TextSearchInput() {
const { t } = useTranslation()
const { filter, setFilter } = useSearchContext()
const [value, setValue] = useState(filter.text || "")
const [history, setHistory] = useState<string[]>([])
@@ -31,6 +33,7 @@ export function TextSearchInput() {
setValue(filter.text || "")
}, [filter.text])
const saveToHistory = (term: string) => {
if (!term.trim()) return
@@ -82,6 +85,7 @@ export function TextSearchInput() {
const handleSelectHistory = (term: string) => {
setValue(term)
setShowHistory(false)
// 如果需要点击历史立即搜索,可以在这里调用 handleSearch()
}
const handleClearHistory = () => {
@@ -108,49 +112,59 @@ export function TextSearchInput() {
return (
<div ref={containerRef} className="relative w-full max-w-[550px] min-w-[280px]">
<div className="relative flex items-center gap-1.5">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setShowHistory(true)}
onKeyDown={handleKeyDown}
placeholder='Search messages... (use "double quotes" for exact phrases)'
className={cn(
"h-9 pl-9 pr-9 text-sm",
isActive && "border-primary/50 focus-visible:ring-primary/30"
<div className="flex flex-col gap-1.5">
<div className="relative flex items-center gap-1.5">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setShowHistory(true)}
onKeyDown={handleKeyDown}
placeholder={t('search_input.placeholder')}
className={cn(
"h-9 pl-9 pr-9 text-sm",
isActive && "border-primary/50 focus-visible:ring-primary/30"
)}
/>
{value && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={handleClear}
>
<X className="h-4 w-4" />
</Button>
)}
/>
{value && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={handleClear}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<Button
size="sm"
className="h-9 px-5"
onClick={handleSearch}
disabled={!value.trim()}
>
{t('search_input.button')}
</Button>
</div>
<Button
size="sm"
className="h-9 px-5"
onClick={handleSearch}
disabled={!value.trim()}
>
Search
</Button>
{/* 搜索范围提示 */}
<div className="flex items-center gap-1 px-1 opacity-60">
<Info className="h-3 w-3 text-muted-foreground" />
<span className="text-[10px] text-muted-foreground">
{t('search_input.hint')}
</span>
</div>
</div>
{showHistory && (
<div className="absolute top-full left-0 w-full mt-1 bg-popover border rounded-md shadow-md z-50 max-h-[280px] overflow-auto">
<div className="py-1.5 px-3 text-xs text-muted-foreground font-medium border-b flex items-center justify-between">
<div className="absolute top-10 left-0 w-full mt-1 bg-popover border rounded-md shadow-md z-50 max-h-[280px] overflow-auto">
<div className="py-1.5 px-3 text-xs text-muted-foreground font-medium border-b flex items-center justify-between sticky top-0 bg-popover z-10">
<div className="flex items-center gap-1.5">
<Clock className="h-3 w-3" />
Recent searches
{t('search_input.recent_title')}
</div>
{history.length > 0 && (
<button
@@ -158,7 +172,7 @@ export function TextSearchInput() {
className="text-xs text-destructive hover:text-destructive/80 flex items-center gap-1 hover:underline"
>
<Trash2 className="h-3 w-3" />
Clear all
{t('search_input.clear_history')}
</button>
)}
</div>
@@ -176,7 +190,7 @@ export function TextSearchInput() {
))
) : (
<div className="px-3 py-4 text-xs text-center text-muted-foreground">
No recent searches
{t('search_input.no_history')}
</div>
)}
</div>
+31 -26
View File
@@ -1,5 +1,7 @@
import * as React from 'react'
import { CalendarRange, ChevronDown, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { format } from 'date-fns'
import {
Popover,
PopoverContent,
@@ -14,12 +16,24 @@ import { DatePicker } from '@/components/date-picker'
const DAY = 86400000
export function TimePopover() {
const { t } = useTranslation()
const { filter, setFilter } = useSearchContext()
const [customDays, setCustomDays] = React.useState<string>('')
const since = filter.since
const before = filter.before
const toDate = (ts: number) => {
return format(ts, t('time.format'))
}
const label = (s?: number, b?: number) => {
if (!s && !b) return t('time.label')
if (s && b) return `${toDate(s)}${toDate(b)}`
if (s) return `${t('time.since')} ${toDate(s)}`
return `${t('time.before')} ${toDate(b!)}`
}
const setRange = (s?: number, b?: number) => {
setFilter(prev => {
const next = { ...prev }
@@ -62,23 +76,23 @@ export function TimePopover() {
</PopoverTrigger>
<PopoverContent align="start" className="w-[530px] p-4 space-y-6">
<Section title="Recent Range (Since...)">
<Section title={t('time.recent_range')}>
<div className="space-y-4 w-full">
<div className="flex flex-wrap gap-2">
{[1, 7, 30].map(d => (
<Quick key={d} onClick={() => setRange(Date.now() - d * DAY, undefined)}>
Last {d === 1 ? 'day' : `${d} days`}
{d === 1 ? t('time.last_day') : t('time.last_days', { count: d })}
</Quick>
))}
{[3, 6].map(m => (
<Quick key={m} onClick={() => setRange(Date.now() - m * 30 * DAY, undefined)}>
Last {m} months
{t('time.last_months', { count: m })}
</Quick>
))}
</div>
<div className="flex items-center gap-2 pt-3 border-t border-border/50">
<span className="text-[10px] uppercase font-bold opacity-40 shrink-0">Recent:</span>
<span className="text-[10px] uppercase font-bold opacity-40 shrink-0">{t('time.recent_prefix')}</span>
<Input
type="number"
min={1}
@@ -88,19 +102,19 @@ export function TimePopover() {
onChange={e => setCustomDays(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleApplyRecent()}
/>
<span className="text-xs text-muted-foreground shrink-0">days ago to now</span>
<span className="text-xs text-muted-foreground shrink-0">{t('time.days_ago_to_now')}</span>
<Button
size="sm"
variant="secondary"
className="h-8 px-3 ml-auto text-xs"
onClick={handleApplyRecent}
>
Apply
{t('time.apply')}
</Button>
</div>
</div>
</Section>
<Section title="Historical (Older than...)">
<Section title={t('time.historical')}>
<div className="flex flex-wrap gap-2 w-full">
{[1, 2, 3, 5, 10].map(y => (
<Quick
@@ -108,25 +122,28 @@ export function TimePopover() {
onClick={() => setRange(undefined, Date.now() - y * 365 * DAY)}
className="border-orange-200 hover:border-orange-400 hover:text-orange-600"
>
Over {y} {y === 1 ? 'year' : 'years'} ago
{t('time.over_years_ago', {
count: y,
unit: y === 1 ? t('time.year') : t('time.years')
})}
</Quick>
))}
</div>
</Section>
<Section title="Absolute Date Range">
<Section title={t('time.absolute_range')}>
<div className="flex gap-3 w-full">
<div className="flex-1 min-w-0 space-y-1.5">
<span className="text-[10px] pl-1 opacity-50 font-medium">SINCE</span>
<span className="text-[10px] pl-1 opacity-50 font-medium">{t('time.since').toUpperCase()}</span>
<DatePicker
placeholder="Start date"
placeholder={t('time.start_date')}
selected={since ? new Date(since) : undefined}
onSelect={(date) => setSince(date?.getTime())}
/>
</div>
<div className="flex-1 min-w-0 space-y-1.5">
<span className="text-[10px] pl-1 opacity-50 font-medium">BEFORE</span>
<span className="text-[10px] pl-1 opacity-50 font-medium">{t('time.before').toUpperCase()}</span>
<DatePicker
placeholder="End date"
placeholder={t('time.end_date')}
selected={before ? new Date(before) : undefined}
onSelect={(date) => setBefore(date?.getTime())}
/>
@@ -143,7 +160,7 @@ export function TimePopover() {
className="h-7 w-full justify-start text-xs text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
>
<X className="mr-2 h-3.5 w-3.5" />
Clear time filters
{t('time.clear_filters')}
</Button>
</div>
)}
@@ -152,18 +169,6 @@ export function TimePopover() {
)
}
function toDate(ts: number) {
const d = new Date(ts)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
function label(s?: number, b?: number) {
if (!s && !b) return 'Time'
if (s && b) return `${toDate(s)}${toDate(b)}`
if (s) return `Since ${toDate(s)}`
return `Older than ${toDate(b!)}`
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="flex flex-col items-start w-full">
+7 -2
View File
@@ -20,18 +20,24 @@
import { EmailEnvelope, PaginatedResponse } from '@/api';
import { search_messages } from '@/api/search/api';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { useState } from 'react';
export function useSearchMessages() {
// const queryClient = useQueryClient();
const [filter, setFilter] = useState<Record<string, any>>({});
const [filter, _setFilter] = useState<Record<string, any>>({});
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(30);
const [sortBy, setSortBy] = useState<"DATE" | "SIZE">("DATE");
const [sortOrder, setSortOrder] = useState<"desc" | "asc">("desc");
const setFilter = React.useCallback((val: any) => {
_setFilter(val);
setPage(1);
}, []);
const onSubmit = (cleaned: Record<string, any>) => {
if ('has_attachment' in cleaned && cleaned.has_attachment === false) {
delete cleaned.has_attachment;
@@ -51,7 +57,6 @@ export function useSearchMessages() {
const reset = () => {
setFilter({});
setPage(1);
}
const {
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "تمت استعادة الرسائل المختارة إلى خادم IMAP بنجاح.",
"failed": "فشلت استعادة الرسائل",
"failedTitle": "فشل الاستعادة"
},
"time": {
"label": "الوقت",
"since": "منذ",
"before": "قبل",
"recent_range": "النطاق الأخير (منذ...)",
"historical": "النطاق السابق (قبل...)",
"absolute_range": "نطاق تاريخ محدد",
"last_day": "آخر يوم واحد",
"last_days": "آخر {{count}} أيام",
"last_months": "آخر {{count}} أشهر",
"over_years_ago": "منذ {{count}} {{unit}}",
"year": "سنة",
"years": "سنوات",
"recent_prefix": "الأخيرة:",
"days_ago_to_now": "منذ أيام حتى الآن",
"apply": "تطبيق",
"start_date": "تاريخ البدء",
"end_date": "تاريخ الانتهاء",
"clear_filters": "مسح فلترة الوقت",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "الوسوم",
"search_placeholder": "بحث عن الوسوم...",
"clear_all": "مسح جميع الوسوم",
"no_tags_found": "لم يتم العثور على وسوم"
},
"search_accounts": {
"label": "حسابات البريد",
"search_placeholder": "بحث عن حسابات البريد...",
"clear_accounts": "مسح الحسابات المحددة",
"no_accounts_found": "لم يتم العثور على حسابات"
},
"search_mailbox": {
"label": "مجلدات البريد",
"search_placeholder": "بحث في المجلدات",
"clear_mailboxes": "مسح المجلدات المحددة",
"select_account_first": "يرجى اختيار حساب بريد أولاً",
"no_mailbox_found": "لم يتم العثور على مجلد"
},
"search_contacts": {
"label": "جهات الاتصال",
"label_with_count": "جهات الاتصال ({{count}})",
"any": "الكل",
"search_placeholder": "بحث في {{field}}...",
"reset_all": "إعادة تعيين جميع جهات الاتصال",
"no_contact_found": "لم يتم العثور على جهات اتصال",
"loading": "جارٍ التحميل...",
"showing_limit": "عرض أول 100 نتيجة فقط • المجموع {{total}}"
},
"search_more": {
"trigger_label": "متقدم",
"title": "فلاتر متقدمة",
"reset": "إعادة تعيين",
"has_attachment": "يحتوي على مرفق",
"attachment_name_label": "اسم المرفق",
"attachment_name_placeholder": "مثال: invoice.pdf",
"message_size_label": "حجم الرسالة",
"message_id_label": "معرّف Message-ID الأصلي",
"message_id_description": "البحث باستخدام Message-ID في ترويسة البريد",
"apply": "تطبيق الفلاتر",
"size_presets": {
"any": "أي حجم",
"tiny": "صغير جداً (< 15 كيلوبايت)",
"small": "صغير (< 2 ميغابايت)",
"medium": "متوسط (2 10 ميغابايت)",
"large": "كبير (10 20 ميغابايت)",
"huge": "ضخم (> 20 ميغابايت)"
}
},
"search_view": {
"button_label": "العرض",
"menu_title": "إعدادات الأعمدة"
},
"search_reset": {
"label": "إعادة تعيين",
"tooltip": "مسح جميع شروط التصفية"
},
"search_input": {
"placeholder": "البحث في البريد... (استخدم \"علامات الاقتباس\" للمطابقة التامة)",
"button": "بحث",
"recent_title": "عمليات البحث الأخيرة",
"clear_history": "مسح السجل",
"no_history": "لا يوجد سجل بحث",
"hint": "نطاق البحث الافتراضي: العنوان، المحتوى، وأسماء المرفقات"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "De valgte meddelelser er blevet gendannet til IMAP-serveren.",
"failed": "Kunne ikke gendanne meddelelser",
"failedTitle": "Gendannelse mislykkedes"
},
"time": {
"label": "Tid",
"since": "Siden",
"before": "Før",
"recent_range": "Seneste interval (siden...)",
"historical": "Historisk interval (før...)",
"absolute_range": "Absolut datointerval",
"last_day": "Seneste 1 dag",
"last_days": "Seneste {{count}} dage",
"last_months": "Seneste {{count}} måneder",
"over_years_ago": "For {{count}} {{unit}} siden",
"year": "år",
"years": "år",
"recent_prefix": "Seneste:",
"days_ago_to_now": "dage siden til nu",
"apply": "Anvend",
"start_date": "Startdato",
"end_date": "Slutdato",
"clear_filters": "Ryd tidsfiltre",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Tags",
"search_placeholder": "Søg tags...",
"clear_all": "Ryd alle tags",
"no_tags_found": "Ingen tags fundet"
},
"search_accounts": {
"label": "Mailkonti",
"search_placeholder": "Søg mailkonti...",
"clear_accounts": "Ryd valgte konti",
"no_accounts_found": "Ingen konti fundet"
},
"search_mailbox": {
"label": "Postkasser",
"search_placeholder": "Søg postkasser",
"clear_mailboxes": "Ryd valgte mapper",
"select_account_first": "Vælg venligst en mailkonto først",
"no_mailbox_found": "Ingen mappe fundet"
},
"search_contacts": {
"label": "Kontakter",
"label_with_count": "Kontakter ({{count}})",
"any": "Alle",
"search_placeholder": "Søg i {{field}}...",
"reset_all": "Nulstil alle kontakter",
"no_contact_found": "Ingen kontakter fundet",
"loading": "Indlæser...",
"showing_limit": "Viser kun de første 100 resultater • I alt {{total}}"
},
"search_more": {
"trigger_label": "Avanceret",
"title": "Avancerede filtre",
"reset": "Nulstil",
"has_attachment": "Har vedhæftning",
"attachment_name_label": "Navn på vedhæftning",
"attachment_name_placeholder": "F.eks.: invoice.pdf",
"message_size_label": "Mailstørrelse",
"message_id_label": "Original Message-ID",
"message_id_description": "Søg via Message-ID i mailheaderen",
"apply": "Anvend filtre",
"size_presets": {
"any": "Alle størrelser",
"tiny": "Meget lille (< 15 KB)",
"small": "Lille (< 2 MB)",
"medium": "Mellem (2 10 MB)",
"large": "Stor (10 20 MB)",
"huge": "Meget stor (> 20 MB)"
}
},
"search_view": {
"button_label": "Visning",
"menu_title": "Indstillinger for kolonner"
},
"search_reset": {
"label": "Nulstil",
"tooltip": "Ryd alle filtre"
},
"search_input": {
"placeholder": "Søg i mails... (brug \"anførselstegn\" til nøjagtig match)",
"button": "Søg",
"recent_title": "Seneste søgninger",
"clear_history": "Ryd historik",
"no_history": "Ingen søgehistorik",
"hint": "Standard søgeområde: emne, indhold og vedhæftningsnavne"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "Die ausgewählten Nachrichten wurden erfolgreich auf dem IMAP-Server wiederhergestellt.",
"failed": "Wiederherstellung fehlgeschlagen",
"failedTitle": "Fehler bei der Wiederherstellung"
},
"time": {
"label": "Zeit",
"since": "Seit",
"before": "Vor",
"recent_range": "Letzter Zeitraum (seit …)",
"historical": "Historischer Zeitraum (vor …)",
"absolute_range": "Absoluter Datumsbereich",
"last_day": "Letzter 1 Tag",
"last_days": "Letzte {{count}} Tage",
"last_months": "Letzte {{count}} Monate",
"over_years_ago": "Vor {{count}} {{unit}}",
"year": "Jahr",
"years": "Jahre",
"recent_prefix": "Kürzlich:",
"days_ago_to_now": "Tage bis heute",
"apply": "Anwenden",
"start_date": "Startdatum",
"end_date": "Enddatum",
"clear_filters": "Zeitfilter löschen",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Tags",
"search_placeholder": "Tags suchen...",
"clear_all": "Alle Tags löschen",
"no_tags_found": "Keine Tags gefunden"
},
"search_accounts": {
"label": "E-Mail-Konten",
"search_placeholder": "E-Mail-Konten suchen...",
"clear_accounts": "Ausgewählte Konten löschen",
"no_accounts_found": "Keine Konten gefunden"
},
"search_mailbox": {
"label": "Postfächer",
"search_placeholder": "Postfächer durchsuchen",
"clear_mailboxes": "Ausgewählte Ordner löschen",
"select_account_first": "Bitte zuerst ein E-Mail-Konto auswählen",
"no_mailbox_found": "Kein Ordner gefunden"
},
"search_contacts": {
"label": "Kontakte",
"label_with_count": "Kontakte ({{count}})",
"any": "Alle",
"search_placeholder": "{{field}} suchen...",
"reset_all": "Alle Kontakte zurücksetzen",
"no_contact_found": "Keine Kontakte gefunden",
"loading": "Wird geladen...",
"showing_limit": "Nur die ersten 100 Ergebnisse angezeigt • Gesamt {{total}}"
},
"search_more": {
"trigger_label": "Erweitert",
"title": "Erweiterte Filter",
"reset": "Zurücksetzen",
"has_attachment": "Mit Anhang",
"attachment_name_label": "Anhangname",
"attachment_name_placeholder": "z. B. invoice.pdf",
"message_size_label": "Nachrichtengröße",
"message_id_label": "Originale Message-ID",
"message_id_description": "Suche über die Message-ID im E-Mail-Header",
"apply": "Filter anwenden",
"size_presets": {
"any": "Beliebige Größe",
"tiny": "Sehr klein (< 15 KB)",
"small": "Klein (< 2 MB)",
"medium": "Mittel (2 10 MB)",
"large": "Groß (10 20 MB)",
"huge": "Sehr groß (> 20 MB)"
}
},
"search_view": {
"button_label": "Ansicht",
"menu_title": "Spalteneinstellungen"
},
"search_reset": {
"label": "Zurücksetzen",
"tooltip": "Alle Filter entfernen"
},
"search_input": {
"placeholder": "E-Mails durchsuchen... (\"Anführungszeichen\" für exakte Übereinstimmung)",
"button": "Suchen",
"recent_title": "Letzte Suchanfragen",
"clear_history": "Verlauf löschen",
"no_history": "Kein Suchverlauf",
"hint": "Standard-Suchbereich: Betreff, Inhalt und Anhangnamen"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "The selected messages have been successfully restored to the IMAP server.",
"failed": "Failed to restore messages",
"failedTitle": "Restore Failed"
},
"time": {
"label": "Time",
"since": "Since",
"before": "Before",
"recent_range": "Recent range (since...)",
"historical": "Historical range (before...)",
"absolute_range": "Absolute date range",
"last_day": "Last 1 day",
"last_days": "Last {{count}} days",
"last_months": "Last {{count}} months",
"over_years_ago": "{{count}} {{unit}} ago",
"year": "year",
"years": "years",
"recent_prefix": "Recent:",
"days_ago_to_now": "days ago to now",
"apply": "Apply",
"start_date": "Start date",
"end_date": "End date",
"clear_filters": "Clear time filters",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Tags",
"search_placeholder": "Search tags...",
"clear_all": "Clear all tags",
"no_tags_found": "No tags found"
},
"search_accounts": {
"label": "Mail accounts",
"search_placeholder": "Search mail accounts...",
"clear_accounts": "Clear selected accounts",
"no_accounts_found": "No accounts found"
},
"search_mailbox": {
"label": "Mailboxes",
"search_placeholder": "Search mailboxes",
"clear_mailboxes": "Clear selected mailboxes",
"select_account_first": "Please select an account first",
"no_mailbox_found": "No mailbox found"
},
"search_contacts": {
"label": "Contacts",
"label_with_count": "Contacts ({{count}})",
"any": "Any",
"search_placeholder": "Search {{field}}...",
"reset_all": "Reset all contacts",
"no_contact_found": "No contacts found",
"loading": "Loading...",
"showing_limit": "Showing first 100 results • Total {{total}}"
},
"search_more": {
"trigger_label": "Advanced",
"title": "Advanced filters",
"reset": "Reset",
"has_attachment": "Has attachment",
"attachment_name_label": "Attachment name",
"attachment_name_placeholder": "e.g. invoice.pdf",
"message_size_label": "Message size",
"message_id_label": "Original Message-ID",
"message_id_description": "Search by Message-ID header",
"apply": "Apply filters",
"size_presets": {
"any": "Any size",
"tiny": "Tiny (< 15 KB)",
"small": "Small (< 2 MB)",
"medium": "Medium (2 10 MB)",
"large": "Large (10 20 MB)",
"huge": "Huge (> 20 MB)"
}
},
"search_view": {
"button_label": "View",
"menu_title": "Column settings"
},
"search_reset": {
"label": "Reset",
"tooltip": "Clear all filters"
},
"search_input": {
"placeholder": "Search emails... (use \"quotes\" for exact match)",
"button": "Search",
"recent_title": "Recent searches",
"clear_history": "Clear history",
"no_history": "No search history",
"hint": "Default scope: subject, body and attachment names"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "Los mensajes seleccionados se han restaurado correctamente en el servidor IMAP.",
"failed": "Error al restaurar los mensajes",
"failedTitle": "Error de restauración"
},
"time": {
"label": "Tiempo",
"since": "Desde",
"before": "Antes de",
"recent_range": "Rango reciente (desde...)",
"historical": "Rango histórico (antes de...)",
"absolute_range": "Rango de fechas absoluto",
"last_day": "Último 1 día",
"last_days": "Últimos {{count}} días",
"last_months": "Últimos {{count}} meses",
"over_years_ago": "Hace {{count}} {{unit}}",
"year": "año",
"years": "años",
"recent_prefix": "Reciente:",
"days_ago_to_now": "días atrás hasta ahora",
"apply": "Aplicar",
"start_date": "Fecha de inicio",
"end_date": "Fecha de fin",
"clear_filters": "Borrar filtros de tiempo",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Etiquetas",
"search_placeholder": "Buscar etiquetas...",
"clear_all": "Borrar todas las etiquetas",
"no_tags_found": "No se encontraron etiquetas"
},
"search_accounts": {
"label": "Cuentas de correo",
"search_placeholder": "Buscar cuentas de correo...",
"clear_accounts": "Borrar cuentas seleccionadas",
"no_accounts_found": "No se encontraron cuentas"
},
"search_mailbox": {
"label": "Buzones",
"search_placeholder": "Buscar buzones",
"clear_mailboxes": "Borrar carpetas seleccionadas",
"select_account_first": "Seleccione primero una cuenta de correo",
"no_mailbox_found": "No se encontró ningún buzón"
},
"search_contacts": {
"label": "Contactos",
"label_with_count": "Contactos ({{count}})",
"any": "Cualquiera",
"search_placeholder": "Buscar {{field}}...",
"reset_all": "Restablecer todos los contactos",
"no_contact_found": "No se encontraron contactos",
"loading": "Cargando...",
"showing_limit": "Mostrando solo los primeros 100 resultados • Total {{total}}"
},
"search_more": {
"trigger_label": "Avanzado",
"title": "Filtros avanzados",
"reset": "Restablecer",
"has_attachment": "Con archivo adjunto",
"attachment_name_label": "Nombre del archivo adjunto",
"attachment_name_placeholder": "Ej.: invoice.pdf",
"message_size_label": "Tamaño del mensaje",
"message_id_label": "Message-ID original",
"message_id_description": "Buscar usando el encabezado Message-ID del correo",
"apply": "Aplicar filtros",
"size_presets": {
"any": "Cualquier tamaño",
"tiny": "Muy pequeño (< 15 KB)",
"small": "Pequeño (< 2 MB)",
"medium": "Mediano (2 10 MB)",
"large": "Grande (10 20 MB)",
"huge": "Muy grande (> 20 MB)"
}
},
"search_view": {
"button_label": "Vista",
"menu_title": "Configuración de columnas"
},
"search_reset": {
"label": "Restablecer",
"tooltip": "Eliminar todos los filtros"
},
"search_input": {
"placeholder": "Buscar correos... (usa \"comillas\" para coincidencia exacta)",
"button": "Buscar",
"recent_title": "Búsquedas recientes",
"clear_history": "Borrar historial",
"no_history": "Sin historial de búsqueda",
"hint": "Ámbito de búsqueda predeterminado: asunto, contenido y nombres de archivos adjuntos"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "Valitut viestit on palautettu onnistuneesti IMAP-palvelimelle.",
"failed": "Viestien palautus epäonnistui",
"failedTitle": "Palautus epäonnistui"
},
"time": {
"label": "Aika",
"since": "Alkaen",
"before": "Ennen",
"recent_range": "Viimeisin aikaväli (alkaen...)",
"historical": "Historiallinen aikaväli (ennen...)",
"absolute_range": "Kiinteä päivämääräväli",
"last_day": "Viimeiset 1 päivä",
"last_days": "Viimeiset {{count}} päivää",
"last_months": "Viimeiset {{count}} kuukautta",
"over_years_ago": "{{count}} {{unit}} sitten",
"year": "vuosi",
"years": "vuotta",
"recent_prefix": "Viimeisin:",
"days_ago_to_now": "päivää sitten nyt",
"apply": "Käytä",
"start_date": "Aloituspäivä",
"end_date": "Päättymispäivä",
"clear_filters": "Tyhjennä aikasuodattimet",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Tunnisteet",
"search_placeholder": "Hae tunnisteita...",
"clear_all": "Tyhjennä kaikki tunnisteet",
"no_tags_found": "Tunnisteita ei löytynyt"
},
"search_accounts": {
"label": "Sähköpostitilit",
"search_placeholder": "Hae sähköpostitilejä...",
"clear_accounts": "Poista valitut tilit",
"no_accounts_found": "Tilejä ei löytynyt"
},
"search_mailbox": {
"label": "Postilaatikot",
"search_placeholder": "Hae postilaatikoita",
"clear_mailboxes": "Poista valitut kansiot",
"select_account_first": "Valitse ensin sähköpostitili",
"no_mailbox_found": "Kansiota ei löytynyt"
},
"search_contacts": {
"label": "Yhteystiedot",
"label_with_count": "Yhteystiedot ({{count}})",
"any": "Kaikki",
"search_placeholder": "Hae {{field}}...",
"reset_all": "Palauta kaikki yhteystiedot",
"no_contact_found": "Yhteystietoja ei löytynyt",
"loading": "Ladataan...",
"showing_limit": "Näytetään vain ensimmäiset 100 tulosta • Yhteensä {{total}}"
},
"search_more": {
"trigger_label": "Lisäasetukset",
"title": "Lisäsuodattimet",
"reset": "Palauta",
"has_attachment": "Sisältää liitteen",
"attachment_name_label": "Liitteen nimi",
"attachment_name_placeholder": "Esim.: invoice.pdf",
"message_size_label": "Viestin koko",
"message_id_label": "Alkuperäinen Message-ID",
"message_id_description": "Hae sähköpostin Message-ID-otsikon perusteella",
"apply": "Käytä suodattimia",
"size_presets": {
"any": "Mikä tahansa koko",
"tiny": "Erittäin pieni (< 15 KB)",
"small": "Pieni (< 2 MB)",
"medium": "Keskikokoinen (2 10 MB)",
"large": "Suuri (10 20 MB)",
"huge": "Erittäin suuri (> 20 MB)"
}
},
"search_view": {
"button_label": "Näkymä",
"menu_title": "Sarakeasetukset"
},
"search_reset": {
"label": "Palauta",
"tooltip": "Poista kaikki suodattimet"
},
"search_input": {
"placeholder": "Hae sähköposteja... (käytä \"lainausmerkkejä\" tarkkaan hakuun)",
"button": "Hae",
"recent_title": "Viimeisimmät haut",
"clear_history": "Tyhjennä historia",
"no_history": "Ei hakuhistoriaa",
"hint": "Oletushakualue: otsikko, sisältö ja liitteiden nimet"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "Les messages sélectionnés ont été restaurés avec succès sur le serveur IMAP.",
"failed": "Échec de la restauration des messages",
"failedTitle": "Échec de la restauration"
},
"time": {
"label": "Temps",
"since": "Depuis",
"before": "Avant",
"recent_range": "Période récente (depuis...)",
"historical": "Période historique (avant...)",
"absolute_range": "Plage de dates absolue",
"last_day": "Dernier jour",
"last_days": "Derniers {{count}} jours",
"last_months": "Derniers {{count}} mois",
"over_years_ago": "Il y a {{count}} {{unit}}",
"year": "an",
"years": "ans",
"recent_prefix": "Récent :",
"days_ago_to_now": "jours jusqu’à aujourdhui",
"apply": "Appliquer",
"start_date": "Date de début",
"end_date": "Date de fin",
"clear_filters": "Effacer les filtres de temps",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Étiquettes",
"search_placeholder": "Rechercher des étiquettes...",
"clear_all": "Effacer toutes les étiquettes",
"no_tags_found": "Aucune étiquette trouvée"
},
"search_accounts": {
"label": "Comptes e-mail",
"search_placeholder": "Rechercher des comptes e-mail...",
"clear_accounts": "Effacer les comptes sélectionnés",
"no_accounts_found": "Aucun compte trouvé"
},
"search_mailbox": {
"label": "Boîtes mail",
"search_placeholder": "Rechercher des dossiers",
"clear_mailboxes": "Effacer les dossiers sélectionnés",
"select_account_first": "Veuillez dabord sélectionner un compte e-mail",
"no_mailbox_found": "Aucun dossier trouvé"
},
"search_contacts": {
"label": "Contacts",
"label_with_count": "Contacts ({{count}})",
"any": "Tous",
"search_placeholder": "Rechercher {{field}}...",
"reset_all": "Réinitialiser tous les contacts",
"no_contact_found": "Aucun contact trouvé",
"loading": "Chargement...",
"showing_limit": "Affichage des 100 premiers résultats uniquement • Total {{total}}"
},
"search_more": {
"trigger_label": "Avancé",
"title": "Filtres avancés",
"reset": "Réinitialiser",
"has_attachment": "Contient une pièce jointe",
"attachment_name_label": "Nom de la pièce jointe",
"attachment_name_placeholder": "Ex. : invoice.pdf",
"message_size_label": "Taille du message",
"message_id_label": "Message-ID dorigine",
"message_id_description": "Recherche via len-tête Message-ID de le-mail",
"apply": "Appliquer les filtres",
"size_presets": {
"any": "Toutes tailles",
"tiny": "Très petite (< 15 Ko)",
"small": "Petite (< 2 Mo)",
"medium": "Moyenne (2 10 Mo)",
"large": "Grande (10 20 Mo)",
"huge": "Très grande (> 20 Mo)"
}
},
"search_view": {
"button_label": "Affichage",
"menu_title": "Paramètres des colonnes"
},
"search_reset": {
"label": "Réinitialiser",
"tooltip": "Effacer tous les filtres"
},
"search_input": {
"placeholder": "Rechercher des e-mails... (utilisez les \"guillemets\" pour une correspondance exacte)",
"button": "Rechercher",
"recent_title": "Recherches récentes",
"clear_history": "Effacer lhistorique",
"no_history": "Aucun historique de recherche",
"hint": "Portée par défaut : objet, contenu et noms des pièces jointes"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "I messaggi selezionati sono stati ripristinati con successo sul server IMAP.",
"failed": "Impossibile ripristinare i messaggi",
"failedTitle": "Ripristino fallito"
},
"time": {
"label": "Tempo",
"since": "Da",
"before": "Prima di",
"recent_range": "Intervallo recente (da...)",
"historical": "Intervallo storico (prima di...)",
"absolute_range": "Intervallo di date assoluto",
"last_day": "Ultimo 1 giorno",
"last_days": "Ultimi {{count}} giorni",
"last_months": "Ultimi {{count}} mesi",
"over_years_ago": "{{count}} {{unit}} fa",
"year": "anno",
"years": "anni",
"recent_prefix": "Recenti:",
"days_ago_to_now": "giorni fa fino ad oggi",
"apply": "Applica",
"start_date": "Data di inizio",
"end_date": "Data di fine",
"clear_filters": "Cancella filtri temporali",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Tag",
"search_placeholder": "Cerca tag...",
"clear_all": "Cancella tutti i tag",
"no_tags_found": "Nessun tag trovato"
},
"search_accounts": {
"label": "Account email",
"search_placeholder": "Cerca account email...",
"clear_accounts": "Cancella account selezionati",
"no_accounts_found": "Nessun account trovato"
},
"search_mailbox": {
"label": "Caselle di posta",
"search_placeholder": "Cerca cartelle",
"clear_mailboxes": "Cancella cartelle selezionate",
"select_account_first": "Seleziona prima un account email",
"no_mailbox_found": "Nessuna cartella trovata"
},
"search_contacts": {
"label": "Contatti",
"label_with_count": "Contatti ({{count}})",
"any": "Qualsiasi",
"search_placeholder": "Cerca {{field}}...",
"reset_all": "Reimposta tutti i contatti",
"no_contact_found": "Nessun contatto trovato",
"loading": "Caricamento...",
"showing_limit": "Mostrati solo i primi 100 risultati • Totale {{total}}"
},
"search_more": {
"trigger_label": "Avanzato",
"title": "Filtri avanzati",
"reset": "Reimposta",
"has_attachment": "Con allegato",
"attachment_name_label": "Nome allegato",
"attachment_name_placeholder": "Es.: invoice.pdf",
"message_size_label": "Dimensione email",
"message_id_label": "Message-ID originale",
"message_id_description": "Ricerca tramite Message-ID nellintestazione email",
"apply": "Applica filtri",
"size_presets": {
"any": "Qualsiasi dimensione",
"tiny": "Molto piccola (< 15 KB)",
"small": "Piccola (< 2 MB)",
"medium": "Media (2 10 MB)",
"large": "Grande (10 20 MB)",
"huge": "Molto grande (> 20 MB)"
}
},
"search_view": {
"button_label": "Vista",
"menu_title": "Impostazioni colonne"
},
"search_reset": {
"label": "Reimposta",
"tooltip": "Cancella tutti i filtri"
},
"search_input": {
"placeholder": "Cerca email... (usa le \"virgolette\" per corrispondenza esatta)",
"button": "Cerca",
"recent_title": "Ricerche recenti",
"clear_history": "Cancella cronologia",
"no_history": "Nessuna cronologia di ricerca",
"hint": "Ambito predefinito: oggetto, contenuto e nomi degli allegati"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "選択したメッセージがIMAPサーバーに正常に復元されました。",
"failed": "メッセージの復元に失敗しました",
"failedTitle": "復元失敗"
},
"time": {
"label": "時間",
"since": "以降",
"before": "以前",
"recent_range": "最近の範囲(以降)",
"historical": "過去の範囲(以前)",
"absolute_range": "絶対日付範囲",
"last_day": "過去 1 日",
"last_days": "過去 {{count}} 日",
"last_months": "過去 {{count}} か月",
"over_years_ago": "{{count}}{{unit}}前",
"year": "年",
"years": "年",
"recent_prefix": "最近:",
"days_ago_to_now": "日前〜現在",
"apply": "適用",
"start_date": "開始日",
"end_date": "終了日",
"clear_filters": "時間フィルターをクリア",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "タグ",
"search_placeholder": "タグを検索...",
"clear_all": "すべてのタグをクリア",
"no_tags_found": "タグが見つかりません"
},
"search_accounts": {
"label": "メールアカウント",
"search_placeholder": "アカウントを検索...",
"clear_accounts": "選択したアカウントをクリア",
"no_accounts_found": "アカウントが見つかりません"
},
"search_mailbox": {
"label": "メールボックス",
"search_placeholder": "メールボックスを検索",
"clear_mailboxes": "選択したフォルダをクリア",
"select_account_first": "先にアカウントを選択してください",
"no_mailbox_found": "フォルダが見つかりません"
},
"search_contacts": {
"label": "連絡先",
"label_with_count": "連絡先 ({{count}})",
"any": "指定なし",
"search_placeholder": "{{field}} を検索...",
"reset_all": "連絡先をリセット",
"no_contact_found": "連絡先が見つかりません",
"loading": "読み込み中...",
"showing_limit": "最初の100件のみ表示 • 合計 {{total}} 件"
},
"search_more": {
"trigger_label": "詳細",
"title": "詳細フィルター",
"reset": "リセット",
"has_attachment": "添付ファイルあり",
"attachment_name_label": "添付ファイル名",
"attachment_name_placeholder": "例: invoice.pdf",
"message_size_label": "メールサイズ",
"message_id_label": "Message-ID",
"message_id_description": "メールヘッダーの Message-ID で検索",
"apply": "適用",
"size_presets": {
"any": "指定なし",
"tiny": "極小 (< 15 KB)",
"small": "小 (< 2 MB)",
"medium": "中 (2 10 MB)",
"large": "大 (10 20 MB)",
"huge": "特大 (> 20 MB)"
}
},
"search_view": {
"button_label": "表示",
"menu_title": "列の設定"
},
"search_reset": {
"label": "リセット",
"tooltip": "すべての条件をクリア"
},
"search_input": {
"placeholder": "メールを検索…(\"引用符\"で完全一致)",
"button": "検索",
"recent_title": "最近の検索",
"clear_history": "履歴をクリア",
"no_history": "履歴なし",
"hint": "検索対象:件名・本文・添付ファイル名"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "선택한 메시지가 IMAP 서버로 성공적으로 복원되었습니다.",
"failed": "메시지 복원 실패",
"failedTitle": "복원 실패"
},
"time": {
"label": "시간",
"since": "이후",
"before": "이전",
"recent_range": "최근 범위 (이후...)",
"historical": "과거 범위 (이전...)",
"absolute_range": "절대 날짜 범위",
"last_day": "최근 1일",
"last_days": "최근 {{count}}일",
"last_months": "최근 {{count}}개월",
"over_years_ago": "{{count}}{{unit}} 전",
"year": "년",
"years": "년",
"recent_prefix": "최근:",
"days_ago_to_now": "며칠 전부터 현재까지",
"apply": "적용",
"start_date": "시작 날짜",
"end_date": "종료 날짜",
"clear_filters": "시간 필터 초기화",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "태그",
"search_placeholder": "태그 검색...",
"clear_all": "모든 태그 지우기",
"no_tags_found": "태그를 찾을 수 없습니다"
},
"search_accounts": {
"label": "메일 계정",
"search_placeholder": "메일 계정 검색...",
"clear_accounts": "선택한 계정 지우기",
"no_accounts_found": "계정을 찾을 수 없습니다"
},
"search_mailbox": {
"label": "메일함",
"search_placeholder": "메일함 검색",
"clear_mailboxes": "선택한 폴더 지우기",
"select_account_first": "먼저 메일 계정을 선택하세요",
"no_mailbox_found": "폴더를 찾을 수 없습니다"
},
"search_contacts": {
"label": "연락처",
"label_with_count": "연락처 ({{count}})",
"any": "제한 없음",
"search_placeholder": "{{field}} 검색...",
"reset_all": "모든 연락처 초기화",
"no_contact_found": "연락처를 찾을 수 없습니다",
"loading": "불러오는 중...",
"showing_limit": "상위 100개 결과만 표시 • 전체 {{total}}개"
},
"search_more": {
"trigger_label": "고급",
"title": "고급 필터",
"reset": "초기화",
"has_attachment": "첨부파일 포함",
"attachment_name_label": "첨부파일 이름",
"attachment_name_placeholder": "예: invoice.pdf",
"message_size_label": "메일 크기",
"message_id_label": "원본 Message-ID",
"message_id_description": "메일 헤더의 Message-ID로 검색",
"apply": "필터 적용",
"size_presets": {
"any": "크기 제한 없음",
"tiny": "매우 작음 (< 15 KB)",
"small": "작음 (< 2 MB)",
"medium": "보통 (2 10 MB)",
"large": "큼 (10 20 MB)",
"huge": "매우 큼 (> 20 MB)"
}
},
"search_view": {
"button_label": "보기",
"menu_title": "표시 열 설정"
},
"search_reset": {
"label": "초기화",
"tooltip": "모든 필터 조건 지우기"
},
"search_input": {
"placeholder": "메일 검색... (\"따옴표\"로 정확히 일치)",
"button": "검색",
"recent_title": "최근 검색",
"clear_history": "기록 지우기",
"no_history": "검색 기록 없음",
"hint": "기본 검색 범위: 제목, 본문 및 첨부파일 이름"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "De geselecteerde berichten zijn succesvol hersteld op de IMAP-server.",
"failed": "Herstellen van berichten mislukt",
"failedTitle": "Herstel mislukt"
},
"time": {
"label": "Tijd",
"since": "Sinds",
"before": "Voor",
"recent_range": "Recente periode (sinds...)",
"historical": "Historische periode (voor...)",
"absolute_range": "Absolute datumbereik",
"last_day": "Laatste 1 dag",
"last_days": "Laatste {{count}} dagen",
"last_months": "Laatste {{count}} maanden",
"over_years_ago": "{{count}} {{unit}} geleden",
"year": "jaar",
"years": "jaar",
"recent_prefix": "Recent:",
"days_ago_to_now": "dagen geleden tot nu",
"apply": "Toepassen",
"start_date": "Startdatum",
"end_date": "Einddatum",
"clear_filters": "Tijdfilters wissen",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Tags",
"search_placeholder": "Tags zoeken...",
"clear_all": "Alle tags wissen",
"no_tags_found": "Geen tags gevonden"
},
"search_accounts": {
"label": "E-mailaccounts",
"search_placeholder": "E-mailaccounts zoeken...",
"clear_accounts": "Geselecteerde accounts wissen",
"no_accounts_found": "Geen accounts gevonden"
},
"search_mailbox": {
"label": "Mailboxen",
"search_placeholder": "Mailboxen zoeken",
"clear_mailboxes": "Geselecteerde mappen wissen",
"select_account_first": "Selecteer eerst een e-mailaccount",
"no_mailbox_found": "Geen mailbox gevonden"
},
"search_contacts": {
"label": "Contacten",
"label_with_count": "Contacten ({{count}})",
"any": "Alle",
"search_placeholder": "{{field}} zoeken...",
"reset_all": "Alle contacten resetten",
"no_contact_found": "Geen contacten gevonden",
"loading": "Bezig met laden...",
"showing_limit": "Alleen de eerste 100 resultaten • Totaal {{total}}"
},
"search_more": {
"trigger_label": "Geavanceerd",
"title": "Geavanceerde filters",
"reset": "Resetten",
"has_attachment": "Bevat bijlage",
"attachment_name_label": "Bijlagenaam",
"attachment_name_placeholder": "Bijv.: invoice.pdf",
"message_size_label": "Berichtgrootte",
"message_id_label": "Originele Message-ID",
"message_id_description": "Zoeken via de Message-ID in de e-mailheader",
"apply": "Filters toepassen",
"size_presets": {
"any": "Elke grootte",
"tiny": "Zeer klein (< 15 KB)",
"small": "Klein (< 2 MB)",
"medium": "Gemiddeld (2 10 MB)",
"large": "Groot (10 20 MB)",
"huge": "Zeer groot (> 20 MB)"
}
},
"search_view": {
"button_label": "Weergave",
"menu_title": "Kolominstellingen"
},
"search_reset": {
"label": "Resetten",
"tooltip": "Alle filters wissen"
},
"search_input": {
"placeholder": "E-mails zoeken... (gebruik \"aanhalingstekens\" voor exacte overeenkomst)",
"button": "Zoeken",
"recent_title": "Recente zoekopdrachten",
"clear_history": "Geschiedenis wissen",
"no_history": "Geen zoekgeschiedenis",
"hint": "Standaard zoekbereik: onderwerp, inhoud en bijlagenamen"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "De valgte meldingene har blitt gjenopprettet til IMAP-serveren.",
"failed": "Kunne ikke gjenopprette meldinger",
"failedTitle": "Gjenoppretting mislyktes"
},
"time": {
"label": "Tid",
"since": "Siden",
"before": "Før",
"recent_range": "Nylig periode (siden...)",
"historical": "Historisk periode (før...)",
"absolute_range": "Absolutt datointervall",
"last_day": "Siste 1 dag",
"last_days": "Siste {{count}} dager",
"last_months": "Siste {{count}} måneder",
"over_years_ago": "For {{count}} {{unit}} siden",
"year": "år",
"years": "år",
"recent_prefix": "Nylig:",
"days_ago_to_now": "dager siden til nå",
"apply": "Bruk",
"start_date": "Startdato",
"end_date": "Sluttdato",
"clear_filters": "Fjern tidsfiltre",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Etiketter",
"search_placeholder": "Søk etter etiketter...",
"clear_all": "Fjern alle etiketter",
"no_tags_found": "Ingen etiketter funnet"
},
"search_accounts": {
"label": "E-postkontoer",
"search_placeholder": "Søk etter e-postkontoer...",
"clear_accounts": "Fjern valgte kontoer",
"no_accounts_found": "Ingen kontoer funnet"
},
"search_mailbox": {
"label": "Postbokser",
"search_placeholder": "Søk i postbokser",
"clear_mailboxes": "Fjern valgte mapper",
"select_account_first": "Velg først en e-postkonto",
"no_mailbox_found": "Ingen mappe funnet"
},
"search_contacts": {
"label": "Kontakter",
"label_with_count": "Kontakter ({{count}})",
"any": "Alle",
"search_placeholder": "Søk i {{field}}...",
"reset_all": "Tilbakestill alle kontakter",
"no_contact_found": "Ingen kontakter funnet",
"loading": "Laster...",
"showing_limit": "Viser kun de første 100 resultatene • Totalt {{total}}"
},
"search_more": {
"trigger_label": "Avansert",
"title": "Avanserte filtre",
"reset": "Tilbakestill",
"has_attachment": "Har vedlegg",
"attachment_name_label": "Vedleggsnavn",
"attachment_name_placeholder": "F.eks.: invoice.pdf",
"message_size_label": "Meldingsstørrelse",
"message_id_label": "Opprinnelig Message-ID",
"message_id_description": "Søk via Message-ID i e-postheaderen",
"apply": "Bruk filtre",
"size_presets": {
"any": "Alle størrelser",
"tiny": "Svært liten (< 15 KB)",
"small": "Liten (< 2 MB)",
"medium": "Middels (2 10 MB)",
"large": "Stor (10 20 MB)",
"huge": "Svært stor (> 20 MB)"
}
},
"search_view": {
"button_label": "Visning",
"menu_title": "Kolonneinnstillinger"
},
"search_reset": {
"label": "Tilbakestill",
"tooltip": "Fjern alle filtre"
},
"search_input": {
"placeholder": "Søk i e-poster... (bruk \"anførselstegn\" for eksakt treff)",
"button": "Søk",
"recent_title": "Nylige søk",
"clear_history": "Tøm historikk",
"no_history": "Ingen søkehistorikk",
"hint": "Standard søkeområde: emne, innhold og vedleggsnavn"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "Wybrane wiadomości zostały pomyślnie przywrócone na serwer IMAP.",
"failed": "Nie udało się przywrócić wiadomości",
"failedTitle": "Przywracanie nie powiodło się"
},
"time": {
"label": "Czas",
"since": "Od",
"before": "Przed",
"recent_range": "Ostatni zakres (od...)",
"historical": "Zakres historyczny (przed...)",
"absolute_range": "Bezwzględny zakres dat",
"last_day": "Ostatni 1 dzień",
"last_days": "Ostatnie {{count}} dni",
"last_months": "Ostatnie {{count}} miesiące",
"over_years_ago": "{{count}} {{unit}} temu",
"year": "rok",
"years": "lata",
"recent_prefix": "Ostatnio:",
"days_ago_to_now": "dni temu do teraz",
"apply": "Zastosuj",
"start_date": "Data rozpoczęcia",
"end_date": "Data zakończenia",
"clear_filters": "Wyczyść filtry czasu",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Tagi",
"search_placeholder": "Szukaj tagów...",
"clear_all": "Wyczyść wszystkie tagi",
"no_tags_found": "Nie znaleziono tagów"
},
"search_accounts": {
"label": "Konta e-mail",
"search_placeholder": "Szukaj kont e-mail...",
"clear_accounts": "Wyczyść wybrane konta",
"no_accounts_found": "Nie znaleziono kont"
},
"search_mailbox": {
"label": "Skrzynki pocztowe",
"search_placeholder": "Szukaj skrzynek",
"clear_mailboxes": "Wyczyść wybrane foldery",
"select_account_first": "Najpierw wybierz konto e-mail",
"no_mailbox_found": "Nie znaleziono folderu"
},
"search_contacts": {
"label": "Kontakty",
"label_with_count": "Kontakty ({{count}})",
"any": "Dowolne",
"search_placeholder": "Szukaj {{field}}...",
"reset_all": "Zresetuj wszystkie kontakty",
"no_contact_found": "Nie znaleziono kontaktów",
"loading": "Ładowanie...",
"showing_limit": "Wyświetlane tylko pierwsze 100 wyników • Łącznie {{total}}"
},
"search_more": {
"trigger_label": "Zaawansowane",
"title": "Filtry zaawansowane",
"reset": "Resetuj",
"has_attachment": "Zawiera załącznik",
"attachment_name_label": "Nazwa załącznika",
"attachment_name_placeholder": "Np.: invoice.pdf",
"message_size_label": "Rozmiar wiadomości",
"message_id_label": "Oryginalny Message-ID",
"message_id_description": "Wyszukiwanie według nagłówka Message-ID",
"apply": "Zastosuj filtry",
"size_presets": {
"any": "Dowolny rozmiar",
"tiny": "Bardzo mały (< 15 KB)",
"small": "Mały (< 2 MB)",
"medium": "Średni (2 10 MB)",
"large": "Duży (10 20 MB)",
"huge": "Bardzo duży (> 20 MB)"
}
},
"search_view": {
"button_label": "Widok",
"menu_title": "Ustawienia kolumn"
},
"search_reset": {
"label": "Reset",
"tooltip": "Wyczyść wszystkie filtry"
},
"search_input": {
"placeholder": "Szukaj e-maili... (użyj \"cudzysłowów\" dla dokładnego dopasowania)",
"button": "Szukaj",
"recent_title": "Ostatnie wyszukiwania",
"clear_history": "Wyczyść historię",
"no_history": "Brak historii wyszukiwania",
"hint": "Domyślny zakres wyszukiwania: temat, treść i nazwy załączników"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "As mensagens selecionadas foram restauradas com sucesso para o servidor IMAP.",
"failed": "Falha ao restaurar mensagens",
"failedTitle": "Falha na restauração"
},
"time": {
"label": "Tempo",
"since": "Desde",
"before": "Antes de",
"recent_range": "Intervalo recente (desde...)",
"historical": "Intervalo histórico (antes de...)",
"absolute_range": "Intervalo de datas absoluto",
"last_day": "Último 1 dia",
"last_days": "Últimos {{count}} dias",
"last_months": "Últimos {{count}} meses",
"over_years_ago": "Há {{count}} {{unit}}",
"year": "ano",
"years": "anos",
"recent_prefix": "Recente:",
"days_ago_to_now": "dias atrás até agora",
"apply": "Aplicar",
"start_date": "Data de início",
"end_date": "Data de fim",
"clear_filters": "Limpar filtros de tempo",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Etiquetas",
"search_placeholder": "Pesquisar etiquetas...",
"clear_all": "Limpar todas as etiquetas",
"no_tags_found": "Nenhuma etiqueta encontrada"
},
"search_accounts": {
"label": "Contas de e-mail",
"search_placeholder": "Pesquisar contas de e-mail...",
"clear_accounts": "Limpar contas selecionadas",
"no_accounts_found": "Nenhuma conta encontrada"
},
"search_mailbox": {
"label": "Caixas de correio",
"search_placeholder": "Pesquisar pastas",
"clear_mailboxes": "Limpar pastas selecionadas",
"select_account_first": "Selecione primeiro uma conta de e-mail",
"no_mailbox_found": "Nenhuma pasta encontrada"
},
"search_contacts": {
"label": "Contatos",
"label_with_count": "Contatos ({{count}})",
"any": "Qualquer",
"search_placeholder": "Pesquisar {{field}}...",
"reset_all": "Redefinir todos os contatos",
"no_contact_found": "Nenhum contato encontrado",
"loading": "Carregando...",
"showing_limit": "Mostrando apenas os primeiros 100 resultados • Total {{total}}"
},
"search_more": {
"trigger_label": "Avançado",
"title": "Filtros avançados",
"reset": "Redefinir",
"has_attachment": "Contém anexo",
"attachment_name_label": "Nome do anexo",
"attachment_name_placeholder": "Ex.: invoice.pdf",
"message_size_label": "Tamanho da mensagem",
"message_id_label": "Message-ID original",
"message_id_description": "Pesquisar pelo Message-ID no cabeçalho do e-mail",
"apply": "Aplicar filtros",
"size_presets": {
"any": "Qualquer tamanho",
"tiny": "Muito pequeno (< 15 KB)",
"small": "Pequeno (< 2 MB)",
"medium": "Médio (2 10 MB)",
"large": "Grande (10 20 MB)",
"huge": "Muito grande (> 20 MB)"
}
},
"search_view": {
"button_label": "Visualização",
"menu_title": "Configurações de colunas"
},
"search_reset": {
"label": "Redefinir",
"tooltip": "Limpar todos os filtros"
},
"search_input": {
"placeholder": "Pesquisar e-mails... (use \"aspas\" para correspondência exata)",
"button": "Pesquisar",
"recent_title": "Pesquisas recentes",
"clear_history": "Limpar histórico",
"no_history": "Nenhum histórico de pesquisa",
"hint": "Escopo padrão: assunto, conteúdo e nomes dos anexos"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "Выбранные сообщения были успешно восстановлены на IMAP-сервере.",
"failed": "Не удалось восстановить сообщения",
"failedTitle": "Ошибка восстановления"
},
"time": {
"label": "Время",
"since": "С",
"before": "До",
"recent_range": "Недавний диапазон (с...)",
"historical": "Исторический диапазон (до...)",
"absolute_range": "Абсолютный диапазон дат",
"last_day": "Последний 1 день",
"last_days": "Последние {{count}} дней",
"last_months": "Последние {{count}} месяцев",
"over_years_ago": "{{count}} {{unit}} назад",
"year": "год",
"years": "лет",
"recent_prefix": "Недавно:",
"days_ago_to_now": "дней назад — по настоящее время",
"apply": "Применить",
"start_date": "Дата начала",
"end_date": "Дата окончания",
"clear_filters": "Очистить фильтр времени",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Теги",
"search_placeholder": "Поиск тегов...",
"clear_all": "Очистить все теги",
"no_tags_found": "Теги не найдены"
},
"search_accounts": {
"label": "Почтовые аккаунты",
"search_placeholder": "Поиск почтовых аккаунтов...",
"clear_accounts": "Очистить выбранные аккаунты",
"no_accounts_found": "Аккаунты не найдены"
},
"search_mailbox": {
"label": "Почтовые папки",
"search_placeholder": "Поиск папок",
"clear_mailboxes": "Очистить выбранные папки",
"select_account_first": "Сначала выберите почтовый аккаунт",
"no_mailbox_found": "Папки не найдены"
},
"search_contacts": {
"label": "Контакты",
"label_with_count": "Контакты ({{count}})",
"any": "Любые",
"search_placeholder": "Поиск по {{field}}...",
"reset_all": "Сбросить все контакты",
"no_contact_found": "Контакты не найдены",
"loading": "Загрузка...",
"showing_limit": "Показаны первые 100 результатов • Всего {{total}}"
},
"search_more": {
"trigger_label": "Дополнительно",
"title": "Расширенные фильтры",
"reset": "Сброс",
"has_attachment": "Есть вложение",
"attachment_name_label": "Имя вложения",
"attachment_name_placeholder": "Например: invoice.pdf",
"message_size_label": "Размер письма",
"message_id_label": "Исходный Message-ID",
"message_id_description": "Поиск по Message-ID из заголовков письма",
"apply": "Применить фильтры",
"size_presets": {
"any": "Любой размер",
"tiny": "Очень маленький (< 15 КБ)",
"small": "Маленький (< 2 МБ)",
"medium": "Средний (210 МБ)",
"large": "Большой (1020 МБ)",
"huge": "Очень большой (> 20 МБ)"
}
},
"search_view": {
"button_label": "Вид",
"menu_title": "Настройки отображаемых столбцов"
},
"search_reset": {
"label": "Сброс",
"tooltip": "Очистить все фильтры"
},
"search_input": {
"placeholder": "Поиск писем... (используйте \"кавычки\" для точного совпадения)",
"button": "Поиск",
"recent_title": "Недавние поиски",
"clear_history": "Очистить историю",
"no_history": "История поиска пуста",
"hint": "По умолчанию поиск выполняется по теме, содержимому и именам вложений"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "De valda meddelandena har återställts till IMAP-servern.",
"failed": "Misslyckades med att återställa meddelanden",
"failedTitle": "Återställning misslyckades"
},
"time": {
"label": "Tid",
"since": "Sedan",
"before": "Före",
"recent_range": "Senaste intervall (sedan...)",
"historical": "Historiskt intervall (före...)",
"absolute_range": "Absolut datumintervall",
"last_day": "Senaste 1 dagen",
"last_days": "Senaste {{count}} dagarna",
"last_months": "Senaste {{count}} månaderna",
"over_years_ago": "För {{count}} {{unit}} sedan",
"year": "år",
"years": "år",
"recent_prefix": "Senaste:",
"days_ago_to_now": "dagar sedan till nu",
"apply": "Använd",
"start_date": "Startdatum",
"end_date": "Slutdatum",
"clear_filters": "Rensa tidsfilter",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "Taggar",
"search_placeholder": "Sök taggar...",
"clear_all": "Rensa alla taggar",
"no_tags_found": "Inga taggar hittades"
},
"search_accounts": {
"label": "E-postkonton",
"search_placeholder": "Sök e-postkonton...",
"clear_accounts": "Rensa valda konton",
"no_accounts_found": "Inga konton hittades"
},
"search_mailbox": {
"label": "E-postmappar",
"search_placeholder": "Sök mappar",
"clear_mailboxes": "Rensa valda mappar",
"select_account_first": "Välj först ett e-postkonto",
"no_mailbox_found": "Inga mappar hittades"
},
"search_contacts": {
"label": "Kontakter",
"label_with_count": "Kontakter ({{count}})",
"any": "Alla",
"search_placeholder": "Sök {{field}}...",
"reset_all": "Återställ alla kontakter",
"no_contact_found": "Inga kontakter hittades",
"loading": "Laddar...",
"showing_limit": "Visar endast de första 100 resultaten • Totalt {{total}}"
},
"search_more": {
"trigger_label": "Avancerat",
"title": "Avancerade filter",
"reset": "Återställ",
"has_attachment": "Har bilaga",
"attachment_name_label": "Bilagans namn",
"attachment_name_placeholder": "t.ex. invoice.pdf",
"message_size_label": "Meddelandestorlek",
"message_id_label": "Ursprungligt Message-ID",
"message_id_description": "Sök via Message-ID i e-posthuvudet",
"apply": "Använd filter",
"size_presets": {
"any": "Valfri storlek",
"tiny": "Mycket liten (< 15 KB)",
"small": "Liten (< 2 MB)",
"medium": "Mellan (210 MB)",
"large": "Stor (1020 MB)",
"huge": "Mycket stor (> 20 MB)"
}
},
"search_view": {
"button_label": "Vy",
"menu_title": "Inställningar för kolumner"
},
"search_reset": {
"label": "Återställ",
"tooltip": "Rensa alla filter"
},
"search_input": {
"placeholder": "Sök e-post... (använd \"citattecken\" för exakt matchning)",
"button": "Sök",
"recent_title": "Senaste sökningar",
"clear_history": "Rensa historik",
"no_history": "Ingen sökhistorik",
"hint": "Standardomfattning: ämne, innehåll och bilagenamn"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "選定的郵件已成功還原到 IMAP 伺服器。",
"failed": "還原郵件失敗",
"failedTitle": "還原失敗"
},
"time": {
"label": "時間",
"since": "自",
"before": "早於",
"recent_range": "近期範圍(自…)",
"historical": "歷史範圍(早於…)",
"absolute_range": "絕對日期範圍",
"last_day": "最近 1 天",
"last_days": "最近 {{count}} 天",
"last_months": "最近 {{count}} 個月",
"over_years_ago": "{{count}} {{unit}} 前",
"year": "年",
"years": "年",
"recent_prefix": "最近:",
"days_ago_to_now": "天前至今",
"apply": "套用",
"start_date": "開始日期",
"end_date": "結束日期",
"clear_filters": "清除時間篩選",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "標籤",
"search_placeholder": "搜尋標籤…",
"clear_all": "清除所有標籤",
"no_tags_found": "未找到標籤"
},
"search_accounts": {
"label": "郵件帳戶",
"search_placeholder": "搜尋郵件帳戶…",
"clear_accounts": "清除已選帳戶",
"no_accounts_found": "未找到帳戶"
},
"search_mailbox": {
"label": "信箱資料夾",
"search_placeholder": "搜尋信箱資料夾",
"clear_mailboxes": "清除已選資料夾",
"select_account_first": "請先選擇郵件帳戶",
"no_mailbox_found": "未找到資料夾"
},
"search_contacts": {
"label": "聯絡人",
"label_with_count": "聯絡人({{count}}",
"any": "不限",
"search_placeholder": "搜尋 {{field}}…",
"reset_all": "重設所有聯絡人",
"no_contact_found": "未找到聯絡人",
"loading": "載入中…",
"showing_limit": "僅顯示前 100 筆結果 • 共 {{total}} 筆"
},
"search_more": {
"trigger_label": "進階",
"title": "進階篩選",
"reset": "重設",
"has_attachment": "包含附件",
"attachment_name_label": "附件名稱",
"attachment_name_placeholder": "例如:invoice.pdf",
"message_size_label": "郵件大小",
"message_id_label": "原始 Message-ID",
"message_id_description": "依郵件標頭中的 Message-ID 搜尋",
"apply": "套用篩選",
"size_presets": {
"any": "不限大小",
"tiny": "極小(< 15 KB",
"small": "較小(< 2 MB",
"medium": "一般(210 MB",
"large": "較大(1020 MB",
"huge": "極大(> 20 MB"
}
},
"search_view": {
"button_label": "檢視",
"menu_title": "顯示欄位設定"
},
"search_reset": {
"label": "重設",
"tooltip": "清除所有篩選條件"
},
"search_input": {
"placeholder": "搜尋郵件…(使用「雙引號」進行精確比對)",
"button": "搜尋",
"recent_title": "最近搜尋",
"clear_history": "清除紀錄",
"no_history": "尚無搜尋紀錄",
"hint": "預設搜尋範圍:郵件標題、內容與附件名稱"
}
}
+86
View File
@@ -1461,5 +1461,91 @@
"successDesc": "选定的邮件已成功恢复到 IMAP 服务器。",
"failed": "还原邮件失败",
"failedTitle": "还原失败"
},
"time": {
"label": "时间",
"since": "自",
"before": "早于",
"recent_range": "近期范围 (自...)",
"historical": "历史范围 (早于...)",
"absolute_range": "绝对日期范围",
"last_day": "最近 1 天",
"last_days": "最近 {{count}} 天",
"last_months": "最近 {{count}} 个月",
"over_years_ago": "{{count}} {{unit}}前",
"year": "年",
"years": "年",
"recent_prefix": "最近:",
"days_ago_to_now": "天前至今",
"apply": "应用",
"start_date": "开始日期",
"end_date": "结束日期",
"clear_filters": "清除时间筛选",
"format": "yyyy-MM-dd"
},
"tag": {
"label": "标签",
"search_placeholder": "搜索标签...",
"clear_all": "清除所有标签",
"no_tags_found": "未找到标签"
},
"search_accounts": {
"label": "邮件账户",
"search_placeholder": "搜索邮件账户...",
"clear_accounts": "清除已选账户",
"no_accounts_found": "未找到账户"
},
"search_mailbox": {
"label": "邮箱文件夹",
"search_placeholder": "搜索邮箱文件夹",
"clear_mailboxes": "清除已选文件夹",
"select_account_first": "请先选择邮件账户",
"no_mailbox_found": "未找到文件夹"
},
"search_contacts": {
"label": "联系人",
"label_with_count": "联系人 ({{count}})",
"any": "不限",
"search_placeholder": "搜索 {{field}}...",
"reset_all": "重置所有联系人",
"no_contact_found": "未找到联系人",
"loading": "加载中...",
"showing_limit": "仅显示前 100 条结果 • 总计 {{total}} 条"
},
"search_more": {
"trigger_label": "高级",
"title": "高级筛选",
"reset": "重置",
"has_attachment": "包含附件",
"attachment_name_label": "附件名称",
"attachment_name_placeholder": "例如: invoice.pdf",
"message_size_label": "邮件大小",
"message_id_label": "原始消息 ID",
"message_id_description": "通过邮件头中的 'Message-ID' 进行搜索",
"apply": "应用筛选",
"size_presets": {
"any": "不限大小",
"tiny": "极小 (< 15 KB)",
"small": "较小 (< 2 MB)",
"medium": "普通 (2 - 10 MB)",
"large": "较大 (10 - 20 MB)",
"huge": "极大 (> 20 MB)"
}
},
"search_view": {
"button_label": "视图",
"menu_title": "显示列设置"
},
"search_reset": {
"label": "重置",
"tooltip": "清除所有筛选条件"
},
"search_input": {
"placeholder": "搜索邮件... (使用 \"双引号\" 进行精确匹配)",
"button": "搜索",
"recent_title": "最近搜索",
"clear_history": "清空记录",
"no_history": "暂无搜索记录",
"hint": "默认搜索范围:邮件标题、正文及附件名称"
}
}