fix: start out of office with real text and never switch on a blank reply

Out of office used grey placeholder text, so switching the reply on sent
an empty message. A reply never written now opens with an editable subject
and message in the user's language; the switch stays off while the message
is empty and says "Write a message first", and a reply that is on can't be
saved with its text deleted. Forwarding won't switch on without an address,
and the address field shows while it is off. The page puts the switch
first, then "Reply to" with plain choices, then "From/Until (optional)",
then one message editor with formatting instead of a text box plus a
second "Formatted message (HTML)" box. A message nobody edited is sent as
the server has it. A failed save is no longer retried once at once when
the last change was a click.

Test changes: the forward tests type the address before switching
forwarding on (it no longer switches on without one) and check the new
order and option labels; the HTML-body and template tests use the one
editor instead of the text box and HTML switch, which are gone. The
search index names the new rows.
This commit is contained in:
Linus Rath
2026-11-04 06:29:53 +01:00
parent 5a020cf2f9
commit 94b9fe8803
37 changed files with 1091 additions and 254 deletions
@@ -112,8 +112,8 @@ describe('forwarding in the vacation card', () => {
expect(screen.getByText('forward.title')).toBeInTheDocument();
fireEvent.click(screen.getByRole('switch', { name: 'status.label' }));
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
await save(server);
expect(saved()).toBe(true);
@@ -136,8 +136,8 @@ describe('forwarding in the vacation card', () => {
const server = stalwart([rule('a')]);
await openCard(server);
// The auto-reply stays off; only forwarding is switched on.
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
await save(server);
expect(saved()).toBe(true);
@@ -153,8 +153,8 @@ describe('forwarding in the vacation card', () => {
const server = stalwart([]);
await openCard(server);
fireEvent.click(screen.getByRole('switch', { name: 'status.label' }));
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
await save(server);
expect(server.active()).toBe('filters');
@@ -170,27 +170,46 @@ describe('forwarding in the vacation card', () => {
const server = stalwart([rule('a')]);
await openCard(server);
fireEvent.click(screen.getByRole('switch', { name: 'status.label' }));
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
fireEvent.click(keepSwitch());
await save(server);
expect(server.content('filters')).toContain(' redirect :copy "kollege@example.com";');
expect(server.content('filters')).not.toContain(' stop;');
});
it('will not save forwarding without a usable address', async () => {
it('will not switch forwarding on without a usable address, and says to enter one', async () => {
const server = stalwart([rule('a')]);
await openCard(server);
// The address field is there before forwarding is on.
expect(addressInput().value).toBe('');
fireEvent.click(forwardSwitch());
expect(forwardSwitch().getAttribute('aria-checked')).toBe('false');
expect(screen.getByText('enter_address_first')).toBeInTheDocument();
fireEvent.change(addressInput(), { target: { value: 'kollege' } });
expect(screen.getByText('warnings.forward_address')).toBeInTheDocument();
fireEvent.click(forwardSwitch());
expect(forwardSwitch().getAttribute('aria-checked')).toBe('false');
await settle();
expect(server.client.setVacationResponse).not.toHaveBeenCalled();
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
expect(screen.queryByText('warnings.forward_address')).toBeNull();
expect(screen.queryByText('enter_address_first')).toBeNull();
fireEvent.click(forwardSwitch());
expect(forwardSwitch().getAttribute('aria-checked')).toBe('true');
await save(server);
});
it('will not save forwarding that is on once its address is made unusable', async () => {
const server = stalwart([rule('a')]);
await openCard(server);
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
await save(server);
fireEvent.change(addressInput(), { target: { value: 'kollege' } });
expect(screen.getByText('warnings.forward_address')).toBeInTheDocument();
await settle();
expect(server.client.setVacationResponse).toHaveBeenCalledTimes(1);
});
it('does not count an address typed and switched off again as a change', async () => {
const server = stalwart([rule('a')]);
await openCard(server);
@@ -205,8 +224,8 @@ describe('forwarding in the vacation card', () => {
it('warns that a forwarding rule is skipped when a copy is kept and the server allows one redirect', async () => {
const server = stalwart([rule('f', { actions: [{ type: 'forward', value: 'chef@example.com' }] })], { maxNumberRedirects: 1 });
await openCard(server);
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
expect(screen.queryByText(/^forward_limit/)).toBeNull();
fireEvent.click(keepSwitch());
expect(screen.getByText('forward_limit 1')).toBeInTheDocument();
@@ -216,8 +235,8 @@ describe('forwarding in the vacation card', () => {
const server = stalwart([rule('a')]);
await openCard(server);
fireEvent.click(screen.getByRole('switch', { name: 'status.label' }));
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
await save(server);
// Back as stored after the save, as the server has it now.
@@ -256,9 +275,9 @@ describe('forwarding in the vacation card', () => {
const server = stalwart([rule('a')]);
await openCard(server);
server.client.updateSieveScript.mockRejectedValueOnce(new Error('server said no'));
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(screen.getByRole('switch', { name: 'status.label' }));
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
await save(server);
expect(toast.error).toHaveBeenCalledWith('vacation_filters_save_failed');
expect(saved()).toBe(false);
@@ -318,8 +337,8 @@ describe('forwarding in the vacation card', () => {
it('starts over on another account, even where both store the same', async () => {
const server = stalwart([rule('a')]);
await openCard(server);
fireEvent.click(forwardSwitch());
fireEvent.change(addressInput(), { target: { value: 'chef@a.example' } });
fireEvent.click(forwardSwitch());
// Neither account has forwarding stored, so no stored value changes.
act(() => { useAuthStore.setState({ activeAccountId: 'other-login' }); });
@@ -406,7 +425,7 @@ describe('who gets the auto-reply, in the vacation card', () => {
withIdentities();
await openCard(stalwart([rule('a')]));
expect(audienceSelect().value).toBe('all');
expect(screen.getByText('audience.description example.com, example.org')).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'audience_internal example.com, example.org' })).toBeInTheDocument();
});
it('is not offered without identities to tell the own domains', async () => {
@@ -511,20 +530,25 @@ describe('saving the out-of-office card', () => {
describe('layout of the out-of-office card', () => {
const follows = (a: Node, b: Node) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
it('puts the period first, then the auto-reply and forwarding, each with its own switch and state', async () => {
it('puts the switch first, then the optional dates, the message and forwarding, each with its own switch and state', async () => {
await openCard(stalwart([rule('a')]));
const [page, reply, forwarding] = ['title', 'auto_reply.title', 'forward.title'].map((key) => screen.getByText(key));
const start = screen.getByText('date_range.start');
expect(follows(page, start)).toBe(true);
expect(follows(start, reply)).toBe(true);
expect(follows(reply, forwarding)).toBe(true);
expect(screen.queryByText('date_range.title')).toBeNull();
const [page, message, forwarding] = ['title', 'message_title', 'forward.title'].map((key) => screen.getByText(key));
const replySwitch = screen.getByRole('switch', { name: 'status.label' });
const [from, until] = ['from_label', 'until_label'].map((key) => screen.getByText(key));
expect(follows(page, replySwitch)).toBe(true);
expect(follows(replySwitch, from)).toBe(true);
expect(follows(from, until)).toBe(true);
expect(follows(until, message)).toBe(true);
expect(follows(message, forwarding)).toBe(true);
// One message editor, no second "formatted message" box.
expect(screen.queryByText('message.html_label')).toBeNull();
// Two switches, each saying whether it is on, independent of the other.
const replyRow = screen.getByRole('switch', { name: 'status.label' }).parentElement!;
const forwardRow = forwardSwitch().parentElement!;
expect(within(replyRow).getByText('status.inactive')).toBeInTheDocument();
expect(within(forwardRow).getByText('status.inactive')).toBeInTheDocument();
fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } });
fireEvent.click(forwardSwitch());
expect(within(forwardRow).getByText('status.active')).toBeInTheDocument();
expect(within(replyRow).getByText('status.inactive')).toBeInTheDocument();
@@ -1,108 +0,0 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import React from 'react';
import { mockStalwartAccount } from '@/lib/filters/__tests__/sieve-mock';
import { useAuthStore } from '@/stores/auth-store';
import { useVacationStore } from '@/stores/vacation-store';
import { VacationSettings } from '../vacation-settings';
// The HTML reply and its plain-text part in the real vacation card: a save
// derives the text from the HTML when the Body is left blank, and what the
// server then holds must not look like a change still waiting to be saved.
const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), info: vi.fn() }));
vi.mock('@/stores/toast-store', () => ({ toast }));
vi.mock('next-intl', () => {
const t = (key: string, values?: Record<string, unknown>) => (values ? `${key} ${Object.values(values).join(' ')}` : key);
return { useTranslations: () => t, useLocale: () => 'en' };
});
// The editor hands the card its HTML as typed.
vi.mock('@/components/email/rich-text-editor', () => ({
RichTextEditor: ({ content, onChange }: { content: string; onChange: (html: string) => void }) => (
<textarea data-testid="html-editor" value={content} onChange={(e) => onChange(e.target.value)} />
),
}));
const initial = { auth: useAuthStore.getState(), vacation: useVacationStore.getState() };
afterEach(() => {
useAuthStore.setState(initial.auth, true);
useVacationStore.setState(initial.vacation, true);
});
type Server = ReturnType<typeof mockStalwartAccount>;
async function openCard(server: Server) {
useAuthStore.setState({ client: server.client as never });
useVacationStore.setState({ isSupported: true });
render(<VacationSettings />);
await waitFor(() => expect(useVacationStore.getState().isLoading).toBe(false));
await screen.findByText('status.label');
}
const body = () => screen.getByPlaceholderText('message.body_placeholder') as HTMLTextAreaElement;
const htmlSwitch = () => screen.getByRole('switch', { name: 'message.html_label' });
const editor = () => screen.getByTestId('html-editor') as HTMLTextAreaElement;
const sends = (server: Server) => server.client.setVacationResponse.mock.calls.length;
const lastSent = (server: Server) =>
server.client.setVacationResponse.mock.calls[sends(server) - 1][0] as { textBody: string; htmlBody: string | null };
async function saved(server: Server, count: number) {
await waitFor(() => expect(sends(server)).toBe(count), { timeout: 3000 });
await waitFor(() => expect(useVacationStore.getState().isSaving).toBe(false));
}
/** Long enough for a save that was due to have started, twice over. */
const settle = () => act(() => new Promise((resolve) => setTimeout(resolve, 2500)));
describe('HTML auto-reply and its plain-text part', () => {
it('saves an HTML reply with a blank Body once, with the text derived from it', async () => {
const server = mockStalwartAccount('b', [], ['vacation'], { vacation: { textBody: '' } });
await openCard(server);
fireEvent.click(htmlSwitch());
fireEvent.change(editor(), { target: { value: '<p>Away until Monday.</p>' } });
await saved(server, 1);
expect(lastSent(server)).toMatchObject({ htmlBody: '<p>Away until Monday.</p>', textBody: 'Away until Monday.' });
await settle();
expect(sends(server)).toBe(1);
expect(screen.getByRole('status').textContent).toMatch(/saved/);
}, 15_000);
it('saves HTML the sanitizer rewrites once', async () => {
const server = mockStalwartAccount('b', [], ['vacation'], { vacation: { textBody: '' } });
await openCard(server);
fireEvent.change(body(), { target: { value: 'Away.' } });
fireEvent.click(htmlSwitch());
fireEvent.change(editor(), { target: { value: '<p>Away<br/>until Monday.</p>' } });
await saved(server, 1);
expect(lastSent(server).htmlBody).toBe('<p>Away<br>until Monday.</p>');
await settle();
expect(sends(server)).toBe(1);
}, 15_000);
it('derives the text again after a reload, and keeps it when HTML is switched off', async () => {
const server = mockStalwartAccount('b', [], ['vacation'], {
vacation: { htmlBody: '<p>Back on 5 May.</p>', textBody: 'Back on 5 May.' },
});
await openCard(server);
expect(body().value).toBe('');
fireEvent.change(editor(), { target: { value: '<p>Back on 12 May.</p>' } });
await saved(server, 1);
expect(lastSent(server)).toMatchObject({ htmlBody: '<p>Back on 12 May.</p>', textBody: 'Back on 12 May.' });
fireEvent.click(htmlSwitch());
expect(body().value).toBe('Back on 12 May.');
await saved(server, 2);
expect(lastSent(server)).toMatchObject({ htmlBody: null, textBody: 'Back on 12 May.' });
}, 15_000);
it('keeps a plain-text part the user wrote', async () => {
const server = mockStalwartAccount('b', [], ['vacation'], {
vacation: { htmlBody: '<p>Back on 5 May.</p>', textBody: 'Back soon.' },
});
await openCard(server);
expect(body().value).toBe('Back soon.');
fireEvent.change(editor(), { target: { value: '<p>Back on 12 May.</p>' } });
await saved(server, 1);
expect(lastSent(server)).toMatchObject({ htmlBody: '<p>Back on 12 May.</p>', textBody: 'Back soon.' });
}, 15_000);
});
@@ -0,0 +1,138 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import React from 'react';
import { mockStalwartAccount } from '@/lib/filters/__tests__/sieve-mock';
import { useAuthStore } from '@/stores/auth-store';
import { useVacationStore } from '@/stores/vacation-store';
import { VacationSettings } from '../vacation-settings';
// The out-of-office message in the real vacation card: one editor with
// formatting, whose plain-text part a save derives; real starting text for a
// reply never written; and no way to switch on (or keep on) a blank reply.
const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), info: vi.fn() }));
vi.mock('@/stores/toast-store', () => ({ toast }));
vi.mock('next-intl', () => {
const t = (key: string, values?: Record<string, unknown>) => (values ? `${key} ${Object.values(values).join(' ')}` : key);
return { useTranslations: () => t, useLocale: () => 'en' };
});
// The editor hands the card its HTML as typed.
vi.mock('@/components/email/rich-text-editor', () => ({
RichTextEditor: ({ content, onChange }: { content: string; onChange: (html: string) => void }) => (
<textarea data-testid="message-editor" value={content} onChange={(e) => onChange(e.target.value)} />
),
}));
const initial = { auth: useAuthStore.getState(), vacation: useVacationStore.getState() };
afterEach(() => {
useAuthStore.setState(initial.auth, true);
useVacationStore.setState(initial.vacation, true);
});
type Server = ReturnType<typeof mockStalwartAccount>;
async function openCard(server: Server) {
useAuthStore.setState({ client: server.client as never });
useVacationStore.setState({ isSupported: true });
render(<VacationSettings />);
await waitFor(() => expect(useVacationStore.getState().isLoading).toBe(false));
await screen.findByText('status.label');
}
const editor = () => screen.getByTestId('message-editor') as HTMLTextAreaElement;
const subject = () => screen.getByRole('textbox', { name: 'message.subject_label' }) as HTMLInputElement;
const replySwitch = () => screen.getByRole('switch', { name: 'status.label' });
const sends = (server: Server) => server.client.setVacationResponse.mock.calls.length;
const lastSent = (server: Server) =>
server.client.setVacationResponse.mock.calls[sends(server) - 1][0] as {
isEnabled: boolean; subject: string; textBody: string; htmlBody: string | null;
};
async function saved(server: Server, count: number) {
await waitFor(() => expect(sends(server)).toBe(count), { timeout: 3000 });
await waitFor(() => expect(useVacationStore.getState().isSaving).toBe(false));
}
/** Long enough for a save that was due to have started, twice over. */
const settle = () => act(() => new Promise((resolve) => setTimeout(resolve, 2500)));
const blank = () => mockStalwartAccount('b', [], ['vacation'], { vacation: { subject: '', textBody: '', htmlBody: null } });
describe('the out-of-office message', () => {
it('starts a reply never written with real text, not a placeholder, and saves it only once used', async () => {
const server = blank();
await openCard(server);
expect(subject().value).toBe('default_subject');
expect(editor().value).toBe('<p>default_message</p>');
// Opening the page saves nothing.
await settle();
expect(sends(server)).toBe(0);
fireEvent.click(replySwitch());
await saved(server, 1);
expect(lastSent(server)).toMatchObject({
isEnabled: true,
subject: 'default_subject',
htmlBody: '<p>default_message</p>',
textBody: 'default_message',
});
}, 15_000);
it('keeps the switch off while the message is empty, and says to write one', async () => {
const server = blank();
await openCard(server);
fireEvent.change(editor(), { target: { value: '<p></p>' } });
fireEvent.click(replySwitch());
expect(replySwitch().getAttribute('aria-checked')).toBe('false');
expect(screen.getByText('write_message_first')).toBeInTheDocument();
await settle();
expect(server.client.setVacationResponse.mock.calls.some(([sent]) => (sent as { isEnabled: boolean }).isEnabled)).toBe(false);
fireEvent.change(editor(), { target: { value: '<p>Back Monday.</p>' } });
expect(screen.queryByText('write_message_first')).toBeNull();
fireEvent.click(replySwitch());
expect(replySwitch().getAttribute('aria-checked')).toBe('true');
}, 15_000);
it('does not save a reply that is on once its text is deleted', async () => {
const server = blank();
await openCard(server);
fireEvent.click(replySwitch());
await saved(server, 1);
fireEvent.change(editor(), { target: { value: '<p></p>' } });
expect(screen.getByText('write_message_first')).toBeInTheDocument();
await settle();
expect(sends(server)).toBe(1);
}, 15_000);
it('saves a formatted message once, with the plain-text part derived from it', async () => {
const server = mockStalwartAccount('b', [], ['vacation'], { vacation: { textBody: 'away' } });
await openCard(server);
// A stored text-only reply opens in the editor.
expect(editor().value).toBe('<p>away</p>');
fireEvent.change(editor(), { target: { value: '<p>Away until <b>Monday</b></p>' } });
await saved(server, 1);
expect(lastSent(server)).toMatchObject({ htmlBody: '<p>Away until <b>Monday</b></p>', textBody: 'Away until Monday' });
await settle();
expect(sends(server)).toBe(1);
expect(screen.getByRole('status').textContent).toMatch(/saved/);
}, 15_000);
it('saves HTML the sanitizer rewrites once', async () => {
const server = mockStalwartAccount('b', [], ['vacation'], { vacation: { textBody: 'away' } });
await openCard(server);
fireEvent.change(editor(), { target: { value: '<p>Away<br/>until Monday.</p>' } });
await saved(server, 1);
expect(lastSent(server).htmlBody).toBe('<p>Away<br>until Monday.</p>');
await settle();
expect(sends(server)).toBe(1);
}, 15_000);
it('switches a stored reply on without rewriting a message nobody edited', async () => {
const server = mockStalwartAccount('b', [], ['vacation'], { vacation: { htmlBody: '<p>Back on 5 May.</p>', textBody: 'Back soon.' } });
await openCard(server);
expect(editor().value).toBe('<p>Back on 5 May.</p>');
fireEvent.click(replySwitch());
await saved(server, 1);
expect(lastSent(server)).toMatchObject({ isEnabled: true, htmlBody: '<p>Back on 5 May.</p>', textBody: 'Back soon.' });
}, 15_000);
});
@@ -22,6 +22,13 @@ vi.mock('next-intl', () => {
const t = (key: string, values?: Record<string, unknown>) => (values ? `${key} ${Object.values(values).join(' ')}` : key);
return { useTranslations: () => t, useLocale: () => 'en-GB' };
});
// The message editor hands the card its HTML as typed.
vi.mock('@/components/email/rich-text-editor', () => ({
RichTextEditor: ({ content, onChange }: { content: string; onChange: (html: string) => void }) => (
<textarea data-testid="message-editor" value={content} onChange={(e) => onChange(e.target.value)} />
),
}));
const editor = () => screen.getByTestId('message-editor') as HTMLTextAreaElement;
const TEMPLATE: VacationTemplate = {
id: 'holiday',
@@ -78,7 +85,7 @@ describe('out-of-office templates', () => {
fireEvent.change(screen.getByTestId('vacation-template-select'), { target: { value: 'holiday' } });
await settle();
expect(server.client.setVacationResponse).not.toHaveBeenCalled();
expect((screen.getByPlaceholderText('message.body_placeholder') as HTMLTextAreaElement).value).toBe('Old text');
expect(editor().value).toBe('<p>Old text</p>');
// Applying without a deputy refuses and says why.
fireEvent.click(screen.getByTestId('vacation-template-apply'));
@@ -92,11 +99,11 @@ describe('out-of-office templates', () => {
fireEvent.click(screen.getByTestId('vacation-template-apply'));
// ICU versions differ in commas, not in the day.
const filled = (screen.getByPlaceholderText('message.body_placeholder') as HTMLTextAreaElement).value;
expect(filled).toMatch(/^I am away until Monday,? 4 January 2027\.\nPlease write to bob@example\.org\.\nAlice Example$/);
// The text template goes into the one message editor, line by line.
expect(editor().value).toMatch(/^<p>I am away until Monday,? 4 January 2027\.<br>Please write to bob@example\.org\.<br>Alice Example<\/p>$/);
await waitFor(() => expect(server.client.setVacationResponse).toHaveBeenCalledTimes(1), { timeout: 3000 });
const sent = server.client.setVacationResponse.mock.calls[0][0] as { subject: string; textBody: string };
expect(sent.textBody).toBe(filled);
expect(sent.textBody).toMatch(/^I am away until Monday,? 4 January 2027\.\nPlease write to bob@example\.org\.\nAlice Example$/);
expect(sent.subject).toMatch(/^Away until Monday,? 4 January 2027$/);
});
@@ -122,7 +129,6 @@ describe('out-of-office templates', () => {
expect((inputs[1] as HTMLInputElement).value).toBe('2027-01-04T08:00');
fireEvent.change(screen.getByTestId('vacation-template-select'), { target: { value: 'holiday' } });
fireEvent.click(screen.getByTestId('vacation-template-apply'));
expect((screen.getByPlaceholderText('message.body_placeholder') as HTMLTextAreaElement).value)
.toMatch(/^Back Monday,? 4 January 2027 at 0?8:00$/);
expect(editor().value).toMatch(/^<p>Back Monday,? 4 January 2027 at 0?8:00<\/p>$/);
});
});
+159 -117
View File
@@ -25,6 +25,7 @@ import { usePolicyStore } from '@/stores/policy-store';
import { useSettingsStore } from '@/stores/settings-store';
import { companyName } from '@/lib/org/signature-state';
import { fromWallClock, getWallClock, parseLocalDateTime, resolveTimeZone } from '@/lib/timezone';
import { textToParagraphHtml } from '@/lib/text-to-paragraph-html';
const STALWART_VACATION_LIMITS = { subject: 511, body: 2047 };
@@ -42,6 +43,16 @@ function byteLength(value: string): number {
return new TextEncoder().encode(value).length;
}
/** Whether HTML holds any text a recipient would read. */
function hasReadableText(html: string | null | undefined): boolean {
return !!html && !!htmlToPlainText(html).trim();
}
/** A reply the server holds with nothing in it: off, no subject, no message. */
function isBlankReply(stored: { isEnabled: boolean; subject: string; textBody: string; htmlBody: string | null }): boolean {
return !stored.isEnabled && !stored.subject.trim() && !stored.textBody.trim() && !hasReadableText(stored.htmlBody);
}
/** A switch with its state written out next to it ("Active" / "Inactive"). */
function StatusSwitch({ checked, onChange, activeLabel, inactiveLabel }: {
checked: boolean;
@@ -100,6 +111,7 @@ function sameForward(a: VacationForwardSettings | null, b: VacationForward | nul
export function VacationSettings() {
const t = useTranslations('settings.vacation');
const tx = useTranslations('uxs_vacation');
const tFilters = useTranslations('settings.filters');
const tNotifications = useTranslations('notifications');
const locale = useLocale();
@@ -159,9 +171,15 @@ export function VacationSettings() {
}, [timeZone]);
const datesUnusable = localFromDate === null || localToDate === null || halfFilled.from || halfFilled.to;
const [localSubject, setLocalSubject] = useState(subject);
const [localTextBody, setLocalTextBody] = useState(textBody);
const [htmlEnabled, setHtmlEnabled] = useState(!!htmlBody);
const [localHtmlBody, setLocalHtmlBody] = useState(htmlBody || '');
// One message editor with formatting. What it holds is HTML; the plain-text
// part a save sends is derived from it. Until the message is edited (or
// filled with the starting text) a save sends the stored parts unchanged.
const [localBody, setLocalBody] = useState(() => htmlBody || textToParagraphHtml(textBody));
const [bodyEdited, setBodyEdited] = useState(false);
// The user tried to switch the reply on with no message, or forwarding on
// with no address: said under the field until it is filled in.
const [needMessage, setNeedMessage] = useState(false);
const [needAddress, setNeedAddress] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [forwardEnabled, setForwardEnabled] = useState(forward?.enabled ?? false);
const [forwardTo, setForwardTo] = useState(forward?.to ?? '');
@@ -214,14 +232,19 @@ export function VacationSettings() {
setToInput(stored.toDate ? utcToLocalDatetime(stored.toDate, timeZone) : '');
setInputZone(timeZone);
setHalfFilled({ from: false, to: false });
setLocalSubject(stored.subject);
// Text a save derived from the HTML leaves the Body blank, so the next
// HTML edit derives it again instead of keeping the old wording.
setLocalTextBody(stored.htmlBody && stored.textBody.trim() === textFromHtml(sanitizeEmailHtml(stored.htmlBody)).trim()
? ''
: stored.textBody);
setHtmlEnabled(!!stored.htmlBody);
setLocalHtmlBody(stored.htmlBody || '');
setNeedMessage(false);
if (isBlankReply(stored)) {
// Nothing written yet: start with real text in the user's language,
// which they can change, so switching on never sends a blank reply.
// Saved with the first change the user makes, not by opening the page.
setLocalSubject(tx('default_subject'));
setLocalBody(textToParagraphHtml(tx('default_message')));
setBodyEdited(true);
} else {
setLocalSubject(stored.subject);
setLocalBody(stored.htmlBody || textToParagraphHtml(stored.textBody));
setBodyEdited(false);
}
}
// Forwarding a save didn't send takes what the server has now; one it
// sent stays as set here, also when its filters part failed and it
@@ -230,6 +253,7 @@ export function VacationSettings() {
setForwardEnabled(stored.forward?.enabled ?? false);
setForwardTo(stored.forward?.to ?? '');
setForwardKeep(stored.forward?.keepCopy ?? false);
setNeedAddress(false);
}
setAudienceOnly(stored.audience?.only ?? 'all');
// The time zone is read when the fields load, not followed while typing.
@@ -253,9 +277,15 @@ export function VacationSettings() {
touched.current = true;
saveDelay.current = SAVE_AFTER_TYPING;
if (filled.subject) setLocalSubject(filled.subject);
setLocalTextBody(filled.text);
setHtmlEnabled(!!filled.html);
setLocalHtmlBody(filled.html);
setLocalBody(filled.html || textToParagraphHtml(filled.text));
setBodyEdited(true);
};
const editBody = (html: string) => {
touched.current = true;
saveDelay.current = SAVE_AFTER_TYPING;
setLocalBody(html);
setBodyEdited(true);
};
// Reads a date field as the browser holds it, half filled in or not.
@@ -283,9 +313,11 @@ export function VacationSettings() {
// Forwarding as a save stores it: an address that is off is kept for next
// time if it is usable, and dropped otherwise.
const forwardInvalid = forwardAvailable && forwardEnabled && !isValidEmail(forwardTo);
// The address field shows while forwarding is off too, so typing one
// there before switching on is not a change of its own.
const forwardSettings = !forwardAvailable
? undefined
: forwardEnabled || isValidEmail(forwardTo)
: forwardEnabled || (forward && isValidEmail(forwardTo))
? { enabled: forwardEnabled, to: forwardTo, keepCopy: forwardKeep }
: null;
// Only what was changed here is sent: the rest stays as the server has it,
@@ -298,16 +330,15 @@ export function VacationSettings() {
forwardAvailable && forwardEnabled && forwardKeep &&
typeof forwardLimit === 'number' && forwardLimit > 0 && 1 + otherForwards > forwardLimit;
// What a save sends: the sanitized HTML and a plain-text part, derived
// from the HTML when the user left it blank (for clients without HTML).
// What a save sends: the sanitized HTML and the plain-text part derived
// from it (for mail apps without HTML). A message nobody edited goes as
// the server has it, so switching the reply on rewrites nothing.
const payload = useMemo(() => {
const sanitizedHtml =
htmlEnabled && htmlToPlainText(localHtmlBody).trim()
? sanitizeEmailHtml(localHtmlBody)
: null;
const text = localTextBody.trim() || !sanitizedHtml ? localTextBody : textFromHtml(sanitizedHtml);
return { sanitizedHtml, textBody: text };
}, [htmlEnabled, localHtmlBody, localTextBody]);
if (!bodyEdited) return { sanitizedHtml: htmlBody || null, textBody };
const sanitizedHtml = hasReadableText(localBody) ? sanitizeEmailHtml(localBody) : null;
return { sanitizedHtml, textBody: sanitizedHtml ? textFromHtml(sanitizedHtml) : '' };
}, [bodyEdited, localBody, htmlBody, textBody]);
const bodyEmpty = !payload.textBody.trim() && !hasReadableText(payload.sanitizedHtml);
// Stalwart refuses a subject of 512 bytes or more and a text or HTML body
// of 2048 bytes or more, answering only "Field could not be set."
@@ -333,10 +364,11 @@ export function VacationSettings() {
const fromWarning = !fromError && localFromDate && localFromDate !== fromDate && new Date(localFromDate) < todayStart
? t('warnings.start_in_past')
: null;
const hasHtmlContent = htmlEnabled && !!htmlToPlainText(localHtmlBody).trim();
const bodyWarning = oversize.body && sizeLimits
// An empty message never goes live: the switch won't turn on, and a reply
// that is on can't be saved with its text deleted.
const bodyError = oversize.body && sizeLimits
? t('warnings.body_too_long', { max: sizeLimits.body })
: localEnabled && !localTextBody.trim() && !hasHtmlContent ? t('warnings.empty_body') : null;
: (localEnabled || needMessage) && bodyEmpty ? tx('write_message_first') : null;
// The bodies are compared as a save sends them: the server keeps the
// sanitized HTML and the text derived from it, never what was typed.
@@ -353,7 +385,24 @@ export function VacationSettings() {
const hasBlockingError =
datesUnusable ||
!!(localFromDate && localToDate && new Date(localToDate) <= new Date(localFromDate)) ||
oversize.subject || oversize.body || forwardInvalid;
oversize.subject || oversize.body || forwardInvalid || (localEnabled && bodyEmpty);
const switchReply = (on: boolean) => {
if (on && bodyEmpty) {
setNeedMessage(true);
return;
}
setNeedMessage(false);
changeNow(setLocalEnabled)(on);
};
const switchForward = (on: boolean) => {
if (on && !isValidEmail(forwardTo)) {
setNeedAddress(true);
return;
}
setNeedAddress(false);
changeNow(setForwardEnabled)(on);
};
// What a save would store, to tell one change from the next.
const snapshot = JSON.stringify([
@@ -361,6 +410,9 @@ export function VacationSettings() {
]);
// A save that failed or was stopped isn't tried again until something changes.
const [refusedSnapshot, setRefusedSnapshot] = useState<string | null>(null);
// The same, at once: a timer set while the failed save's last render was
// on its way must not send it again.
const refusedRef = useRef<string | null>(null);
const [savedOnce, setSavedOnce] = useState(false);
const savingSnapshot = useRef<string | null>(null);
@@ -398,9 +450,13 @@ export function VacationSettings() {
// false: a plugin stopped the change and is expected to have said why.
if (saved) setSavedOnce(true);
else setRefusedSnapshot(sent);
else {
refusedRef.current = sent;
setRefusedSnapshot(sent);
}
} catch (error) {
console.error('Failed to save vacation response:', error);
refusedRef.current = sent;
setRefusedSnapshot(sent);
toast.error(tNotifications(
error instanceof VacationFiltersError ? 'vacation_filters_save_failed' : 'vacation_save_failed',
@@ -426,7 +482,10 @@ export function VacationSettings() {
useEffect(() => {
if (!canAutoSave || isSaving || !touched.current) return;
const timer = setTimeout(() => { void saveRef.current(); }, saveDelay.current);
const due = snapshot;
const timer = setTimeout(() => {
if (refusedRef.current !== due) void saveRef.current();
}, saveDelay.current);
return () => clearTimeout(timer);
}, [canAutoSave, isSaving, snapshot]);
@@ -479,11 +538,33 @@ export function VacationSettings() {
</Button>
</div>
)}
{/* The period is the absence: the auto-reply and forwarding both keep to it. */}
<SettingsSection title={t('title')} description={t('description')}>
{/* The switch first, then who gets it and when; the period applies to forwarding too. */}
<SettingsSection id="reply" title={t('title')} description={t('status.description')}>
<SettingItem label={t('status.label')}>
<StatusSwitch
checked={localEnabled}
onChange={switchReply}
activeLabel={t('status.active')}
inactiveLabel={t('status.inactive')}
/>
</SettingItem>
{!localEnabled && <VacationFilterWarning accountId={managedAccountId ?? undefined} />}
{canNarrow && (
<SettingItem label={t('audience.label')}>
<Select
value={audienceOnly}
onChange={(value) => changeNow(setAudienceOnly)(value as 'all' | 'internal' | 'external')}
options={[
{ value: 'all', label: tx('audience_all') },
{ value: 'internal', label: tx('audience_internal', { domains: domains.join(', ') }) },
{ value: 'external', label: tx('audience_external') },
]}
/>
</SettingItem>
)}
<SettingItem
label={t('date_range.start')}
description={t('date_range.start_description')}
label={tx('from_label')}
description={tx('from_description')}
>
<input
ref={fromRef}
@@ -492,6 +573,7 @@ export function VacationSettings() {
value={fromInput}
onChange={(e) => readDateField('from', e.currentTarget)}
onBlur={(e) => readDateField('from', e.currentTarget)}
aria-label={tx('from_label')}
aria-invalid={!!fromError}
aria-describedby={fromError ? 'vacation-from-error' : undefined}
className={cn(DATE_FIELD, fromError && 'border-destructive hover:border-destructive')}
@@ -500,8 +582,8 @@ export function VacationSettings() {
<FieldMessage id="vacation-from-error" text={fromError} />
<FieldMessage text={fromWarning} warning />
<SettingItem
label={t('date_range.end')}
description={t('date_range.end_description')}
label={tx('until_label')}
description={tx('until_description')}
>
<input
ref={toRef}
@@ -510,6 +592,7 @@ export function VacationSettings() {
value={toInput}
onChange={(e) => readDateField('to', e.currentTarget)}
onBlur={(e) => readDateField('to', e.currentTarget)}
aria-label={tx('until_label')}
aria-invalid={!!toError}
aria-describedby={toError ? 'vacation-to-error' : undefined}
className={cn(DATE_FIELD, toError && 'border-destructive hover:border-destructive')}
@@ -533,32 +616,7 @@ export function VacationSettings() {
onApply={applyTemplate}
/>
<SettingsSection id="reply" title={t('auto_reply.title')} description={t('status.description')}>
<SettingItem label={t('status.label')}>
<StatusSwitch
checked={localEnabled}
onChange={changeNow(setLocalEnabled)}
activeLabel={t('status.active')}
inactiveLabel={t('status.inactive')}
/>
</SettingItem>
{!localEnabled && <VacationFilterWarning accountId={managedAccountId ?? undefined} />}
{canNarrow && (
<SettingItem
label={t('audience.label')}
description={t('audience.description', { domains: domains.join(', ') })}
>
<Select
value={audienceOnly}
onChange={(value) => changeNow(setAudienceOnly)(value as 'all' | 'internal' | 'external')}
options={[
{ value: 'all', label: t('audience.all') },
{ value: 'internal', label: t('audience.internal') },
{ value: 'external', label: t('audience.external') },
]}
/>
</SettingItem>
)}
<SettingsSection id="reply-message" title={tx('message_title')}>
<SettingItem
label={t('message.subject_label')}
description={t('message.subject_description')}
@@ -568,57 +626,36 @@ export function VacationSettings() {
value={localSubject}
onChange={(e) => changeTyped(setLocalSubject)(e.target.value)}
placeholder={t('message.subject_placeholder')}
aria-label={t('message.subject_label')}
aria-invalid={oversize.subject}
className="w-64 max-w-full px-3 py-1.5 text-sm rounded-md bg-muted border border-border text-foreground placeholder:text-muted-foreground focus:outline-hidden transition-colors duration-150 hover:border-muted-foreground"
/>
</SettingItem>
<FieldMessage text={oversize.subject && sizeLimits ? t('warnings.subject_too_long', { max: sizeLimits.subject }) : null} />
<div className="py-3">
<label htmlFor="vacation-body" className="text-sm font-medium text-foreground block mb-1">
{t('message.body_label')}
</label>
<div className="py-3" data-search-label={tx('message_label')}>
<span id="vacation-body-label" className="text-sm font-medium text-foreground block mb-1">
{tx('message_label')}
</span>
<p className="text-xs text-muted-foreground mb-2">
{t('message.body_description')}
{tx('message_description')}
</p>
<textarea
id="vacation-body"
value={localTextBody}
onChange={(e) => changeTyped(setLocalTextBody)(e.target.value)}
placeholder={t('message.body_placeholder')}
rows={6}
className="w-full px-3 py-2 text-sm rounded-md bg-muted border border-border text-foreground placeholder:text-muted-foreground focus:outline-hidden transition-colors duration-150 hover:border-muted-foreground resize-y"
/>
<FieldMessage text={bodyWarning} warning={!oversize.body} />
</div>
<SettingItem
label={t('message.html_label')}
description={t('message.html_description')}
>
<ToggleSwitch
checked={htmlEnabled}
onChange={changeNow((on: boolean) => {
// Switched off, a blank Body keeps the text derived from the HTML.
if (!on && !localTextBody.trim()) setLocalTextBody(payload.textBody);
setHtmlEnabled(on);
})}
/>
</SettingItem>
{htmlEnabled && (
<div className="pb-3">
<div className="rounded-md border border-border overflow-hidden">
<RichTextEditor
content={localHtmlBody}
onChange={changeTyped(setLocalHtmlBody)}
placeholder={t('message.html_placeholder')}
/>
</div>
<div className={cn('rounded-md border overflow-hidden', bodyError ? 'border-destructive' : 'border-border')}>
<RichTextEditor
content={localBody}
onChange={editBody}
placeholder={tx('default_message')}
ariaLabel={tx('message_label')}
ariaInvalid={!!bodyError}
ariaDescribedBy={bodyError ? 'vacation-body-error' : undefined}
/>
</div>
)}
<FieldMessage id="vacation-body-error" text={bodyError} />
</div>
</SettingsSection>
{(() => {
const showHtmlPreview = htmlEnabled && !!htmlToPlainText(localHtmlBody).trim();
if (!localTextBody.trim() && !showHtmlPreview) return null;
const showHtmlPreview = hasReadableText(payload.sanitizedHtml);
if (bodyEmpty) return null;
return (
<SettingsSection title={t('preview.title')}>
<button
@@ -647,10 +684,10 @@ export function VacationSettings() {
window.open(anchor.href, '_blank', 'noopener,noreferrer');
}
}}
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(localHtmlBody) }}
dangerouslySetInnerHTML={{ __html: payload.sanitizedHtml ?? '' }}
/>
) : (
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{localTextBody}</p>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{payload.textBody}</p>
)}
</div>
)}
@@ -663,25 +700,30 @@ export function VacationSettings() {
<SettingItem label={t('forward.enabled_label')}>
<StatusSwitch
checked={forwardEnabled}
onChange={changeNow(setForwardEnabled)}
onChange={switchForward}
activeLabel={t('status.active')}
inactiveLabel={t('status.inactive')}
/>
</SettingItem>
{/* The address shows while forwarding is off, so it can be filled in before switching on. */}
<SettingItem label={t('forward.to_label')}>
<input
type="email"
value={forwardTo}
onChange={(e) => changeTyped(setForwardTo)(e.target.value)}
placeholder={t('forward.to_placeholder')}
aria-label={t('forward.to_label')}
aria-invalid={forwardInvalid || (needAddress && !isValidEmail(forwardTo))}
className="w-64 max-w-full px-3 py-1.5 text-sm rounded-md bg-muted border border-border text-foreground placeholder:text-muted-foreground focus:outline-hidden transition-colors duration-150 hover:border-muted-foreground"
/>
</SettingItem>
<FieldMessage
text={forwardInvalid
? t('warnings.forward_address')
: needAddress && !isValidEmail(forwardTo) ? tx('enter_address_first') : null}
/>
{forwardEnabled && (
<>
<SettingItem label={t('forward.to_label')}>
<input
type="email"
value={forwardTo}
onChange={(e) => changeTyped(setForwardTo)(e.target.value)}
placeholder={t('forward.to_placeholder')}
aria-label={t('forward.to_label')}
aria-invalid={forwardInvalid}
className="w-64 max-w-full px-3 py-1.5 text-sm rounded-md bg-muted border border-border text-foreground placeholder:text-muted-foreground focus:outline-hidden transition-colors duration-150 hover:border-muted-foreground"
/>
</SettingItem>
<FieldMessage text={forwardInvalid ? t('warnings.forward_address') : null} />
<SettingItem label={t('forward.keep_label')} description={t('forward.keep_description')}>
<ToggleSwitch checked={forwardKeep} onChange={changeNow(setForwardKeep)} />
</SettingItem>
+4 -4
View File
@@ -268,12 +268,12 @@ export const tabSearchEntries: Record<Tab, SettingsSearchEntry[]> = {
{ label: 'settings.email_behavior.signatures_link.label' },
],
vacation: [
{ label: 'settings.vacation.date_range.start' },
{ label: 'settings.vacation.date_range.end' },
{ label: 'settings.vacation.auto_reply.title' },
{ label: 'uxs_vacation.from_label' },
{ label: 'uxs_vacation.until_label' },
{ label: 'uxs_vacation.message_title' },
{ label: 'settings.vacation.status.label' },
{ label: 'settings.vacation.message.subject_label' },
{ label: 'settings.vacation.message.html_label' },
{ label: 'uxs_vacation.message_label' },
{ label: 'settings.vacation.forward.title' },
{ label: 'settings.vacation.forward.enabled_label' },
],
+15
View File
@@ -0,0 +1,15 @@
import { escapeHtml } from '@/lib/email-sanitization';
/**
* Plain text as the formatting editor takes it: a paragraph per block of
* lines, a line break per single newline, everything escaped. Used where a
* text-only out-of-office reply or signature opens in the editor.
*/
export function textToParagraphHtml(text: string): string {
const trimmed = text.replace(/\r\n?/g, '\n').trim();
if (!trimmed) return '';
return trimmed
.split(/\n{2,}/)
.map((block) => `<p>${escapeHtml(block).replace(/\n/g, '<br>')}</p>`)
.join('');
}
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "استخدام هذا النص",
"apply_hint": "يستبدل الموضوع والنص أدناه."
},
"uxs_vacation": {
"default_subject": "خارج المكتب",
"default_message": "شكرًا على رسالتك. أنا خارج المكتب حاليًا وسأرد عند عودتي.",
"write_message_first": "اكتب رسالة أولًا. بدونها سيتلقى الناس ردًا فارغًا.",
"enter_address_first": "أدخل أولًا العنوان الذي ستُعاد التوجيه إليه.",
"message_title": "ردّك",
"message_label": "الرسالة",
"message_description": "ما يتلقاه من يراسلك. يمكنك استخدام التنسيق والروابط.",
"from_label": "من (اختياري)",
"from_description": "اتركه فارغًا للبدء فورًا.",
"until_label": "حتى (اختياري)",
"until_description": "اتركه فارغًا لمواصلة الرد حتى توقفه.",
"audience_all": "الجميع",
"audience_internal": "مؤسستي فقط ({domains})",
"audience_external": "من هم خارج مؤسستي فقط",
"signature_label": "التوقيع",
"signature_plain_label": "التوقيع (نص عادي)",
"signature_plain_description": "أنت تكتب نصًا عاديًا فقط (الكتابة)، لذا توقيعك نص عادي أيضًا.",
"edit_as_html": "تحرير بتنسيق HTML",
"edit_formatted": "العودة إلى محرر التنسيق",
"html_source_label": "التوقيع بتنسيق HTML",
"signature_size": "مستخدم {bytes} من {max} بايت",
"signature_too_long": "طويل جدًا بالنسبة للخادم ({bytes} من {max} بايت). اجعله أقصر."
},
"retention_notice": {
"line": "تُحذف الرسائل هنا بعد {count, plural, one {يوم واحد} two {يومين} few {# أيام} many {# يومًا} other {# يوم}} من نقلها إلى هنا."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Utilitza aquest text",
"apply_hint": "Substitueix l'assumpte i el text de sota."
},
"uxs_vacation": {
"default_subject": "Fora de l'oficina",
"default_message": "Gràcies pel teu correu. Soc fora de l'oficina i respondré quan torni.",
"write_message_first": "Escriu primer un missatge. Sense missatge, la gent rebria una resposta buida.",
"enter_address_first": "Escriu primer l'adreça a la qual reenviar.",
"message_title": "La teva resposta",
"message_label": "Missatge",
"message_description": "El que rep qui t'escriu. Pots fer servir format i enllaços.",
"from_label": "Des de (opcional)",
"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.",
"audience_all": "Tothom",
"audience_internal": "Només la meva organització ({domains})",
"audience_external": "Només persones de fora de la meva organització",
"signature_label": "Signatura",
"signature_plain_label": "Signatura (text sense format)",
"signature_plain_description": "Escrius només text sense format (Redacció), així que la signatura també ho és.",
"edit_as_html": "Edita com a HTML",
"edit_formatted": "Torna a l'editor amb format",
"html_source_label": "Signatura en HTML",
"signature_size": "{bytes} de {max} bytes utilitzats",
"signature_too_long": "Massa llarga per al servidor ({bytes} de {max} bytes). Escurça-la."
},
"retention_notice": {
"line": "Els missatges d'aquí s'eliminen {count, plural, one {# dia} other {# dies}} després de moure'ls aquí."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Použít tento text",
"apply_hint": "Nahradí předmět a text níže."
},
"uxs_vacation": {
"default_subject": "Mimo kancelář",
"default_message": "Děkuji za váš e-mail. Jsem mimo kancelář a odpovím, až se vrátím.",
"write_message_first": "Nejdřív napište zprávu. Bez ní by lidé dostali prázdnou odpověď.",
"enter_address_first": "Nejdřív zadejte adresu, na kterou se má přeposílat.",
"message_title": "Vaše odpověď",
"message_label": "Zpráva",
"message_description": "Co dostanou lidé, kteří vám napíšou. Můžete použít formátování a odkazy.",
"from_label": "Od (nepovinné)",
"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.",
"audience_all": "Všem",
"audience_internal": "Jen mé organizaci ({domains})",
"audience_external": "Jen lidem mimo mou organizaci",
"signature_label": "Podpis",
"signature_plain_label": "Podpis (prostý text)",
"signature_plain_description": "Píšete jen prostý text (Psaní), takže i váš podpis je prostý text.",
"edit_as_html": "Upravit jako HTML",
"edit_formatted": "Zpět do editoru s formátováním",
"html_source_label": "Podpis jako HTML",
"signature_size": "Využito {bytes} z {max} bajtů",
"signature_too_long": "Pro server příliš dlouhý ({bytes} z {max} bajtů). Zkraťte ho."
},
"retention_notice": {
"line": "Zprávy zde se smažou {count, plural, one {# den} few {# dny} many {# dne} other {# dní}} poté, co sem byly přesunuty."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Brug denne tekst",
"apply_hint": "Erstatter emnet og teksten nedenfor."
},
"uxs_vacation": {
"default_subject": "Ikke på kontoret",
"default_message": "Tak for din mail. Jeg er ikke på kontoret og svarer, når jeg er tilbage.",
"write_message_first": "Skriv en besked først. Uden den ville folk få et tomt svar.",
"enter_address_first": "Indtast først den adresse, der skal videresendes til.",
"message_title": "Dit svar",
"message_label": "Besked",
"message_description": "Det, folk får, når de skriver til dig. Du kan bruge formatering og links.",
"from_label": "Fra (valgfrit)",
"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.",
"audience_all": "Alle",
"audience_internal": "Kun min organisation ({domains})",
"audience_external": "Kun folk uden for min organisation",
"signature_label": "Signatur",
"signature_plain_label": "Signatur (ren tekst)",
"signature_plain_description": "Du skriver kun ren tekst (Skrivning), så din signatur er også ren tekst.",
"edit_as_html": "Rediger som HTML",
"edit_formatted": "Tilbage til editoren med formatering",
"html_source_label": "Signatur som HTML",
"signature_size": "{bytes} af {max} byte brugt",
"signature_too_long": "For lang til serveren ({bytes} af {max} byte). Gør den kortere."
},
"retention_notice": {
"line": "Beskeder her slettes {count, plural, one {# dag} other {# dage}} efter, at de blev flyttet hertil."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Diesen Text verwenden",
"apply_hint": "Ersetzt Betreff und Text unten."
},
"uxs_vacation": {
"default_subject": "Abwesend",
"default_message": "Vielen Dank für Ihre E-Mail. Ich bin zurzeit nicht im Büro und antworte, sobald ich zurück bin.",
"write_message_first": "Schreiben Sie zuerst eine Nachricht. Ohne Text bekämen die Leute eine leere Antwort.",
"enter_address_first": "Geben Sie zuerst die Adresse ein, an die weitergeleitet wird.",
"message_title": "Ihre Antwort",
"message_label": "Nachricht",
"message_description": "Das bekommen Leute, die Ihnen schreiben. Sie können Formatierung und Links verwenden.",
"from_label": "Von (optional)",
"from_description": "Leer lassen, um sofort zu beginnen.",
"until_label": "Bis (optional)",
"until_description": "Leer lassen, um weiter zu antworten, bis Sie es ausschalten.",
"audience_all": "Allen",
"audience_internal": "Nur meiner Organisation ({domains})",
"audience_external": "Nur Leuten außerhalb meiner Organisation",
"signature_label": "Signatur",
"signature_plain_label": "Signatur (nur Text)",
"signature_plain_description": "Sie schreiben nur einfachen Text (Verfassen), daher ist auch Ihre Signatur einfacher Text.",
"edit_as_html": "Als HTML bearbeiten",
"edit_formatted": "Zurück zum Editor mit Formatierung",
"html_source_label": "Signatur als HTML",
"signature_size": "{bytes} von {max} Byte belegt",
"signature_too_long": "Zu lang für den Server ({bytes} von {max} Byte). Kürzen Sie die Signatur."
},
"retention_notice": {
"line": "Nachrichten hier werden {count, plural, one {# Tag} other {# Tage}} nach dem Verschieben hierher gelöscht."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Use this text",
"apply_hint": "Replaces the subject and text below."
},
"uxs_vacation": {
"default_subject": "Out of office",
"default_message": "Thank you for your email. I'm out of the office and will reply when I'm back.",
"write_message_first": "Write a message first. Without one, people would get an empty reply.",
"enter_address_first": "Enter the address to forward to first.",
"message_title": "Your reply",
"message_label": "Message",
"message_description": "What people get when they write to you. You can use formatting and links.",
"from_label": "From (optional)",
"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.",
"audience_all": "Everyone",
"audience_internal": "Only my organisation ({domains})",
"audience_external": "Only people outside my organisation",
"signature_label": "Signature",
"signature_plain_label": "Signature (plain text)",
"signature_plain_description": "You write plain text only (Composing), so your signature is plain text too.",
"edit_as_html": "Edit as HTML",
"edit_formatted": "Back to the formatting editor",
"html_source_label": "Signature as HTML",
"signature_size": "{bytes} of {max} bytes used",
"signature_too_long": "Too long for the server ({bytes} of {max} bytes). Make it shorter."
},
"retention_notice": {
"line": "Messages here are deleted {count, plural, one {# day} other {# days}} after they were moved here."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Usar este texto",
"apply_hint": "Sustituye el asunto y el texto de abajo."
},
"uxs_vacation": {
"default_subject": "Fuera de la oficina",
"default_message": "Gracias por tu correo. Estoy fuera de la oficina y responderé cuando vuelva.",
"write_message_first": "Escribe primero un mensaje. Sin él, la gente recibiría una respuesta vacía.",
"enter_address_first": "Escribe primero la dirección a la que reenviar.",
"message_title": "Tu respuesta",
"message_label": "Mensaje",
"message_description": "Lo que recibe quien te escribe. Puedes usar formato y enlaces.",
"from_label": "Desde (opcional)",
"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.",
"audience_all": "Todos",
"audience_internal": "Solo mi organización ({domains})",
"audience_external": "Solo personas de fuera de mi organización",
"signature_label": "Firma",
"signature_plain_label": "Firma (texto sin formato)",
"signature_plain_description": "Escribes solo texto sin formato (Redacción), así que tu firma también lo es.",
"edit_as_html": "Editar como HTML",
"edit_formatted": "Volver al editor con formato",
"html_source_label": "Firma en HTML",
"signature_size": "{bytes} de {max} bytes usados",
"signature_too_long": "Demasiado larga para el servidor ({bytes} de {max} bytes). Acórtala."
},
"retention_notice": {
"line": "Los mensajes de aquí se eliminan {count, plural, one {# día} other {# días}} después de moverlos aquí."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Kasuta seda teksti",
"apply_hint": "Asendab allpool oleva teema ja teksti."
},
"uxs_vacation": {
"default_subject": "Kontorist eemal",
"default_message": "Aitäh kirja eest. Olen kontorist eemal ja vastan, kui tagasi olen.",
"write_message_first": "Kirjuta esmalt sõnum. Ilma selleta saaksid inimesed tühja vastuse.",
"enter_address_first": "Sisesta esmalt aadress, kuhu edasi saata.",
"message_title": "Sinu vastus",
"message_label": "Sõnum",
"message_description": "Seda saavad need, kes sulle kirjutavad. Võid kasutada vormindust ja linke.",
"from_label": "Alates (valikuline)",
"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.",
"audience_all": "Kõigile",
"audience_internal": "Ainult minu organisatsioonile ({domains})",
"audience_external": "Ainult inimestele väljastpoolt minu organisatsiooni",
"signature_label": "Allkiri",
"signature_plain_label": "Allkiri (lihttekst)",
"signature_plain_description": "Kirjutad ainult lihtteksti (Kirjutamine), seega on ka allkiri lihttekst.",
"edit_as_html": "Muuda HTML-ina",
"edit_formatted": "Tagasi vormindusega redaktorisse",
"html_source_label": "Allkiri HTML-ina",
"signature_size": "Kasutatud {bytes} / {max} baiti",
"signature_too_long": "Serveri jaoks liiga pikk ({bytes} / {max} baiti). Tee lühemaks."
},
"retention_notice": {
"line": "Siin olevad kirjad kustutatakse {count, plural, one {# päev} other {# päeva}} pärast siia teisaldamist."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "استفاده از این متن",
"apply_hint": "موضوع و متن زیر را جایگزین می‌کند."
},
"uxs_vacation": {
"default_subject": "خارج از دفتر",
"default_message": "از ایمیل شما سپاسگزارم. در حال حاضر خارج از دفتر هستم و پس از بازگشت پاسخ می‌دهم.",
"write_message_first": "اول یک پیام بنویسید. بدون آن، افراد پاسخی خالی دریافت می‌کنند.",
"enter_address_first": "اول نشانی‌ای را که باید به آن هدایت شود وارد کنید.",
"message_title": "پاسخ شما",
"message_label": "پیام",
"message_description": "آنچه افرادی که به شما می‌نویسند دریافت می‌کنند. می‌توانید از قالب‌بندی و پیوند استفاده کنید.",
"from_label": "از (اختیاری)",
"from_description": "برای شروع فوری خالی بگذارید.",
"until_label": "تا (اختیاری)",
"until_description": "خالی بگذارید تا پاسخ دادن تا زمانی که خاموشش کنید ادامه یابد.",
"audience_all": "همه",
"audience_internal": "فقط سازمان من ({domains})",
"audience_external": "فقط افراد بیرون از سازمان من",
"signature_label": "امضا",
"signature_plain_label": "امضا (متن ساده)",
"signature_plain_description": "شما فقط متن ساده می‌نویسید (نوشتن)، پس امضای شما هم متن ساده است.",
"edit_as_html": "ویرایش به‌صورت HTML",
"edit_formatted": "بازگشت به ویرایشگر قالب‌بندی",
"html_source_label": "امضا به‌صورت HTML",
"signature_size": "{bytes} از {max} بایت استفاده شده",
"signature_too_long": "برای سرور بیش از حد طولانی است ({bytes} از {max} بایت). کوتاه‌ترش کنید."
},
"retention_notice": {
"line": "پیام‌های اینجا {count, plural, one {# روز} other {# روز}} پس از انتقال به اینجا حذف می‌شوند."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Utiliser ce texte",
"apply_hint": "Remplace l'objet et le texte ci-dessous."
},
"uxs_vacation": {
"default_subject": "Absent du bureau",
"default_message": "Merci pour votre message. Je suis absent du bureau et répondrai à mon retour.",
"write_message_first": "Écrivez d'abord un message. Sans lui, les gens recevraient une réponse vide.",
"enter_address_first": "Saisissez d'abord l'adresse vers laquelle transférer.",
"message_title": "Votre réponse",
"message_label": "Message",
"message_description": "Ce que reçoivent les personnes qui vous écrivent. Vous pouvez utiliser la mise en forme et des liens.",
"from_label": "Du (facultatif)",
"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.",
"audience_all": "Tout le monde",
"audience_internal": "Seulement mon organisation ({domains})",
"audience_external": "Seulement les personnes hors de mon organisation",
"signature_label": "Signature",
"signature_plain_label": "Signature (texte brut)",
"signature_plain_description": "Vous écrivez uniquement en texte brut (Rédaction), votre signature l'est donc aussi.",
"edit_as_html": "Modifier en HTML",
"edit_formatted": "Revenir à l'éditeur avec mise en forme",
"html_source_label": "Signature en HTML",
"signature_size": "{bytes} octets utilisés sur {max}",
"signature_too_long": "Trop longue pour le serveur ({bytes} octets sur {max}). Raccourcissez-la."
},
"retention_notice": {
"line": "Les messages ici sont supprimés {count, plural, one {# jour} other {# jours}} après y avoir été déplacés."
},
+24
View File
@@ -1288,6 +1288,30 @@
"apply": "שימוש בטקסט הזה",
"apply_hint": "מחליף את הנושא והטקסט שלמטה."
},
"uxs_vacation": {
"default_subject": "מחוץ למשרד",
"default_message": "תודה על הודעתך. אני מחוץ למשרד ואענה כשאחזור.",
"write_message_first": "כתבו קודם הודעה. בלעדיה אנשים יקבלו תשובה ריקה.",
"enter_address_first": "הזינו קודם את הכתובת להעברה.",
"message_title": "התשובה שלך",
"message_label": "הודעה",
"message_description": "מה שמקבלים מי שכותבים אליך. אפשר להשתמש בעיצוב ובקישורים.",
"from_label": "מ־ (לא חובה)",
"from_description": "השאירו ריק כדי להתחיל מיד.",
"until_label": "עד (לא חובה)",
"until_description": "השאירו ריק כדי להמשיך להשיב עד שתכבו.",
"audience_all": "כולם",
"audience_internal": "רק הארגון שלי ({domains})",
"audience_external": "רק אנשים מחוץ לארגון שלי",
"signature_label": "חתימה",
"signature_plain_label": "חתימה (טקסט פשוט)",
"signature_plain_description": "את/ה כותב/ת רק טקסט פשוט (הרכבה), ולכן גם החתימה היא טקסט פשוט.",
"edit_as_html": "עריכה כ־HTML",
"edit_formatted": "חזרה לעורך העיצוב",
"html_source_label": "חתימה כ־HTML",
"signature_size": "בשימוש {bytes} מתוך {max} בתים",
"signature_too_long": "ארוכה מדי עבור השרת ({bytes} מתוך {max} בתים). קצרו אותה."
},
"retention_notice": {
"line": "ההודעות כאן נמחקות {count, plural, one {יום אחד} two {יומיים} other {# ימים}} אחרי שהועברו לכאן."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Ennek a szövegnek a használata",
"apply_hint": "Lecseréli a lenti tárgyat és szöveget."
},
"uxs_vacation": {
"default_subject": "Házon kívül",
"default_message": "Köszönöm a levelét. Jelenleg nem vagyok az irodában, visszatérésem után válaszolok.",
"write_message_first": "Előbb írjon üzenetet. Nélküle az emberek üres választ kapnának.",
"enter_address_first": "Előbb adja meg a címet, ahová továbbítson.",
"message_title": "Az Ön válasza",
"message_label": "Üzenet",
"message_description": "Ezt kapják, akik írnak Önnek. Használhat formázást és hivatkozásokat.",
"from_label": "Ettől (nem kötelező)",
"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.",
"audience_all": "Mindenkinek",
"audience_internal": "Csak a szervezetemnek ({domains})",
"audience_external": "Csak a szervezetemen kívülieknek",
"signature_label": "Aláírás",
"signature_plain_label": "Aláírás (egyszerű szöveg)",
"signature_plain_description": "Csak egyszerű szöveget ír (Írás), ezért az aláírása is egyszerű szöveg.",
"edit_as_html": "Szerkesztés HTML-ként",
"edit_formatted": "Vissza a formázó szerkesztőhöz",
"html_source_label": "Aláírás HTML-ként",
"signature_size": "{bytes} / {max} bájt felhasználva",
"signature_too_long": "Túl hosszú a szervernek ({bytes} / {max} bájt). Rövidítse le."
},
"retention_notice": {
"line": "Az itt lévő üzenetek az ide helyezésük után {count, plural, one {# nappal} other {# nappal}} törlődnek."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Usa questo testo",
"apply_hint": "Sostituisce l'oggetto e il testo qui sotto."
},
"uxs_vacation": {
"default_subject": "Fuori ufficio",
"default_message": "Grazie per la tua email. Sono fuori ufficio e risponderò al mio rientro.",
"write_message_first": "Scrivi prima un messaggio. Senza, le persone riceverebbero una risposta vuota.",
"enter_address_first": "Inserisci prima l'indirizzo a cui inoltrare.",
"message_title": "La tua risposta",
"message_label": "Messaggio",
"message_description": "Ciò che riceve chi ti scrive. Puoi usare formattazione e link.",
"from_label": "Dal (facoltativo)",
"from_description": "Lascia vuoto per iniziare subito.",
"until_label": "Al (facoltativo)",
"until_description": "Lascia vuoto per continuare a rispondere finché non lo disattivi.",
"audience_all": "Tutti",
"audience_internal": "Solo la mia organizzazione ({domains})",
"audience_external": "Solo persone esterne alla mia organizzazione",
"signature_label": "Firma",
"signature_plain_label": "Firma (testo semplice)",
"signature_plain_description": "Scrivi solo testo semplice (Composizione), quindi anche la firma è testo semplice.",
"edit_as_html": "Modifica come HTML",
"edit_formatted": "Torna all'editor con formattazione",
"html_source_label": "Firma in HTML",
"signature_size": "{bytes} di {max} byte usati",
"signature_too_long": "Troppo lunga per il server ({bytes} di {max} byte). Accorciala."
},
"retention_notice": {
"line": "I messaggi qui vengono eliminati {count, plural, one {# giorno} other {# giorni}} dopo essere stati spostati qui."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "このテキストを使う",
"apply_hint": "下の件名と本文を置き換えます。"
},
"uxs_vacation": {
"default_subject": "不在",
"default_message": "メールをありがとうございます。現在不在のため、戻り次第お返事します。",
"write_message_first": "先にメッセージを入力してください。入力しないと、空の返信が送られます。",
"enter_address_first": "先に転送先のアドレスを入力してください。",
"message_title": "返信内容",
"message_label": "メッセージ",
"message_description": "あなたにメールを送った人が受け取る内容です。書式やリンクを使えます。",
"from_label": "開始(任意)",
"from_description": "空欄にするとすぐに始まります。",
"until_label": "終了(任意)",
"until_description": "空欄にすると、オフにするまで返信を続けます。",
"audience_all": "全員",
"audience_internal": "自分の組織のみ({domains})",
"audience_external": "組織外の人のみ",
"signature_label": "署名",
"signature_plain_label": "署名(プレーンテキスト)",
"signature_plain_description": "プレーンテキストのみで作成する設定(作成)のため、署名もプレーンテキストです。",
"edit_as_html": "HTML で編集",
"edit_formatted": "書式エディターに戻る",
"html_source_label": "HTML の署名",
"signature_size": "{max} バイト中 {bytes} バイト使用",
"signature_too_long": "サーバーには長すぎます({max} バイト中 {bytes} バイト)。短くしてください。"
},
"retention_notice": {
"line": "ここにあるメッセージは、移動されてから{count, plural, other {#日}}後に削除されます。"
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "이 내용 사용",
"apply_hint": "아래 제목과 본문을 바꿉니다."
},
"uxs_vacation": {
"default_subject": "부재중",
"default_message": "이메일 감사합니다. 지금은 자리에 없으며 돌아오면 답장드리겠습니다.",
"write_message_first": "먼저 메시지를 작성하세요. 메시지가 없으면 빈 답장이 보내집니다.",
"enter_address_first": "먼저 전달할 주소를 입력하세요.",
"message_title": "답장 내용",
"message_label": "메시지",
"message_description": "나에게 메일을 보낸 사람이 받는 내용입니다. 서식과 링크를 쓸 수 있습니다.",
"from_label": "시작 (선택)",
"from_description": "비워 두면 바로 시작합니다.",
"until_label": "종료 (선택)",
"until_description": "비워 두면 끌 때까지 계속 답장합니다.",
"audience_all": "모든 사람",
"audience_internal": "내 조직만 ({domains})",
"audience_external": "내 조직 밖의 사람만",
"signature_label": "서명",
"signature_plain_label": "서명 (일반 텍스트)",
"signature_plain_description": "일반 텍스트로만 작성하도록 설정되어 있어(작성) 서명도 일반 텍스트입니다.",
"edit_as_html": "HTML로 편집",
"edit_formatted": "서식 편집기로 돌아가기",
"html_source_label": "HTML 서명",
"signature_size": "{max}바이트 중 {bytes}바이트 사용",
"signature_too_long": "서버에 비해 너무 깁니다({max}바이트 중 {bytes}바이트). 줄여 주세요."
},
"retention_notice": {
"line": "여기 있는 메일은 이곳으로 옮겨진 지 {count, plural, other {#일}} 후에 삭제됩니다."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Izmantot šo tekstu",
"apply_hint": "Aizstāj tālāk esošo tēmu un tekstu."
},
"uxs_vacation": {
"default_subject": "Ārpus biroja",
"default_message": "Paldies par vēstuli. Esmu ārpus biroja un atbildēšu, kad atgriezīšos.",
"write_message_first": "Vispirms uzrakstiet ziņojumu. Bez tā cilvēki saņemtu tukšu atbildi.",
"enter_address_first": "Vispirms ievadiet adresi, uz kuru pārsūtīt.",
"message_title": "Jūsu atbilde",
"message_label": "Ziņojums",
"message_description": "To saņem tie, kas jums raksta. Varat izmantot formatējumu un saites.",
"from_label": "No (neobligāti)",
"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.",
"audience_all": "Visiem",
"audience_internal": "Tikai manai organizācijai ({domains})",
"audience_external": "Tikai cilvēkiem ārpus manas organizācijas",
"signature_label": "Paraksts",
"signature_plain_label": "Paraksts (vienkāršs teksts)",
"signature_plain_description": "Jūs rakstāt tikai vienkāršu tekstu (Rakstīšana), tāpēc arī paraksts ir vienkāršs teksts.",
"edit_as_html": "Rediģēt kā HTML",
"edit_formatted": "Atpakaļ uz formatēšanas redaktoru",
"html_source_label": "Paraksts kā HTML",
"signature_size": "Izmantoti {bytes} no {max} baitiem",
"signature_too_long": "Serverim par garu ({bytes} no {max} baitiem). Saīsiniet to."
},
"retention_notice": {
"line": "Šeit esošie ziņojumi tiek dzēsti {count, plural, zero {# dienas} one {# dienu} other {# dienas}} pēc to pārvietošanas šeit."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Энэ текстийг ашиглах",
"apply_hint": "Доорх гарчиг болон текстийг солино."
},
"uxs_vacation": {
"default_subject": "Ажлаас гадуур",
"default_message": "Имэйл илгээсэнд баярлалаа. Би одоогоор ажлаас гадуур байгаа бөгөөд буцаж ирээд хариулна.",
"write_message_first": "Эхлээд мессеж бичнэ үү. Үгүй бол хүмүүс хоосон хариу авна.",
"enter_address_first": "Эхлээд дамжуулах хаягаа оруулна уу.",
"message_title": "Таны хариу",
"message_label": "Мессеж",
"message_description": "Танд бичсэн хүмүүсийн авах зүйл. Хэлбэржүүлэлт, холбоос ашиглаж болно.",
"from_label": "Эхлэх (заавал биш)",
"from_description": "Шууд эхлүүлэхийн тулд хоосон үлдээнэ үү.",
"until_label": "Дуусах (заавал биш)",
"until_description": "Унтраах хүртэл хариулсаар байхын тулд хоосон үлдээнэ үү.",
"audience_all": "Бүгдэд",
"audience_internal": "Зөвхөн миний байгууллагад ({domains})",
"audience_external": "Зөвхөн байгууллагаас гадуурх хүмүүст",
"signature_label": "Гарын үсэг",
"signature_plain_label": "Гарын үсэг (энгийн текст)",
"signature_plain_description": "Та зөвхөн энгийн текстээр бичдэг (Зохиоглох) тул гарын үсэг тань ч энгийн текст байна.",
"edit_as_html": "HTML хэлбэрээр засах",
"edit_formatted": "Хэлбэржүүлэх засварлагч руу буцах",
"html_source_label": "HTML гарын үсэг",
"signature_size": "{max} байтаас {bytes} байт ашигласан",
"signature_too_long": "Серверт хэт урт байна ({max} байтаас {bytes} байт). Богиносгоно уу."
},
"retention_notice": {
"line": "Энд байгаа захиа энд шилжүүлснээс хойш {count, plural, other {# хоногийн}} дараа устна."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Bruk denne teksten",
"apply_hint": "Erstatter emnet og teksten nedenfor."
},
"uxs_vacation": {
"default_subject": "Ikke på kontoret",
"default_message": "Takk for e-posten. Jeg er ikke på kontoret og svarer når jeg er tilbake.",
"write_message_first": "Skriv en melding først. Uten den ville folk fått et tomt svar.",
"enter_address_first": "Skriv først inn adressen det skal videresendes til.",
"message_title": "Svaret ditt",
"message_label": "Melding",
"message_description": "Dette får de som skriver til deg. Du kan bruke formatering og lenker.",
"from_label": "Fra (valgfritt)",
"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.",
"audience_all": "Alle",
"audience_internal": "Bare organisasjonen min ({domains})",
"audience_external": "Bare folk utenfor organisasjonen min",
"signature_label": "Signatur",
"signature_plain_label": "Signatur (ren tekst)",
"signature_plain_description": "Du skriver bare ren tekst (Skriving), så signaturen din er også ren tekst.",
"edit_as_html": "Rediger som HTML",
"edit_formatted": "Tilbake til redigeringsverktøyet med formatering",
"html_source_label": "Signatur som HTML",
"signature_size": "{bytes} av {max} byte brukt",
"signature_too_long": "For lang for serveren ({bytes} av {max} byte). Gjør den kortere."
},
"retention_notice": {
"line": "Meldinger her slettes {count, plural, one {# dag} other {# dager}} etter at de ble flyttet hit."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Deze tekst gebruiken",
"apply_hint": "Vervangt het onderwerp en de tekst hieronder."
},
"uxs_vacation": {
"default_subject": "Afwezig",
"default_message": "Bedankt voor je e-mail. Ik ben niet op kantoor en antwoord zodra ik terug ben.",
"write_message_first": "Schrijf eerst een bericht. Zonder bericht krijgen mensen een leeg antwoord.",
"enter_address_first": "Vul eerst het adres in om naar door te sturen.",
"message_title": "Je antwoord",
"message_label": "Bericht",
"message_description": "Wat mensen krijgen als ze je mailen. Je kunt opmaak en links gebruiken.",
"from_label": "Vanaf (optioneel)",
"from_description": "Laat leeg om meteen te beginnen.",
"until_label": "Tot (optioneel)",
"until_description": "Laat leeg om te blijven antwoorden tot je het uitzet.",
"audience_all": "Iedereen",
"audience_internal": "Alleen mijn organisatie ({domains})",
"audience_external": "Alleen mensen buiten mijn organisatie",
"signature_label": "Handtekening",
"signature_plain_label": "Handtekening (platte tekst)",
"signature_plain_description": "Je schrijft alleen platte tekst (Opstellen), dus je handtekening is ook platte tekst.",
"edit_as_html": "Bewerken als HTML",
"edit_formatted": "Terug naar de editor met opmaak",
"html_source_label": "Handtekening als HTML",
"signature_size": "{bytes} van {max} bytes gebruikt",
"signature_too_long": "Te lang voor de server ({bytes} van {max} bytes). Maak hem korter."
},
"retention_notice": {
"line": "Berichten hier worden {count, plural, one {# dag} other {# dagen}} nadat ze hierheen zijn verplaatst verwijderd."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Użyj tego tekstu",
"apply_hint": "Zastępuje temat i tekst poniżej."
},
"uxs_vacation": {
"default_subject": "Poza biurem",
"default_message": "Dziękuję za wiadomość. Jestem poza biurem i odpowiem po powrocie.",
"write_message_first": "Najpierw napisz wiadomość. Bez niej ludzie dostaliby pustą odpowiedź.",
"enter_address_first": "Najpierw wpisz adres, na który przekazywać.",
"message_title": "Twoja odpowiedź",
"message_label": "Wiadomość",
"message_description": "To dostaną osoby, które do Ciebie piszą. Możesz używać formatowania i linków.",
"from_label": "Od (opcjonalnie)",
"from_description": "Zostaw puste, aby zacząć od razu.",
"until_label": "Do (opcjonalnie)",
"until_description": "Zostaw puste, aby odpowiadać, dopóki tego nie wyłączysz.",
"audience_all": "Wszystkim",
"audience_internal": "Tylko mojej organizacji ({domains})",
"audience_external": "Tylko osobom spoza mojej organizacji",
"signature_label": "Podpis",
"signature_plain_label": "Podpis (zwykły tekst)",
"signature_plain_description": "Piszesz tylko zwykłym tekstem (Tworzenie), więc podpis też jest zwykłym tekstem.",
"edit_as_html": "Edytuj jako HTML",
"edit_formatted": "Wróć do edytora z formatowaniem",
"html_source_label": "Podpis w HTML",
"signature_size": "Użyto {bytes} z {max} bajtów",
"signature_too_long": "Za długi dla serwera ({bytes} z {max} bajtów). Skróć go."
},
"retention_notice": {
"line": "Wiadomości są tu usuwane {count, plural, one {# dzień} few {# dni} many {# dni} other {# dnia}} po przeniesieniu."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Usar este texto",
"apply_hint": "Substitui o assunto e o texto abaixo."
},
"uxs_vacation": {
"default_subject": "Fora do escritório",
"default_message": "Obrigado pelo seu e-mail. Estou fora do escritório e responderei quando voltar.",
"write_message_first": "Escreva primeiro uma mensagem. Sem ela, as pessoas receberiam uma resposta vazia.",
"enter_address_first": "Introduza primeiro o endereço para onde reencaminhar.",
"message_title": "A sua resposta",
"message_label": "Mensagem",
"message_description": "O que recebem as pessoas que lhe escrevem. Pode usar formatação e ligações.",
"from_label": "De (opcional)",
"from_description": "Deixe vazio para começar já.",
"until_label": "Até (opcional)",
"until_description": "Deixe vazio para continuar a responder até desligar.",
"audience_all": "Todos",
"audience_internal": "Só a minha organização ({domains})",
"audience_external": "Só pessoas de fora da minha organização",
"signature_label": "Assinatura",
"signature_plain_label": "Assinatura (texto simples)",
"signature_plain_description": "Escreve apenas texto simples (Composição), por isso a assinatura também é texto simples.",
"edit_as_html": "Editar como HTML",
"edit_formatted": "Voltar ao editor com formatação",
"html_source_label": "Assinatura em HTML",
"signature_size": "{bytes} de {max} bytes usados",
"signature_too_long": "Demasiado longa para o servidor ({bytes} de {max} bytes). Encurte-a."
},
"retention_notice": {
"line": "As mensagens aqui são eliminadas {count, plural, one {# dia} other {# dias}} depois de serem movidas para cá."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Folosiți acest text",
"apply_hint": "Înlocuiește subiectul și textul de mai jos."
},
"uxs_vacation": {
"default_subject": "În afara biroului",
"default_message": "Vă mulțumesc pentru e-mail. Sunt în afara biroului și voi răspunde când mă întorc.",
"write_message_first": "Scrieți mai întâi un mesaj. Fără el, oamenii ar primi un răspuns gol.",
"enter_address_first": "Introduceți mai întâi adresa către care se redirecționează.",
"message_title": "Răspunsul dvs.",
"message_label": "Mesaj",
"message_description": "Ce primesc cei care vă scriu. Puteți folosi formatare și linkuri.",
"from_label": "De la (opțional)",
"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.",
"audience_all": "Tuturor",
"audience_internal": "Doar organizației mele ({domains})",
"audience_external": "Doar persoanelor din afara organizației mele",
"signature_label": "Semnătură",
"signature_plain_label": "Semnătură (text simplu)",
"signature_plain_description": "Scrieți doar text simplu (Redactarea), deci și semnătura este text simplu.",
"edit_as_html": "Editați ca HTML",
"edit_formatted": "Înapoi la editorul cu formatare",
"html_source_label": "Semnătură HTML",
"signature_size": "{bytes} din {max} octeți folosiți",
"signature_too_long": "Prea lungă pentru server ({bytes} din {max} octeți). Scurtați-o."
},
"retention_notice": {
"line": "Mesajele de aici sunt șterse la {count, plural, one {# zi} few {# zile} other {# de zile}} după ce au fost mutate aici."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Использовать этот текст",
"apply_hint": "Заменяет тему и текст ниже."
},
"uxs_vacation": {
"default_subject": "Нет на месте",
"default_message": "Спасибо за письмо. Меня сейчас нет на месте, я отвечу, когда вернусь.",
"write_message_first": "Сначала напишите сообщение. Без него люди получат пустой ответ.",
"enter_address_first": "Сначала введите адрес для пересылки.",
"message_title": "Ваш ответ",
"message_label": "Сообщение",
"message_description": "Это получат те, кто вам напишет. Можно использовать форматирование и ссылки.",
"from_label": "С (необязательно)",
"from_description": "Оставьте пустым, чтобы начать сразу.",
"until_label": "По (необязательно)",
"until_description": "Оставьте пустым, чтобы отвечать, пока вы это не выключите.",
"audience_all": "Всем",
"audience_internal": "Только моей организации ({domains})",
"audience_external": "Только тем, кто вне моей организации",
"signature_label": "Подпись",
"signature_plain_label": "Подпись (простой текст)",
"signature_plain_description": "Вы пишете только простым текстом (Написание), поэтому и подпись — простой текст.",
"edit_as_html": "Изменить как HTML",
"edit_formatted": "Вернуться к редактору с форматированием",
"html_source_label": "Подпись в HTML",
"signature_size": "Использовано {bytes} из {max} байт",
"signature_too_long": "Слишком длинная для сервера ({bytes} из {max} байт). Сократите её."
},
"retention_notice": {
"line": "Письма здесь удаляются через {count, plural, one {# день} few {# дня} many {# дней} other {# дня}} после перемещения сюда."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Použiť tento text",
"apply_hint": "Nahradí predmet a text nižšie."
},
"uxs_vacation": {
"default_subject": "Mimo kancelárie",
"default_message": "Ďakujem za váš e-mail. Som mimo kancelárie a odpoviem, keď sa vrátim.",
"write_message_first": "Najprv napíšte správu. Bez nej by ľudia dostali prázdnu odpoveď.",
"enter_address_first": "Najprv zadajte adresu, na ktorú sa má preposielať.",
"message_title": "Vaša odpoveď",
"message_label": "Správa",
"message_description": "Toto dostanú ľudia, ktorí vám napíšu. Môžete použiť formátovanie a odkazy.",
"from_label": "Od (nepovinné)",
"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.",
"audience_all": "Všetkým",
"audience_internal": "Len mojej organizácii ({domains})",
"audience_external": "Len ľuďom mimo mojej organizácie",
"signature_label": "Podpis",
"signature_plain_label": "Podpis (obyčajný text)",
"signature_plain_description": "Píšete len obyčajný text (Písanie), takže aj váš podpis je obyčajný text.",
"edit_as_html": "Upraviť ako HTML",
"edit_formatted": "Späť do editora s formátovaním",
"html_source_label": "Podpis ako HTML",
"signature_size": "Využitých {bytes} z {max} bajtov",
"signature_too_long": "Pre server príliš dlhý ({bytes} z {max} bajtov). Skráťte ho."
},
"retention_notice": {
"line": "Správy tu sa vymažú {count, plural, one {# deň} few {# dni} many {# dňa} other {# dní}} po presunutí sem."
},
+24
View File
@@ -712,6 +712,30 @@
"apply": "ใช้ข้อความนี้",
"apply_hint": "แทนที่หัวเรื่องและข้อความด้านล่าง"
},
"uxs_vacation": {
"default_subject": "ไม่อยู่ที่สำนักงาน",
"default_message": "ขอบคุณสำหรับอีเมล ตอนนี้ฉันไม่อยู่ที่สำนักงาน และจะตอบกลับเมื่อกลับมา",
"write_message_first": "เขียนข้อความก่อน ถ้าไม่มีข้อความ ผู้คนจะได้รับคำตอบว่างเปล่า",
"enter_address_first": "ใส่ที่อยู่ที่จะส่งต่อไปก่อน",
"message_title": "คำตอบของคุณ",
"message_label": "ข้อความ",
"message_description": "สิ่งที่คนที่เขียนถึงคุณจะได้รับ ใช้การจัดรูปแบบและลิงก์ได้",
"from_label": "ตั้งแต่ (ไม่บังคับ)",
"from_description": "เว้นว่างไว้เพื่อเริ่มทันที",
"until_label": "จนถึง (ไม่บังคับ)",
"until_description": "เว้นว่างไว้เพื่อตอบกลับต่อไปจนกว่าคุณจะปิด",
"audience_all": "ทุกคน",
"audience_internal": "เฉพาะองค์กรของฉัน ({domains})",
"audience_external": "เฉพาะคนนอกองค์กรของฉัน",
"signature_label": "ลายเซ็น",
"signature_plain_label": "ลายเซ็น (ข้อความธรรมดา)",
"signature_plain_description": "คุณเขียนเป็นข้อความธรรมดาเท่านั้น (การเขียน) ลายเซ็นจึงเป็นข้อความธรรมดาด้วย",
"edit_as_html": "แก้ไขเป็น HTML",
"edit_formatted": "กลับไปที่ตัวแก้ไขแบบจัดรูปแบบ",
"html_source_label": "ลายเซ็นแบบ HTML",
"signature_size": "ใช้ไป {bytes} จาก {max} ไบต์",
"signature_too_long": "ยาวเกินไปสำหรับเซิร์ฟเวอร์ ({bytes} จาก {max} ไบต์) โปรดทำให้สั้นลง"
},
"retention_notice": {
"line": "ข้อความที่นี่จะถูกลบ {count, plural, other {# วัน}} หลังจากย้ายมาที่นี่"
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Bu metni kullan",
"apply_hint": "Aşağıdaki konu ve metnin yerine geçer."
},
"uxs_vacation": {
"default_subject": "Ofis dışında",
"default_message": "E-postanız için teşekkürler. Şu an ofis dışındayım, döndüğümde yanıt vereceğim.",
"write_message_first": "Önce bir ileti yazın. İleti olmadan insanlar boş bir yanıt alır.",
"enter_address_first": "Önce iletilecek adresi girin.",
"message_title": "Yanıtınız",
"message_label": "İleti",
"message_description": "Size yazan kişilerin alacağı metin. Biçimlendirme ve bağlantı kullanabilirsiniz.",
"from_label": "Başlangıç (isteğe bağlı)",
"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.",
"audience_all": "Herkese",
"audience_internal": "Yalnızca kuruluşuma ({domains})",
"audience_external": "Yalnızca kuruluşum dışındakilere",
"signature_label": "İmza",
"signature_plain_label": "İmza (düz metin)",
"signature_plain_description": "Yalnızca düz metin yazıyorsunuz (Yazma), bu yüzden imzanız da düz metin.",
"edit_as_html": "HTML olarak düzenle",
"edit_formatted": "Biçimlendirme düzenleyicisine dön",
"html_source_label": "HTML imza",
"signature_size": "{max} bayttan {bytes} bayt kullanıldı",
"signature_too_long": "Sunucu için çok uzun ({max} bayttan {bytes} bayt). Kısaltın."
},
"retention_notice": {
"line": "Buradaki iletiler buraya taşındıktan {count, plural, one {# gün} other {# gün}} sonra silinir."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "Використати цей текст",
"apply_hint": "Замінює тему й текст нижче."
},
"uxs_vacation": {
"default_subject": "Немає на місці",
"default_message": "Дякую за лист. Мене зараз немає на місці, я відповім, коли повернуся.",
"write_message_first": "Спершу напишіть повідомлення. Без нього люди отримають порожню відповідь.",
"enter_address_first": "Спершу введіть адресу для пересилання.",
"message_title": "Ваша відповідь",
"message_label": "Повідомлення",
"message_description": "Це отримають ті, хто вам напише. Можна використовувати форматування та посилання.",
"from_label": "З (необов'язково)",
"from_description": "Залиште порожнім, щоб почати одразу.",
"until_label": "До (необов'язково)",
"until_description": "Залиште порожнім, щоб відповідати, доки ви це не вимкнете.",
"audience_all": "Усім",
"audience_internal": "Лише моїй організації ({domains})",
"audience_external": "Лише тим, хто поза моєю організацією",
"signature_label": "Підпис",
"signature_plain_label": "Підпис (звичайний текст)",
"signature_plain_description": "Ви пишете лише звичайним текстом (Написання), тому й підпис — звичайний текст.",
"edit_as_html": "Редагувати як HTML",
"edit_formatted": "Повернутися до редактора з форматуванням",
"html_source_label": "Підпис у HTML",
"signature_size": "Використано {bytes} з {max} байт",
"signature_too_long": "Задовгий для сервера ({bytes} з {max} байт). Скоротіть його."
},
"retention_notice": {
"line": "Листи тут видаляються через {count, plural, one {# день} few {# дні} many {# днів} other {# дня}} після переміщення сюди."
},
+24
View File
@@ -1357,6 +1357,30 @@
"apply": "Dùng nội dung này",
"apply_hint": "Thay thế tiêu đề và nội dung bên dưới."
},
"uxs_vacation": {
"default_subject": "Vắng mặt",
"default_message": "Cảm ơn bạn đã gửi thư. Tôi đang vắng mặt và sẽ trả lời khi quay lại.",
"write_message_first": "Hãy viết thư trước. Nếu không, mọi người sẽ nhận được thư trả lời trống.",
"enter_address_first": "Hãy nhập địa chỉ cần chuyển tiếp đến trước.",
"message_title": "Thư trả lời của bạn",
"message_label": "Nội dung",
"message_description": "Những gì người gửi thư cho bạn sẽ nhận được. Bạn có thể dùng định dạng và liên kết.",
"from_label": "Từ (không bắt buộc)",
"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.",
"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",
"signature_label": "Chữ ký",
"signature_plain_label": "Chữ ký (văn bản thuần)",
"signature_plain_description": "Bạn chỉ viết văn bản thuần (Soạn thư), nên chữ ký cũng là văn bản thuần.",
"edit_as_html": "Sửa dưới dạng HTML",
"edit_formatted": "Quay lại trình soạn thảo có định dạng",
"html_source_label": "Chữ ký dạng HTML",
"signature_size": "Đã dùng {bytes}/{max} byte",
"signature_too_long": "Quá dài đối với máy chủ ({bytes}/{max} byte). Hãy rút ngắn."
},
"retention_notice": {
"line": "Thư ở đây sẽ bị xóa {count, plural, other {# ngày}} sau khi được chuyển vào đây."
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "使用此文字",
"apply_hint": "取代下方的主旨和內文。"
},
"uxs_vacation": {
"default_subject": "外出",
"default_message": "感謝您的來信。我目前不在辦公室,回來後會盡快回覆。",
"write_message_first": "請先寫一則訊息。沒有訊息的話,對方會收到空白回覆。",
"enter_address_first": "請先輸入要轉寄到的地址。",
"message_title": "您的回覆",
"message_label": "訊息",
"message_description": "寫信給您的人會收到的內容。可以使用格式和連結。",
"from_label": "開始(選填)",
"from_description": "留空即立即開始。",
"until_label": "結束(選填)",
"until_description": "留空即持續回覆,直到您關閉。",
"audience_all": "所有人",
"audience_internal": "僅限我的組織({domains})",
"audience_external": "僅限組織外的人",
"signature_label": "簽名",
"signature_plain_label": "簽名(純文字)",
"signature_plain_description": "您設定為只寫純文字(撰寫),所以簽名也是純文字。",
"edit_as_html": "以 HTML 編輯",
"edit_formatted": "返回格式編輯器",
"html_source_label": "HTML 簽名",
"signature_size": "已用 {bytes} / {max} 位元組",
"signature_too_long": "對伺服器來說太長({bytes} / {max} 位元組)。請縮短。"
},
"retention_notice": {
"line": "此處的郵件會在移入 {count, plural, other {# 天}} 後刪除。"
},
+24
View File
@@ -1345,6 +1345,30 @@
"apply": "使用此文字",
"apply_hint": "替换下方的主题和正文。"
},
"uxs_vacation": {
"default_subject": "外出",
"default_message": "感谢您的来信。我目前不在办公室,回来后会尽快回复。",
"write_message_first": "请先写一条消息。没有消息的话,对方会收到空白回复。",
"enter_address_first": "请先输入要转发到的地址。",
"message_title": "您的回复",
"message_label": "消息",
"message_description": "给您写信的人会收到的内容。可以使用格式和链接。",
"from_label": "开始(可选)",
"from_description": "留空则立即开始。",
"until_label": "结束(可选)",
"until_description": "留空则一直回复,直到您关闭。",
"audience_all": "所有人",
"audience_internal": "仅限我的组织({domains})",
"audience_external": "仅限组织外的人",
"signature_label": "签名",
"signature_plain_label": "签名(纯文本)",
"signature_plain_description": "您设置为只写纯文本(撰写),所以签名也是纯文本。",
"edit_as_html": "以 HTML 编辑",
"edit_formatted": "返回格式编辑器",
"html_source_label": "HTML 签名",
"signature_size": "已用 {bytes} / {max} 字节",
"signature_too_long": "对服务器来说太长({bytes} / {max} 字节)。请缩短。"
},
"retention_notice": {
"line": "此处的邮件会在移入 {count, plural, other {# 天}} 后删除。"
},