Merge branch 'feat/do-ux-p2-settings-misc' into feat/do-ux-p2-settings-fix

This commit is contained in:
Linus Rath
2026-11-04 12:39:55 +01:00
48 changed files with 691 additions and 103 deletions
@@ -0,0 +1,28 @@
import { render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import React from 'react';
import { RichTextEditor } from '../rich-text-editor';
// settings-p2-19: a three-line out-of-office reply doesn't need the
// composer's headings, code block and table. The short toolbar keeps bold,
// italic, underline, the lists and link.
async function setup(toolbar?: 'full' | 'short') {
let ready = false;
render(<RichTextEditor content="<p>Hello</p>" onChange={() => {}} toolbar={toolbar} onEditorReady={() => { ready = true; }} />);
await waitFor(() => expect(ready).toBe(true));
return Array.from(screen.getByRole('toolbar', { name: 'formatting' }).querySelectorAll<HTMLElement>('[data-toolbar-item]'))
.map((el) => el.dataset.toolbarItem);
}
describe('the short formatting toolbar', () => {
it('has only bold, italic, underline, the lists and link', async () => {
expect(await setup('short')).toEqual(['bold', 'italic', 'underline', 'bullet-list', 'ordered-list', 'link']);
});
it('leaves the full toolbar as it was', async () => {
const items = await setup();
for (const name of ['font-size', 'heading-1', 'code-block', 'table', 'text-color', 'undo']) expect(items).toContain(name);
});
});
+12
View File
@@ -178,6 +178,11 @@ interface RichTextEditorProps {
* away entirely.
*/
toolbarPlacement?: 'top' | 'bottom' | 'hidden';
/**
* "short" keeps bold, italic, underline, the lists and link: enough for a
* few lines such as an out-of-office reply. "full" is the composer's.
*/
toolbar?: 'full' | 'short';
/** The editing area's accessible name ("Message"). */
ariaLabel?: string;
/** The body failed validation: exposed as aria-invalid on the editing area. */
@@ -478,6 +483,7 @@ export function RichTextEditor({
onMentionAdd,
collapseToolbarOnMobile = false,
toolbarPlacement = 'top',
toolbar = 'full',
ariaLabel,
ariaInvalid,
ariaDescribedBy,
@@ -911,7 +917,9 @@ export function RichTextEditor({
// A group that doesn't fit renders in the "More formatting" panel instead, in row order.
const folded = new Set<FoldableGroup>(FOLDABLE_GROUPS.slice(0, foldCount).map((group) => group.id));
const overflow: React.ReactNode[] = [];
const short = toolbar === 'short';
const place = (group: FoldableGroup, node: React.ReactNode) => {
if (short) return null;
if (!folded.has(group)) return node;
overflow.push(<React.Fragment key={group}>{node}</React.Fragment>);
return null;
@@ -1016,6 +1024,7 @@ export function RichTextEditor({
>
{/* Font size - two baseline-aligned A's read as "size" at a glance;
once a size is chosen the button shows that number instead. */}
{!short && <>
<div ref={fontSizeWrapperRef} className="relative">
<ToolbarButton
ref={fontSizeButtonRef}
@@ -1081,6 +1090,7 @@ export function RichTextEditor({
</div>
<ToolbarSeparator />
</>}
<ToolbarButton
item="bold"
@@ -1106,6 +1116,7 @@ export function RichTextEditor({
>
<UnderlineIcon className="w-4 h-4" />
</ToolbarButton>
{!short && <>
<ToolbarButton
item="strike"
pressed={editor.isActive("strike")}
@@ -1199,6 +1210,7 @@ export function RichTextEditor({
</ToolbarPopover>
)}
</div>
</>}
<ToolbarSeparator />
+52 -10
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useCallback, useMemo, useRef } from "react";
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { useLocale, useTranslations } from "next-intl";
import { sortFolderTree } from "@/lib/move-menu";
import { Button } from "@/components/ui/button";
@@ -41,6 +41,16 @@ interface FilterRuleModalProps {
suggestions?: RuleSuggestion[];
/** Offer "Also apply to existing messages in this folder". */
offerApplyToExisting?: boolean;
/**
* The folder the offer is about, by name ("Inbox"), when the dialog isn't
* opened from that folder (Settings › Filters).
*/
applyExistingFolder?: string;
/**
* How many messages in that folder the rule as typed would change. The
* offer says the number, and is greyed when it is 0.
*/
countExisting?: (rule: FilterRule) => Promise<number>;
mailboxes: Mailbox[];
/** Server cap on redirects per message (Sieve `maxNumberRedirects`). */
maxRedirects?: number | null;
@@ -198,6 +208,8 @@ export function FilterRuleModal({
initialRule,
suggestions = [],
offerApplyToExisting = false,
applyExistingFolder,
countExisting,
mailboxes,
maxRedirects,
forwardsBefore = 0,
@@ -345,6 +357,30 @@ export function FilterRuleModal({
return retroactiveSupport({ conditions: validConditions, actions: checkActions }).ok;
}, [offerApplyToExisting, validConditions, validActions, periodOn]);
// How many messages the rule would change there, asked of the server a
// moment after the conditions and actions stop changing. null: not known
// (still asking, no action yet, or the server didn't answer).
const [existingCount, setExistingCount] = useState<number | null>(null);
const countKey = canApplyToExisting && countExisting && validActions.length > 0
? JSON.stringify([matchType, validConditions, validActions])
: null;
const countRef = useRef(countExisting);
countRef.current = countExisting;
useEffect(() => {
setExistingCount(null);
if (!countKey || !countRef.current) return;
const [type, conds, acts] = JSON.parse(countKey) as [FilterRule["matchType"], FilterCondition[], FilterAction[]];
let stale = false;
const timer = setTimeout(() => {
countRef.current!({ id: "count", name: "", enabled: true, matchType: type, conditions: conds, actions: acts, stopProcessing: false })
.then((count) => { if (!stale) setExistingCount(count); })
.catch(() => { /* the offer stays, without a number */ });
}, 400);
return () => { stale = true; clearTimeout(timer); };
}, [countKey]);
const noneToApply = existingCount === 0;
const canTickApply = canApplyToExisting && !noneToApply;
// What keeps the rule from being saved, said under the part it is about
// once Save was pressed, and updated as the user fixes it.
const [showErrors, setShowErrors] = useState(false);
@@ -427,8 +463,8 @@ export function FilterRuleModal({
includeSpam: includeSpam && validActions.some((a) => ACTIONS_WITH_MAILBOX.has(a.type)) ? true : undefined,
activeFrom: activeFrom ?? undefined,
activeUntil: activeUntil ?? undefined,
}, { applyToExisting: applyToExisting && canApplyToExisting });
}, [name, matchType, validConditions, validActions, stopProcessing, includeSpam, periodOn, activeFrom, activeUntil, hasErrors, start, onSave, applyToExisting, canApplyToExisting]);
}, { applyToExisting: applyToExisting && canTickApply });
}, [name, matchType, validConditions, validActions, stopProcessing, includeSpam, periodOn, activeFrom, activeUntil, hasErrors, start, onSave, applyToExisting, canTickApply]);
const visibleSuggestions = suggestions.filter((s) => !usedSuggestions.has(s.id));
@@ -954,22 +990,28 @@ export function FilterRuleModal({
<input
type="checkbox"
id="applyToExisting"
checked={applyToExisting && canApplyToExisting}
disabled={!canApplyToExisting}
checked={applyToExisting && canTickApply}
disabled={!canTickApply}
onChange={(e) => setApplyToExisting(e.target.checked)}
className="rounded border-input disabled:opacity-50"
aria-describedby={canApplyToExisting ? undefined : "applyToExistingHint"}
aria-describedby={canTickApply ? undefined : "applyToExistingHint"}
/>
<label
htmlFor="applyToExisting"
className={cn("text-sm", canApplyToExisting ? "text-foreground" : "text-muted-foreground")}
className={cn("text-sm", canTickApply ? "text-foreground" : "text-muted-foreground")}
>
{t("apply_existing")}
{applyExistingFolder === undefined
? t("apply_existing")
: existingCount && canApplyToExisting
? t("apply_existing_count", { count: existingCount, folder: applyExistingFolder })
: t("apply_existing_in", { folder: applyExistingFolder })}
</label>
</div>
{!canApplyToExisting && (
{!canTickApply && (
<p id="applyToExistingHint" className="mt-1 ms-6 text-xs text-muted-foreground">
{periodOn ? t("apply_existing_period") : t("apply_existing_unsupported")}
{!canApplyToExisting
? (periodOn ? t("apply_existing_period") : t("apply_existing_unsupported"))
: t("apply_existing_none", { folder: applyExistingFolder ?? "" })}
</p>
)}
</div>
@@ -1,5 +1,5 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
// "Vacation responder is active" opens Settings > Out of Office, not
@@ -21,10 +21,13 @@ vi.mock('@/hooks/use-mailbox-drop', () => ({
}));
vi.mock('@/hooks/use-tag-drop', () => ({ useTagDrop: () => ({ dropHandlers: {}, isValidDropTarget: false }) }));
vi.mock('../account-switcher', () => ({ AccountSwitcher: () => null }));
const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), info: vi.fn(), progress: vi.fn(), dismiss: vi.fn() }));
vi.mock('@/stores/toast-store', () => ({ toast }));
const { Sidebar } = await import('../sidebar');
const { useVacationStore } = await import('@/stores/vacation-store');
const { buildSettingsPath } = await import('@/lib/deep-links');
const { useAuthStore } = await import('@/stores/auth-store');
describe('vacation banner', () => {
it('opens the Out of Office settings', () => {
@@ -34,4 +37,25 @@ describe('vacation banner', () => {
expect(nav.push).toHaveBeenCalledWith(buildSettingsPath('vacation'));
expect(buildSettingsPath('vacation')).not.toBe('/settings');
});
// settings-p2-12: the banner says "Out of office is on", as the setting is
// called, and turns it off without a trip to Settings, like Gmail's "End now".
it('turns out of office off on the server', async () => {
const update = vi.fn(async () => true);
useAuthStore.setState({ client: {} as never });
useVacationStore.setState({ isEnabled: true, isSupported: true, updateVacationResponse: update } as never);
render(<Sidebar mailboxes={[]} />);
fireEvent.click(screen.getByRole('button', { name: 'vacation_turn_off' }));
await waitFor(() => expect(update).toHaveBeenCalledWith(expect.anything(), { isEnabled: false }, undefined));
expect(toast.error).not.toHaveBeenCalled();
});
it('says so when turning it off fails', async () => {
const update = vi.fn(async () => { throw new Error('down'); });
useAuthStore.setState({ client: {} as never });
useVacationStore.setState({ isEnabled: true, isSupported: true, updateVacationResponse: update } as never);
render(<Sidebar mailboxes={[]} />);
fireEvent.click(screen.getByRole('button', { name: 'vacation_turn_off' }));
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('vacation_save_failed'));
});
});
+44 -10
View File
@@ -76,6 +76,7 @@ import { sidebarFolded, useUIStore } from "@/stores/ui-store";
import { useAuthStore } from "@/stores/auth-store";
import { unifiedLoadProgress } from "@/lib/unified-mailbox";
import { useVacationStore } from "@/stores/vacation-store";
import { useManagedAccountStore } from "@/stores/managed-account-store";
import { useSettingsStore, getKeywordVisibility } from "@/stores/settings-store";
import { usePolicyStore } from "@/stores/policy-store";
import { useEmailStore, buildTagViewAccountClients } from "@/stores/email-store";
@@ -1070,26 +1071,59 @@ function DemoBanner({ ruled }: { ruled: boolean }) {
);
}
function VacationBanner() {
export function VacationBanner() {
const t = useTranslations('sidebar');
const tNotifications = useTranslations('notifications');
const tFilters = useTranslations('settings.filters');
const router = useRouter();
const { isEnabled, isSupported } = useVacationStore(useShallow((state) => ({ isEnabled: state.isEnabled, isSupported: state.isSupported })));
const { isEnabled, isSupported, isSaving } = useVacationStore(useShallow((state) => ({
isEnabled: state.isEnabled, isSupported: state.isSupported, isSaving: state.isSaving,
})));
// The store holds the reply of the account Settings last loaded it for.
const managedAccountId = useManagedAccountStore((s) => s.managedAccountId);
const client = useAuthStore((s) => s.client);
const [turningOff, setTurningOff] = useState(false);
if (!isSupported || !isEnabled) return null;
// Like Gmail's "End now": off on the server, without opening Settings.
const turnOff = async () => {
if (!client || turningOff) return;
setTurningOff(true);
try {
await useVacationStore.getState().updateVacationResponse(client, { isEnabled: false }, managedAccountId ?? undefined);
} catch {
toast.error(tNotifications('vacation_save_failed'));
} finally {
setTurningOff(false);
}
};
return (
<button
onClick={() => router.push(buildSettingsPath('vacation'))}
<div
className={cn(
"flex items-center gap-2 w-full px-3 py-2 text-xs",
"flex items-center gap-2 w-full px-3 py-1.5 text-xs",
"bg-amber-500/10 dark:bg-amber-400/10 text-amber-700 dark:text-amber-400",
"hover:bg-amber-500/15 dark:hover:bg-amber-400/15 transition-colors"
)}
>
<Palmtree className="w-3.5 h-3.5 flex-shrink-0" />
<span className="truncate font-medium">{t("vacation_active")}</span>
<Settings className="w-3 h-3 ms-auto flex-shrink-0 opacity-60" />
</button>
<button
type="button"
onClick={() => router.push(buildSettingsPath('vacation'))}
className="flex items-center gap-2 min-w-0 flex-1 py-0.5 text-start hover:underline"
>
<Palmtree className="w-3.5 h-3.5 flex-shrink-0" />
<span className="truncate font-medium">{tFilters("vacation_active")}</span>
</button>
<button
type="button"
onClick={() => void turnOff()}
disabled={turningOff || isSaving}
className="inline-flex flex-shrink-0 items-center gap-1 px-2 py-0.5 rounded-sm font-medium hover:bg-amber-500/15 dark:hover:bg-amber-400/15 disabled:opacity-60 transition-colors"
>
{turningOff ? <Loader2 className="w-3 h-3 animate-spin" aria-hidden /> : null}
{t("vacation_turn_off")}
</button>
</div>
);
}
@@ -65,4 +65,12 @@ describe('Settings > Calendar on a phone', () => {
expect(screen.queryByRole('button', { name: 'title' })).toBeNull();
expect(screen.getByRole('button', { name: 'add_calendar' })).toBeInTheDocument();
});
// settings-p2-21: at 800 px the three buttons ran 4 px past the edge.
it('lets the New, Import and Subscribe buttons wrap when a row is too narrow', () => {
render(<CalendarManagementSettings />);
const row = screen.getByTestId('calendar-create-buttons');
expect(row.className).toMatch(/(^|\s)sm:flex-wrap(\s|$)/);
expect(row.querySelectorAll('button')).toHaveLength(3);
});
});
@@ -9,8 +9,8 @@ import { useFilterStore } from '@/stores/filter-store';
import { useSettingsStore } from '@/stores/settings-store';
// settings-13 (usability pass 2026-11-04): a rule made in Settings only sees
// new mail, so after Save the page offers to apply it to the Inbox; the raw
// Sieve editor is an advanced tool.
// new mail, so its dialog offers to apply it to the Inbox; the raw Sieve
// editor is an advanced tool.
const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), info: vi.fn(), progress: vi.fn(), dismiss: vi.fn() }));
vi.mock('@/stores/toast-store', () => ({ toast }));
@@ -57,6 +57,7 @@ beforeEach(() => {
updateSieveScript: vi.fn(async () => {}),
getSieveAccountId: () => 'primary',
queryEmailFields,
batchUpdateKeywords: vi.fn(async () => {}),
};
target.value = {
client, clientAccountId: null, key: 'k', accountId: 'primary', sieveAccountId: 'primary',
@@ -89,40 +90,71 @@ function showSettings(rules: FilterRule[] = [], extra: Record<string, unknown> =
render(<FilterSettings />);
}
async function createRule(from: string, action: string) {
async function fillRule(from: string, action: string) {
fireEvent.click(screen.getByText('add_rule'));
fireEvent.change(screen.getByLabelText('rule_name'), { target: { value: 'News' } });
fireEvent.change(screen.getByLabelText('condition_value'), { target: { value: from } });
fireEvent.change(screen.getAllByLabelText('actions')[0], { target: { value: action } });
fireEvent.click(screen.getByText('save'));
}
describe('a new rule in Settings › Filters', () => {
it('offers to apply it to the messages already in the Inbox', async () => {
// settings-p2-6: the offer is a box in the rule's dialog, with the number of
// messages it would change, not a toast that is gone in a few seconds.
describe('a rule in Settings › Filters', () => {
it('offers, in its dialog, to apply it to the messages already in the Inbox', async () => {
showSettings();
await createRule('news@example.org', 'star');
await waitFor(() => expect(toast.success).toHaveBeenCalled());
const [, options] = toast.success.mock.calls.at(-1)!;
expect(options.action.label).toBe('apply_to_existing');
await fillRule('news@example.org', 'star');
const box = await screen.findByRole('checkbox', { name: 'apply_existing_count' }, { timeout: 2000 });
expect(box).toBeEnabled();
expect(queryEmailFields).toHaveBeenCalled();
fireEvent.click(box);
queryEmailFields.mockClear();
fireEvent.click(screen.getByText('save'));
await waitFor(() => expect(toast.success).toHaveBeenCalledWith('rule_applied', expect.anything()));
// Saving ran the rule; no toast offered it.
expect(queryEmailFields).toHaveBeenCalled();
expect(toast.success.mock.calls.some(([, options]) => options?.action)).toBe(false);
});
it('says nothing more when no message in the Inbox matches', async () => {
it('runs nothing on old mail when the box is left empty', async () => {
showSettings();
await createRule('nobody@example.com', 'star');
await waitFor(() => expect(queryEmailFields).toHaveBeenCalled());
await new Promise((r) => setTimeout(r, 0));
expect(toast.success).not.toHaveBeenCalled();
});
it('is not offered for a rule that can only act on arriving mail', async () => {
showSettings();
await createRule('news@example.org', 'discard');
await fillRule('news@example.org', 'star');
await screen.findByRole('checkbox', { name: 'apply_existing_count' }, { timeout: 2000 });
queryEmailFields.mockClear();
fireEvent.click(screen.getByText('save'));
await waitFor(() => expect(client.updateSieveScript).toHaveBeenCalled());
await new Promise((r) => setTimeout(r, 0));
expect(queryEmailFields).not.toHaveBeenCalled();
expect(toast.success).not.toHaveBeenCalled();
});
it('greys the box when no message in the Inbox matches', async () => {
showSettings();
await fillRule('nobody@example.com', 'star');
await waitFor(() => expect(queryEmailFields).toHaveBeenCalled(), { timeout: 2000 });
await waitFor(() => expect(screen.getByText('apply_existing_none')).toBeInTheDocument());
expect(screen.getByRole('checkbox', { name: 'apply_existing_in' })).toBeDisabled();
});
it('is greyed for a rule that can only act on arriving mail', async () => {
showSettings();
await fillRule('news@example.org', 'discard');
expect(screen.getByRole('checkbox', { name: 'apply_existing_in' })).toBeDisabled();
expect(screen.getByText('apply_existing_unsupported')).toBeInTheDocument();
await new Promise((r) => setTimeout(r, 500));
expect(queryEmailFields).not.toHaveBeenCalled();
});
});
describe('the rule list', () => {
it('says what its switch does', () => {
showSettings([{
id: 'r1', name: 'News', enabled: true, matchType: 'all',
conditions: [{ field: 'from', comparator: 'contains', value: 'news@example.org' }],
actions: [{ type: 'star' }], stopProcessing: false,
} as FilterRule]);
expect(screen.getByRole('switch', { name: 'show_rule_details' })).toBeInTheDocument();
expect(screen.queryByText('expanded_view')).toBeNull();
});
});
describe('the raw Sieve editor button', () => {
@@ -73,4 +73,27 @@ describe('time zone setting', () => {
fireEvent.change(input, { target: { value: '+05:45' } });
expect(optionTexts()).toEqual([expect.stringMatching(/^Kathmandu/)]);
});
// settings-p2-20: abbreviations and an exact offset find their zones first.
it('finds zones by abbreviation', () => {
const input = openTimeZones();
fireEvent.change(input, { target: { value: 'CET' } });
const found = optionTexts();
expect(found.length).toBeGreaterThan(0);
expect(found).toEqual(expect.arrayContaining([expect.stringMatching(/^Berlin/), expect.stringMatching(/^Paris/)]));
fireEvent.change(input, { target: { value: 'pst' } });
expect(optionTexts()[0]).toMatch(/^Los Angeles/);
});
it('lists the exact offset before offsets that start with the same digits', () => {
const input = openTimeZones();
fireEvent.change(input, { target: { value: 'GMT+1' } });
const found = optionTexts();
const offsetOf = (text: string) => /GMT[+-]\d{2}:\d{2}/.exec(text)?.[0];
const first = offsetOf(found[0]);
// Whatever +1 is today (winter or summer time), it's listed first.
expect(first).toMatch(/^GMT\+01:00$/);
const firstOther = found.findIndex((text) => offsetOf(text) !== first);
expect(found.slice(firstOther).every((text) => offsetOf(text) !== 'GMT+01:00')).toBe(true);
});
});
@@ -378,6 +378,9 @@ describe('the vacation period in the card', () => {
it('checks the fields again on save, which a field filled in only half may never have told', async () => {
const server = stalwart([rule('a')]);
await openCard(server);
// Typing in it: an empty field left alone turns into a text field with a
// placeholder (settings-p2-19).
fireEvent.focus(dateField(1));
fireEvent.change(dateField(1), { target: { value: '' } });
halfFilledIn(dateField(1), true);
expect(await screen.findByText('warnings.invalid_date', undefined, { timeout: 3000 })).toBeInTheDocument();
@@ -18,8 +18,8 @@ vi.mock('next-intl', () => {
});
// The editor hands the card its HTML as typed.
vi.mock('@/components/email/rich-text-editor', () => ({
RichTextEditor: ({ content, onChange, placeholder }: { content: string; onChange: (html: string) => void; placeholder?: string }) => (
<textarea data-testid="message-editor" value={content} placeholder={placeholder} onChange={(e) => onChange(e.target.value)} />
RichTextEditor: ({ content, onChange, placeholder, toolbar }: { content: string; onChange: (html: string) => void; placeholder?: string; toolbar?: string }) => (
<textarea data-testid="message-editor" data-toolbar={toolbar ?? 'full'} value={content} placeholder={placeholder} onChange={(e) => onChange(e.target.value)} />
),
}));
@@ -138,3 +138,33 @@ describe('the out-of-office message', () => {
expect(lastSent(server)).toMatchObject({ isEnabled: true, htmlBody: '<p>Back on 5 May.</p>', textBody: 'Back soon.' });
}, 15_000);
});
// settings-p2-19: a short toolbar for a few lines, and date fields that say
// what an empty one means instead of the browser's "dd.mm.yyyy --:--".
describe('the out-of-office form', () => {
it('gives the message the short formatting toolbar', async () => {
await openCard(blank());
expect(editor().dataset.toolbar).toBe('short');
});
it('shows empty dates as words, and the date picker once a field is used', async () => {
await openCard(blank());
const from = screen.getByLabelText('from_label') as HTMLInputElement;
const until = screen.getByLabelText('until_label') as HTMLInputElement;
expect(from.type).toBe('text');
expect(from.placeholder).toBe('from_placeholder');
expect(until.type).toBe('text');
expect(until.placeholder).toBe('until_placeholder');
fireEvent.focus(from);
expect(from.type).toBe('datetime-local');
fireEvent.change(from, { target: { value: '2030-01-02T09:00' } });
fireEvent.blur(from);
// A field with a date keeps the picker; the empty one goes back to words.
expect(from.type).toBe('datetime-local');
expect(from.value).toBe('2030-01-02T09:00');
fireEvent.focus(until);
fireEvent.blur(until);
expect(until.type).toBe('text');
});
});
@@ -858,8 +858,9 @@ export function CalendarManagementSettings() {
isLoading={isLoading}
/>
) : (
// Stacked on a phone: side by side the three labels wrapped.
<div className="flex flex-col sm:flex-row gap-2">
// Stacked on a phone: side by side the three labels wrapped. Too
// narrow for all three in a row (800 px), the last one goes under.
<div data-testid="calendar-create-buttons" className="flex flex-col sm:flex-row sm:flex-wrap gap-2">
<button
type="button"
onClick={() => setIsCreating(true)}
+34 -23
View File
@@ -1,7 +1,7 @@
"use client";
import { MoveButtons } from "@/components/ui/move-buttons";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { useTranslations } from "next-intl";
import { SettingsSection, ToggleSwitch } from "./settings-section";
import { Button } from "@/components/ui/button";
@@ -24,9 +24,8 @@ import { supportsRegex } from "@/lib/sieve/regex";
import { formatDateTime } from "@/lib/utils";
import { useVacationStore } from "@/stores/vacation-store";
import { useManagedAccountStore } from "@/stores/managed-account-store";
import { offerRuleOnFolder } from "@/lib/filters/quick-rule-flow";
import { planOnServer, runRuleOnFolder } from "@/lib/filters/quick-rule-flow";
import { resolveMailboxRuleTarget } from "@/lib/filters/quick-rule-target";
import { retroactiveSupport } from "@/lib/filters/retroactive";
import { localizeMailboxName } from "@/lib/mailbox-label";
import {
Plus,
@@ -215,7 +214,6 @@ export function FilterSettings() {
const tNotifications = useTranslations("notifications");
const tMenu = useTranslations("context_menu");
const tSidebar = useTranslations("sidebar");
const tFolders = useTranslations("uxs_folders");
// The raw script editor is for people who asked for the advanced settings,
// unless the script is one only it can edit.
const showAdvanced = useSettingsStore((s) => s.showAdvancedSettings);
@@ -384,8 +382,24 @@ export function FilterSettings() {
[client, deleteRule, saveFilters, tNotifications]
);
// A rule only sees new mail. Its dialog offers to sort what is already in
// the Inbox too, as Gmail's "Also apply filter to matching conversations"
// does: for the user's own account, where the Inbox can be searched.
const inboxOffer = useMemo(() => {
if (!isPrimaryAccount || !client) return null;
const inbox = storeMailboxes.find((m) => m.role === "inbox" && !m.isShared);
const target = inbox ? resolveMailboxRuleTarget(inbox) : null;
if (!inbox || !target || target.client !== client) return null;
const folder = localizeMailboxName(inbox.role, inbox.name, (k) => tSidebar(`mailboxes.${k}`));
return { inbox, target, folder };
}, [isPrimaryAccount, client, storeMailboxes, tSidebar]);
const countInInbox = useCallback(async (rule: FilterRule) => {
if (!inboxOffer) return 0;
return (await planOnServer(inboxOffer.target, rule, inboxOffer.inbox.id)).ids.length;
}, [inboxOffer]);
const handleSaveRule = useCallback(
async (rule: FilterRule) => {
async (rule: FilterRule, options?: { applyToExisting: boolean }) => {
const previousRules = [...rules];
if (editingRule) {
updateRule(rule.id, rule);
@@ -403,25 +417,19 @@ export function FilterSettings() {
toast.error(tNotifications("filters_save_failed"));
return;
}
// A new rule only sees new mail. Offer to sort what is already in the
// Inbox, as Gmail's "Also apply to matching conversations" does.
if (!editingRule && isPrimaryAccount && retroactiveSupport(rule).ok) {
const inbox = storeMailboxes.find((m) => m.role === "inbox" && !m.isShared);
const target = inbox ? resolveMailboxRuleTarget(inbox) : null;
if (inbox && target && target.client === client) {
const folder = localizeMailboxName(inbox.role, inbox.name, (k) => tSidebar(`mailboxes.${k}`));
void offerRuleOnFolder(
target,
rule,
inbox.id,
{ notifications: tNotifications, filters: t, menu: tMenu },
(count) => tFolders("apply_to_existing", { count, folder }),
);
}
// The box in the dialog: run the saved rule over the Inbox now.
if (options?.applyToExisting && inboxOffer) {
void runRuleOnFolder(
inboxOffer.target,
rule,
inboxOffer.inbox.id,
inboxOffer.folder,
{ notifications: tNotifications, filters: t, menu: tMenu },
);
}
}
},
[editingRule, updateRule, addRule, rules, client, saveFilters, tNotifications, isPrimaryAccount, storeMailboxes, tSidebar, t, tMenu, tFolders]
[editingRule, updateRule, addRule, rules, client, saveFilters, tNotifications, inboxOffer, t, tMenu]
);
const handleSaveSieve = useCallback(
@@ -804,11 +812,11 @@ export function FilterSettings() {
)}
{!isOpaque && rules.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{t("expanded_view")}</span>
<span className="text-xs text-muted-foreground">{t("show_rule_details")}</span>
<ToggleSwitch
checked={expandedFilterView}
onChange={(v) => updateSetting("expandedFilterView", v)}
ariaLabel={t("expanded_view")}
ariaLabel={t("show_rule_details")}
/>
</div>
)}
@@ -824,6 +832,9 @@ export function FilterSettings() {
regexSupported={supportsRegex(sieveCapabilities?.sieveExtensions)}
forwardsBefore={forwardsOfEdited.before}
forwardsAfter={forwardsOfEdited.after}
offerApplyToExisting={!!inboxOffer}
applyExistingFolder={inboxOffer?.folder}
countExisting={inboxOffer ? countInInbox : undefined}
onSave={handleSaveRule}
onClose={() => {
setShowRuleModal(false);
+12 -3
View File
@@ -1,10 +1,10 @@
"use client";
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
import { SearchableSelect, type SearchableOption } from '@/components/ui/searchable-select';
import { AUTO_TIME_ZONE, getBrowserTimeZone, listTimeZones } from '@/lib/timezone';
import { timeZoneChoices, timeZoneCity, type TimeZoneChoice } from '@/lib/time-zone-choices';
import { timeZoneChoices, timeZoneCity, timeZoneSearchRank, type TimeZoneChoice } from '@/lib/time-zone-choices';
import { getEffectiveLocale } from '@/i18n/detect-locale';
/**
@@ -25,11 +25,13 @@ export function TimeZonePicker({ value, onChange }: { value: string; onChange: (
setZones(listTimeZones());
}, []);
const offsets = useMemo(() => new Map<string, number>(), []);
const options = useMemo<SearchableOption[]>(() => {
// A zone synced from another browser (newer tz database) may be missing
// from this runtime's list; keep it selectable.
const extra = value !== AUTO_TIME_ZONE ? [value] : [];
const { common, all } = timeZoneChoices(zones, { locale: getEffectiveLocale(), extra });
for (const choice of all) offsets.set(choice.value, choice.offsetMinutes);
const toOption = (group: string) => (choice: TimeZoneChoice): SearchableOption => ({
value: choice.value,
label: choice.city,
@@ -42,7 +44,13 @@ export function TimeZonePicker({ value, onChange }: { value: string; onChange: (
...common.map(toOption(tz('time_zone_common'))),
...all.map(toOption(tz('time_zone_all'))),
];
}, [zones, value, browserTimeZone, t, tz]);
}, [zones, value, browserTimeZone, t, tz, offsets]);
// "CET" and "GMT+1" find their zones first (settings-p2-20).
const rank = useCallback(
(query: string, option: SearchableOption, wordsMatch: boolean) =>
timeZoneSearchRank(query, { value: option.value, offsetMinutes: offsets.get(option.value) }, wordsMatch),
[offsets],
);
const current = options.find((option) => option.value === value);
return (
@@ -52,6 +60,7 @@ export function TimeZonePicker({ value, onChange }: { value: string; onChange: (
value={value}
options={options}
onSelect={onChange}
rank={rank}
ariaLabel={`${t('time_zone.label')}: ${current?.label ?? timeZoneCity(value)}`}
searchLabel={tz('time_zone_search')}
noMatchLabel={tz('time_zone_no_match')}
+24 -6
View File
@@ -159,6 +159,19 @@ export function VacationSettings() {
const [halfFilled, setHalfFilled] = useState({ from: false, to: false });
const fromRef = useRef<HTMLInputElement>(null);
const toRef = useRef<HTMLInputElement>(null);
// An empty date field that isn't being edited is a text field with a
// placeholder in words: the browser's own date field shows its raw pattern
// ("dd.mm.yyyy --:--") there, whatever the app's date format.
const [dateEditing, setDateEditing] = useState<'from' | 'to' | null>(null);
const dateFieldType = (field: 'from' | 'to') =>
dateEditing === field || halfFilled[field] || (field === 'from' ? fromInput : toInput) ? 'datetime-local' : 'text';
useEffect(() => {
if (!dateEditing) return;
const input = (dateEditing === 'from' ? fromRef : toRef).current;
// Opens the browser's picker after a click; a Tab into the field has no
// user activation and just leaves the field focused for typing.
try { input?.showPicker?.(); } catch { /* not allowed without a click */ }
}, [dateEditing]);
const localFromDate = fieldMoment(fromInput, fromDate, inputZone);
const localToDate = fieldMoment(toInput, toDate, inputZone);
useEffect(() => {
@@ -568,11 +581,13 @@ export function VacationSettings() {
>
<input
ref={fromRef}
type="datetime-local"
max={DATE_INPUT_MAX}
type={dateFieldType('from')}
max={dateFieldType('from') === 'datetime-local' ? DATE_INPUT_MAX : undefined}
value={fromInput}
placeholder={tx('from_placeholder')}
onFocus={() => setDateEditing('from')}
onChange={(e) => readDateField('from', e.currentTarget)}
onBlur={(e) => readDateField('from', e.currentTarget)}
onBlur={(e) => { readDateField('from', e.currentTarget); setDateEditing(null); }}
aria-label={tx('from_label')}
aria-invalid={!!fromError}
aria-describedby={fromError ? 'vacation-from-error' : undefined}
@@ -587,11 +602,13 @@ export function VacationSettings() {
>
<input
ref={toRef}
type="datetime-local"
max={DATE_INPUT_MAX}
type={dateFieldType('to')}
max={dateFieldType('to') === 'datetime-local' ? DATE_INPUT_MAX : undefined}
value={toInput}
placeholder={tx('until_placeholder')}
onFocus={() => setDateEditing('to')}
onChange={(e) => readDateField('to', e.currentTarget)}
onBlur={(e) => readDateField('to', e.currentTarget)}
onBlur={(e) => { readDateField('to', e.currentTarget); setDateEditing(null); }}
aria-label={tx('until_label')}
aria-invalid={!!toError}
aria-describedby={toError ? 'vacation-to-error' : undefined}
@@ -643,6 +660,7 @@ export function VacationSettings() {
<RichTextEditor
content={localBody}
onChange={editBody}
toolbar="short"
ariaLabel={tx('message_label')}
ariaInvalid={!!bodyError}
ariaDescribedBy={bodyError ? 'vacation-body-error' : undefined}
+21 -1
View File
@@ -33,6 +33,13 @@ interface SearchableSelectProps {
renderValue?: (option: SearchableOption | undefined) => ReactNode;
className?: string;
menuClassName?: string;
/**
* Orders the results, lower first, and can add or drop options:
* `wordsMatch` says whether every typed word is in the option's text.
* null leaves the option out. Without it, the matching options keep
* their order.
*/
rank?: (query: string, option: SearchableOption, wordsMatch: boolean) => number | null;
}
/**
@@ -50,6 +57,7 @@ export function SearchableSelect({
renderValue,
className,
menuClassName,
rank,
}: SearchableSelectProps) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
@@ -74,12 +82,24 @@ export function SearchableSelect({
const words = needle.split(/\s+/);
// An option listed in two groups shows once in the results.
const seen = new Set<string>();
if (rank) {
const ranked: { entry: (typeof entries)[number]; rank: number }[] = [];
for (const e of entries) {
if (seen.has(e.value)) continue;
const r = rank(query, e, words.every((w) => e.haystack.includes(w)));
if (r === null) continue;
seen.add(e.value);
ranked.push({ entry: e, rank: r });
}
// Array sort is stable: equal ranks keep the list's order.
return ranked.sort((a, b) => a.rank - b.rank).map((r) => r.entry);
}
return entries.filter((e) => {
if (seen.has(e.value) || !words.every((w) => e.haystack.includes(w))) return false;
seen.add(e.value);
return true;
});
}, [entries, query]);
}, [entries, query, rank]);
const close = (refocus: boolean) => {
setOpen(false);
+21 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { timeZoneChoices, timeZoneCity } from '../time-zone-choices';
import { parseTypedOffset, timeZoneChoices, timeZoneCity, timeZoneSearchRank } from '../time-zone-choices';
const JAN = new Date(Date.UTC(2026, 0, 15, 12));
@@ -38,3 +38,23 @@ describe('time zone choices', () => {
expect(all.map((c) => c.value)).toContain('America/Ciudad_Juarez');
});
});
describe('time zone search', () => {
it('reads offsets as people type them', () => {
expect(parseTypedOffset('GMT+1')).toBe(60);
expect(parseTypedOffset('utc-5')).toBe(-300);
expect(parseTypedOffset('+05:30')).toBe(330);
expect(parseTypedOffset('-0800')).toBe(-480);
expect(parseTypedOffset('Berlin')).toBeNull();
expect(parseTypedOffset('GMT')).toBeNull();
});
it('puts the zones of an abbreviation and the exact offset first', () => {
expect(timeZoneSearchRank('CET', { value: 'Europe/Berlin', offsetMinutes: 60 }, false)).toBe(0);
expect(timeZoneSearchRank('cest', { value: 'Europe/Paris', offsetMinutes: 120 }, false)).toBe(0);
expect(timeZoneSearchRank('CET', { value: 'Asia/Tokyo', offsetMinutes: 540 }, false)).toBeNull();
expect(timeZoneSearchRank('GMT+1', { value: 'Europe/Berlin', offsetMinutes: 60 }, true)).toBe(0);
expect(timeZoneSearchRank('GMT+1', { value: 'Australia/Sydney', offsetMinutes: 660 }, true)).toBe(2);
expect(timeZoneSearchRank('Berlin', { value: 'Europe/Berlin', offsetMinutes: 60 }, true)).toBe(1);
});
});
-24
View File
@@ -213,30 +213,6 @@ async function applyAndReport(
}
}
/**
* After a rule was saved in Settings › Filters: when it would change mail
* already in `mailboxId` (the Inbox), say how much and offer to apply it
* there. Nothing is said when it would change nothing or can't be checked.
*/
export async function offerRuleOnFolder(
target: QuickRuleTarget,
rule: FilterRule,
mailboxId: string,
text: QuickRuleText,
applyLabel: (count: number) => string,
): Promise<void> {
const plan = await planSafely(target, rule, mailboxId);
if (plan.ids.length === 0) return;
toast.success(text.notifications('rule_created'), {
message: rule.name,
duration: SAVED_TOAST_MS,
action: {
label: applyLabel(plan.ids.length),
onClick: () => { void applyAndReport(target, rule, mailboxId, text); },
},
});
}
/**
* Run a saved rule over the mail already in a folder, from the folder's
* context menu. A progress toast stays up while the server is asked which
+87
View File
@@ -134,3 +134,90 @@ export function timeZoneChoices(
}
return { common, all };
}
// Abbreviations people type for a zone, with the zones they stand for. Both
// the winter and the summer name, since people type either all year.
const CET = ['Europe/Berlin', 'Europe/Paris', 'Europe/Madrid', 'Europe/Rome', 'Europe/Amsterdam', 'Europe/Brussels',
'Europe/Vienna', 'Europe/Zurich', 'Europe/Stockholm', 'Europe/Oslo', 'Europe/Copenhagen', 'Europe/Warsaw',
'Europe/Prague', 'Europe/Budapest', 'Europe/Belgrade', 'Europe/Luxembourg'];
const EET = ['Europe/Athens', 'Europe/Helsinki', 'Europe/Kyiv', 'Europe/Kiev', 'Europe/Bucharest', 'Europe/Sofia',
'Europe/Riga', 'Europe/Tallinn', 'Europe/Vilnius', 'Africa/Cairo'];
const WET = ['Europe/Lisbon', 'Atlantic/Canary', 'Atlantic/Madeira', 'Atlantic/Faroe', 'Atlantic/Faeroe'];
const US_EAST = ['America/New_York', 'America/Toronto', 'America/Detroit', 'America/Indiana/Indianapolis'];
const US_CENTRAL = ['America/Chicago', 'America/Winnipeg', 'America/Mexico_City'];
const US_MOUNTAIN = ['America/Denver', 'America/Phoenix', 'America/Edmonton', 'America/Boise'];
const US_PACIFIC = ['America/Los_Angeles', 'America/Vancouver', 'America/Tijuana'];
const AU_EAST = ['Australia/Sydney', 'Australia/Melbourne', 'Australia/Brisbane', 'Australia/Hobart', 'Australia/Canberra'];
const UK = ['Europe/London', 'Europe/Dublin'];
export const TIME_ZONE_ABBREVIATIONS: Record<string, readonly string[]> = {
UTC: ['UTC', 'Etc/UTC'],
GMT: ['UTC', 'Etc/UTC', ...UK],
BST: UK,
IST: ['Asia/Kolkata', 'Asia/Calcutta'],
CET, CEST: CET, MEZ: CET, MESZ: CET,
EET, EEST: EET,
WET, WEST: WET,
MSK: ['Europe/Moscow'],
EST: US_EAST, EDT: US_EAST, ET: US_EAST,
CST: [...US_CENTRAL, 'Asia/Shanghai'], CDT: US_CENTRAL, CT: US_CENTRAL,
MST: US_MOUNTAIN, MDT: US_MOUNTAIN, MT: US_MOUNTAIN,
PST: US_PACIFIC, PDT: US_PACIFIC, PT: US_PACIFIC,
AKST: ['America/Anchorage'], AKDT: ['America/Anchorage'],
HST: ['Pacific/Honolulu'],
JST: ['Asia/Tokyo'],
KST: ['Asia/Seoul'],
HKT: ['Asia/Hong_Kong'],
SGT: ['Asia/Singapore'],
AEST: AU_EAST, AEDT: AU_EAST,
ACST: ['Australia/Adelaide', 'Australia/Darwin'], ACDT: ['Australia/Adelaide'],
AWST: ['Australia/Perth'],
NZST: ['Pacific/Auckland'], NZDT: ['Pacific/Auckland'],
BRT: ['America/Sao_Paulo'],
ART: ['America/Argentina/Buenos_Aires', 'America/Buenos_Aires'],
SAST: ['Africa/Johannesburg'],
WAT: ['Africa/Lagos'],
EAT: ['Africa/Nairobi'],
PKT: ['Asia/Karachi'],
WIB: ['Asia/Jakarta'],
ICT: ['Asia/Bangkok', 'Asia/Ho_Chi_Minh', 'Asia/Saigon'],
PHT: ['Asia/Manila'],
GST: ['Asia/Dubai'],
};
/**
* An offset typed in the search ("GMT+1", "UTC-5", "+05:30", "-0800"), in
* minutes east of UTC; null when the text isn't one. "GMT" alone is UTC.
*/
export function parseTypedOffset(text: string): number | null {
const m = /^(?:gmt|utc)?\s*([+\-−])\s*(\d{1,2})(?::?(\d{2}))?$/i.exec(text.trim());
if (!m) return null;
const hours = Number(m[2]);
const minutes = m[3] ? Number(m[3]) : 0;
if (hours > 14 || minutes > 59) return null;
return (m[1] === '+' ? 1 : -1) * (hours * 60 + minutes);
}
/**
* Where a zone goes in the search results, lower first; null leaves it out.
* An abbreviation ("CET") or an exact offset ("GMT+1") finds its zones
* first; anything the words match comes after. Without this, "GMT+1" listed
* GMT+11 and GMT+10 before GMT+01:00, and "CET" found nothing.
*/
export function timeZoneSearchRank(
query: string,
zone: { value: string; offsetMinutes?: number },
wordsMatch: boolean,
): number | null {
const typed = query.trim();
const zones = TIME_ZONE_ABBREVIATIONS[typed.toUpperCase()];
if (zones?.includes(zone.value)) return 0;
const offset = parseTypedOffset(typed);
if (offset !== null && zone.offsetMinutes !== undefined) {
if (zone.offsetMinutes === offset) return 0;
// "+1" also finds "+10", but after the zones at +01:00.
return wordsMatch ? 2 : null;
}
return wordsMatch ? 1 : null;
}
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "مسح البحث",
"vacation_active": "الرد التلقائي أثناء الغياب مفعّل",
"vacation_turn_off": "إيقاف",
"demo_banner": "وضع تجريبي",
"demo_reset": "إعادة تعيين",
"demo_tour": "جولة",
@@ -1393,6 +1394,8 @@
"from_description": "اتركه فارغًا للبدء فورًا.",
"until_label": "حتى (اختياري)",
"until_description": "اتركه فارغًا لمواصلة الرد حتى توقفه.",
"from_placeholder": "فورًا",
"until_placeholder": "حتى توقفه",
"audience_all": "الجميع",
"audience_internal": "مؤسستي فقط ({domains})",
"audience_external": "من هم خارج مؤسستي فقط",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "فتح محرر Sieve الخام",
"fetch_error": "فشل تحميل المرشحات",
"expanded_view": "عرض موسّع",
"show_rule_details": "عرض شروط كل قاعدة وإجراءاتها",
"expanded_view_description": "إظهار قواعد المرشح مع كتل الشروط والإجراءات المفصّلة",
"if": "إذا",
"then": "عندئذٍ",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "أدخل تاريخ بدء أو انتهاء للفترة",
"validation_period_invalid": "أدخل تاريخًا صالحًا مع الوقت للفترة، أو اترك الحقل فارغًا",
"apply_existing_period": "القاعدة ذات الفترة الزمنية تعمل فقط على الرسائل التي تصل خلالها، لذا لا يمكن تطبيقها على الرسائل الموجودة.",
"apply_existing_in": "طبّقها أيضًا على الرسائل الموجودة في {folder}",
"apply_existing_count": "{count, plural, zero {طبّقها أيضًا على الرسائل الموجودة في {folder}} one {طبّقها أيضًا على الرسالة الموجودة في {folder}} two {طبّقها أيضًا على الرسالتين الموجودتين في {folder}} few {طبّقها أيضًا على # رسائل موجودة في {folder}} many {طبّقها أيضًا على # رسالة موجودة في {folder}} other {طبّقها أيضًا على # رسالة موجودة في {folder}}}",
"apply_existing_none": "لا توجد رسالة في {folder} تطابق هذه القاعدة بعد.",
"period_status_scheduled": "مجدولة",
"period_status_active": "نشطة",
"period_status_expired": "منتهية"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Neteja la cerca",
"vacation_active": "La resposta automàtica està activa",
"vacation_turn_off": "Desactiva",
"demo_banner": "Mode de demostració",
"demo_reset": "Reinicia",
"demo_tour": "Visita guiada",
@@ -1393,6 +1394,8 @@
"from_description": "Deixa-ho buit per començar ara mateix.",
"until_label": "Fins a (opcional)",
"until_description": "Deixa-ho buit per continuar responent fins que ho desactivis.",
"from_placeholder": "Des d’ara",
"until_placeholder": "Fins que el desactiveu",
"audience_all": "Tothom",
"audience_internal": "Només la meva organització ({domains})",
"audience_external": "Només persones de fora de la meva organització",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "Obre l'editor Sieve en brut",
"fetch_error": "No s'han pogut carregar els filtres",
"expanded_view": "Vista ampliada",
"show_rule_details": "Mostra les condicions i les accions de cada regla",
"expanded_view_description": "Mostra les regles de filtre amb blocs detallats de condicions i accions",
"if": "Si",
"then": "Aleshores",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "Introdueix una data d'inici o de finalització per al període",
"validation_period_invalid": "Introdueix una data vàlida amb l'hora per al període, o deixa el camp buit",
"apply_existing_period": "Una regla amb període només actua sobre els missatges que arriben dins d'aquest període, de manera que no es pot aplicar als missatges existents.",
"apply_existing_in": "Aplica-la també als missatges que ja són a {folder}",
"apply_existing_count": "{count, plural, one {Aplica-la també al # missatge que ja és a {folder}} other {Aplica-la també als # missatges que ja són a {folder}}}",
"apply_existing_none": "Encara cap missatge de {folder} no compleix aquesta regla.",
"period_status_scheduled": "programada",
"period_status_active": "activa",
"period_status_expired": "caducada"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Vymazat hledání",
"vacation_active": "Automatická odpověď je aktivní",
"vacation_turn_off": "Vypnout",
"demo_banner": "Demo režim",
"demo_reset": "Resetovat",
"demo_tour": "Průvodce",
@@ -1393,6 +1394,8 @@
"from_description": "Nechte prázdné, aby to začalo hned.",
"until_label": "Do (nepovinné)",
"until_description": "Nechte prázdné, aby se odpovídalo, dokud to nevypnete.",
"from_placeholder": "Hned",
"until_placeholder": "Dokud ji nevypnete",
"audience_all": "Všem",
"audience_internal": "Jen mé organizaci ({domains})",
"audience_external": "Jen lidem mimo mou organizaci",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Otevřít textový editor Sieve",
"fetch_error": "Nepodařilo se načíst filtry",
"expanded_view": "Rozšířené zobrazení",
"show_rule_details": "Zobrazit podmínky a akce každého pravidla",
"expanded_view_description": "Zobrazovat pravidla filtrů s podrobnými bloky podmínek a akcí",
"if": "Pokud",
"then": "Pak",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Zadejte datum zahájení nebo ukončení období",
"validation_period_invalid": "Zadejte pro období platné datum včetně času, nebo pole nechte prázdné",
"apply_existing_period": "Pravidlo s obdobím působí jen na zprávy, které v něm přijdou, a proto ho nelze použít na existující zprávy.",
"apply_existing_in": "Použít i na zprávy, které už jsou ve složce {folder}",
"apply_existing_count": "{count, plural, one {Použít i na # zprávu, která už je ve složce {folder}} few {Použít i na # zprávy, které už jsou ve složce {folder}} many {Použít i na # zprávy, které už jsou ve složce {folder}} other {Použít i na # zpráv, které už jsou ve složce {folder}}}",
"apply_existing_none": "Tomuto pravidlu zatím neodpovídá žádná zpráva ve složce {folder}.",
"period_status_scheduled": "naplánováno",
"period_status_active": "aktivní",
"period_status_expired": "vypršelo"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Ryd søgning",
"vacation_active": "Fraværsbeskeder er aktive",
"vacation_turn_off": "Slå fra",
"demo_banner": "Demo-tilstand",
"demo_reset": "Nulstil",
"demo_tour": "Rundvisning",
@@ -1393,6 +1394,8 @@
"from_description": "Lad feltet være tomt for at starte med det samme.",
"until_label": "Til (valgfrit)",
"until_description": "Lad feltet være tomt for at blive ved med at svare, til du slår det fra.",
"from_placeholder": "Med det samme",
"until_placeholder": "Indtil du slår det fra",
"audience_all": "Alle",
"audience_internal": "Kun min organisation ({domains})",
"audience_external": "Kun folk uden for min organisation",
@@ -2909,6 +2912,7 @@
"open_sieve_editor": "Åbn raw Sieve-editor",
"fetch_error": "Kunne ikke indlæse filtre",
"expanded_view": "Udvidet visning",
"show_rule_details": "Vis hver regels betingelser og handlinger",
"expanded_view_description": "Vis filterregler med detaljerede betingelses- og handlingsblokke",
"if": "Hvis",
"then": "Så",
@@ -2987,6 +2991,9 @@
"validation_period_empty": "Angiv en start- eller slutdato for perioden",
"validation_period_invalid": "Angiv en gyldig dato med klokkeslæt for perioden, eller lad feltet stå tomt",
"apply_existing_period": "En regel med en periode virker kun på beskeder, der ankommer i perioden, og kan derfor ikke køres på eksisterende beskeder.",
"apply_existing_in": "Anvend også på beskederne, der allerede er i {folder}",
"apply_existing_count": "{count, plural, one {Anvend også på den # besked, der allerede er i {folder}} other {Anvend også på de # beskeder, der allerede er i {folder}}}",
"apply_existing_none": "Ingen beskeder i {folder} passer til reglen endnu.",
"period_status_scheduled": "planlagt",
"period_status_active": "aktiv",
"period_status_expired": "udløbet"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Suche löschen",
"vacation_active": "Abwesenheitsnotiz ist aktiv",
"vacation_turn_off": "Ausschalten",
"demo_banner": "Demo-Modus",
"demo_reset": "Zurücksetzen",
"demo_tour": "Tour",
@@ -1393,6 +1394,8 @@
"from_description": "Leer lassen, um sofort zu beginnen.",
"until_label": "Bis (optional)",
"until_description": "Leer lassen, um weiter zu antworten, bis Sie es ausschalten.",
"from_placeholder": "Ab sofort",
"until_placeholder": "Bis Sie sie ausschalten",
"audience_all": "Allen",
"audience_internal": "Nur meiner Organisation ({domains})",
"audience_external": "Nur Leuten außerhalb meiner Organisation",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Sieve-Skript-Editor öffnen",
"fetch_error": "Filter konnten nicht geladen werden",
"expanded_view": "Erweiterte Ansicht",
"show_rule_details": "Bedingungen und Aktionen jeder Regel anzeigen",
"expanded_view_description": "Filterregeln mit detaillierten Bedingungs- und Aktionsblöcken anzeigen",
"if": "Wenn",
"then": "Dann",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Bitte ein Start- oder Enddatum für den Zeitraum angeben",
"validation_period_invalid": "Bitte für den Zeitraum ein gültiges Datum mit Uhrzeit angeben oder das Feld leer lassen",
"apply_existing_period": "Eine Regel mit Zeitraum wirkt nur auf Nachrichten, die in diesem Zeitraum eintreffen, und lässt sich daher nicht auf vorhandene Nachrichten anwenden.",
"apply_existing_in": "Auch auf die Nachrichten anwenden, die schon in {folder} sind",
"apply_existing_count": "{count, plural, one {Auch auf die # Nachricht anwenden, die schon in {folder} ist} other {Auch auf die # Nachrichten anwenden, die schon in {folder} sind}}",
"apply_existing_none": "Noch keine Nachricht in {folder} passt zu dieser Regel.",
"period_status_scheduled": "geplant",
"period_status_active": "aktiv",
"period_status_expired": "abgelaufen"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Clear search",
"vacation_active": "Vacation responder is active",
"vacation_turn_off": "Turn off",
"demo_banner": "Demo Mode",
"demo_reset": "Reset",
"demo_tour": "Tour",
@@ -1393,6 +1394,8 @@
"from_description": "Leave it empty to start right away.",
"until_label": "Until (optional)",
"until_description": "Leave it empty to keep replying until you turn it off.",
"from_placeholder": "Right away",
"until_placeholder": "Until you turn it off",
"audience_all": "Everyone",
"audience_internal": "Only my organisation ({domains})",
"audience_external": "Only people outside my organisation",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "Open raw Sieve editor",
"fetch_error": "Couldn't load the filters",
"expanded_view": "Expanded view",
"show_rule_details": "Show each rule’s conditions and actions",
"expanded_view_description": "Show filter rules with detailed condition and action blocks",
"if": "If",
"then": "Then",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "Enter a start or end date for the period",
"validation_period_invalid": "Enter a valid date with a time for the period, or leave the field empty",
"apply_existing_period": "A rule with a period acts only on messages that arrive within it, so it cannot run on existing messages.",
"apply_existing_in": "Also apply to the messages already in {folder}",
"apply_existing_count": "{count, plural, one {Also apply to the # message already in {folder}} other {Also apply to the # messages already in {folder}}}",
"apply_existing_none": "No message in {folder} matches this rule yet.",
"period_status_scheduled": "scheduled",
"period_status_active": "active",
"period_status_expired": "expired"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Limpiar búsqueda",
"vacation_active": "Respuesta automática activa",
"vacation_turn_off": "Desactivar",
"demo_banner": "Modo demo",
"demo_reset": "Restablecer",
"demo_tour": "Tour",
@@ -1393,6 +1394,8 @@
"from_description": "Déjalo vacío para empezar ya.",
"until_label": "Hasta (opcional)",
"until_description": "Déjalo vacío para seguir respondiendo hasta que lo desactives.",
"from_placeholder": "Desde ahora",
"until_placeholder": "Hasta que lo desactive",
"audience_all": "Todos",
"audience_internal": "Solo mi organización ({domains})",
"audience_external": "Solo personas de fuera de mi organización",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Abrir editor Sieve",
"fetch_error": "Error al cargar los filtros",
"expanded_view": "Vista expandida",
"show_rule_details": "Mostrar las condiciones y acciones de cada regla",
"expanded_view_description": "Mostrar reglas de filtro con bloques detallados de condiciones y acciones",
"if": "Si",
"then": "Entonces",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Introduce una fecha de inicio o de fin para el período",
"validation_period_invalid": "Introduce una fecha válida con hora para el período o deja el campo vacío",
"apply_existing_period": "Una regla con período solo actúa sobre los mensajes que llegan dentro de él, por lo que no se puede aplicar a los mensajes existentes.",
"apply_existing_in": "Aplicar también a los mensajes que ya están en {folder}",
"apply_existing_count": "{count, plural, one {Aplicar también al # mensaje que ya está en {folder}} other {Aplicar también a los # mensajes que ya están en {folder}}}",
"apply_existing_none": "Ningún mensaje de {folder} coincide todavía con esta regla.",
"period_status_scheduled": "programada",
"period_status_active": "activa",
"period_status_expired": "caducada"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Tühjenda otsing",
"vacation_active": "Puhkuse vastaja on aktiivne",
"vacation_turn_off": "Lülita välja",
"demo_banner": "Demorežiim",
"demo_reset": "Lähtesta",
"demo_tour": "Tutvustus",
@@ -1393,6 +1394,8 @@
"from_description": "Jäta tühjaks, et alustada kohe.",
"until_label": "Kuni (valikuline)",
"until_description": "Jäta tühjaks, et vastata, kuni selle välja lülitad.",
"from_placeholder": "Kohe",
"until_placeholder": "Kuni selle välja lülitate",
"audience_all": "Kõigile",
"audience_internal": "Ainult minu organisatsioonile ({domains})",
"audience_external": "Ainult inimestele väljastpoolt minu organisatsiooni",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "Ava Sieve lähtekoodi redaktor",
"fetch_error": "Filtrite laadimine ebaõnnestus",
"expanded_view": "Laiendatud vaade",
"show_rule_details": "Näita iga reegli tingimusi ja toiminguid",
"expanded_view_description": "Näita filtrireegleid koos üksikasjalike tingimuste ja tegevuste plokkidega",
"if": "Kui",
"then": "Siis",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "Sisesta ajavahemiku algus- või lõppkuupäev",
"validation_period_invalid": "Sisesta ajavahemiku jaoks kehtiv kuupäev koos kellaajaga või jäta väli tühjaks",
"apply_existing_period": "Ajavahemikuga reegel toimib ainult kirjadele, mis saabuvad selle jooksul, seega ei saa seda olemasolevatele kirjadele rakendada.",
"apply_existing_in": "Rakenda ka kirjadele, mis on juba kaustas {folder}",
"apply_existing_count": "{count, plural, one {Rakenda ka # kirjale, mis on juba kaustas {folder}} other {Rakenda ka # kirjale, mis on juba kaustas {folder}}}",
"apply_existing_none": "Kaustas {folder} pole veel ühtegi kirja, mis selle reegliga sobiks.",
"period_status_scheduled": "ajastatud",
"period_status_active": "aktiivne",
"period_status_expired": "aegunud"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "پاک کردن جستجو",
"vacation_active": "پاسخ‌گوی خودکار فعال است",
"vacation_turn_off": "خاموش کردن",
"demo_banner": "حالت آزمایشی",
"demo_reset": "بازنشانی",
"demo_tour": "تور",
@@ -1393,6 +1394,8 @@
"from_description": "برای شروع فوری خالی بگذارید.",
"until_label": "تا (اختیاری)",
"until_description": "خالی بگذارید تا پاسخ دادن تا زمانی که خاموشش کنید ادامه یابد.",
"from_placeholder": "از همین حالا",
"until_placeholder": "تا وقتی خاموشش کنید",
"audience_all": "همه",
"audience_internal": "فقط سازمان من ({domains})",
"audience_external": "فقط افراد بیرون از سازمان من",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "باز کردن ویرایشگر خام Sieve",
"fetch_error": "بارگذاری فیلترها ناموفق بود",
"expanded_view": "نمای گسترده",
"show_rule_details": "نمایش شرط‌ها و کارهای هر قانون",
"expanded_view_description": "نمایش قوانین فیلتر با بلوک‌های شرط و عملیات جزئی",
"if": "اگر",
"then": "آنگاه",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "تاریخ شروع یا پایان بازه را وارد کنید",
"validation_period_invalid": "برای بازه یک تاریخ معتبر همراه با ساعت وارد کنید یا فیلد را خالی بگذارید",
"apply_existing_period": "قانونی که بازه زمانی دارد فقط روی پیام‌هایی عمل می‌کند که در آن بازه می‌رسند، بنابراین روی پیام‌های موجود اجرا نمی‌شود.",
"apply_existing_in": "روی پیام‌هایی هم که الان در {folder} هستند اعمال شود",
"apply_existing_count": "{count, plural, one {روی # پیامی هم که الان در {folder} است اعمال شود} other {روی # پیامی هم که الان در {folder} هستند اعمال شود}}",
"apply_existing_none": "هنوز هیچ پیامی در {folder} با این قانون جور نیست.",
"period_status_scheduled": "زمان‌بندی‌شده",
"period_status_active": "فعال",
"period_status_expired": "منقضی‌شده"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Effacer la recherche",
"vacation_active": "Répondeur d'absence activé",
"vacation_turn_off": "Désactiver",
"demo_banner": "Mode démo",
"demo_reset": "Réinitialiser",
"demo_tour": "Visite",
@@ -1393,6 +1394,8 @@
"from_description": "Laissez vide pour commencer tout de suite.",
"until_label": "Au (facultatif)",
"until_description": "Laissez vide pour continuer à répondre jusqu'à ce que vous le désactiviez.",
"from_placeholder": "Dès maintenant",
"until_placeholder": "Jusqu’à ce que vous le désactiviez",
"audience_all": "Tout le monde",
"audience_internal": "Seulement mon organisation ({domains})",
"audience_external": "Seulement les personnes hors de mon organisation",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Ouvrir l'éditeur Sieve brut",
"fetch_error": "Échec du chargement des filtres",
"expanded_view": "Vue étendue",
"show_rule_details": "Afficher les conditions et actions de chaque règle",
"expanded_view_description": "Afficher les règles de filtre avec des blocs de conditions et d'actions détaillés",
"if": "Si",
"then": "Alors",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Indiquez une date de début ou de fin pour la période",
"validation_period_invalid": "Indiquez une date valide avec l'heure pour la période, ou laissez le champ vide",
"apply_existing_period": "Une règle avec une période n'agit que sur les messages qui arrivent pendant celle-ci ; elle ne peut donc pas s'appliquer aux messages existants.",
"apply_existing_in": "Appliquer aussi aux messages déjà dans {folder}",
"apply_existing_count": "{count, plural, one {Appliquer aussi au # message déjà dans {folder}} other {Appliquer aussi aux # messages déjà dans {folder}}}",
"apply_existing_none": "Aucun message de {folder} ne correspond encore à cette règle.",
"period_status_scheduled": "planifiée",
"period_status_active": "active",
"period_status_expired": "expirée"
+7
View File
@@ -165,6 +165,7 @@
},
"clear_search": "נקה חיפוש",
"vacation_active": "משיב חופשה פעיל",
"vacation_turn_off": "כיבוי",
"demo_banner": "מצב הדגמה",
"demo_reset": "אִתחוּל",
"demo_tour": "סִיוּר",
@@ -1336,6 +1337,8 @@
"from_description": "השאירו ריק כדי להתחיל מיד.",
"until_label": "עד (לא חובה)",
"until_description": "השאירו ריק כדי להמשיך להשיב עד שתכבו.",
"from_placeholder": "מיד",
"until_placeholder": "עד שתכבו",
"audience_all": "כולם",
"audience_internal": "רק הארגון שלי ({domains})",
"audience_external": "רק אנשים מחוץ לארגון שלי",
@@ -2803,6 +2806,7 @@
"open_sieve_editor": "פתח את עורך Sieve הגולמי",
"fetch_error": "טעינת המסננים נכשלה",
"expanded_view": "תצוגה מורחבת",
"show_rule_details": "הצגת התנאים והפעולות של כל כלל",
"expanded_view_description": "הצג כללי סינון עם תנאים מפורטים ואבני פעולה",
"if": "אם",
"then": "ואז",
@@ -2884,6 +2888,9 @@
"validation_period_empty": "יש להזין תאריך התחלה או סיום לטווח",
"validation_period_invalid": "יש להזין תאריך תקין עם שעה לטווח, או להשאיר את השדה ריק",
"apply_existing_period": "כלל עם טווח זמן פועל רק על הודעות שמגיעות בתוכו, ולכן לא ניתן להחיל אותו על הודעות קיימות.",
"apply_existing_in": "להחיל גם על ההודעות שכבר נמצאות בתיקייה {folder}",
"apply_existing_count": "{count, plural, one {להחיל גם על ההודעה שכבר נמצאת בתיקייה {folder}} other {להחיל גם על # ההודעות שכבר נמצאות בתיקייה {folder}}}",
"apply_existing_none": "עדיין אין הודעה בתיקייה {folder} שמתאימה לכלל הזה.",
"period_status_scheduled": "מתוזמן",
"period_status_active": "פעיל",
"period_status_expired": "פג תוקף"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Keresés törlése",
"vacation_active": "Automatikus válasz aktív",
"vacation_turn_off": "Kikapcsolás",
"demo_banner": "Demó mód",
"demo_reset": "Visszaállítás",
"demo_tour": "Bemutató",
@@ -1393,6 +1394,8 @@
"from_description": "Hagyja üresen az azonnali kezdéshez.",
"until_label": "Eddig (nem kötelező)",
"until_description": "Hagyja üresen, hogy a válaszolás addig tartson, amíg ki nem kapcsolja.",
"from_placeholder": "Azonnal",
"until_placeholder": "Amíg ki nem kapcsolja",
"audience_all": "Mindenkinek",
"audience_internal": "Csak a szervezetemnek ({domains})",
"audience_external": "Csak a szervezetemen kívülieknek",
@@ -2909,6 +2912,7 @@
"open_sieve_editor": "Nyers Sieve szerkesztő megnyitása",
"fetch_error": "Nem sikerült betölteni a szűrőket",
"expanded_view": "Részletes nézet",
"show_rule_details": "Minden szabály feltételeinek és műveleteinek megjelenítése",
"expanded_view_description": "Szűrőszabályok megjelenítése részletes feltétel- és műveletblokkokkal",
"if": "Ha",
"then": "Akkor",
@@ -2987,6 +2991,9 @@
"validation_period_empty": "Adjon meg kezdő vagy befejező dátumot az időszakhoz",
"validation_period_invalid": "Adjon meg érvényes dátumot időponttal az időszakhoz, vagy hagyja üresen a mezőt",
"apply_existing_period": "Az időszakhoz kötött szabály csak az abban érkező üzenetekre hat, ezért meglévő üzenetekre nem alkalmazható.",
"apply_existing_in": "Alkalmazás a már itt lévő üzenetekre is: {folder}",
"apply_existing_count": "{count, plural, one {Alkalmazás a már itt lévő # üzenetre is: {folder}} other {Alkalmazás a már itt lévő # üzenetre is: {folder}}}",
"apply_existing_none": "Itt még egy üzenet sem felel meg ennek a szabálynak: {folder}.",
"period_status_scheduled": "ütemezett",
"period_status_active": "aktív",
"period_status_expired": "lejárt"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Cancella ricerca",
"vacation_active": "Risponditore automatico attivo",
"vacation_turn_off": "Disattiva",
"demo_banner": "Modalità demo",
"demo_reset": "Reimposta",
"demo_tour": "Tour",
@@ -1393,6 +1394,8 @@
"from_description": "Lascia vuoto per iniziare subito.",
"until_label": "Al (facoltativo)",
"until_description": "Lascia vuoto per continuare a rispondere finché non lo disattivi.",
"from_placeholder": "Da subito",
"until_placeholder": "Finché non lo disattivi",
"audience_all": "Tutti",
"audience_internal": "Solo la mia organizzazione ({domains})",
"audience_external": "Solo persone esterne alla mia organizzazione",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Apri editor Sieve",
"fetch_error": "Impossibile caricare i filtri",
"expanded_view": "Vista espansa",
"show_rule_details": "Mostra condizioni e azioni di ogni regola",
"expanded_view_description": "Mostra le regole dei filtri con blocchi dettagliati di condizioni e azioni",
"if": "Se",
"then": "Allora",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Inserisci una data di inizio o di fine per il periodo",
"validation_period_invalid": "Inserisci una data valida con l'ora per il periodo oppure lascia vuoto il campo",
"apply_existing_period": "Una regola con periodo agisce solo sui messaggi che arrivano al suo interno, quindi non si può applicare ai messaggi esistenti.",
"apply_existing_in": "Applica anche ai messaggi già in {folder}",
"apply_existing_count": "{count, plural, one {Applica anche al # messaggio già in {folder}} other {Applica anche ai # messaggi già in {folder}}}",
"apply_existing_none": "Nessun messaggio in {folder} corrisponde ancora a questa regola.",
"period_status_scheduled": "programmata",
"period_status_active": "attiva",
"period_status_expired": "scaduta"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "検索をクリア",
"vacation_active": "不在応答が有効です",
"vacation_turn_off": "オフにする",
"demo_banner": "デモモード",
"demo_reset": "リセット",
"demo_tour": "ツアー",
@@ -1393,6 +1394,8 @@
"from_description": "空欄にするとすぐに始まります。",
"until_label": "終了(任意)",
"until_description": "空欄にすると、オフにするまで返信を続けます。",
"from_placeholder": "今すぐ",
"until_placeholder": "オフにするまで",
"audience_all": "全員",
"audience_internal": "自分の組織のみ({domains})",
"audience_external": "組織外の人のみ",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Sieveスクリプトエディタを開く",
"fetch_error": "フィルターの読み込みに失敗しました",
"expanded_view": "詳細表示",
"show_rule_details": "各ルールの条件と動作を表示",
"expanded_view_description": "フィルタールールを条件とアクションのブロックで表示",
"if": "条件",
"then": "実行",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "期間の開始日または終了日を入力してください",
"validation_period_invalid": "期間には時刻付きの有効な日付を入力するか、欄を空のままにしてください",
"apply_existing_period": "期間付きのルールは期間内に届いたメッセージにのみ適用されるため、既存のメッセージには適用できません。",
"apply_existing_in": "{folder} にあるメッセージにも適用する",
"apply_existing_count": "{count, plural, other {{folder} にある # 件にも適用する}}",
"apply_existing_none": "{folder} にこのルールに合うメッセージはまだありません。",
"period_status_scheduled": "予定",
"period_status_active": "有効",
"period_status_expired": "期限切れ"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "검색어 지우기",
"vacation_active": "부재중 자동 응답이 켜져 있어요",
"vacation_turn_off": "끄기",
"demo_banner": "데모 모드",
"demo_reset": "초기화",
"demo_tour": "둘러보기",
@@ -1393,6 +1394,8 @@
"from_description": "비워 두면 바로 시작합니다.",
"until_label": "종료 (선택)",
"until_description": "비워 두면 끌 때까지 계속 답장합니다.",
"from_placeholder": "지금부터",
"until_placeholder": "끌 때까지",
"audience_all": "모든 사람",
"audience_internal": "내 조직만 ({domains})",
"audience_external": "내 조직 밖의 사람만",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "원본 Sieve 에디터 열기",
"fetch_error": "필터를 불러오지 못했어요",
"expanded_view": "자세히 보기",
"show_rule_details": "각 규칙의 조건과 동작 표시",
"expanded_view_description": "필터 규칙의 조건과 동작을 모두 펼쳐서 보여줘요",
"if": "만약 (If)",
"then": "다음 동작 실행 (Then)",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "기간의 시작일 또는 종료일을 입력해 주세요",
"validation_period_invalid": "기간에 시간을 포함한 올바른 날짜를 입력하거나 칸을 비워 두세요",
"apply_existing_period": "기간이 설정된 규칙은 그 기간에 도착한 메시지에만 적용되므로 기존 메시지에는 적용할 수 없어요.",
"apply_existing_in": "{folder}에 이미 있는 메일에도 적용",
"apply_existing_count": "{count, plural, other {{folder}에 이미 있는 메일 #개에도 적용}}",
"apply_existing_none": "{folder}에 이 규칙에 맞는 메일이 아직 없습니다.",
"period_status_scheduled": "예정",
"period_status_active": "활성",
"period_status_expired": "만료"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Notīrīt meklēšanu",
"vacation_active": "Atvaļinājuma atbilde ir aktīva",
"vacation_turn_off": "Izslēgt",
"demo_banner": "Demo režīms",
"demo_reset": "Atiestatīt",
"demo_tour": "Ekskursija",
@@ -1393,6 +1394,8 @@
"from_description": "Atstājiet tukšu, lai sāktu uzreiz.",
"until_label": "Līdz (neobligāti)",
"until_description": "Atstājiet tukšu, lai atbildētu, līdz to izslēgsiet.",
"from_placeholder": "Uzreiz",
"until_placeholder": "Līdz to izslēgsiet",
"audience_all": "Visiem",
"audience_internal": "Tikai manai organizācijai ({domains})",
"audience_external": "Tikai cilvēkiem ārpus manas organizācijas",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Atvērt Sieve redaktoru",
"fetch_error": "Neizdevās ielādēt filtrus",
"expanded_view": "Izvērstais skats",
"show_rule_details": "Rādīt katra noteikuma nosacījumus un darbības",
"expanded_view_description": "Rādīt filtra noteikumus ar detalizētiem nosacījumiem un darbībām",
"if": "Ja",
"then": "Tad",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Norādiet laika posma sākuma vai beigu datumu",
"validation_period_invalid": "Norādiet laika posmam derīgu datumu ar laiku vai atstājiet lauku tukšu",
"apply_existing_period": "Noteikums ar laika posmu darbojas tikai ar ziņojumiem, kas pienāk šajā posmā, tāpēc to nevar lietot esošajiem ziņojumiem.",
"apply_existing_in": "Piemērot arī vēstulēm, kas jau ir mapē {folder}",
"apply_existing_count": "{count, plural, zero {Piemērot arī # vēstulēm, kas jau ir mapē {folder}} one {Piemērot arī # vēstulei, kas jau ir mapē {folder}} other {Piemērot arī # vēstulēm, kas jau ir mapē {folder}}}",
"apply_existing_none": "Mapē {folder} vēl nav nevienas vēstules, kas atbilst šim noteikumam.",
"period_status_scheduled": "ieplānots",
"period_status_active": "aktīvs",
"period_status_expired": "beidzies"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Хайлтыг арилгах",
"vacation_active": "Амралтын хариулагч идэвхтэй байна",
"vacation_turn_off": "Унтраах",
"demo_banner": "Демо горим",
"demo_reset": "Дахин тохируулах",
"demo_tour": "Аялал",
@@ -1393,6 +1394,8 @@
"from_description": "Шууд эхлүүлэхийн тулд хоосон үлдээнэ үү.",
"until_label": "Дуусах (заавал биш)",
"until_description": "Унтраах хүртэл хариулсаар байхын тулд хоосон үлдээнэ үү.",
"from_placeholder": "Одоо даруй",
"until_placeholder": "Таныг унтраах хүртэл",
"audience_all": "Бүгдэд",
"audience_internal": "Зөвхөн миний байгууллагад ({domains})",
"audience_external": "Зөвхөн байгууллагаас гадуурх хүмүүст",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "Raw Sieve редакторыг нээнэ үү",
"fetch_error": "Шүүлтүүрийг ачаалж чадсангүй",
"expanded_view": "Өргөтгөсөн харах",
"show_rule_details": "Дүрэм бүрийн нөхцөл, үйлдлийг харуулах",
"expanded_view_description": "Шүүлтүүрийн дүрмийг нарийвчилсан нөхцөл, үйлдлийн блокоор харуулах",
"if": "Хэрэв",
"then": "Дараа нь",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "Хугацааны эхлэх эсвэл төгсгөлийн огноог оруулна уу",
"validation_period_invalid": "Хугацаанд цагтай хүчинтэй огноо оруулах эсвэл талбарыг хоосон үлдээнэ үү",
"apply_existing_period": "Хугацаатай дүрэм зөвхөн тэр хугацаанд ирсэн мессежид үйлчилдэг тул одоо байгаа мессежид хэрэглэх боломжгүй.",
"apply_existing_in": "{folder} доторх байгаа захианд бас хэрэглэх",
"apply_existing_count": "{count, plural, one {{folder} доторх байгаа # захианд бас хэрэглэх} other {{folder} доторх байгаа # захианд бас хэрэглэх}}",
"apply_existing_none": "{folder} дотор энэ дүрэмд тохирох захиа одоогоор алга.",
"period_status_scheduled": "төлөвлөсөн",
"period_status_active": "идэвхтэй",
"period_status_expired": "хугацаа дууссан"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Tøm søket",
"vacation_active": "Feriesvar er aktivt",
"vacation_turn_off": "Slå av",
"demo_banner": "Demomodus",
"demo_reset": "Tilbakestill",
"demo_tour": "Omvisning",
@@ -1393,6 +1394,8 @@
"from_description": "La feltet stå tomt for å starte med en gang.",
"until_label": "Til (valgfritt)",
"until_description": "La feltet stå tomt for å fortsette å svare til du slår det av.",
"from_placeholder": "Med en gang",
"until_placeholder": "Til du slår det av",
"audience_all": "Alle",
"audience_internal": "Bare organisasjonen min ({domains})",
"audience_external": "Bare folk utenfor organisasjonen min",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "Åpne redigereren for Sieve-kode",
"fetch_error": "Kunne ikke laste inn filtre",
"expanded_view": "Utvidet visning",
"show_rule_details": "Vis betingelsene og handlingene for hver regel",
"expanded_view_description": "Vis filterregler med detaljerte blokker for vilkår og handlinger",
"if": "Hvis",
"then": "Gjør følgende",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "Angi en start- eller sluttdato for perioden",
"validation_period_invalid": "Angi en gyldig dato med klokkeslett for perioden, eller la feltet stå tomt",
"apply_existing_period": "En regel med periode virker bare på meldinger som kommer i perioden, og kan derfor ikke brukes på eksisterende meldinger.",
"apply_existing_in": "Bruk også på meldingene som allerede er i {folder}",
"apply_existing_count": "{count, plural, one {Bruk også på den # meldingen som allerede er i {folder}} other {Bruk også på de # meldingene som allerede er i {folder}}}",
"apply_existing_none": "Ingen meldinger i {folder} passer med regelen ennå.",
"period_status_scheduled": "planlagt",
"period_status_active": "aktiv",
"period_status_expired": "utløpt"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Zoekopdracht wissen",
"vacation_active": "Afwezigheidsmelder is actief",
"vacation_turn_off": "Uitzetten",
"demo_banner": "Demomodus",
"demo_reset": "Resetten",
"demo_tour": "Rondleiding",
@@ -1393,6 +1394,8 @@
"from_description": "Laat leeg om meteen te beginnen.",
"until_label": "Tot (optioneel)",
"until_description": "Laat leeg om te blijven antwoorden tot je het uitzet.",
"from_placeholder": "Meteen",
"until_placeholder": "Tot u het uitzet",
"audience_all": "Iedereen",
"audience_internal": "Alleen mijn organisatie ({domains})",
"audience_external": "Alleen mensen buiten mijn organisatie",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Sieve-scripteditor openen",
"fetch_error": "Filters konden niet worden geladen",
"expanded_view": "Uitgebreide weergave",
"show_rule_details": "Voorwaarden en acties van elke regel tonen",
"expanded_view_description": "Filterregels weergeven met gedetailleerde voorwaarde- en actieblokken",
"if": "Als",
"then": "Dan",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Vul een start- of einddatum voor de periode in",
"validation_period_invalid": "Vul voor de periode een geldige datum met tijd in, of laat het veld leeg",
"apply_existing_period": "Een regel met een periode werkt alleen op berichten die binnen die periode binnenkomen en kan dus niet op bestaande berichten worden toegepast.",
"apply_existing_in": "Ook toepassen op de berichten die al in {folder} staan",
"apply_existing_count": "{count, plural, one {Ook toepassen op het # bericht dat al in {folder} staat} other {Ook toepassen op de # berichten die al in {folder} staan}}",
"apply_existing_none": "Nog geen bericht in {folder} past bij deze regel.",
"period_status_scheduled": "gepland",
"period_status_active": "actief",
"period_status_expired": "verlopen"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Wyczyść wyszukiwanie",
"vacation_active": "Autoresponder wakacyjny jest aktywny",
"vacation_turn_off": "Wyłącz",
"demo_banner": "Tryb demo",
"demo_reset": "Resetuj",
"demo_tour": "Przewodnik",
@@ -1393,6 +1394,8 @@
"from_description": "Zostaw puste, aby zacząć od razu.",
"until_label": "Do (opcjonalnie)",
"until_description": "Zostaw puste, aby odpowiadać, dopóki tego nie wyłączysz.",
"from_placeholder": "Od teraz",
"until_placeholder": "Dopóki jej nie wyłączysz",
"audience_all": "Wszystkim",
"audience_internal": "Tylko mojej organizacji ({domains})",
"audience_external": "Tylko osobom spoza mojej organizacji",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Otwórz surowy edytor Sieve",
"fetch_error": "Nie udało się załadować filtrów",
"expanded_view": "Widok rozszerzony",
"show_rule_details": "Pokaż warunki i działania każdej reguły",
"expanded_view_description": "Pokazuj reguły filtrów ze szczegółowymi blokami warunków i akcji",
"if": "Jeśli",
"then": "Wtedy",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Podaj datę rozpoczęcia lub zakończenia okresu",
"validation_period_invalid": "Podaj prawidłową datę z godziną dla okresu albo pozostaw pole puste",
"apply_existing_period": "Reguła z okresem działa tylko na wiadomości, które przychodzą w tym okresie, dlatego nie można jej zastosować do istniejących wiadomości.",
"apply_existing_in": "Zastosuj też do wiadomości, które już są w folderze {folder}",
"apply_existing_count": "{count, plural, one {Zastosuj też do # wiadomości, która już jest w folderze {folder}} few {Zastosuj też do # wiadomości, które już są w folderze {folder}} many {Zastosuj też do # wiadomości, które już są w folderze {folder}} other {Zastosuj też do # wiadomości, które już są w folderze {folder}}}",
"apply_existing_none": "Żadna wiadomość w folderze {folder} nie pasuje jeszcze do tej reguły.",
"period_status_scheduled": "zaplanowana",
"period_status_active": "aktywna",
"period_status_expired": "wygasła"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Limpar busca",
"vacation_active": "Resposta automática ativa",
"vacation_turn_off": "Desativar",
"demo_banner": "Modo de demonstração",
"demo_reset": "Repor",
"demo_tour": "Tour",
@@ -1393,6 +1394,8 @@
"from_description": "Deixe vazio para começar já.",
"until_label": "Até (opcional)",
"until_description": "Deixe vazio para continuar a responder até desligar.",
"from_placeholder": "A partir de agora",
"until_placeholder": "Até desativar",
"audience_all": "Todos",
"audience_internal": "Só a minha organização ({domains})",
"audience_external": "Só pessoas de fora da minha organização",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Abrir editor Sieve",
"fetch_error": "Falha ao carregar filtros",
"expanded_view": "Vista expandida",
"show_rule_details": "Mostrar as condições e ações de cada regra",
"expanded_view_description": "Mostrar regras de filtro com blocos detalhados de condições e ações",
"if": "Se",
"then": "Então",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Indique uma data de início ou de fim para o período",
"validation_period_invalid": "Indique uma data válida com hora para o período ou deixe o campo vazio",
"apply_existing_period": "Uma regra com período só atua sobre as mensagens que chegam dentro dele, por isso não pode ser aplicada às mensagens existentes.",
"apply_existing_in": "Aplicar também às mensagens que já estão em {folder}",
"apply_existing_count": "{count, plural, one {Aplicar também à # mensagem que já está em {folder}} other {Aplicar também às # mensagens que já estão em {folder}}}",
"apply_existing_none": "Nenhuma mensagem em {folder} corresponde ainda a esta regra.",
"period_status_scheduled": "agendada",
"period_status_active": "ativa",
"period_status_expired": "expirada"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Șterge căutarea",
"vacation_active": "Răspunsul automat de vacanță este activ",
"vacation_turn_off": "Dezactivați",
"demo_banner": "Modul demo",
"demo_reset": "Resetare",
"demo_tour": "Tur de prezentare",
@@ -1393,6 +1394,8 @@
"from_description": "Lăsați gol pentru a începe imediat.",
"until_label": "Până la (opțional)",
"until_description": "Lăsați gol pentru a răspunde până îl dezactivați.",
"from_placeholder": "Imediat",
"until_placeholder": "Până îl dezactivați",
"audience_all": "Tuturor",
"audience_internal": "Doar organizației mele ({domains})",
"audience_external": "Doar persoanelor din afara organizației mele",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "Deschideți editorul de fișiere Sieve în format brut",
"fetch_error": "Nu s-au putut încărca filtrele",
"expanded_view": "Vizualizare extinsă",
"show_rule_details": "Afișați condițiile și acțiunile fiecărei reguli",
"expanded_view_description": "Afișați regulile de filtrare cu blocuri detaliate de condiții și acțiuni",
"if": "Dacă",
"then": "Apoi",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "Introduceți o dată de începere sau de încheiere pentru perioadă",
"validation_period_invalid": "Introduceți o dată validă cu oră pentru perioadă sau lăsați câmpul gol",
"apply_existing_period": "O regulă cu perioadă acționează doar asupra mesajelor care sosesc în acea perioadă, deci nu poate fi aplicată mesajelor existente.",
"apply_existing_in": "Aplicați și mesajelor care sunt deja în {folder}",
"apply_existing_count": "{count, plural, one {Aplicați și # mesajului care este deja în {folder}} few {Aplicați și celor # mesaje care sunt deja în {folder}} other {Aplicați și celor # de mesaje care sunt deja în {folder}}}",
"apply_existing_none": "Niciun mesaj din {folder} nu se potrivește încă acestei reguli.",
"period_status_scheduled": "programată",
"period_status_active": "activă",
"period_status_expired": "expirată"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Очистить поиск",
"vacation_active": "Автоответ на отпуск активен",
"vacation_turn_off": "Выключить",
"demo_banner": "Режим демо",
"demo_reset": "Сбросить",
"demo_tour": "Тур",
@@ -1393,6 +1394,8 @@
"from_description": "Оставьте пустым, чтобы начать сразу.",
"until_label": "По (необязательно)",
"until_description": "Оставьте пустым, чтобы отвечать, пока вы это не выключите.",
"from_placeholder": "Сразу",
"until_placeholder": "Пока вы не выключите",
"audience_all": "Всем",
"audience_internal": "Только моей организации ({domains})",
"audience_external": "Только тем, кто вне моей организации",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Открыть редактор Sieve",
"fetch_error": "Не удалось загрузить фильтры",
"expanded_view": "Расширенный вид",
"show_rule_details": "Показывать условия и действия каждого правила",
"expanded_view_description": "Показывать правила фильтрации с подробными блоками условий и действий",
"if": "Если",
"then": "То",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Укажите дату начала или окончания периода",
"validation_period_invalid": "Укажите для периода корректную дату со временем или оставьте поле пустым",
"apply_existing_period": "Правило с периодом действует только на письма, пришедшие в этот период, поэтому его нельзя применить к уже имеющимся письмам.",
"apply_existing_in": "Применить и к письмам, которые уже есть в папке {folder}",
"apply_existing_count": "{count, plural, one {Применить и к # письму, которое уже есть в папке {folder}} few {Применить и к # письмам, которые уже есть в папке {folder}} many {Применить и к # письмам, которые уже есть в папке {folder}} other {Применить и к # письма, которые уже есть в папке {folder}}}",
"apply_existing_none": "В папке {folder} пока нет писем под это правило.",
"period_status_scheduled": "запланировано",
"period_status_active": "активно",
"period_status_expired": "истекло"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Vymazať hľadanie",
"vacation_active": "Automatická odpoveď je aktívna",
"vacation_turn_off": "Vypnúť",
"demo_banner": "Demo režim",
"demo_reset": "Resetovať",
"demo_tour": "Sprievodca",
@@ -1393,6 +1394,8 @@
"from_description": "Nechajte prázdne, aby sa začalo hneď.",
"until_label": "Do (nepovinné)",
"until_description": "Nechajte prázdne, aby sa odpovedalo, kým to nevypnete.",
"from_placeholder": "Hneď",
"until_placeholder": "Kým ju nevypnete",
"audience_all": "Všetkým",
"audience_internal": "Len mojej organizácii ({domains})",
"audience_external": "Len ľuďom mimo mojej organizácie",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "Otvoriť textový editor Sieve",
"fetch_error": "Nepodarilo sa načítať filtre",
"expanded_view": "Rozšírené zobrazenie",
"show_rule_details": "Zobraziť podmienky a akcie každého pravidla",
"expanded_view_description": "Zobrazovať pravidlá filtrov s podrobnými blokmi podmienok a akcií",
"if": "Ak",
"then": "Potom",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "Zadajte dátum začiatku alebo ukončenia obdobia",
"validation_period_invalid": "Zadajte pre obdobie platný dátum aj s časom alebo pole nechajte prázdne",
"apply_existing_period": "Pravidlo s obdobím pôsobí len na správy, ktoré v ňom prídu, preto ho nemožno použiť na existujúce správy.",
"apply_existing_in": "Použiť aj na správy, ktoré už sú v priečinku {folder}",
"apply_existing_count": "{count, plural, one {Použiť aj na # správu, ktorá už je v priečinku {folder}} few {Použiť aj na # správy, ktoré už sú v priečinku {folder}} many {Použiť aj na # správy, ktoré už sú v priečinku {folder}} other {Použiť aj na # správ, ktoré už sú v priečinku {folder}}}",
"apply_existing_none": "Tomuto pravidlu zatiaľ nezodpovedá žiadna správa v priečinku {folder}.",
"period_status_scheduled": "naplánované",
"period_status_active": "aktívne",
"period_status_expired": "uplynulo"
+7
View File
@@ -320,6 +320,7 @@
},
"clear_search": "ล้างการค้นหา",
"vacation_active": "เปิดใช้งานการตอบกลับอัตโนมัติอยู่",
"vacation_turn_off": "ปิด",
"demo_banner": "โหมดทดลองใช้งาน (Demo)",
"demo_reset": "รีเซ็ตข้อมูล",
"demo_tour": "แนะนำการใช้งาน",
@@ -749,6 +750,8 @@
"from_description": "เว้นว่างไว้เพื่อเริ่มทันที",
"until_label": "จนถึง (ไม่บังคับ)",
"until_description": "เว้นว่างไว้เพื่อตอบกลับต่อไปจนกว่าคุณจะปิด",
"from_placeholder": "ทันที",
"until_placeholder": "จนกว่าคุณจะปิด",
"audience_all": "ทุกคน",
"audience_internal": "เฉพาะองค์กรของฉัน ({domains})",
"audience_external": "เฉพาะคนนอกองค์กรของฉัน",
@@ -5913,6 +5916,7 @@
"open_sieve_editor": "เปิดตัวแก้ไข Sieve โดยตรง",
"fetch_error": "โหลดตัวกรองไม่สำเร็จ",
"expanded_view": "มุมมองขยาย",
"show_rule_details": "แสดงเงื่อนไขและการทำงานของแต่ละกฎ",
"expanded_view_description": "แสดงกฎตัวกรองพร้อมบล็อกเงื่อนไขและการดำเนินการโดยละเอียด",
"if": "ถ้า (If)",
"then": "แล้ว (Then)",
@@ -5991,6 +5995,9 @@
"validation_period_empty": "ป้อนวันที่เริ่มต้นหรือวันที่สิ้นสุดสำหรับช่วงเวลา",
"validation_period_invalid": "ป้อนวันที่และเวลาที่ถูกต้อง หรือเว้นว่างไว้",
"apply_existing_period": "กฎที่มีการกำหนดช่วงเวลาจะมีผลกับข้อความที่มาถึงในช่วงเวลานั้นเท่านั้น จึงไม่สามารถใช้กับข้อความที่มีอยู่ได้",
"apply_existing_in": "ใช้กับข้อความที่อยู่ใน {folder} แล้วด้วย",
"apply_existing_count": "{count, plural, other {ใช้กับข้อความ # ฉบับที่อยู่ใน {folder} แล้วด้วย}}",
"apply_existing_none": "ยังไม่มีข้อความใน {folder} ที่ตรงกับกฎนี้",
"period_status_scheduled": "กำหนดเวลาแล้ว",
"period_status_active": "ใช้งานอยู่",
"period_status_expired": "หมดอายุแล้ว"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Aramayı temizle",
"vacation_active": "Tatil otomatik yanıtı aktif",
"vacation_turn_off": "Kapat",
"demo_banner": "Demo Modu",
"demo_reset": "Sıfırla",
"demo_tour": "Tur",
@@ -1393,6 +1394,8 @@
"from_description": "Hemen başlamak için boş bırakın.",
"until_label": "Bitiş (isteğe bağlı)",
"until_description": "Siz kapatana kadar yanıt vermeye devam etmek için boş bırakın.",
"from_placeholder": "Hemen",
"until_placeholder": "Siz kapatana kadar",
"audience_all": "Herkese",
"audience_internal": "Yalnızca kuruluşuma ({domains})",
"audience_external": "Yalnızca kuruluşum dışındakilere",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Ham Sieve düzenleyicisini aç",
"fetch_error": "Filtreler yüklenemedi",
"expanded_view": "Genişletilmiş görünüm",
"show_rule_details": "Her kuralın koşullarını ve işlemlerini göster",
"expanded_view_description": "Filtre kurallarını ayrıntılı koşul ve işlem bloklarıyla göster",
"if": "Eğer",
"then": "O zaman",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Dönem için bir başlangıç veya bitiş tarihi girin",
"validation_period_invalid": "Dönem için saatiyle birlikte geçerli bir tarih girin veya alanı boş bırakın",
"apply_existing_period": "Dönemi olan bir kural yalnızca o dönemde gelen iletilere uygulanır, bu nedenle mevcut iletilere uygulanamaz.",
"apply_existing_in": "{folder} içindeki mevcut iletilere de uygula",
"apply_existing_count": "{count, plural, one {{folder} içindeki # mevcut iletiye de uygula} other {{folder} içindeki # mevcut iletiye de uygula}}",
"apply_existing_none": "{folder} içinde bu kurala uyan ileti henüz yok.",
"period_status_scheduled": "planlandı",
"period_status_active": "etkin",
"period_status_expired": "süresi doldu"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Очистити пошук",
"vacation_active": "Автовідповідач активний",
"vacation_turn_off": "Вимкнути",
"demo_banner": "Демонстраційний режим",
"demo_reset": "Скинути",
"demo_tour": "Тур",
@@ -1393,6 +1394,8 @@
"from_description": "Залиште порожнім, щоб почати одразу.",
"until_label": "До (необов'язково)",
"until_description": "Залиште порожнім, щоб відповідати, доки ви це не вимкнете.",
"from_placeholder": "Одразу",
"until_placeholder": "Доки ви не вимкнете",
"audience_all": "Усім",
"audience_internal": "Лише моїй організації ({domains})",
"audience_external": "Лише тим, хто поза моєю організацією",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "Відкрийте редактор Raw Sieve",
"fetch_error": "Не вдалося завантажити фільтри",
"expanded_view": "Розгорнутий вигляд",
"show_rule_details": "Показувати умови й дії кожного правила",
"expanded_view_description": "Показати правила фільтрації з детальними умовами та блоками дій",
"if": "Якщо",
"then": "Потім",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "Вкажіть дату початку або закінчення періоду",
"validation_period_invalid": "Вкажіть для періоду дійсну дату з часом або залиште поле порожнім",
"apply_existing_period": "Правило з періодом діє лише на листи, що надходять у цей період, тому його не можна застосувати до наявних листів.",
"apply_existing_in": "Застосувати й до листів, які вже є в теці {folder}",
"apply_existing_count": "{count, plural, one {Застосувати й до # листа, який уже є в теці {folder}} few {Застосувати й до # листів, які вже є в теці {folder}} many {Застосувати й до # листів, які вже є в теці {folder}} other {Застосувати й до # листа, які вже є в теці {folder}}}",
"apply_existing_none": "У теці {folder} поки немає листів під це правило.",
"period_status_scheduled": "заплановано",
"period_status_active": "активне",
"period_status_expired": "завершено"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "Xóa tìm kiếm",
"vacation_active": "Đang bật trả lời tự động khi vắng mặt",
"vacation_turn_off": "Tắt",
"demo_banner": "Chế độ demo",
"demo_reset": "Đặt lại",
"demo_tour": "Tham quan",
@@ -1405,6 +1406,8 @@
"from_description": "Để trống để bắt đầu ngay.",
"until_label": "Đến (không bắt buộc)",
"until_description": "Để trống để tiếp tục trả lời cho đến khi bạn tắt.",
"from_placeholder": "Ngay bây giờ",
"until_placeholder": "Cho đến khi bạn tắt",
"audience_all": "Mọi người",
"audience_internal": "Chỉ tổ chức của tôi ({domains})",
"audience_external": "Chỉ người ngoài tổ chức của tôi",
@@ -2925,6 +2928,7 @@
"open_sieve_editor": "Mở trình chỉnh sửa mã Sieve",
"fetch_error": "Không thể tải bộ lọc",
"expanded_view": "Chế độ xem mở rộng",
"show_rule_details": "Hiện điều kiện và hành động của từng quy tắc",
"expanded_view_description": "Hiển thị quy tắc lọc với các khối hành động và điều kiện chi tiết",
"if": "Nếu",
"then": "Sau đó",
@@ -3003,6 +3007,9 @@
"validation_period_empty": "Nhập ngày bắt đầu hoặc ngày kết thúc cho khoảng thời gian",
"validation_period_invalid": "Nhập ngày hợp lệ cùng với thời gian trong khoảng thời gian hoặc để trống trường",
"apply_existing_period": "Quy tắc có giới hạn thời gian chỉ áp dụng cho thư đến trong khoảng đó, nên không thể chạy trên các thư hiện có.",
"apply_existing_in": "Áp dụng cả cho các thư đã có trong {folder}",
"apply_existing_count": "{count, plural, other {Áp dụng cả cho # thư đã có trong {folder}}}",
"apply_existing_none": "Chưa có thư nào trong {folder} khớp với quy tắc này.",
"period_status_scheduled": "Theo lịch trình",
"period_status_active": "Hoạt động",
"period_status_expired": "Hết hạn"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "清除搜尋",
"vacation_active": "休假自動回覆已開啟",
"vacation_turn_off": "關閉",
"demo_banner": "示範模式",
"demo_reset": "重設",
"demo_tour": "功能導覽",
@@ -1393,6 +1394,8 @@
"from_description": "留空即立即開始。",
"until_label": "結束(選填)",
"until_description": "留空即持續回覆,直到您關閉。",
"from_placeholder": "立即",
"until_placeholder": "直到您關閉",
"audience_all": "所有人",
"audience_internal": "僅限我的組織({domains})",
"audience_external": "僅限組織外的人",
@@ -2913,6 +2916,7 @@
"open_sieve_editor": "開啟原始 Sieve 編輯器",
"fetch_error": "無法載入篩選規則",
"expanded_view": "展開檢視",
"show_rule_details": "顯示每條規則的條件和動作",
"expanded_view_description": "顯示包含詳細條件與動作區塊的篩選規則",
"if": "如果",
"then": "然後",
@@ -2991,6 +2995,9 @@
"validation_period_empty": "請輸入期間的開始日期或結束日期",
"validation_period_invalid": "請為期間輸入含時間的有效日期,或將該欄位留空",
"apply_existing_period": "設定了期間的規則只作用於在該期間內收到的郵件,因此無法套用到既有郵件。",
"apply_existing_in": "同時套用到{folder}中既有的郵件",
"apply_existing_count": "{count, plural, other {同時套用到{folder}中既有的 # 封郵件}}",
"apply_existing_none": "{folder}中還沒有符合此規則的郵件。",
"period_status_scheduled": "尚未開始",
"period_status_active": "生效中",
"period_status_expired": "已過期"
+7
View File
@@ -174,6 +174,7 @@
},
"clear_search": "清除搜索",
"vacation_active": "假期自动回复已开启",
"vacation_turn_off": "关闭",
"demo_banner": "演示模式",
"demo_reset": "重置",
"demo_tour": "引导",
@@ -1393,6 +1394,8 @@
"from_description": "留空则立即开始。",
"until_label": "结束(可选)",
"until_description": "留空则一直回复,直到您关闭。",
"from_placeholder": "立即",
"until_placeholder": "直到您关闭",
"audience_all": "所有人",
"audience_internal": "仅限我的组织({domains})",
"audience_external": "仅限组织外的人",
@@ -2906,6 +2909,7 @@
"open_sieve_editor": "打开原始 Sieve 编辑器",
"fetch_error": "加载过滤器失败",
"expanded_view": "展开视图",
"show_rule_details": "显示每条规则的条件和操作",
"expanded_view_description": "显示包含详细条件和操作的过滤规则",
"if": "如果",
"then": "然后",
@@ -2984,6 +2988,9 @@
"validation_period_empty": "请输入时间段的开始日期或结束日期",
"validation_period_invalid": "请为时间段输入带时间的有效日期,或将该字段留空",
"apply_existing_period": "设置了时间段的规则只作用于在该时间段内收到的邮件,因此无法应用于已有邮件。",
"apply_existing_in": "同时应用于{folder}中已有的邮件",
"apply_existing_count": "{count, plural, other {同时应用于{folder}中已有的 # 封邮件}}",
"apply_existing_none": "{folder}中还没有符合此规则的邮件。",
"period_status_scheduled": "未开始",
"period_status_active": "生效中",
"period_status_expired": "已过期"