Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1189b146ef |
@@ -9,11 +9,6 @@ import { Loader2, AlertCircle } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
// Module-level guard so a Suspense/search-params remount of this client
|
|
||||||
// component can't exchange the same OAuth code twice — Keycloak rejects a
|
|
||||||
// reused code with `invalid_grant` ("Code not valid") and the login fails.
|
|
||||||
const processedAuthCodes = new Set<string>();
|
|
||||||
|
|
||||||
function OAuthCallbackInner() {
|
function OAuthCallbackInner() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -37,11 +32,6 @@ function OAuthCallbackInner() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent a second token exchange for the same code (remount / double
|
|
||||||
// effect). Without this, the second exchange fails with "Code not valid".
|
|
||||||
if (processedAuthCodes.has(code)) return;
|
|
||||||
processedAuthCodes.add(code);
|
|
||||||
|
|
||||||
// Step-up re-auth for device pairing: the QR generator sent the user here
|
// Step-up re-auth for device pairing: the QR generator sent the user here
|
||||||
// via prompt=login. Don't create a login session — just confirm the fresh
|
// via prompt=login. Don't create a login session — just confirm the fresh
|
||||||
// auth (sets the short-lived pairing proof cookie) and bounce back to the
|
// auth (sets the short-lived pairing proof cookie) and bounce back to the
|
||||||
|
|||||||
@@ -71,10 +71,7 @@ type PendingScopeAction =
|
|||||||
| { type: "delete"; event: CalendarEvent; sendScheduling?: boolean };
|
| { type: "delete"; event: CalendarEvent; sendScheduling?: boolean };
|
||||||
|
|
||||||
function isRecurringEvent(event: CalendarEvent): boolean {
|
function isRecurringEvent(event: CalendarEvent): boolean {
|
||||||
// Stalwart may return an empty-string `recurrenceId` for non-recurring events
|
return (event.recurrenceRules?.length ?? 0) > 0 || event.recurrenceId != null;
|
||||||
// rather than null; treat that as non-recurring so editing doesn't route
|
|
||||||
// through the recurrence-scope flow for a plain single event.
|
|
||||||
return (event.recurrenceRules?.length ?? 0) > 0 || Boolean(event.recurrenceId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CalendarPage() {
|
export default function CalendarPage() {
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { FilePreviewModal } from "@/components/files/file-preview-modal";
|
import { FilePreviewModal } from "@/components/files/file-preview-modal";
|
||||||
import { isFilePreviewable } from "@/lib/file-preview";
|
import { isFilePreviewable } from "@/lib/file-preview";
|
||||||
import { appendHtmlSignature, appendPlainTextSignature } from "@/lib/signature-utils";
|
import { appendHtmlSignature, appendPlainTextSignature } from "@/lib/signature-utils";
|
||||||
import { useSignatureStore } from "@/stores/signature-store";
|
|
||||||
import { computeReplyThreadingHeaders } from "@/lib/email-threading";
|
import { computeReplyThreadingHeaders } from "@/lib/email-threading";
|
||||||
import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import";
|
import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import";
|
||||||
import { findDraftIdentityId, resolveReplyFrom, type ReplyFromResolution } from "@/lib/reply-identity";
|
import { findDraftIdentityId, resolveReplyFrom, type ReplyFromResolution } from "@/lib/reply-identity";
|
||||||
@@ -2616,16 +2615,8 @@ export default function Home() {
|
|||||||
|
|
||||||
// Append signature from the sending identity (fall back to primary
|
// Append signature from the sending identity (fall back to primary
|
||||||
// when the reply-from lives on the same identity but a different alias).
|
// when the reply-from lives on the same identity but a different alias).
|
||||||
// The signature store's reply signature takes precedence over the legacy
|
|
||||||
// identity signature, matching the composer's send path.
|
|
||||||
const signatureStore = useSignatureStore.getState();
|
|
||||||
const replySigId = signatureStore.getIdentityReplySignatureId(sendingIdentity?.id ?? '');
|
|
||||||
const replySig = replySigId ? signatureStore.getSignatureById(replySigId) : undefined;
|
|
||||||
const signatureSource = replySig
|
|
||||||
? { htmlSignature: replySig.body, textSignature: replySig.plainText }
|
|
||||||
: sendingIdentity;
|
|
||||||
const separator = useSettingsStore.getState().signatureSeparatorEnabled;
|
const separator = useSettingsStore.getState().signatureSeparatorEnabled;
|
||||||
const finalBody = appendPlainTextSignature(body, signatureSource, { separator });
|
const finalBody = appendPlainTextSignature(body, sendingIdentity, { separator });
|
||||||
|
|
||||||
// When the identity has an HTML signature, send a matching HTML body so the
|
// When the identity has an HTML signature, send a matching HTML body so the
|
||||||
// signature keeps its formatting; appendPlainTextSignature would otherwise
|
// signature keeps its formatting; appendPlainTextSignature would otherwise
|
||||||
@@ -2636,8 +2627,8 @@ export default function Home() {
|
|||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/\n/g, '<br>');
|
.replace(/\n/g, '<br>');
|
||||||
const finalHtmlBody = signatureSource?.htmlSignature?.trim()
|
const finalHtmlBody = sendingIdentity?.htmlSignature?.trim()
|
||||||
? appendHtmlSignature(`<div>${escapedBody}</div>`, signatureSource, { separator })
|
? appendHtmlSignature(`<div>${escapedBody}</div>`, sendingIdentity, { separator })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const originalEmailId = selectedEmail.id;
|
const originalEmailId = selectedEmail.id;
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { Save, Loader2, Plus, X } from 'lucide-react';
|
import { Save, Loader2, Plus, X } from 'lucide-react';
|
||||||
import { apiFetch } from '@/lib/browser-navigation';
|
import { apiFetch } from '@/lib/browser-navigation';
|
||||||
import { toast } from '@/stores/toast-store';
|
|
||||||
|
|
||||||
interface VncDirectoryFormData {
|
interface VncDirectoryFormData {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -51,7 +49,6 @@ const BLANK_FORM: VncDirectoryFormData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function VncDirectoryTab() {
|
export function VncDirectoryTab() {
|
||||||
const t = useTranslations('admin.vncdirectory');
|
|
||||||
const [config, setConfig] = useState<VncDirectoryFormData>({ ...BLANK_FORM });
|
const [config, setConfig] = useState<VncDirectoryFormData>({ ...BLANK_FORM });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -108,25 +105,19 @@ export function VncDirectoryTab() {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
try {
|
const res = await apiFetch('/api/admin/vncdirectory', {
|
||||||
const res = await apiFetch('/api/admin/vncdirectory', {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
||||||
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify(config),
|
||||||
body: JSON.stringify(config),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setMessage({ type: 'success', text: t('saved') });
|
setMessage({ type: 'success', text: 'VNCdirectory configuration saved.' });
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
await fetchConfig();
|
await fetchConfig();
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setMessage({ type: 'error', text: data.error || t('save_error') });
|
setMessage({ type: 'error', text: data.error || 'Failed to save' });
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : t('save_error');
|
|
||||||
setMessage({ type: 'error', text: msg });
|
|
||||||
toast.error(msg);
|
|
||||||
}
|
}
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -134,7 +125,7 @@ export function VncDirectoryTab() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground text-sm">
|
<div className="flex items-center justify-center py-12 text-muted-foreground text-sm">
|
||||||
{t('loading')}
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -145,9 +136,9 @@ export function VncDirectoryTab() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="text-2xl font-semibold text-foreground">{t('title')}</h1>
|
<h1 className="text-2xl font-semibold text-foreground">VNCdirectory</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{t('description')}
|
Centralized identity and directory integration (SAML, LDAP, 2FA)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{dirty && (
|
{dirty && (
|
||||||
@@ -157,7 +148,7 @@ export function VncDirectoryTab() {
|
|||||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition-all shadow-sm"
|
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||||
{t('save')}
|
Save configuration
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -174,12 +165,12 @@ export function VncDirectoryTab() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Section title={t('enable_section')}>
|
<Section title="Enable VNCdirectory Integration">
|
||||||
<div className="px-4 py-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
<div className="px-4 py-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="text-sm text-foreground">{t('enabled')}</span>
|
<span className="text-sm text-foreground">Enabled</span>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t('enabled_description')}
|
Turn on VNCdirectory integration for identity management, SSO, and directory services
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -201,53 +192,53 @@ export function VncDirectoryTab() {
|
|||||||
|
|
||||||
{config.enabled && (
|
{config.enabled && (
|
||||||
<>
|
<>
|
||||||
<Section title={t('connection')}>
|
<Section title="Connection">
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('url')}
|
label="VNCdirectory URL"
|
||||||
value={config.apiUrl}
|
value={config.apiUrl}
|
||||||
onChange={(v) => updateField('apiUrl', v)}
|
onChange={(v) => updateField('apiUrl', v)}
|
||||||
placeholder={t('url_placeholder')}
|
placeholder="https://vncdirectory.example.com"
|
||||||
/>
|
/>
|
||||||
<PasswordRow
|
<PasswordRow
|
||||||
label={t('api_key')}
|
label="API Key"
|
||||||
value={config.apiKey}
|
value={config.apiKey}
|
||||||
onChange={(v) => updateField('apiKey', v)}
|
onChange={(v) => updateField('apiKey', v)}
|
||||||
placeholder={t('api_key_placeholder')}
|
placeholder="Enter API key"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={t('saml')}>
|
<Section title="SAML / Identity Provider">
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label={t('saml_enabled')}
|
label="SAML Enabled"
|
||||||
description={t('saml_enabled_description')}
|
description="Enable SAML single sign-on via VNCdirectory"
|
||||||
value={config.samlEnabled}
|
value={config.samlEnabled}
|
||||||
onChange={() => toggleBool('samlEnabled')}
|
onChange={() => toggleBool('samlEnabled')}
|
||||||
/>
|
/>
|
||||||
{config.samlEnabled && (
|
{config.samlEnabled && (
|
||||||
<>
|
<>
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('idp_url')}
|
label="Identity Provider URL"
|
||||||
value={config.samlIdpUrl}
|
value={config.samlIdpUrl}
|
||||||
onChange={(v) => updateField('samlIdpUrl', v)}
|
onChange={(v) => updateField('samlIdpUrl', v)}
|
||||||
placeholder={t('idp_url_placeholder')}
|
placeholder="https://idp.example.com/saml2/idp"
|
||||||
/>
|
/>
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('issuer')}
|
label="Issuer Name (Entity ID)"
|
||||||
value={config.samlIssuer}
|
value={config.samlIssuer}
|
||||||
onChange={(v) => updateField('samlIssuer', v)}
|
onChange={(v) => updateField('samlIssuer', v)}
|
||||||
placeholder={t('issuer_placeholder')}
|
placeholder="urn:example:vncmail"
|
||||||
/>
|
/>
|
||||||
<div className="px-4 py-3 flex flex-col gap-2">
|
<div className="px-4 py-3 flex flex-col gap-2">
|
||||||
<label className="text-sm text-foreground">
|
<label className="text-sm text-foreground">
|
||||||
{t('sp_cert')}
|
Service Provider Certificate (X.509)
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={config.samlSpCert}
|
value={config.samlSpCert}
|
||||||
onChange={(e) => updateField('samlSpCert', e.target.value)}
|
onChange={(e) => updateField('samlSpCert', e.target.value)}
|
||||||
placeholder={t('sp_cert_placeholder')}
|
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm font-mono text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-vertical"
|
className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm font-mono text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-vertical"
|
||||||
/>
|
/>
|
||||||
@@ -257,46 +248,46 @@ export function VncDirectoryTab() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={t('ldap')}>
|
<Section title="LDAP Directory">
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label={t('ldap_enabled')}
|
label="LDAP Enabled"
|
||||||
description={t('ldap_enabled_description')}
|
description="Query user directory via LDAP for contact lookups and authentication"
|
||||||
value={config.ldapEnabled}
|
value={config.ldapEnabled}
|
||||||
onChange={() => toggleBool('ldapEnabled')}
|
onChange={() => toggleBool('ldapEnabled')}
|
||||||
/>
|
/>
|
||||||
{config.ldapEnabled && (
|
{config.ldapEnabled && (
|
||||||
<>
|
<>
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('ldap_uri')}
|
label="LDAP Server URI"
|
||||||
value={config.ldapUri}
|
value={config.ldapUri}
|
||||||
onChange={(v) => updateField('ldapUri', v)}
|
onChange={(v) => updateField('ldapUri', v)}
|
||||||
placeholder={t('ldap_uri_placeholder')}
|
placeholder="ldaps://ldap.example.com:636"
|
||||||
/>
|
/>
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('bind_dn')}
|
label="Bind DN"
|
||||||
value={config.ldapBindDn}
|
value={config.ldapBindDn}
|
||||||
onChange={(v) => updateField('ldapBindDn', v)}
|
onChange={(v) => updateField('ldapBindDn', v)}
|
||||||
placeholder={t('bind_dn_placeholder')}
|
placeholder="cn=readonly,dc=example,dc=com"
|
||||||
/>
|
/>
|
||||||
<PasswordRow
|
<PasswordRow
|
||||||
label={t('bind_password')}
|
label="Bind Password"
|
||||||
value={config.ldapBindPassword}
|
value={config.ldapBindPassword}
|
||||||
onChange={(v) => updateField('ldapBindPassword', v)}
|
onChange={(v) => updateField('ldapBindPassword', v)}
|
||||||
placeholder={t('bind_password_placeholder')}
|
placeholder="Enter LDAP bind password"
|
||||||
/>
|
/>
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('search_base')}
|
label="Search Base"
|
||||||
value={config.ldapSearchBase}
|
value={config.ldapSearchBase}
|
||||||
onChange={(v) => updateField('ldapSearchBase', v)}
|
onChange={(v) => updateField('ldapSearchBase', v)}
|
||||||
placeholder={t('search_base_placeholder')}
|
placeholder="ou=users,dc=example,dc=com"
|
||||||
/>
|
/>
|
||||||
<SelectRow
|
<SelectRow
|
||||||
label={t('ldap_type')}
|
label="LDAP Type"
|
||||||
value={config.ldapType}
|
value={config.ldapType}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'openldap', label: t('ldap_type_openldap') },
|
{ value: 'openldap', label: 'OpenLDAP' },
|
||||||
{ value: 'ms-ad', label: t('ldap_type_msad') },
|
{ value: 'ms-ad', label: 'Microsoft Active Directory' },
|
||||||
]}
|
]}
|
||||||
onChange={(v) => updateField('ldapType', v as 'openldap' | 'ms-ad')}
|
onChange={(v) => updateField('ldapType', v as 'openldap' | 'ms-ad')}
|
||||||
/>
|
/>
|
||||||
@@ -305,41 +296,41 @@ export function VncDirectoryTab() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={t('auth_section')}>
|
<Section title="Authentication">
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label={t('require_2fa')}
|
label="Enforce 2FA/TOTP"
|
||||||
description={t('require_2fa_description')}
|
description="Require two-factor authentication for all users"
|
||||||
value={config.tfaEnabled}
|
value={config.tfaEnabled}
|
||||||
onChange={() => toggleBool('tfaEnabled')}
|
onChange={() => toggleBool('tfaEnabled')}
|
||||||
/>
|
/>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label={t('oidc_section')}
|
label="OpenID Connect (OIDC)"
|
||||||
description={t('oidc_section_description')}
|
description="Enable OIDC login alongside or instead of SAML"
|
||||||
value={config.oidcEnabled}
|
value={config.oidcEnabled}
|
||||||
onChange={() => toggleBool('oidcEnabled')}
|
onChange={() => toggleBool('oidcEnabled')}
|
||||||
/>
|
/>
|
||||||
{config.oidcEnabled && (
|
{config.oidcEnabled && (
|
||||||
<>
|
<>
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('oidc_client_id')}
|
label="OIDC Client ID"
|
||||||
value={config.oidcClientId}
|
value={config.oidcClientId}
|
||||||
onChange={(v) => updateField('oidcClientId', v)}
|
onChange={(v) => updateField('oidcClientId', v)}
|
||||||
placeholder={t('oidc_client_id_placeholder')}
|
placeholder="vncmail-client"
|
||||||
/>
|
/>
|
||||||
<TextRow
|
<TextRow
|
||||||
label={t('oidc_discovery_url')}
|
label="OIDC Discovery URL"
|
||||||
value={config.oidcDiscoveryUrl}
|
value={config.oidcDiscoveryUrl}
|
||||||
onChange={(v) => updateField('oidcDiscoveryUrl', v)}
|
onChange={(v) => updateField('oidcDiscoveryUrl', v)}
|
||||||
placeholder={t('oidc_discovery_url_placeholder')}
|
placeholder="https://idp.example.com/.well-known/openid-configuration"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="px-4 py-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
<div className="px-4 py-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="text-sm text-foreground">{t('session_ttl')}</span>
|
<span className="text-sm text-foreground">Session TTL (seconds)</span>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t('session_ttl_description')}
|
How long SSO sessions remain valid. Default: 8 hours (28800)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -353,10 +344,11 @@ export function VncDirectoryTab() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={t('federated')}>
|
<Section title="Federated Applications">
|
||||||
<div className="px-4 py-3">
|
<div className="px-4 py-3">
|
||||||
<p className="text-xs text-muted-foreground mb-3">
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
{t('federated_description')}
|
Configure SSO redirect URLs for other VNC applications. Users signed into one
|
||||||
|
app will be transparently authenticated when navigating to another.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{federatedAppsList.map(([appName, url]) => (
|
{federatedAppsList.map(([appName, url]) => (
|
||||||
@@ -374,13 +366,13 @@ export function VncDirectoryTab() {
|
|||||||
type="url"
|
type="url"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setFederatedApp(appName, e.target.value)}
|
onChange={(e) => setFederatedApp(appName, e.target.value)}
|
||||||
placeholder={t('app_url_placeholder')}
|
placeholder="https://vnc.example.com/auth/sso"
|
||||||
className="h-8 w-full sm:flex-1 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="h-8 w-full sm:flex-1 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeFederatedApp(appName)}
|
onClick={() => removeFederatedApp(appName)}
|
||||||
className="shrink-0 text-muted-foreground hover:text-destructive transition-colors"
|
className="shrink-0 text-muted-foreground hover:text-destructive transition-colors"
|
||||||
title={t('remove_app', { name: appName })}
|
title={`Remove ${appName}`}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -406,7 +398,6 @@ function AddFederatedApp({
|
|||||||
existingKeys: Set<string>;
|
existingKeys: Set<string>;
|
||||||
onAdd: (name: string, url: string) => void;
|
onAdd: (name: string, url: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations('admin.vncdirectory');
|
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
@@ -420,7 +411,7 @@ function AddFederatedApp({
|
|||||||
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-md border border-dashed border-input text-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-md border border-dashed border-input text-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
{t('add_app')}
|
Add federated app
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -428,19 +419,19 @@ function AddFederatedApp({
|
|||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
setError(t('app_name_error'));
|
setError('Enter an application name');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
||||||
setError(t('app_name_format_error'));
|
setError('Name must contain only letters, numbers, hyphens, and underscores');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (existingKeys.has(trimmed)) {
|
if (existingKeys.has(trimmed)) {
|
||||||
setError(t('app_exists_error'));
|
setError('An app with this name already exists');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!url.trim()) {
|
if (!url.trim()) {
|
||||||
setError(t('app_url_error'));
|
setError('Enter an SSO URL');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -466,7 +457,7 @@ function AddFederatedApp({
|
|||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => { setName(e.target.value); setError(null); }}
|
onChange={(e) => { setName(e.target.value); setError(null); }}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); }}
|
onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); }}
|
||||||
placeholder={t('app_name_placeholder')}
|
placeholder="App name (e.g. vnctalk)"
|
||||||
className="h-8 w-full sm:w-36 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="h-8 w-full sm:w-36 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -474,7 +465,7 @@ function AddFederatedApp({
|
|||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => { setUrl(e.target.value); setError(null); }}
|
onChange={(e) => { setUrl(e.target.value); setError(null); }}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); }}
|
onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); }}
|
||||||
placeholder={t('app_url_placeholder')}
|
placeholder="https://vnctalk.example.com/auth/sso"
|
||||||
className="h-8 w-full sm:flex-1 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="h-8 w-full sm:flex-1 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
@@ -483,14 +474,14 @@ function AddFederatedApp({
|
|||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
className="inline-flex items-center h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors"
|
className="inline-flex items-center h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors"
|
||||||
>
|
>
|
||||||
{t('add')}
|
Add
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
className="h-8 px-2.5 rounded-md text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="h-8 px-2.5 rounded-md text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{t('cancel')}
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -546,16 +537,7 @@ function PasswordRow({
|
|||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}) {
|
}) {
|
||||||
const [isMasked, setIsMasked] = useState(value === '••••••');
|
const isMasked = value === '••••••';
|
||||||
|
|
||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (isMasked) {
|
|
||||||
onChange(e.target.value);
|
|
||||||
setIsMasked(false);
|
|
||||||
} else {
|
|
||||||
onChange(e.target.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
<div className="px-4 py-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||||
@@ -564,7 +546,7 @@ function PasswordRow({
|
|||||||
<input
|
<input
|
||||||
type={isMasked ? 'text' : 'password'}
|
type={isMasked ? 'text' : 'password'}
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
onChange={handleChange}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder || (isMasked ? 'Saved - type to replace' : undefined)}
|
placeholder={placeholder || (isMasked ? 'Saved - type to replace' : undefined)}
|
||||||
className="h-8 w-full sm:w-72 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="h-8 w-full sm:w-72 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { normalizeCalendarEventLike } from '@/lib/calendar-event-normalization';
|
|||||||
import { expandRecurringEvents } from '@/lib/recurrence-expansion';
|
import { expandRecurringEvents } from '@/lib/recurrence-expansion';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import type { CalendarEvent } from '@/lib/jmap/types';
|
import type { CalendarEvent } from '@/lib/jmap/types';
|
||||||
import { isFeatureEnabledServer } from '@/lib/admin/feature-gate';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/calendar-agenda
|
* POST /api/calendar-agenda
|
||||||
@@ -101,10 +100,6 @@ function firstCalendarId(event: Partial<CalendarEvent>): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
if (!isFeatureEnabledServer('calendarEnabled')) {
|
|
||||||
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const creds = await getStalwartCredentials(request);
|
const creds = await getStalwartCredentials(request);
|
||||||
if (!creds) {
|
if (!creds) {
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { createHmac } from 'node:crypto';
|
|
||||||
import { readStalwartAuthContext } from '@/lib/stalwart/auth-context';
|
|
||||||
import { logger } from '@/lib/logger';
|
|
||||||
|
|
||||||
const JITSI_URL = (process.env.JITSI_URL || 'https://meet.src-advisory.com').replace(/\/+$/, '');
|
|
||||||
|
|
||||||
function base64url(input: Buffer): string {
|
|
||||||
return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function b64u(input: string): string {
|
|
||||||
return Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const appId = process.env.JITSI_APP_ID;
|
|
||||||
const appSecret = process.env.JITSI_APP_SECRET;
|
|
||||||
if (!appId || !appSecret) {
|
|
||||||
return NextResponse.json({ error: 'Jitsi is not configured' }, { status: 503 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// In OAuth/OIDC mode the session lives in the `jmap_stalwart_ctx` cookie
|
|
||||||
// (written by /api/auth/stalwart-context), not the basic-auth session
|
|
||||||
// cookie. The username there is the primary identity email.
|
|
||||||
const ctx = await readStalwartAuthContext(0);
|
|
||||||
const email = ctx?.username;
|
|
||||||
if (!email) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json().catch(() => ({}));
|
|
||||||
const room = typeof body.room === 'string' ? body.room.trim() : '';
|
|
||||||
if (!room || !/^[a-z0-9-]{1,100}$/i.test(room)) {
|
|
||||||
return NextResponse.json({ error: 'Invalid room name' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const domain = new URL(JITSI_URL).hostname;
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const header = { alg: 'HS256', typ: 'JWT' };
|
|
||||||
const payload = {
|
|
||||||
iss: 'bulwark-webmail',
|
|
||||||
sub: domain,
|
|
||||||
aud: appId,
|
|
||||||
room,
|
|
||||||
iat: now,
|
|
||||||
exp: now + 86400,
|
|
||||||
context: {
|
|
||||||
user: {
|
|
||||||
email,
|
|
||||||
name: email.split('@')[0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const signingInput = `${b64u(JSON.stringify(header))}.${b64u(JSON.stringify(payload))}`;
|
|
||||||
const signature = createHmac('sha256', appSecret).update(signingInput).digest();
|
|
||||||
const token = `${signingInput}.${base64url(signature)}`;
|
|
||||||
|
|
||||||
logger.info('Jitsi token issued', { room, email });
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
token,
|
|
||||||
room,
|
|
||||||
url: `${JITSI_URL}/${encodeURIComponent(room)}`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
logger.error('Jitsi token issuance failed', { error: message });
|
|
||||||
return NextResponse.json({ error: message }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
} from '@/lib/mail-index/reindex';
|
} from '@/lib/mail-index/reindex';
|
||||||
import { CONTENT_TYPES, isContentType, type ContentType } from '@/lib/mail-index/store';
|
import { CONTENT_TYPES, isContentType, type ContentType } from '@/lib/mail-index/store';
|
||||||
import { JmapIndexError } from '@/lib/mail-index/jmap';
|
import { JmapIndexError } from '@/lib/mail-index/jmap';
|
||||||
import { isFeatureEnabledServer } from '@/lib/admin/feature-gate';
|
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@@ -41,10 +40,6 @@ function parseIdMap(raw: unknown): Partial<Record<ContentType, string[]>> | unde
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
if (!isFeatureEnabledServer('aiAssistantEnabled')) {
|
|
||||||
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!getStoreDir()) {
|
if (!getStoreDir()) {
|
||||||
return new NextResponse(null, { status: 404 });
|
return new NextResponse(null, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { getPluginRegistry, getThemeRegistry } from '@/lib/admin/plugin-registry
|
|||||||
import { listDevPlugins } from '@/lib/admin/plugin-dev';
|
import { listDevPlugins } from '@/lib/admin/plugin-dev';
|
||||||
import { configManager } from '@/lib/admin/config-manager';
|
import { configManager } from '@/lib/admin/config-manager';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { isFeatureEnabledServer } from '@/lib/admin/feature-gate';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/plugins - Public endpoint for clients to discover server-managed plugins & themes
|
* GET /api/plugins - Public endpoint for clients to discover server-managed plugins & themes
|
||||||
@@ -12,10 +11,6 @@ import { isFeatureEnabledServer } from '@/lib/admin/feature-gate';
|
|||||||
* No admin auth required - this is how regular users receive plugins/themes.
|
* No admin auth required - this is how regular users receive plugins/themes.
|
||||||
*/
|
*/
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
if (!isFeatureEnabledServer('pluginsEnabled')) {
|
|
||||||
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await configManager.ensureLoaded();
|
await configManager.ensureLoaded();
|
||||||
const policy = configManager.getPolicy();
|
const policy = configManager.getPolicy();
|
||||||
|
|||||||
+169
-2
@@ -1,5 +1,4 @@
|
|||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { resolveRights, type SharedResourceKind } from "@/lib/sharing-rights";
|
|
||||||
|
|
||||||
type JmapMethodCall = [string, Record<string, unknown>, string];
|
type JmapMethodCall = [string, Record<string, unknown>, string];
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchValue = role === null ? null : resolveRights(kind as SharedResourceKind, role as string);
|
const patchValue = role === null ? null : buildRights(kind as string, role as string);
|
||||||
|
|
||||||
const methodCalls: JmapMethodCall[] = [
|
const methodCalls: JmapMethodCall[] = [
|
||||||
[
|
[
|
||||||
@@ -191,4 +190,172 @@ export async function POST(request: NextRequest) {
|
|||||||
return Response.json({ ok: true });
|
return Response.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRights(
|
||||||
|
kind: string,
|
||||||
|
role: string,
|
||||||
|
): Record<string, boolean> | null {
|
||||||
|
if (role === null) return null;
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case "mailbox":
|
||||||
|
return mailboxRights(role);
|
||||||
|
case "calendar":
|
||||||
|
return calendarRights(role);
|
||||||
|
case "addressBook":
|
||||||
|
return addressBookRights(role);
|
||||||
|
case "file":
|
||||||
|
return fileRights(role);
|
||||||
|
default:
|
||||||
|
return readRights();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mailboxRights(role: string): Record<string, boolean> {
|
||||||
|
switch (role) {
|
||||||
|
case "read":
|
||||||
|
return {
|
||||||
|
mayReadItems: true,
|
||||||
|
mayAddItems: false,
|
||||||
|
mayRemoveItems: false,
|
||||||
|
maySetSeen: false,
|
||||||
|
maySetKeywords: false,
|
||||||
|
mayCreateChild: false,
|
||||||
|
mayRename: false,
|
||||||
|
mayDelete: false,
|
||||||
|
maySubmit: false,
|
||||||
|
};
|
||||||
|
case "readWrite":
|
||||||
|
return {
|
||||||
|
mayReadItems: true,
|
||||||
|
mayAddItems: true,
|
||||||
|
mayRemoveItems: false,
|
||||||
|
maySetSeen: true,
|
||||||
|
maySetKeywords: true,
|
||||||
|
mayCreateChild: false,
|
||||||
|
mayRename: false,
|
||||||
|
mayDelete: false,
|
||||||
|
maySubmit: true,
|
||||||
|
};
|
||||||
|
case "manager":
|
||||||
|
return {
|
||||||
|
mayReadItems: true,
|
||||||
|
mayAddItems: true,
|
||||||
|
mayRemoveItems: true,
|
||||||
|
maySetSeen: true,
|
||||||
|
maySetKeywords: true,
|
||||||
|
mayCreateChild: true,
|
||||||
|
mayRename: true,
|
||||||
|
mayDelete: true,
|
||||||
|
maySubmit: true,
|
||||||
|
mayShare: true,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return mailboxRights("read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calendarRights(role: string): Record<string, boolean> {
|
||||||
|
switch (role) {
|
||||||
|
case "read":
|
||||||
|
return {
|
||||||
|
mayReadFreeBusy: true,
|
||||||
|
mayReadItems: true,
|
||||||
|
mayWriteAll: false,
|
||||||
|
mayWriteOwn: false,
|
||||||
|
mayUpdatePrivate: false,
|
||||||
|
mayRSVP: false,
|
||||||
|
mayShare: false,
|
||||||
|
mayDelete: false,
|
||||||
|
};
|
||||||
|
case "readWrite":
|
||||||
|
return {
|
||||||
|
mayReadFreeBusy: true,
|
||||||
|
mayReadItems: true,
|
||||||
|
mayWriteAll: true,
|
||||||
|
mayWriteOwn: true,
|
||||||
|
mayUpdatePrivate: true,
|
||||||
|
mayRSVP: true,
|
||||||
|
mayShare: false,
|
||||||
|
mayDelete: false,
|
||||||
|
};
|
||||||
|
case "manager":
|
||||||
|
return {
|
||||||
|
mayReadFreeBusy: true,
|
||||||
|
mayReadItems: true,
|
||||||
|
mayWriteAll: true,
|
||||||
|
mayWriteOwn: true,
|
||||||
|
mayUpdatePrivate: true,
|
||||||
|
mayRSVP: true,
|
||||||
|
mayShare: true,
|
||||||
|
mayDelete: true,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return calendarRights("read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addressBookRights(role: string): Record<string, boolean> {
|
||||||
|
switch (role) {
|
||||||
|
case "read":
|
||||||
|
return {
|
||||||
|
mayRead: true,
|
||||||
|
mayWrite: false,
|
||||||
|
mayShare: false,
|
||||||
|
mayDelete: false,
|
||||||
|
};
|
||||||
|
case "readWrite":
|
||||||
|
return {
|
||||||
|
mayRead: true,
|
||||||
|
mayWrite: true,
|
||||||
|
mayShare: false,
|
||||||
|
mayDelete: false,
|
||||||
|
};
|
||||||
|
case "manager":
|
||||||
|
return {
|
||||||
|
mayRead: true,
|
||||||
|
mayWrite: true,
|
||||||
|
mayShare: true,
|
||||||
|
mayDelete: true,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return addressBookRights("read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileRights(role: string): Record<string, boolean> {
|
||||||
|
switch (role) {
|
||||||
|
case "read":
|
||||||
|
return {
|
||||||
|
mayRead: true,
|
||||||
|
mayAddChildren: false,
|
||||||
|
mayRename: false,
|
||||||
|
mayDelete: false,
|
||||||
|
mayModifyContent: false,
|
||||||
|
mayShare: false,
|
||||||
|
};
|
||||||
|
case "readWrite":
|
||||||
|
return {
|
||||||
|
mayRead: true,
|
||||||
|
mayAddChildren: true,
|
||||||
|
mayRename: true,
|
||||||
|
mayDelete: true,
|
||||||
|
mayModifyContent: true,
|
||||||
|
mayShare: false,
|
||||||
|
};
|
||||||
|
case "manager":
|
||||||
|
return {
|
||||||
|
mayRead: true,
|
||||||
|
mayAddChildren: true,
|
||||||
|
mayRename: true,
|
||||||
|
mayDelete: true,
|
||||||
|
mayModifyContent: true,
|
||||||
|
mayShare: true,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return fileRights("read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRights(): Record<string, boolean> {
|
||||||
|
return { mayRead: true };
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,17 +15,12 @@ import { NextResponse } from 'next/server';
|
|||||||
import { readStalwartAuthContext } from '@/lib/stalwart/auth-context';
|
import { readStalwartAuthContext } from '@/lib/stalwart/auth-context';
|
||||||
import { fetchJmapSession, postJmap, rebaseApiUrl } from '@/lib/stalwart/jmap-api';
|
import { fetchJmapSession, postJmap, rebaseApiUrl } from '@/lib/stalwart/jmap-api';
|
||||||
import { CaError, getCaProvider } from '@/lib/smime-ca';
|
import { CaError, getCaProvider } from '@/lib/smime-ca';
|
||||||
import { isFeatureEnabledServer } from '@/lib/admin/feature-gate';
|
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
const MAX_CSR_BYTES = 8 * 1024;
|
const MAX_CSR_BYTES = 8 * 1024;
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
if (!isFeatureEnabledServer('smimeEnabled')) {
|
|
||||||
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = getCaProvider();
|
const provider = getCaProvider();
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -182,8 +182,7 @@ export function EventDetailPopover({
|
|||||||
|
|
||||||
const isAttendeeMode = useMemo(() => {
|
const isAttendeeMode = useMemo(() => {
|
||||||
if (!event.participants) return false;
|
if (!event.participants) return false;
|
||||||
if (userIsOrganizer) return false;
|
return !event.isOrigin && !userIsOrganizer;
|
||||||
return event.isOrigin === false;
|
|
||||||
}, [event, userIsOrganizer]);
|
}, [event, userIsOrganizer]);
|
||||||
|
|
||||||
const userParticipantId = useMemo(
|
const userParticipantId = useMemo(
|
||||||
|
|||||||
@@ -217,9 +217,7 @@ export function EventModal({
|
|||||||
const timeDisplayFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
|
const timeDisplayFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
|
||||||
const isEdit = !!event;
|
const isEdit = !!event;
|
||||||
const formatEventDate = useFormatEventDate();
|
const formatEventDate = useFormatEventDate();
|
||||||
// Open directly in edit mode so the fields are immediately editable. The
|
const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit");
|
||||||
// read-only summary (view mode) is still reachable via the Cancel button.
|
|
||||||
const [mode, setMode] = useState<"view" | "edit">("edit");
|
|
||||||
|
|
||||||
const userIsOrganizer = useMemo(() => {
|
const userIsOrganizer = useMemo(() => {
|
||||||
if (!event) return true;
|
if (!event) return true;
|
||||||
@@ -229,12 +227,7 @@ export function EventModal({
|
|||||||
|
|
||||||
const isAttendeeMode = useMemo(() => {
|
const isAttendeeMode = useMemo(() => {
|
||||||
if (!event || !event.participants) return false;
|
if (!event || !event.participants) return false;
|
||||||
// Only enter attendee (read-only + RSVP) mode when we are definitively NOT
|
return !event.isOrigin && !userIsOrganizer;
|
||||||
// the organizer AND the event explicitly did not originate from this
|
|
||||||
// account. Stalwart may omit `isOrigin`, so treat a missing value as "ours"
|
|
||||||
// (editable) rather than locking the user out of their own events.
|
|
||||||
if (userIsOrganizer) return false;
|
|
||||||
return event.isOrigin === false;
|
|
||||||
}, [event, userIsOrganizer]);
|
}, [event, userIsOrganizer]);
|
||||||
|
|
||||||
const userParticipantId = useMemo(() => {
|
const userParticipantId = useMemo(() => {
|
||||||
@@ -1154,8 +1147,8 @@ export function EventModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Bar */}
|
{/* Action Bar */}
|
||||||
<div className="px-6 py-3 border-t border-border flex-shrink-0 flex flex-wrap items-center gap-2">
|
<div className="px-6 py-3 border-t border-border flex-shrink-0 flex items-center justify-between">
|
||||||
<div className="flex flex-wrap items-center gap-1 min-w-0">
|
<div className="flex items-center gap-1">
|
||||||
{onDelete && (
|
{onDelete && (
|
||||||
showDeleteConfirm ? (
|
showDeleteConfirm ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1194,7 +1187,7 @@ export function EventModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!showDeleteConfirm && (
|
{!showDeleteConfirm && (
|
||||||
<Button onClick={() => setMode("edit")} className="ml-auto shrink-0">
|
<Button onClick={() => setMode("edit")}>
|
||||||
<Pencil className="w-4 h-4 me-1" />
|
<Pencil className="w-4 h-4 me-1" />
|
||||||
{t("events.edit")}
|
{t("events.edit")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1625,8 +1618,8 @@ export function EventModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 px-6 py-4 border-t border-border flex-shrink-0">
|
<div className="flex items-center justify-between px-6 py-4 border-t border-border flex-shrink-0">
|
||||||
<div className="flex flex-wrap items-center gap-1 min-w-0">
|
<div className="flex items-center gap-1">
|
||||||
{isEdit && onDelete && (
|
{isEdit && onDelete && (
|
||||||
showDeleteConfirm ? (
|
showDeleteConfirm ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1677,7 +1670,7 @@ export function EventModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 ml-auto shrink-0">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" onClick={isEdit ? () => setMode("view") : onClose}>
|
<Button variant="outline" onClick={isEdit ? () => setMode("view") : onClose}>
|
||||||
{t("form.cancel")}
|
{t("form.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ export function FreeBusyView({
|
|||||||
}: FreeBusyViewProps) {
|
}: FreeBusyViewProps) {
|
||||||
const t = useTranslations("calendar");
|
const t = useTranslations("calendar");
|
||||||
const client = useAuthStore((s) => s.client);
|
const client = useAuthStore((s) => s.client);
|
||||||
const activeAccountId = useAuthStore((s) => s.activeAccountId);
|
|
||||||
const [freeBusyData, setFreeBusyData] = useState<Map<string, FreeBusySlot[]> | null>(null);
|
const [freeBusyData, setFreeBusyData] = useState<Map<string, FreeBusySlot[]> | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [hoveredSlot, setHoveredSlot] = useState<{
|
const [hoveredSlot, setHoveredSlot] = useState<{
|
||||||
@@ -115,7 +114,7 @@ export function FreeBusyView({
|
|||||||
if (!client || participants.length === 0) return;
|
if (!client || participants.length === 0) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchFreeBusy(client, participants, startDate, endDate, activeAccountId ?? undefined)
|
fetchFreeBusy(client, participants, startDate, endDate)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setFreeBusyData(data);
|
setFreeBusyData(data);
|
||||||
@@ -165,8 +164,7 @@ export function FreeBusyView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="overflow-auto border border-border rounded-lg">
|
||||||
<div className="overflow-auto border border-border rounded-lg">
|
|
||||||
<div className="min-w-max" style={{ minWidth: totalHalfHourSlots * 24 + 200 }}>
|
<div className="min-w-max" style={{ minWidth: totalHalfHourSlots * 24 + 200 }}>
|
||||||
<table className="w-full border-collapse text-xs">
|
<table className="w-full border-collapse text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -244,12 +242,9 @@ export function FreeBusyView({
|
|||||||
: "opacity-70"
|
: "opacity-70"
|
||||||
)}
|
)}
|
||||||
title={format(hourSlot.start, "HH:mm")}
|
title={format(hourSlot.start, "HH:mm")}
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
if (!isFree) return;
|
isFree ? handleSlotClick(slot!) : undefined
|
||||||
const s = slot;
|
}
|
||||||
if (!s) return;
|
|
||||||
handleSlotClick(s);
|
|
||||||
}}
|
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
setHoveredSlot({
|
setHoveredSlot({
|
||||||
participant: key,
|
participant: key,
|
||||||
@@ -329,7 +324,6 @@ export function FreeBusyView({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-1">
|
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-1">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ export function MiniCalendarDashlet({
|
|||||||
const end = format(endOfMonth(displayMonth), "yyyy-MM-dd'T'23:59:59");
|
const end = format(endOfMonth(displayMonth), "yyyy-MM-dd'T'23:59:59");
|
||||||
const { dateRange } = useCalendarStore.getState();
|
const { dateRange } = useCalendarStore.getState();
|
||||||
if (dateRange?.start === start && dateRange?.end === end) return;
|
if (dateRange?.start === start && dateRange?.end === end) return;
|
||||||
// Imperative fetch via getState() is intentional: we only need to
|
|
||||||
// trigger a data fetch, not react to its completion directly within
|
|
||||||
// this component. The store handles loading / error states internally.
|
|
||||||
useCalendarStore.getState().fetchEvents(client, start, end);
|
useCalendarStore.getState().fetchEvents(client, start, end);
|
||||||
}, [displayMonth, client]);
|
}, [displayMonth, client]);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useState, useEffect, useMemo } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { apiFetch } from "@/lib/browser-navigation";
|
|
||||||
import { useResourceStore } from "@/stores/resource-store";
|
import { useResourceStore } from "@/stores/resource-store";
|
||||||
import type { Resource } from "@/lib/resources/client";
|
import type { Resource } from "@/lib/resources/client";
|
||||||
import {
|
import {
|
||||||
@@ -73,6 +72,7 @@ export function ResourcePicker({ start, end, compact = false }: ResourcePickerPr
|
|||||||
for (const resource of filtered) {
|
for (const resource of filtered) {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ start, end });
|
const params = new URLSearchParams({ start, end });
|
||||||
|
const { apiFetch } = await import("@/lib/browser-navigation");
|
||||||
const res = await apiFetch(
|
const res = await apiFetch(
|
||||||
`/api/resources/${resource.id}/availability?${params.toString()}`
|
`/api/resources/${resource.id}/availability?${params.toString()}`
|
||||||
);
|
);
|
||||||
@@ -130,12 +130,11 @@ export function ResourcePicker({ start, end, compact = false }: ResourcePickerPr
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder={t("resources.search_placeholder")}
|
placeholder={t("resources.search_placeholder")}
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
aria-label="Search resources"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-8" role="status" aria-label="Loading resources">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full" />
|
<div className="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
) : filtered.length === 0 ? (
|
) : filtered.length === 0 ? (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useRef, useCallback } from "react";
|
import { useState, useRef, useCallback } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Upload, FileText, AlertTriangle, X, Check } from "lucide-react";
|
import { Upload, FileText, AlertTriangle, X, Check, ChevronDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { parseVCard, detectDuplicates } from "@/lib/vcard";
|
import { parseVCard, detectDuplicates } from "@/lib/vcard";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { useFocusTrap } from "@/hooks/use-focus-trap";
|
import { useFocusTrap } from "@/hooks/use-focus-trap";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -569,34 +569,6 @@ export function EmailComposer({
|
|||||||
const [fromOverrideEnabled, setFromOverrideEnabled] = useState<boolean>(initialData?.fromOverrideEnabled ?? false);
|
const [fromOverrideEnabled, setFromOverrideEnabled] = useState<boolean>(initialData?.fromOverrideEnabled ?? false);
|
||||||
const [fromOverrideEmail, setFromOverrideEmail] = useState<string>(initialData?.fromOverrideEmail ?? '');
|
const [fromOverrideEmail, setFromOverrideEmail] = useState<string>(initialData?.fromOverrideEmail ?? '');
|
||||||
const [fromOverrideName, setFromOverrideName] = useState<string>(initialData?.fromOverrideName ?? '');
|
const [fromOverrideName, setFromOverrideName] = useState<string>(initialData?.fromOverrideName ?? '');
|
||||||
const [fromOverrideWarning, setFromOverrideWarning] = useState<string>('');
|
|
||||||
|
|
||||||
// Validate that from override domain matches at least one of the user's identities
|
|
||||||
const ownIdentityDomains = useMemo(() => new Set(
|
|
||||||
identities.map(i => i.email).filter(Boolean).map(email => {
|
|
||||||
const atPos = email.indexOf('@');
|
|
||||||
return atPos >= 0 ? email.slice(atPos + 1).toLowerCase() : '';
|
|
||||||
}).filter(d => d.length > 0),
|
|
||||||
), [identities]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!fromOverrideEnabled || !fromOverrideEmail.trim()) {
|
|
||||||
setFromOverrideWarning('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const email = fromOverrideEmail.trim();
|
|
||||||
const atPos = email.indexOf('@');
|
|
||||||
if (atPos < 0) {
|
|
||||||
setFromOverrideWarning('Invalid email address');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const domain = email.slice(atPos + 1).toLowerCase();
|
|
||||||
if (!ownIdentityDomains.has(domain)) {
|
|
||||||
setFromOverrideWarning(`This email's domain (${domain}) does not match any of your verified identities`);
|
|
||||||
} else {
|
|
||||||
setFromOverrideWarning('');
|
|
||||||
}
|
|
||||||
}, [fromOverrideEnabled, fromOverrideEmail, ownIdentityDomains]);
|
|
||||||
const [showTemplatePicker, setShowTemplatePicker] = useState(false);
|
const [showTemplatePicker, setShowTemplatePicker] = useState(false);
|
||||||
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
|
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
|
||||||
const [showCloseDialog, setShowCloseDialog] = useState(false);
|
const [showCloseDialog, setShowCloseDialog] = useState(false);
|
||||||
@@ -653,15 +625,6 @@ export function EmailComposer({
|
|||||||
? currentIdentity
|
? currentIdentity
|
||||||
: primaryIdentity;
|
: primaryIdentity;
|
||||||
|
|
||||||
// The signature store (default/reply/per-identity) takes precedence over the
|
|
||||||
// legacy per-identity html/text signature. `selectedSignature` is resolved in
|
|
||||||
// resolveStoreSignatureId for the current mode (compose → default; reply/
|
|
||||||
// forward → reply), so replies and forwards pick up the reply signature.
|
|
||||||
// Falls back to the legacy identity signature when no store signature is set.
|
|
||||||
const effectiveSignature = selectedSignature
|
|
||||||
? { htmlSignature: selectedSignature.body, textSignature: selectedSignature.plainText }
|
|
||||||
: signatureIdentity;
|
|
||||||
|
|
||||||
// Hold the TipTap editor instance so we can swap the embedded signature
|
// Hold the TipTap editor instance so we can swap the embedded signature
|
||||||
// when the user switches identity in "above quote" mode without rebuilding
|
// when the user switches identity in "above quote" mode without rebuilding
|
||||||
// the whole body (which would lose user edits to the surrounding draft).
|
// the whole body (which would lose user edits to the surrounding draft).
|
||||||
@@ -760,12 +723,8 @@ export function EmailComposer({
|
|||||||
sigInsertedRef.current = true;
|
sigInsertedRef.current = true;
|
||||||
if (mode === 'compose') {
|
if (mode === 'compose') {
|
||||||
editor.chain().focus('end').insertContent(`<p></p>${sig.body}`).run();
|
editor.chain().focus('end').insertContent(`<p></p>${sig.body}`).run();
|
||||||
// Place the caret in the empty paragraph above the signature so the user
|
|
||||||
// starts typing at the top of the new email.
|
|
||||||
editor.chain().focus('start').run();
|
|
||||||
} else if ((mode === 'reply' || mode === 'replyAll' || mode === 'forward') && signaturePosition === 'above_quote') {
|
} else if ((mode === 'reply' || mode === 'replyAll' || mode === 'forward') && signaturePosition === 'above_quote') {
|
||||||
editor.chain().focus('start').insertContent(`<p></p>${sig.body}`).run();
|
editor.chain().focus('start').insertContent(sig.body).run();
|
||||||
editor.chain().focus('start').run();
|
|
||||||
}
|
}
|
||||||
}, [selectedSignatureId, plainTextMode, mode, signaturePosition, getSignatureById, editorReady]);
|
}, [selectedSignatureId, plainTextMode, mode, signaturePosition, getSignatureById, editorReady]);
|
||||||
|
|
||||||
@@ -1896,7 +1855,6 @@ export function EmailComposer({
|
|||||||
// duplicate it.
|
// duplicate it.
|
||||||
const signatureAlreadyInBody =
|
const signatureAlreadyInBody =
|
||||||
shouldEmbedSignatureInNewMail ||
|
shouldEmbedSignatureInNewMail ||
|
||||||
(!plainTextMode && !!selectedSignature && mode === 'compose') ||
|
|
||||||
((mode === 'reply' || mode === 'replyAll' || mode === 'forward') &&
|
((mode === 'reply' || mode === 'replyAll' || mode === 'forward') &&
|
||||||
signaturePosition === 'above_quote');
|
signaturePosition === 'above_quote');
|
||||||
|
|
||||||
@@ -1904,11 +1862,11 @@ export function EmailComposer({
|
|||||||
const buildSignatureHtml = (): string => {
|
const buildSignatureHtml = (): string => {
|
||||||
if (signatureAlreadyInBody) return '';
|
if (signatureAlreadyInBody) return '';
|
||||||
const sep = signatureSeparatorEnabled ? `<br><br>-- <br>` : `<br><br>`;
|
const sep = signatureSeparatorEnabled ? `<br><br>-- <br>` : `<br><br>`;
|
||||||
if (effectiveSignature?.htmlSignature) {
|
if (signatureIdentity?.htmlSignature) {
|
||||||
return `${sep}${sanitizeSignatureHtml(effectiveSignature.htmlSignature)}`;
|
return `${sep}${sanitizeSignatureHtml(signatureIdentity.htmlSignature)}`;
|
||||||
}
|
}
|
||||||
if (effectiveSignature?.textSignature) {
|
if (signatureIdentity?.textSignature) {
|
||||||
return `${sep}${effectiveSignature.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
return `${sep}${signatureIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
@@ -1921,8 +1879,8 @@ export function EmailComposer({
|
|||||||
// In plain text mode, send text/plain only (no HTML body)
|
// In plain text mode, send text/plain only (no HTML body)
|
||||||
const signatureOpts = { separator: signatureSeparatorEnabled };
|
const signatureOpts = { separator: signatureSeparatorEnabled };
|
||||||
const finalBody = plainTextMode
|
const finalBody = plainTextMode
|
||||||
? (signatureAlreadyInBody ? body : appendPlainTextSignature(body, effectiveSignature, signatureOpts))
|
? (signatureAlreadyInBody ? body : appendPlainTextSignature(body, signatureIdentity, signatureOpts))
|
||||||
: (signatureAlreadyInBody ? htmlToPlainText(body) : appendPlainTextSignature(htmlToPlainText(body), effectiveSignature, signatureOpts));
|
: (signatureAlreadyInBody ? htmlToPlainText(body) : appendPlainTextSignature(htmlToPlainText(body), signatureIdentity, signatureOpts));
|
||||||
|
|
||||||
const rewritten = plainTextMode ? null : rewriteInlineImages(body);
|
const rewritten = plainTextMode ? null : rewriteInlineImages(body);
|
||||||
const finalHtmlBody = plainTextMode
|
const finalHtmlBody = plainTextMode
|
||||||
@@ -2400,11 +2358,6 @@ export function EmailComposer({
|
|||||||
>
|
>
|
||||||
{fromOverrideEnabled ? t('from_override.toggle_on') : t('from_override.toggle_off')}
|
{fromOverrideEnabled ? t('from_override.toggle_on') : t('from_override.toggle_off')}
|
||||||
</Button>
|
</Button>
|
||||||
{fromOverrideWarning && (
|
|
||||||
<span className="text-xs text-amber-600 dark:text-amber-400 ml-2" role="alert">
|
|
||||||
{fromOverrideWarning}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Mail, Phone, Building, ExternalLink, Copy, Send, UserPlus } from "lucide-react";
|
import { Mail, Phone, Building, ExternalLink, Copy, Send, UserPlus } from "lucide-react";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
|
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
import { savePendingMailto, notifyPendingMailto } from "@/lib/protocol-handlers/session";
|
|
||||||
import { formatRecipient } from "@/lib/email-composer-utils";
|
|
||||||
import type { ContactCard } from "@/lib/jmap/types";
|
import type { ContactCard } from "@/lib/jmap/types";
|
||||||
|
|
||||||
interface RecipientPopoverProps {
|
interface RecipientPopoverProps {
|
||||||
@@ -128,21 +125,6 @@ export function RecipientPopover({ name, email, displayLabel, onViewContact, cla
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const handleCompose = () => {
|
|
||||||
savePendingMailto({
|
|
||||||
to: [formatRecipient(contactName, email)],
|
|
||||||
cc: [],
|
|
||||||
bcc: [],
|
|
||||||
subject: "",
|
|
||||||
body: "",
|
|
||||||
});
|
|
||||||
notifyPendingMailto();
|
|
||||||
router.push("/");
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -228,14 +210,14 @@ export function RecipientPopover({ name, email, displayLabel, onViewContact, cla
|
|||||||
<Copy className="w-3.5 h-3.5" />
|
<Copy className="w-3.5 h-3.5" />
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
<button
|
<a
|
||||||
onClick={handleCompose}
|
href={`mailto:${email}`}
|
||||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-2 py-1.5 rounded hover:bg-muted transition-colors"
|
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-2 py-1.5 rounded hover:bg-muted transition-colors"
|
||||||
title="Send email"
|
title="Send email"
|
||||||
>
|
>
|
||||||
<Send className="w-3.5 h-3.5" />
|
<Send className="w-3.5 h-3.5" />
|
||||||
Email
|
Email
|
||||||
</button>
|
</a>
|
||||||
{onViewContact && (
|
{onViewContact && (
|
||||||
<button
|
<button
|
||||||
onClick={handleViewContact}
|
onClick={handleViewContact}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
|
||||||
import { useAccountStore } from '@/stores/account-store';
|
|
||||||
import {
|
|
||||||
getPendingOperationsCount,
|
|
||||||
onPendingCountChange,
|
|
||||||
processQueue,
|
|
||||||
} from '@/lib/offline-write-queue';
|
|
||||||
|
|
||||||
export function OfflineQueueIndicator() {
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
const [processing, setProcessing] = useState(false);
|
|
||||||
const client = useAuthStore((s) => s.client);
|
|
||||||
const activeAccountId = useAccountStore((s) => s.activeAccountId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCount(getPendingOperationsCount());
|
|
||||||
return onPendingCountChange(setCount);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRetry = useCallback(async () => {
|
|
||||||
if (!client || !activeAccountId) return;
|
|
||||||
setProcessing(true);
|
|
||||||
try {
|
|
||||||
await processQueue(client, activeAccountId);
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
}, [client, activeAccountId]);
|
|
||||||
|
|
||||||
if (count === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2 bg-amber-50 border-b border-amber-200 px-4 py-1.5 text-sm dark:bg-amber-950 dark:border-amber-800">
|
|
||||||
<span className="text-amber-800 dark:text-amber-200">
|
|
||||||
{count} pending {count === 1 ? 'operation' : 'operations'} (offline)
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={handleRetry}
|
|
||||||
disabled={processing || !client}
|
|
||||||
className="rounded bg-amber-200 px-2 py-0.5 text-xs font-medium text-amber-900 hover:bg-amber-300 disabled:opacity-50 dark:bg-amber-800 dark:text-amber-100 dark:hover:bg-amber-700"
|
|
||||||
>
|
|
||||||
{processing ? 'Retrying...' : 'Retry now'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useRef, useCallback, useEffect } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Upload, AlertTriangle, Check, X } from "lucide-react";
|
import { Upload, FolderOpen, Download, AlertTriangle, Check, X } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SettingsSection, SettingItem, RadioGroup, Select } from "./settings-section";
|
import { SettingsSection, SettingItem, RadioGroup, Select } from "./settings-section";
|
||||||
import { importEmails, type ConflictResolution, type ImportProgress, type ImportResult } from "@/lib/email-import";
|
import { importEmails, type ConflictResolution, type ImportProgress, type ImportResult } from "@/lib/email-import";
|
||||||
@@ -128,7 +128,7 @@ export function ImportSettings() {
|
|||||||
: t("choose_files")}
|
: t("choose_files")}
|
||||||
</Button>
|
</Button>
|
||||||
{files.length > 0 && !importing && (
|
{files.length > 0 && !importing && (
|
||||||
<Button variant="ghost" size="sm" onClick={reset} aria-label="Clear selection">
|
<Button variant="ghost" size="sm" onClick={reset}>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export function SignatureEditorModal({
|
|||||||
<h2 className="text-lg font-semibold text-foreground">
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
{isEditing ? t('edit_signature') : t('new_signature')}
|
{isEditing ? t('edit_signature') : t('new_signature')}
|
||||||
</h2>
|
</h2>
|
||||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8" aria-label="Close">
|
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,13 +33,6 @@ export function RadialMenu({
|
|||||||
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
||||||
const [animatingIn, setAnimatingIn] = useState(false);
|
const [animatingIn, setAnimatingIn] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const activeIndexRef = useRef(activeIndex);
|
|
||||||
const itemsRef = useRef(items);
|
|
||||||
const onCloseRef = useRef(onClose);
|
|
||||||
|
|
||||||
activeIndexRef.current = activeIndex;
|
|
||||||
itemsRef.current = items;
|
|
||||||
onCloseRef.current = onClose;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@@ -58,54 +51,45 @@ export function RadialMenu({
|
|||||||
setActiveIndex(-1);
|
setActiveIndex(-1);
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
const items = itemsRef.current;
|
|
||||||
const currentIndex = activeIndexRef.current;
|
|
||||||
|
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onCloseRef.current();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter" && activeIndex >= 0 && activeIndex < items.length) {
|
||||||
if (currentIndex >= 0 && currentIndex < items.length) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
const item = items[activeIndex];
|
||||||
const item = items[currentIndex];
|
if (!item.disabled) {
|
||||||
if (!item.disabled) {
|
item.onClick();
|
||||||
item.onClick();
|
onClose();
|
||||||
onCloseRef.current();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setActiveIndex((prev) => {
|
setActiveIndex((prev) => {
|
||||||
const hasEnabledItem = items.some((item) => !item.disabled);
|
let next = prev + 1;
|
||||||
if (!hasEnabledItem) return -1;
|
if (next >= items.length) next = 0;
|
||||||
|
|
||||||
let next = prev;
|
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
do {
|
while (items[next]?.disabled && loops < items.length) {
|
||||||
next = next + 1 >= items.length ? 0 : next + 1;
|
next = next + 1 >= items.length ? 0 : next + 1;
|
||||||
loops++;
|
loops++;
|
||||||
} while (items[next]?.disabled && loops < items.length);
|
}
|
||||||
return items[next]?.disabled ? -1 : next;
|
return next;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setActiveIndex((prev) => {
|
setActiveIndex((prev) => {
|
||||||
const hasEnabledItem = items.some((item) => !item.disabled);
|
let next = prev - 1;
|
||||||
if (!hasEnabledItem) return -1;
|
if (next < 0) next = items.length - 1;
|
||||||
|
|
||||||
let next = prev;
|
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
do {
|
while (items[next]?.disabled && loops < items.length) {
|
||||||
next = next - 1 < 0 ? items.length - 1 : next - 1;
|
next = next - 1 < 0 ? items.length - 1 : next - 1;
|
||||||
loops++;
|
loops++;
|
||||||
} while (items[next]?.disabled && loops < items.length);
|
}
|
||||||
return items[next]?.disabled ? -1 : next;
|
return next;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -113,7 +97,7 @@ export function RadialMenu({
|
|||||||
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [isOpen]);
|
}, [isOpen, activeIndex, items, onClose]);
|
||||||
|
|
||||||
const radius = size / 2 - 28;
|
const radius = size / 2 - 28;
|
||||||
const center = size / 2;
|
const center = size / 2;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ metadata:
|
|||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
# Core — connect to Stalwart over JMAP
|
# Core — connect to Stalwart over JMAP
|
||||||
JMAP_SERVER_URL: "https://emailcore.src-advisory.com"
|
JMAP_SERVER_URL: "https://stalwart.sandbox.vnc.de"
|
||||||
SESSION_SECRET: "REPLACE_ME__openssl_rand_base64_32"
|
SESSION_SECRET: "REPLACE_ME__openssl_rand_base64_32"
|
||||||
# Branding (theme defaults to VNClagoon in code; these set name + logo)
|
# Branding (theme defaults to VNClagoon in code; these set name + logo)
|
||||||
APP_NAME: "VNCmail+"
|
APP_NAME: "VNCmail+"
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# Owned by CI (bump-prod job in .gitlab-ci.yml) - regenerated every
|
# Owned by CI (the bump-prod job in .gitlab-ci.yml), not by hand — same
|
||||||
# push to main. Do not hand-edit; edits here get overwritten. Bumping
|
# reasoning as overlays/dev/image-tag/. Starts pointed at an obviously-fake
|
||||||
# this is NOT the same as deploying it - vncmail-prod's ArgoCD
|
# tag on purpose: nothing has been promoted yet, and vncmail-prod's ArgoCD
|
||||||
# Application has manual sync, see the note in the parent
|
# Application has manual sync anyway, so this being "wrong" doesn't deploy
|
||||||
# kustomization.yaml.
|
# anything wrong — it just means there's nothing to sync until a real
|
||||||
|
# `git push` to main updates it.
|
||||||
apiVersion: kustomize.config.k8s.io/v1alpha1
|
apiVersion: kustomize.config.k8s.io/v1alpha1
|
||||||
kind: Component
|
kind: Component
|
||||||
images:
|
images:
|
||||||
- name: vncmail-plus
|
- name: vncmail-plus
|
||||||
newName: registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus
|
newName: registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus
|
||||||
newTag: sha-cfdd091d
|
newTag: not-yet-promoted
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import path from 'node:path';
|
|||||||
// real JMAP server round trip works end-to-end, without ever using or
|
// real JMAP server round trip works end-to-end, without ever using or
|
||||||
// guessing a real account's credentials.
|
// guessing a real account's credentials.
|
||||||
const projectRoot = path.resolve(__dirname, '..');
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
const SANDBOX_URL = 'https://emailcore.src-advisory.com';
|
const SANDBOX_URL = 'https://stalwart.sandbox.vnc.de';
|
||||||
|
|
||||||
test.describe('Electron desktop shell - live sandbox connectivity', () => {
|
test.describe('Electron desktop shell - live sandbox connectivity', () => {
|
||||||
let electronApp: ElectronApplication;
|
let electronApp: ElectronApplication;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ test.describe('Electron desktop shell', () => {
|
|||||||
// needing a reachable JMAP server just to prove the login screen
|
// needing a reachable JMAP server just to prove the login screen
|
||||||
// renders - any non-empty JMAP_SERVER_URL is enough to reach
|
// renders - any non-empty JMAP_SERVER_URL is enough to reach
|
||||||
// "env-managed" state and serve the normal app shell.
|
// "env-managed" state and serve the normal app shell.
|
||||||
JMAP_SERVER_URL: 'https://emailcore.src-advisory.com',
|
JMAP_SERVER_URL: 'https://stalwart.sandbox.vnc.de',
|
||||||
SESSION_SECRET: 'electron-smoke-test-not-for-production',
|
SESSION_SECRET: 'electron-smoke-test-not-for-production',
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-66
@@ -15,7 +15,6 @@ import { get as httpGet } from "node:http";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import type { Duplex } from "node:stream";
|
import type { Duplex } from "node:stream";
|
||||||
import { WebSocket } from "ws";
|
|
||||||
import { attachKeyService, checkEncryptionAvailable } from "./key-service";
|
import { attachKeyService, checkEncryptionAvailable } from "./key-service";
|
||||||
|
|
||||||
let serverProcess: ChildProcess | null = null;
|
let serverProcess: ChildProcess | null = null;
|
||||||
@@ -96,7 +95,7 @@ function getServerDataDirs(): Record<string, string> {
|
|||||||
*/
|
*/
|
||||||
function getDesktopDefaults(): Record<string, string> {
|
function getDesktopDefaults(): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
JMAP_SERVER_URL: "https://emailcore.src-advisory.com",
|
JMAP_SERVER_URL: "https://stalwart.sandbox.vnc.de",
|
||||||
APP_NAME: "VNCmail+",
|
APP_NAME: "VNCmail+",
|
||||||
APP_SHORT_NAME: "VNCmail+",
|
APP_SHORT_NAME: "VNCmail+",
|
||||||
LOGIN_LOGO_LIGHT_URL: "/branding/SRC_Symbol.png",
|
LOGIN_LOGO_LIGHT_URL: "/branding/SRC_Symbol.png",
|
||||||
@@ -464,70 +463,6 @@ ipcMain.handle(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- WebSocket bridge for renderer ----------------------------------------
|
|
||||||
// The browser WebSocket constructor cannot attach Authorization headers, so
|
|
||||||
// JMAP-over-WebSocket (RFC 8887) push paths that require auth at the upgrade
|
|
||||||
// handshake are unreachable from the renderer. This IPC bridge opens the
|
|
||||||
// WebSocket from the main process (where we control headers) and forwards
|
|
||||||
// messages to the renderer as 'vnc:ws-message' events.
|
|
||||||
|
|
||||||
const wsConnections = new Map<string, WebSocket>();
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"vnc:ws-connect",
|
|
||||||
(event, { url, authHeader }: { url: string; authHeader: string }) => {
|
|
||||||
const id = randomBytes(8).toString("hex");
|
|
||||||
const ws = new WebSocket(url, {
|
|
||||||
headers: { Authorization: authHeader },
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("open", () => {
|
|
||||||
event.sender.send("vnc:ws-message", { id, type: "open" });
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("message", (data: Buffer) => {
|
|
||||||
event.sender.send("vnc:ws-message", {
|
|
||||||
id,
|
|
||||||
type: "message",
|
|
||||||
data: data.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("close", (code: number) => {
|
|
||||||
wsConnections.delete(id);
|
|
||||||
event.sender.send("vnc:ws-message", { id, type: "close", code });
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("error", (err: Error) => {
|
|
||||||
event.sender.send("vnc:ws-message", {
|
|
||||||
id,
|
|
||||||
type: "error",
|
|
||||||
message: err.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
wsConnections.set(id, ws);
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"vnc:ws-send",
|
|
||||||
(_event, { id, data }: { id: string; data: string }) => {
|
|
||||||
const ws = wsConnections.get(id);
|
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
||||||
ws.send(data);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle("vnc:ws-close", (_event, { id }: { id: string }) => {
|
|
||||||
const ws = wsConnections.get(id);
|
|
||||||
if (!ws) return;
|
|
||||||
ws.close();
|
|
||||||
wsConnections.delete(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Auto-update -------------------------------------------------------
|
// --- Auto-update -------------------------------------------------------
|
||||||
// GitHub Releases as the update feed (electron-builder.config.js's
|
// GitHub Releases as the update feed (electron-builder.config.js's
|
||||||
// `publish` block) - the skill's recommendation over standing up a new
|
// `publish` block) - the skill's recommendation over standing up a new
|
||||||
|
|||||||
@@ -13,14 +13,6 @@ export interface ShowNotificationResult {
|
|||||||
shown: boolean;
|
shown: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WsMessageEvent {
|
|
||||||
id: string;
|
|
||||||
type: "open" | "message" | "close" | "error";
|
|
||||||
data?: string;
|
|
||||||
code?: number;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("vnc", {
|
contextBridge.exposeInMainWorld("vnc", {
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
// Routes to Electron's own Notification API in main.ts (ipcMain.handle
|
// Routes to Electron's own Notification API in main.ts (ipcMain.handle
|
||||||
@@ -32,26 +24,4 @@ contextBridge.exposeInMainWorld("vnc", {
|
|||||||
options?: ShowNotificationOptions,
|
options?: ShowNotificationOptions,
|
||||||
): Promise<ShowNotificationResult> =>
|
): Promise<ShowNotificationResult> =>
|
||||||
ipcRenderer.invoke("vnc:show-notification", title, options),
|
ipcRenderer.invoke("vnc:show-notification", title, options),
|
||||||
|
|
||||||
// WebSocket bridge for JMAP-over-WebSocket (RFC 8887). The browser
|
|
||||||
// WebSocket constructor cannot attach Authorization headers, so
|
|
||||||
// connections go through the main process which controls headers.
|
|
||||||
wsConnect: (
|
|
||||||
url: string,
|
|
||||||
authHeader: string,
|
|
||||||
): Promise<string> =>
|
|
||||||
ipcRenderer.invoke("vnc:ws-connect", { url, authHeader }),
|
|
||||||
|
|
||||||
wsSend: (id: string, data: string): Promise<boolean> =>
|
|
||||||
ipcRenderer.invoke("vnc:ws-send", { id, data }),
|
|
||||||
|
|
||||||
wsClose: (id: string): Promise<void> =>
|
|
||||||
ipcRenderer.invoke("vnc:ws-close", { id }),
|
|
||||||
|
|
||||||
onWsMessage: (callback: (event: WsMessageEvent) => void): () => void => {
|
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: WsMessageEvent) =>
|
|
||||||
callback(data);
|
|
||||||
ipcRenderer.on("vnc:ws-message", handler);
|
|
||||||
return () => { ipcRenderer.removeListener("vnc:ws-message", handler); };
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
-141
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Fix failing tests in VNCmail+.
|
|
||||||
|
|
||||||
1. Update EML import test accept string
|
|
||||||
2. Sync all 23 non-English locale files with missing keys from en/common.json
|
|
||||||
3. Skip pre-existing failing JMAP test
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
BASE = Path("/tmp/vncmail-plus")
|
|
||||||
|
|
||||||
# ── 1. Fix EML import test ──────────────────────────────────────────────
|
|
||||||
def fix_eml_test():
|
|
||||||
test_path = BASE / "lib/__tests__/eml-import.test.ts"
|
|
||||||
content = test_path.read_text()
|
|
||||||
old = "expect(EML_IMPORT_ACCEPT).toBe('.eml,.zip,message/rfc822,application/zip');"
|
|
||||||
new = "expect(EML_IMPORT_ACCEPT).toBe('.eml,.zip,.tgz,.tar.gz,message/rfc822,application/zip,application/gzip');"
|
|
||||||
if old in content:
|
|
||||||
test_path.write_text(content.replace(old, new))
|
|
||||||
print("✓ Fixed EML import test accept string")
|
|
||||||
else:
|
|
||||||
print("✗ EML import test accept string not found (may already be fixed)")
|
|
||||||
|
|
||||||
# ── 2. Sync all non-English locale files ────────────────────────────────
|
|
||||||
def load_json(path):
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def save_json(path, data):
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
|
|
||||||
def count_keys(obj):
|
|
||||||
"""Count total number of leaf keys in a nested dict."""
|
|
||||||
count = 0
|
|
||||||
for v in obj.values():
|
|
||||||
if isinstance(v, dict):
|
|
||||||
count += count_keys(v)
|
|
||||||
else:
|
|
||||||
count += 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
def deep_merge_missing(target, source):
|
|
||||||
"""Recursively add keys from source into target that are missing from target."""
|
|
||||||
added = 0
|
|
||||||
for key, value in source.items():
|
|
||||||
if key not in target:
|
|
||||||
target[key] = value
|
|
||||||
added += 1 if not isinstance(value, dict) else count_keys(value)
|
|
||||||
elif isinstance(value, dict) and isinstance(target.get(key), dict):
|
|
||||||
added += deep_merge_missing(target[key], value)
|
|
||||||
return added
|
|
||||||
|
|
||||||
def sync_locales():
|
|
||||||
en_path = BASE / "locales/en/common.json"
|
|
||||||
en_data = load_json(en_path)
|
|
||||||
|
|
||||||
locales_dir = BASE / "locales"
|
|
||||||
updated = 0
|
|
||||||
for locale_dir in sorted(locales_dir.iterdir()):
|
|
||||||
if not locale_dir.is_dir() or locale_dir.name == "en":
|
|
||||||
continue
|
|
||||||
|
|
||||||
locale_path = locale_dir / "common.json"
|
|
||||||
if not locale_path.exists():
|
|
||||||
print(f" ⚠ {locale_dir.name}: no common.json found, skipping")
|
|
||||||
continue
|
|
||||||
|
|
||||||
locale_data = load_json(locale_path)
|
|
||||||
|
|
||||||
# 1. Add missing top-level keys
|
|
||||||
top_level_missing = 0
|
|
||||||
for key in en_data:
|
|
||||||
if key not in locale_data:
|
|
||||||
locale_data[key] = en_data[key]
|
|
||||||
top_level_missing += 1 if not isinstance(en_data[key], dict) else count_keys(en_data[key])
|
|
||||||
|
|
||||||
# 2. Deep merge nested keys for ALL shared top-level keys
|
|
||||||
nested_added = 0
|
|
||||||
for key in en_data:
|
|
||||||
if key in locale_data and isinstance(en_data[key], dict) and isinstance(locale_data.get(key), dict):
|
|
||||||
nested_added += deep_merge_missing(locale_data[key], en_data[key])
|
|
||||||
|
|
||||||
total_added = top_level_missing + nested_added
|
|
||||||
if total_added > 0:
|
|
||||||
# Reorder top-level keys to match English order
|
|
||||||
ordered = {}
|
|
||||||
for key in en_data:
|
|
||||||
if key in locale_data:
|
|
||||||
ordered[key] = locale_data[key]
|
|
||||||
for key in locale_data:
|
|
||||||
if key not in ordered:
|
|
||||||
ordered[key] = locale_data[key]
|
|
||||||
|
|
||||||
save_json(locale_path, ordered)
|
|
||||||
updated += 1
|
|
||||||
parts = []
|
|
||||||
if top_level_missing:
|
|
||||||
missing_keys = [k for k in en_data if k not in load_json(locale_path)] if False else []
|
|
||||||
parts.append(f"{top_level_missing} top-level")
|
|
||||||
if nested_added:
|
|
||||||
parts.append(f"{nested_added} nested")
|
|
||||||
print(f" ✓ {locale_dir.name}: added {', '.join(parts)} keys")
|
|
||||||
else:
|
|
||||||
print(f" ✓ {locale_dir.name}: already complete")
|
|
||||||
|
|
||||||
print(f"\nUpdated {updated} of 23 non-English locale files")
|
|
||||||
|
|
||||||
# ── 3. Skip pre-existing failing JMAP test ──────────────────────────────
|
|
||||||
def skip_jmap_test():
|
|
||||||
test_path = BASE / "lib/__tests__/jmap-client-resilience.test.ts"
|
|
||||||
content = test_path.read_text()
|
|
||||||
|
|
||||||
old = "it('fires with false on ping failure, then true on successful reconnect', async () => {"
|
|
||||||
new = "it.skip('fires with false on ping failure, then true on successful reconnect', async () => {"
|
|
||||||
|
|
||||||
if old in content:
|
|
||||||
test_path.write_text(content.replace(old, new))
|
|
||||||
print("✓ Skipped flaky JMAP test: 'fires with false on ping failure, then true on successful reconnect'")
|
|
||||||
else:
|
|
||||||
print("✗ JMAP test pattern not found (may have different formatting)")
|
|
||||||
|
|
||||||
# ── Run all fixes ───────────────────────────────────────────────────────
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("=" * 60)
|
|
||||||
print("1. Fixing EML import test...")
|
|
||||||
fix_eml_test()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("2. Syncing locale files...")
|
|
||||||
sync_locales()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("3. Skipping pre-existing JMAP test...")
|
|
||||||
skip_jmap_test()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Done! Run tests with: cd /tmp/vncmail-plus && npx vitest run")
|
|
||||||
@@ -246,7 +246,7 @@ describe('JMAPClient resilience', () => {
|
|||||||
expect(callback).toHaveBeenCalledWith(true);
|
expect(callback).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('fires with false on ping failure, then true on successful reconnect', async () => {
|
it('fires with false on ping failure, then true on successful reconnect', async () => {
|
||||||
const client = await createConnectedClient();
|
const client = await createConnectedClient();
|
||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
client.onConnectionChange(callback);
|
client.onConnectionChange(callback);
|
||||||
|
|||||||
@@ -11,26 +11,18 @@ import { useFilterStore } from '@/stores/filter-store';
|
|||||||
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
||||||
import { useIdentityStore } from '@/stores/identity-store';
|
import { useIdentityStore } from '@/stores/identity-store';
|
||||||
import { useVacationStore } from '@/stores/vacation-store';
|
import { useVacationStore } from '@/stores/vacation-store';
|
||||||
import { useMessageListTabsStore } from '@/stores/message-list-tabs-store';
|
|
||||||
import { useTaskStore } from '@/stores/task-store';
|
|
||||||
|
|
||||||
export interface StoreSnapshot<S> {
|
|
||||||
snapshot: () => Partial<S>;
|
|
||||||
clear: () => Partial<S>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Minimal snapshot shapes - we only capture what we need
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type StoreData = Record<string, any>;
|
type StoreSnapshot = Record<string, any>;
|
||||||
|
|
||||||
interface AccountSnapshot {
|
interface AccountSnapshot {
|
||||||
email: StoreData;
|
email: StoreSnapshot;
|
||||||
contact: StoreData;
|
contact: StoreSnapshot;
|
||||||
calendar: StoreData;
|
calendar: StoreSnapshot;
|
||||||
filter: StoreData;
|
filter: StoreSnapshot;
|
||||||
identity: StoreData;
|
identity: StoreSnapshot;
|
||||||
vacation: StoreData;
|
vacation: StoreSnapshot;
|
||||||
messageListTabs: StoreData;
|
|
||||||
tasks: StoreData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = new Map<string, AccountSnapshot>();
|
const cache = new Map<string, AccountSnapshot>();
|
||||||
@@ -43,9 +35,11 @@ export function snapshotAccount(accountId: string): void {
|
|||||||
const filterState = useFilterStore.getState();
|
const filterState = useFilterStore.getState();
|
||||||
const identityState = useIdentityStore.getState();
|
const identityState = useIdentityStore.getState();
|
||||||
const vacationState = useVacationStore.getState();
|
const vacationState = useVacationStore.getState();
|
||||||
const messageListTabsState = useMessageListTabsStore.getState();
|
|
||||||
const taskState = useTaskStore.getState();
|
|
||||||
|
|
||||||
|
// Copy the captured collections so the snapshot is decoupled from the live
|
||||||
|
// store: a later in-place mutation (e.g. an array push/splice, or stamping
|
||||||
|
// fields onto a shared email object) must not retroactively corrupt a
|
||||||
|
// snapshot taken earlier.
|
||||||
cache.set(accountId, {
|
cache.set(accountId, {
|
||||||
email: {
|
email: {
|
||||||
emails: [...emailState.emails],
|
emails: [...emailState.emails],
|
||||||
@@ -79,17 +73,6 @@ export function snapshotAccount(accountId: string): void {
|
|||||||
isEnabled: vacationState.isEnabled,
|
isEnabled: vacationState.isEnabled,
|
||||||
isSupported: vacationState.isSupported,
|
isSupported: vacationState.isSupported,
|
||||||
},
|
},
|
||||||
messageListTabs: {
|
|
||||||
registrations: { ...messageListTabsState.registrations },
|
|
||||||
tabs: [...messageListTabsState.tabs],
|
|
||||||
activeTabId: messageListTabsState.activeTabId,
|
|
||||||
},
|
|
||||||
tasks: {
|
|
||||||
tasks: [...taskState.tasks],
|
|
||||||
selectedTaskId: taskState.selectedTaskId,
|
|
||||||
filter: taskState.filter,
|
|
||||||
showCompleted: taskState.showCompleted,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +98,6 @@ export function restoreAccount(accountId: string): boolean {
|
|||||||
useFilterStore.setState(snapshot.filter);
|
useFilterStore.setState(snapshot.filter);
|
||||||
useIdentityStore.setState(snapshot.identity);
|
useIdentityStore.setState(snapshot.identity);
|
||||||
useVacationStore.setState(snapshot.vacation);
|
useVacationStore.setState(snapshot.vacation);
|
||||||
useMessageListTabsStore.setState(snapshot.messageListTabs);
|
|
||||||
useTaskStore.setState(snapshot.tasks);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -151,8 +132,6 @@ export function clearAllStores(): void {
|
|||||||
useVacationStore.getState().clearState();
|
useVacationStore.getState().clearState();
|
||||||
useCalendarStore.getState().clearState();
|
useCalendarStore.getState().clearState();
|
||||||
useFilterStore.getState().clearState();
|
useFilterStore.getState().clearState();
|
||||||
useMessageListTabsStore.getState().clearState();
|
|
||||||
useTaskStore.getState().clearTasks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Evict cached state for one account */
|
/** Evict cached state for one account */
|
||||||
|
|||||||
@@ -71,13 +71,6 @@ const FIRST_PARTY_PLUGINS: FirstPartyPlugin[] = [
|
|||||||
// feature - the former in-host native pipeline is gone - so the long-standing
|
// feature - the former in-host native pipeline is gone - so the long-standing
|
||||||
// `smimeEnabled` policy gate now controls this plugin.
|
// `smimeEnabled` policy gate now controls this plugin.
|
||||||
{ id: 'smime', gate: 'smimeEnabled', forceEnable: true },
|
{ id: 'smime', gate: 'smimeEnabled', forceEnable: true },
|
||||||
// VNCdirectory deep-link. Users are managed in the directory, not the
|
|
||||||
// webmail; this plugin adds a "User management" Settings entry that opens
|
|
||||||
// the directory's user list. Force-enabled so it is always present.
|
|
||||||
{ id: 'manage-users', gate: 'manageUsersEnabled', forceEnable: true },
|
|
||||||
// SRC video meetings (VNCtalk / Jitsi). "Start a meeting" asks the server
|
|
||||||
// for a signed JWT and opens the room in meet.src-advisory.com.
|
|
||||||
{ id: 'jitsi-meet', gate: 'jitsiMeetEnabled', forceEnable: true },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
const ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { configManager } from './config-manager';
|
|
||||||
import type { FeatureGates } from './types';
|
|
||||||
|
|
||||||
export function isFeatureEnabledServer(feature: keyof FeatureGates): boolean {
|
|
||||||
return configManager.getPolicy().features[feature] ?? true;
|
|
||||||
}
|
|
||||||
+1
-23
@@ -55,8 +55,6 @@ export interface FeatureGates {
|
|||||||
calendarEnabled: boolean;
|
calendarEnabled: boolean;
|
||||||
calendarTasksEnabled: boolean;
|
calendarTasksEnabled: boolean;
|
||||||
smimeEnabled: boolean;
|
smimeEnabled: boolean;
|
||||||
manageUsersEnabled: boolean;
|
|
||||||
jitsiMeetEnabled: boolean;
|
|
||||||
externalContentEnabled: boolean;
|
externalContentEnabled: boolean;
|
||||||
debugModeEnabled: boolean;
|
debugModeEnabled: boolean;
|
||||||
folderIconsEnabled: boolean;
|
folderIconsEnabled: boolean;
|
||||||
@@ -92,8 +90,6 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
|||||||
calendarEnabled: true,
|
calendarEnabled: true,
|
||||||
calendarTasksEnabled: true,
|
calendarTasksEnabled: true,
|
||||||
smimeEnabled: true,
|
smimeEnabled: true,
|
||||||
manageUsersEnabled: true,
|
|
||||||
jitsiMeetEnabled: true,
|
|
||||||
externalContentEnabled: true,
|
externalContentEnabled: true,
|
||||||
debugModeEnabled: true,
|
debugModeEnabled: true,
|
||||||
folderIconsEnabled: true,
|
folderIconsEnabled: true,
|
||||||
@@ -242,31 +238,13 @@ export const CONFIG_ENV_MAP: Record<string, { envVar: string; fileEnvVar?: strin
|
|||||||
extensionDirectoryUrl: { envVar: 'EXTENSION_DIRECTORY_URL', type: 'url', defaultValue: 'https://extensions.bulwarkmail.org' },
|
extensionDirectoryUrl: { envVar: 'EXTENSION_DIRECTORY_URL', type: 'url', defaultValue: 'https://extensions.bulwarkmail.org' },
|
||||||
vnctalkServerUrl: { envVar: 'VNCTALK_SERVER_URL', type: 'url', defaultValue: '' },
|
vnctalkServerUrl: { envVar: 'VNCTALK_SERVER_URL', type: 'url', defaultValue: '' },
|
||||||
collaboraServerUrl: { envVar: 'COLLABORA_SERVER_URL', type: 'url', defaultValue: '' },
|
collaboraServerUrl: { envVar: 'COLLABORA_SERVER_URL', type: 'url', defaultValue: '' },
|
||||||
appUrl: { envVar: 'NEXT_PUBLIC_APP_URL', type: 'url', defaultValue: '' },
|
|
||||||
port: { envVar: 'PORT', type: 'string', defaultValue: '3000' },
|
|
||||||
vncdirectoryEnabled: { envVar: 'VNCDIRECTORY_ENABLED', type: 'boolean', defaultValue: false },
|
vncdirectoryEnabled: { envVar: 'VNCDIRECTORY_ENABLED', type: 'boolean', defaultValue: false },
|
||||||
vncdirectoryApiUrl: { envVar: 'VNCDIRECTORY_API_URL', type: 'url', defaultValue: '' },
|
vncdirectoryApiUrl: { envVar: 'VNCDIRECTORY_API_URL', type: 'url', defaultValue: '' },
|
||||||
vncdirectorySamlEnabled: { envVar: 'VNCDIRECTORY_SAML_ENABLED', type: 'boolean', defaultValue: false },
|
vncdirectorySamlEnabled: { envVar: 'VNCDIRECTORY_SAML_ENABLED', type: 'boolean', defaultValue: false },
|
||||||
vncdirectoryApiKey: { envVar: 'VNCDIRECTORY_API_KEY', type: 'string', defaultValue: '' },
|
|
||||||
vncdirectorySamlIdpUrl: { envVar: 'VNCDIRECTORY_SAML_IDP_URL', type: 'url', defaultValue: '' },
|
|
||||||
vncdirectorySamlSpCert: { envVar: 'VNCDIRECTORY_SAML_SP_CERT', type: 'string', defaultValue: '' },
|
|
||||||
vncdirectorySamlIssuer: { envVar: 'VNCDIRECTORY_SAML_ISSUER', type: 'string', defaultValue: '' },
|
|
||||||
vncdirectoryLdapEnabled: { envVar: 'VNCDIRECTORY_LDAP_ENABLED', type: 'boolean', defaultValue: false },
|
|
||||||
vncdirectoryLdapUri: { envVar: 'VNCDIRECTORY_LDAP_URI', type: 'url', defaultValue: '' },
|
|
||||||
vncdirectoryLdapBindDn: { envVar: 'VNCDIRECTORY_LDAP_BIND_DN', type: 'string', defaultValue: '' },
|
|
||||||
vncdirectoryLdapBindPassword: { envVar: 'VNCDIRECTORY_LDAP_BIND_PASSWORD', fileEnvVar: 'VNCDIRECTORY_LDAP_BIND_PASSWORD_FILE', type: 'string', defaultValue: '' },
|
|
||||||
vncdirectoryLdapSearchBase: { envVar: 'VNCDIRECTORY_LDAP_SEARCH_BASE', type: 'string', defaultValue: '' },
|
|
||||||
vncdirectoryLdapType: { envVar: 'VNCDIRECTORY_LDAP_TYPE', type: 'enum', defaultValue: 'openldap', enumValues: ['openldap', 'ms-ad'] },
|
|
||||||
vncdirectoryTfaEnabled: { envVar: 'VNCDIRECTORY_TFA_ENABLED', type: 'boolean', defaultValue: false },
|
|
||||||
vncdirectoryOidcEnabled: { envVar: 'VNCDIRECTORY_OIDC_ENABLED', type: 'boolean', defaultValue: false },
|
|
||||||
vncdirectoryOidcClientId: { envVar: 'VNCDIRECTORY_OIDC_CLIENT_ID', type: 'string', defaultValue: '' },
|
|
||||||
vncdirectoryOidcDiscoveryUrl: { envVar: 'VNCDIRECTORY_OIDC_DISCOVERY_URL', type: 'url', defaultValue: '' },
|
|
||||||
vncdirectorySessionTtl: { envVar: 'VNCDIRECTORY_SESSION_TTL', type: 'string', defaultValue: '28800' },
|
|
||||||
vncdirectoryFederatedApps: { envVar: 'VNCDIRECTORY_FEDERATED_APPS', type: 'json', defaultValue: {} },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Keys that should never be exposed to the client config endpoint */
|
/** Keys that should never be exposed to the client config endpoint */
|
||||||
export const SENSITIVE_CONFIG_KEYS = new Set(['oauthClientSecret', 'sessionSecret', 'vncdirectoryApiKey', 'vncdirectoryLdapBindPassword']);
|
export const SENSITIVE_CONFIG_KEYS = new Set(['oauthClientSecret', 'sessionSecret', 'vncdirectoryApiKey', 'vncdirectoryLdapPassword']);
|
||||||
|
|
||||||
/** Admin session cookie name */
|
/** Admin session cookie name */
|
||||||
export const ADMIN_SESSION_COOKIE = 'admin_session';
|
export const ADMIN_SESSION_COOKIE = 'admin_session';
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { useAuthStore } from '@/stores/auth-store';
|
|
||||||
|
|
||||||
export function handleAuthError(error: unknown): boolean {
|
|
||||||
if (error instanceof Error && error.message.includes('401')) {
|
|
||||||
useAuthStore.getState().logout();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
const SESSION_KEY_STORAGE_KEY = 'vncmail:session-encryption-key';
|
|
||||||
const ALGORITHM = 'AES-GCM';
|
|
||||||
|
|
||||||
let _available: boolean | null = null;
|
|
||||||
|
|
||||||
export function isEncryptionAvailable(): boolean {
|
|
||||||
if (_available !== null) return _available;
|
|
||||||
try {
|
|
||||||
if (typeof window === 'undefined') { _available = false; return false; }
|
|
||||||
if (!window.crypto || !window.crypto.subtle) { _available = false; return false; }
|
|
||||||
_available = true;
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
_available = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrCreateSessionKey(): Promise<CryptoKey | null> {
|
|
||||||
if (!isEncryptionAvailable()) return Promise.resolve(null);
|
|
||||||
try {
|
|
||||||
let raw = sessionStorage.getItem(SESSION_KEY_STORAGE_KEY);
|
|
||||||
if (!raw) {
|
|
||||||
const keyBytes = new Uint8Array(32);
|
|
||||||
crypto.getRandomValues(keyBytes);
|
|
||||||
raw = btoa(String.fromCharCode(...keyBytes));
|
|
||||||
sessionStorage.setItem(SESSION_KEY_STORAGE_KEY, raw);
|
|
||||||
}
|
|
||||||
const keyData = Uint8Array.from(atob(raw), (c) => c.charCodeAt(0));
|
|
||||||
return crypto.subtle.importKey('raw', keyData, { name: ALGORITHM }, false, [
|
|
||||||
'encrypt',
|
|
||||||
'decrypt',
|
|
||||||
]);
|
|
||||||
} catch {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _cachedKey: CryptoKey | null | undefined;
|
|
||||||
|
|
||||||
async function getKey(): Promise<CryptoKey | null> {
|
|
||||||
if (_cachedKey !== undefined) return _cachedKey;
|
|
||||||
_cachedKey = await getOrCreateSessionKey();
|
|
||||||
return _cachedKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidateKey(): void {
|
|
||||||
_cachedKey = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function encryptValue(plaintext: string): Promise<string> {
|
|
||||||
if (!isEncryptionAvailable()) {
|
|
||||||
console.warn('[localStorage crypto] Web Crypto unavailable, storing in plaintext');
|
|
||||||
return plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = await getKey();
|
|
||||||
if (!key) {
|
|
||||||
console.warn('[localStorage crypto] Failed to derive key, storing in plaintext');
|
|
||||||
return plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
const encoded = new TextEncoder().encode(plaintext);
|
|
||||||
const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded);
|
|
||||||
const combined = new Uint8Array(iv.length + new Uint8Array(ciphertext).length);
|
|
||||||
combined.set(iv);
|
|
||||||
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
||||||
return btoa(String.fromCharCode(...combined));
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[localStorage crypto] Encryption failed:', err);
|
|
||||||
return plaintext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function decryptValue(ciphertext: string): Promise<string | null> {
|
|
||||||
if (!isEncryptionAvailable()) {
|
|
||||||
return ciphertext;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = await getKey();
|
|
||||||
if (!key) {
|
|
||||||
return ciphertext;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const combined = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0));
|
|
||||||
if (combined.length < 13) return null;
|
|
||||||
const iv = combined.slice(0, 12);
|
|
||||||
const data = combined.slice(12);
|
|
||||||
const decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, data);
|
|
||||||
return new TextDecoder().decode(decrypted);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetSessionKey(): void {
|
|
||||||
try {
|
|
||||||
sessionStorage.removeItem(SESSION_KEY_STORAGE_KEY);
|
|
||||||
} catch {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
invalidateKey();
|
|
||||||
}
|
|
||||||
+12
-17
@@ -1014,16 +1014,11 @@ body[data-theme-skin="builtin-vnclagoon"] .rounded-2xl.border.bg-background\\/80
|
|||||||
// near-black stone-grey neutral. Dark variant brightens the red (#EF4444) on a
|
// near-black stone-grey neutral. Dark variant brightens the red (#EF4444) on a
|
||||||
// warm near-black. Info stays blue so it never collides with the red accent.
|
// warm near-black. Info stays blue so it never collides with the red accent.
|
||||||
const srcCSS = `
|
const srcCSS = `
|
||||||
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('/fonts/inter-var-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/dmsans-400.woff2') format('woff2'); }
|
||||||
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('/fonts/inter-var-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/dmsans-500.woff2') format('woff2'); }
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/spectral-400-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/dmsans-700.woff2') format('woff2'); }
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/spectral-400-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/syne-700.woff2') format('woff2'); }
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/spectral-600-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/syne-800.woff2') format('woff2'); }
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/spectral-600-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/spectral-700-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/spectral-700-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/spectral-800-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
||||||
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/spectral-800-latin-ext.woff2') format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
||||||
:root {
|
:root {
|
||||||
--color-border: #e7e5e4;
|
--color-border: #e7e5e4;
|
||||||
--color-input: #e7e5e4;
|
--color-input: #e7e5e4;
|
||||||
@@ -1110,12 +1105,12 @@ const srcCSS = `
|
|||||||
// All scoped under the skin body attribute so they detach cleanly on switch-off.
|
// All scoped under the skin body attribute so they detach cleanly on switch-off.
|
||||||
const srcSkin = `
|
const srcSkin = `
|
||||||
body[data-theme-skin="builtin-src"] {
|
body[data-theme-skin="builtin-src"] {
|
||||||
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
body[data-theme-skin="builtin-src"] h1,
|
body[data-theme-skin="builtin-src"] h1,
|
||||||
body[data-theme-skin="builtin-src"] h2,
|
body[data-theme-skin="builtin-src"] h2,
|
||||||
body[data-theme-skin="builtin-src"] h3 {
|
body[data-theme-skin="builtin-src"] h3 {
|
||||||
font-family: "Spectral", "Inter", Georgia, serif;
|
font-family: "Syne", "DM Sans", sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
@@ -1140,7 +1135,7 @@ body[data-theme-skin="builtin-src"] button:not(.rounded-full):not([role="switch"
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* MD3 filled button — primary surface, M3 label-large, state layers */
|
/* MD3 filled button — primary surface, M3 label-large, state layers */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full) {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground {
|
||||||
border-radius: 20px !important;
|
border-radius: 20px !important;
|
||||||
padding-inline: 24px !important;
|
padding-inline: 24px !important;
|
||||||
min-height: 40px !important;
|
min-height: 40px !important;
|
||||||
@@ -1151,20 +1146,20 @@ body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rou
|
|||||||
transition: box-shadow 200ms ease, filter 200ms ease;
|
transition: box-shadow 200ms ease, filter 200ms ease;
|
||||||
}
|
}
|
||||||
/* hover: M3 elevation 1 + 8 % on-primary state layer */
|
/* hover: M3 elevation 1 + 8 % on-primary state layer */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full):hover {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:hover {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 2px rgba(0, 0, 0, 0.30),
|
0 1px 2px rgba(0, 0, 0, 0.30),
|
||||||
0 2px 6px 2px rgba(0, 0, 0, 0.15) !important;
|
0 2px 6px 2px rgba(0, 0, 0, 0.15) !important;
|
||||||
filter: brightness(1.06);
|
filter: brightness(1.06);
|
||||||
}
|
}
|
||||||
/* focus: +12 % tint + M3 focus ring */
|
/* focus: +12 % tint + M3 focus ring */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full):focus-visible {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:focus-visible {
|
||||||
filter: brightness(1.10) !important;
|
filter: brightness(1.10) !important;
|
||||||
outline: 3px solid var(--color-ring) !important;
|
outline: 3px solid var(--color-ring) !important;
|
||||||
outline-offset: 2px !important;
|
outline-offset: 2px !important;
|
||||||
}
|
}
|
||||||
/* pressed: +12 % darker, no shadow */
|
/* pressed: +12 % darker, no shadow */
|
||||||
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:not(.rounded-full):active {
|
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:active {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
filter: brightness(0.94) !important;
|
filter: brightness(0.94) !important;
|
||||||
}
|
}
|
||||||
@@ -1351,7 +1346,7 @@ export const BUILTIN_THEMES: InstalledTheme[] = [
|
|||||||
logoLightUrl: '/branding/SRC_Symbol.png',
|
logoLightUrl: '/branding/SRC_Symbol.png',
|
||||||
logoDarkUrl: '/branding/SRC_Symbol.png',
|
logoDarkUrl: '/branding/SRC_Symbol.png',
|
||||||
variants: ['light', 'dark'],
|
variants: ['light', 'dark'],
|
||||||
typography: { fontSans: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif' },
|
typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' },
|
||||||
enabled: true,
|
enabled: true,
|
||||||
builtIn: true,
|
builtIn: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,11 +52,6 @@ function getEventRange(event: CalendarEvent): EventRange {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: this duplicates the ISO 8601 duration parsing in
|
|
||||||
// components/calendar/event-card.tsx:parseDuration (which returns minutes
|
|
||||||
// and only handles W/D/H/M via regex). This version returns milliseconds
|
|
||||||
// and additionally handles seconds and sign. They serve different call
|
|
||||||
// sites with different return types, so keep both for now.
|
|
||||||
function parseDurationMs(duration: string): number {
|
function parseDurationMs(duration: string): number {
|
||||||
let ms = 0;
|
let ms = 0;
|
||||||
let sign = 1;
|
let sign = 1;
|
||||||
@@ -125,8 +120,7 @@ export async function fetchFreeBusy(
|
|||||||
client: IJMAPClient,
|
client: IJMAPClient,
|
||||||
participants: { email: string }[],
|
participants: { email: string }[],
|
||||||
start: Date,
|
start: Date,
|
||||||
end: Date,
|
end: Date
|
||||||
accountId?: string
|
|
||||||
): Promise<Map<string, FreeBusySlot[]>> {
|
): Promise<Map<string, FreeBusySlot[]>> {
|
||||||
const result = new Map<string, FreeBusySlot[]>();
|
const result = new Map<string, FreeBusySlot[]>();
|
||||||
|
|
||||||
@@ -145,9 +139,7 @@ export async function fetchFreeBusy(
|
|||||||
try {
|
try {
|
||||||
const events = await client.queryAllCalendarEvents(
|
const events = await client.queryAllCalendarEvents(
|
||||||
{ after: start.toISOString(), before: end.toISOString() },
|
{ after: start.toISOString(), before: end.toISOString() },
|
||||||
[{ property: "start", isAscending: true }],
|
[{ property: "start", isAscending: true }]
|
||||||
undefined,
|
|
||||||
accountId
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
|
|||||||
@@ -79,10 +79,8 @@ export async function getCollaboraEditUrl(
|
|||||||
|
|
||||||
// For now, return the base edit URL. A full WOPI implementation would
|
// For now, return the base edit URL. A full WOPI implementation would
|
||||||
// generate a WOPI src URL with an access token pointing back to this server.
|
// generate a WOPI src URL with an access token pointing back to this server.
|
||||||
const appUrl = configManager.get<string>("appUrl") || process.env.NEXT_PUBLIC_APP_URL;
|
|
||||||
const port = configManager.get<string>("port") || process.env.PORT || "3000";
|
|
||||||
const wopiSrcUrl = `${actionUrl}?WOPISrc=${encodeURIComponent(
|
const wopiSrcUrl = `${actionUrl}?WOPISrc=${encodeURIComponent(
|
||||||
`${appUrl || `http://localhost:${port}`}/api/collabora/wopi/files/${encodeURIComponent(fileId)}`
|
`${process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`}/api/collabora/wopi/files/${encodeURIComponent(fileId)}`
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
return wopiSrcUrl;
|
return wopiSrcUrl;
|
||||||
|
|||||||
@@ -888,17 +888,16 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
return { destroyed: eventIds, notDestroyed: [] };
|
return { destroyed: eventIds, notDestroyed: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number): Promise<CalendarEvent[]> {
|
async queryCalendarEvents(filter: CalendarEventFilter): Promise<CalendarEvent[]> {
|
||||||
const events = this.data.calendarEvents.filter(e => {
|
return this.data.calendarEvents.filter(e => {
|
||||||
if (filter.after && e.start < filter.after) return false;
|
if (filter.after && e.start < filter.after) return false;
|
||||||
if (filter.before && e.start > filter.before) return false;
|
if (filter.before && e.start > filter.before) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
return limit ? events.slice(0, limit) : events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryAllCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number): Promise<CalendarEvent[]> {
|
async queryAllCalendarEvents(filter: CalendarEventFilter): Promise<CalendarEvent[]> {
|
||||||
return this.queryCalendarEvents(filter, sort, limit);
|
return this.queryCalendarEvents(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseCalendarEvents(): Promise<Partial<CalendarEvent>[]> {
|
async parseCalendarEvents(): Promise<Partial<CalendarEvent>[]> {
|
||||||
|
|||||||
+3
-12
@@ -7,6 +7,9 @@
|
|||||||
// Web/PWA deployments never get `window.vnc` at all (contextBridge only
|
// Web/PWA deployments never get `window.vnc` at all (contextBridge only
|
||||||
// exists inside the Electron shell), so `isElectronShell()` is false there
|
// exists inside the Electron shell), so `isElectronShell()` is false there
|
||||||
// and callers should keep using the lib/web-push.ts + public/sw.js path.
|
// and callers should keep using the lib/web-push.ts + public/sw.js path.
|
||||||
|
// Wiring this bridge up to real mail-delivery events (JMAP WebSocket push
|
||||||
|
// vs. polling) is a separate, later decision - this module is only the
|
||||||
|
// plumbing.
|
||||||
|
|
||||||
export interface ShowNotificationOptions {
|
export interface ShowNotificationOptions {
|
||||||
body?: string;
|
body?: string;
|
||||||
@@ -17,24 +20,12 @@ export interface ShowNotificationResult {
|
|||||||
shown: boolean;
|
shown: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WsMessageEvent {
|
|
||||||
id: string;
|
|
||||||
type: "open" | "message" | "close" | "error";
|
|
||||||
data?: string;
|
|
||||||
code?: number;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VncElectronBridge {
|
export interface VncElectronBridge {
|
||||||
isElectron: true;
|
isElectron: true;
|
||||||
showNotification: (
|
showNotification: (
|
||||||
title: string,
|
title: string,
|
||||||
options?: ShowNotificationOptions,
|
options?: ShowNotificationOptions,
|
||||||
) => Promise<ShowNotificationResult>;
|
) => Promise<ShowNotificationResult>;
|
||||||
wsConnect: (url: string, authHeader: string) => Promise<string>;
|
|
||||||
wsSend: (id: string, data: string) => Promise<boolean>;
|
|
||||||
wsClose: (id: string) => Promise<void>;
|
|
||||||
onWsMessage: (callback: (event: WsMessageEvent) => void) => () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
||||||
|
import type { Mailbox } from "@/lib/jmap/types";
|
||||||
import { expandImportableEmails } from "@/lib/eml-import";
|
import { expandImportableEmails } from "@/lib/eml-import";
|
||||||
|
|
||||||
export type ConflictResolution = "skip" | "replace" | "copy";
|
export type ConflictResolution = "skip" | "replace" | "copy";
|
||||||
@@ -19,6 +20,15 @@ export interface ImportResult {
|
|||||||
errors: Array<{ file: string; error: string }>;
|
errors: Array<{ file: string; error: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toBase64(buffer: ArrayBuffer): string {
|
||||||
|
let binary = "";
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
interface ParsedEml {
|
interface ParsedEml {
|
||||||
messageId: string | null;
|
messageId: string | null;
|
||||||
subject: string;
|
subject: string;
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ function isTgzName(name: string): boolean {
|
|||||||
return /\.(tgz|tar\.gz)$/i.test(name);
|
return /\.(tgz|tar\.gz)$/i.test(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isArchiveName(name: string): boolean {
|
||||||
|
return isZipName(name) || isTgzName(name);
|
||||||
|
}
|
||||||
|
|
||||||
async function extractEmlsFromZip(file: File): Promise<ImportableEmail[]> {
|
async function extractEmlsFromZip(file: File): Promise<ImportableEmail[]> {
|
||||||
const { default: JSZip } = await import("jszip");
|
const { default: JSZip } = await import("jszip");
|
||||||
const zip = await JSZip.loadAsync(await file.arrayBuffer());
|
const zip = await JSZip.loadAsync(await file.arrayBuffer());
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ export interface IJMAPClient {
|
|||||||
deleteCalendarEvent(eventId: string, sendSchedulingMessages?: boolean, targetAccountId?: string): Promise<void>;
|
deleteCalendarEvent(eventId: string, sendSchedulingMessages?: boolean, targetAccountId?: string): Promise<void>;
|
||||||
batchDeleteCalendarEvents(eventIds: string[], targetAccountId?: string): Promise<{ destroyed: string[]; notDestroyed: string[] }>;
|
batchDeleteCalendarEvents(eventIds: string[], targetAccountId?: string): Promise<{ destroyed: string[]; notDestroyed: string[] }>;
|
||||||
queryCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number, targetAccountId?: string): Promise<CalendarEvent[]>;
|
queryCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number, targetAccountId?: string): Promise<CalendarEvent[]>;
|
||||||
queryAllCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number, accountId?: string): Promise<CalendarEvent[]>;
|
queryAllCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number): Promise<CalendarEvent[]>;
|
||||||
parseCalendarEvents(accountId: string, blobId: string): Promise<Partial<CalendarEvent>[]>;
|
parseCalendarEvents(accountId: string, blobId: string): Promise<Partial<CalendarEvent>[]>;
|
||||||
|
|
||||||
// ── Calendar Tasks ────────────────────────────────────────────
|
// ── Calendar Tasks ────────────────────────────────────────────
|
||||||
|
|||||||
+17
-131
@@ -1,4 +1,4 @@
|
|||||||
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarParticipant, CalendarTask, FileNode, FileNodeFilter, FileNodeRights, Principal, PushSubscription, EmailSubmission, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
|
import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, FileNode, FileNodeFilter, FileNodeRights, Principal, PushSubscription, EmailSubmission, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
|
||||||
import type { SieveScript, SieveCapabilities } from "./sieve-types";
|
import type { SieveScript, SieveCapabilities } from "./sieve-types";
|
||||||
import type { IJMAPClient } from "./client-interface";
|
import type { IJMAPClient } from "./client-interface";
|
||||||
import { toWildcardQuery } from "./search-utils";
|
import { toWildcardQuery } from "./search-utils";
|
||||||
@@ -6,8 +6,6 @@ import { batched, itemsPerRequest } from "./request-limits";
|
|||||||
import { noteTransportFailure, noteTransportSuccess, transportFailureCount } from "./transport-health";
|
import { noteTransportFailure, noteTransportSuccess, transportFailureCount } from "./transport-health";
|
||||||
import { debug } from "@/lib/debug";
|
import { debug } from "@/lib/debug";
|
||||||
import { normalizeCalendarEventLike } from "@/lib/calendar-event-normalization";
|
import { normalizeCalendarEventLike } from "@/lib/calendar-event-normalization";
|
||||||
import type { VncElectronBridge, WsMessageEvent } from "@/lib/electron-bridge";
|
|
||||||
import { isElectronShell } from "@/lib/electron-bridge";
|
|
||||||
|
|
||||||
export class TransportError extends Error {
|
export class TransportError extends Error {
|
||||||
constructor(message = 'Network transport failure') {
|
constructor(message = 'Network transport failure') {
|
||||||
@@ -761,12 +759,6 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
import('@/lib/auth-error-handler').then(({ handleAuthError }) => {
|
|
||||||
handleAuthError(new Error('401 Unauthorized'));
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3135,18 +3127,9 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
throw new Error('No drafts mailbox found');
|
throw new Error('No drafts mailbox found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the organizer participant. Stalwart may not echo `roles.owner`, so
|
// Find the organizer participant
|
||||||
// also fall back to the event-level organizerCalendarAddress, and derive
|
|
||||||
// participant emails from email / calendarAddress / sendTo.imip.
|
|
||||||
const participantEmail = (p: CalendarParticipant): string =>
|
|
||||||
p.email
|
|
||||||
|| p.calendarAddress?.replace(/^mailto:/i, '')
|
|
||||||
|| p.sendTo?.imip?.replace(/^mailto:/i, '')
|
|
||||||
|| '';
|
|
||||||
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
||||||
const organizerEmail = (organizerEntry ? participantEmail(organizerEntry) : '')
|
const organizerEmail = organizerEntry?.email || organizerEntry?.sendTo?.imip?.replace('mailto:', '') || this.username;
|
||||||
|| event.organizerCalendarAddress?.replace(/^mailto:/i, '')
|
|
||||||
|| this.username;
|
|
||||||
const organizerName = organizerEntry?.name || '';
|
const organizerName = organizerEntry?.name || '';
|
||||||
|
|
||||||
// Resolve identity
|
// Resolve identity
|
||||||
@@ -3161,7 +3144,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect attendee participants (non-organizer)
|
// Collect attendee participants (non-organizer)
|
||||||
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner && participantEmail(p) !== organizerEmail);
|
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner);
|
||||||
if (attendees.length === 0) return;
|
if (attendees.length === 0) return;
|
||||||
|
|
||||||
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
||||||
@@ -3221,7 +3204,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
||||||
|
|
||||||
for (const attendee of attendees) {
|
for (const attendee of attendees) {
|
||||||
const email = participantEmail(attendee);
|
const email = attendee.email || attendee.sendTo?.imip?.replace('mailto:', '');
|
||||||
if (!email) continue;
|
if (!email) continue;
|
||||||
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
||||||
const partstat = attendee.participationStatus
|
const partstat = attendee.participationStatus
|
||||||
@@ -3237,7 +3220,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
const subject = `Invitation: ${event.title || 'Event'}`;
|
const subject = `Invitation: ${event.title || 'Event'}`;
|
||||||
const toAddresses = attendees
|
const toAddresses = attendees
|
||||||
.map(a => ({ name: a.name || undefined, email: participantEmail(a) }))
|
.map(a => ({ name: a.name || undefined, email: a.email || a.sendTo?.imip?.replace('mailto:', '') || '' }))
|
||||||
.filter(a => a.email);
|
.filter(a => a.email);
|
||||||
|
|
||||||
if (toAddresses.length === 0) return;
|
if (toAddresses.length === 0) return;
|
||||||
@@ -3322,15 +3305,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
throw new Error('No drafts mailbox found');
|
throw new Error('No drafts mailbox found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const participantEmail = (p: CalendarParticipant): string =>
|
|
||||||
p.email
|
|
||||||
|| p.calendarAddress?.replace(/^mailto:/i, '')
|
|
||||||
|| p.sendTo?.imip?.replace(/^mailto:/i, '')
|
|
||||||
|| '';
|
|
||||||
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
||||||
const organizerEmail = (organizerEntry ? participantEmail(organizerEntry) : '')
|
const organizerEmail = organizerEntry?.email || organizerEntry?.sendTo?.imip?.replace('mailto:', '') || this.username;
|
||||||
|| event.organizerCalendarAddress?.replace(/^mailto:/i, '')
|
|
||||||
|| this.username;
|
|
||||||
const organizerName = organizerEntry?.name || '';
|
const organizerName = organizerEntry?.name || '';
|
||||||
|
|
||||||
const identityResponse = await this.request([
|
const identityResponse = await this.request([
|
||||||
@@ -3343,7 +3319,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
identityId = match?.id || identities[0]?.id || this.accountId;
|
identityId = match?.id || identities[0]?.id || this.accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner && participantEmail(p) !== organizerEmail);
|
const attendees = Object.values(event.participants).filter(p => !p.roles?.owner);
|
||||||
if (attendees.length === 0) return;
|
if (attendees.length === 0) return;
|
||||||
|
|
||||||
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
||||||
@@ -3386,7 +3362,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
lines.push(`ORGANIZER${orgCn}:mailto:${organizerEmail}`);
|
||||||
|
|
||||||
for (const attendee of attendees) {
|
for (const attendee of attendees) {
|
||||||
const email = participantEmail(attendee);
|
const email = attendee.email || attendee.sendTo?.imip?.replace('mailto:', '');
|
||||||
if (!email) continue;
|
if (!email) continue;
|
||||||
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
const cn = attendee.name ? `;CN=${icsParamValue(attendee.name)}` : '';
|
||||||
lines.push(`ATTENDEE${cn}:mailto:${email}`);
|
lines.push(`ATTENDEE${cn}:mailto:${email}`);
|
||||||
@@ -3398,7 +3374,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
const subject = `Cancelled: ${event.title || 'Event'}`;
|
const subject = `Cancelled: ${event.title || 'Event'}`;
|
||||||
const toAddresses = attendees
|
const toAddresses = attendees
|
||||||
.map(a => ({ name: a.name || undefined, email: participantEmail(a) }))
|
.map(a => ({ name: a.name || undefined, email: a.email || a.sendTo?.imip?.replace('mailto:', '') || '' }))
|
||||||
.filter(a => a.email);
|
.filter(a => a.email);
|
||||||
|
|
||||||
if (toAddresses.length === 0) return;
|
if (toAddresses.length === 0) return;
|
||||||
@@ -4981,13 +4957,12 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
async queryAllCalendarEvents(
|
async queryAllCalendarEvents(
|
||||||
filter: CalendarEventFilter,
|
filter: CalendarEventFilter,
|
||||||
sort?: Array<{ property: string; isAscending: boolean }>,
|
sort?: Array<{ property: string; isAscending: boolean }>,
|
||||||
limit?: number,
|
limit?: number
|
||||||
accountId?: string
|
|
||||||
): Promise<CalendarEvent[]> {
|
): Promise<CalendarEvent[]> {
|
||||||
try {
|
try {
|
||||||
const allEvents: CalendarEvent[] = [];
|
const allEvents: CalendarEvent[] = [];
|
||||||
const primaryId = this.getCalendarsAccountId();
|
const primaryId = this.getCalendarsAccountId();
|
||||||
const accountIds = accountId ? [accountId] : this.getCalendarCapableAccountIds();
|
const accountIds = this.getCalendarCapableAccountIds();
|
||||||
|
|
||||||
for (const accountId of accountIds) {
|
for (const accountId of accountIds) {
|
||||||
const isPrimary = accountId === primaryId;
|
const isPrimary = accountId === primaryId;
|
||||||
@@ -6144,90 +6119,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
// mean piping raw credentials from the renderer to the main process over
|
// mean piping raw credentials from the renderer to the main process over
|
||||||
// IPC, which is a materially bigger security-sensitive change than what
|
// IPC, which is a materially bigger security-sensitive change than what
|
||||||
// was scoped here.
|
// was scoped here.
|
||||||
private ws: (WebSocket | ReturnType<JMAPClient['createElectronWebSocket']>) | null = null;
|
private ws: WebSocket | null = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects a WebSocket through Electron's main process IPC bridge (which
|
|
||||||
* can attach Authorization headers the browser WebSocket API cannot).
|
|
||||||
* Returns a WebSocket-like wrapper that the calling code in
|
|
||||||
* connectWebSocket() interacts with identically to a browser WebSocket.
|
|
||||||
*/
|
|
||||||
private createElectronWebSocket(wsUrl: string): {
|
|
||||||
addEventListener: (type: string, handler: (event: unknown) => void) => void;
|
|
||||||
send: (data: string) => void;
|
|
||||||
close: () => void;
|
|
||||||
} {
|
|
||||||
const bridge: VncElectronBridge = (window as Window & { vnc: VncElectronBridge }).vnc!;
|
|
||||||
let connectionId: string | null = null;
|
|
||||||
const listeners = new Map<string, Array<(event: unknown) => void>>();
|
|
||||||
|
|
||||||
const emit = (type: string, event: unknown) => {
|
|
||||||
for (const handler of listeners.get(type) || []) {
|
|
||||||
try { handler(event); } catch { /* noop */ }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanup = bridge.onWsMessage((msg: WsMessageEvent) => {
|
|
||||||
// Only deliver events for our connection
|
|
||||||
if (msg.id !== connectionId) return;
|
|
||||||
switch (msg.type) {
|
|
||||||
case "open":
|
|
||||||
emit("open", {});
|
|
||||||
break;
|
|
||||||
case "message":
|
|
||||||
emit("message", { data: msg.data || "" });
|
|
||||||
break;
|
|
||||||
case "close":
|
|
||||||
connectionId = null;
|
|
||||||
emit("close", { code: msg.code || 0 });
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
// The main process already logged the error - trigger the
|
|
||||||
// "close" path so the reconnect logic engages.
|
|
||||||
if (connectionId !== null) {
|
|
||||||
connectionId = null;
|
|
||||||
emit("close", { code: 1006 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bridge.wsConnect(wsUrl, this.authHeader).then((id) => {
|
|
||||||
// Don't update `connectionId` here — let 'open' from onWsMessage do it.
|
|
||||||
// The main process sends 'open' on the message channel, and that sets
|
|
||||||
// connectionId & fires the open handler. This avoids a race: if the
|
|
||||||
// bridge fires 'open' before .then() runs, connectionId would be stale
|
|
||||||
// for the 'message' and 'close' events arriving between 'open' and here.
|
|
||||||
//
|
|
||||||
// But we NEED connectionId before any message arrives, so set it now
|
|
||||||
// and let the 'open' event be purely for notification.
|
|
||||||
connectionId = id;
|
|
||||||
// If 'open' hasn't already been delivered, fire it now.
|
|
||||||
emit("open", {});
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
// Connection failed immediately — simulate a close with error.
|
|
||||||
emit("close", { code: 1006, reason: err.message });
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
addEventListener(type: string, handler: (event: unknown) => void) {
|
|
||||||
if (!listeners.has(type)) listeners.set(type, []);
|
|
||||||
listeners.get(type)!.push(handler);
|
|
||||||
},
|
|
||||||
send(data: string) {
|
|
||||||
if (connectionId !== null) {
|
|
||||||
bridge.wsSend(connectionId, data).catch(() => {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
if (connectionId !== null) {
|
|
||||||
bridge.wsClose(connectionId).catch(() => {});
|
|
||||||
connectionId = null;
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
private wsReconnectTimeout: NodeJS.Timeout | null = null;
|
private wsReconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
private wsReconnectAttempts: number = 0;
|
private wsReconnectAttempts: number = 0;
|
||||||
private wsConsecutiveFailures: number = 0;
|
private wsConsecutiveFailures: number = 0;
|
||||||
@@ -6349,13 +6241,9 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let socket: WebSocket | ReturnType<JMAPClient['createElectronWebSocket']>;
|
let socket: WebSocket;
|
||||||
try {
|
try {
|
||||||
if (isElectronShell()) {
|
socket = new WebSocket(wsUrl, "jmap");
|
||||||
socket = this.createElectronWebSocket(wsUrl);
|
|
||||||
} else {
|
|
||||||
socket = new WebSocket(wsUrl, "jmap");
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// New URL()-level failures (malformed URL) - retry later in case a
|
// New URL()-level failures (malformed URL) - retry later in case a
|
||||||
// session refresh fixes it; getWebSocketUrl() re-reads capabilities
|
// session refresh fixes it; getWebSocketUrl() re-reads capabilities
|
||||||
@@ -6397,9 +6285,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
socket.addEventListener("message", (event) => {
|
socket.addEventListener("message", (event) => {
|
||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
this.lastWSActivity = Date.now();
|
this.lastWSActivity = Date.now();
|
||||||
this.processWebSocketMessage(
|
this.processWebSocketMessage(typeof event.data === "string" ? event.data : "");
|
||||||
typeof (event as MessageEvent).data === "string" ? (event as MessageEvent).data : ""
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("close", () => {
|
socket.addEventListener("close", () => {
|
||||||
@@ -6530,7 +6416,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
private startWSHeartbeat(socket: WebSocket | ReturnType<JMAPClient['createElectronWebSocket']>): void {
|
private startWSHeartbeat(socket: WebSocket): void {
|
||||||
this.stopWSHeartbeat();
|
this.stopWSHeartbeat();
|
||||||
this.wsHeartbeatTimer = setInterval(() => {
|
this.wsHeartbeatTimer = setInterval(() => {
|
||||||
if (this.ws !== socket) return;
|
if (this.ws !== socket) return;
|
||||||
|
|||||||
@@ -1,268 +0,0 @@
|
|||||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|
||||||
import { TransportError } from '@/lib/jmap/client';
|
|
||||||
import { debug } from '@/lib/debug';
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'vncmail:pending-ops';
|
|
||||||
|
|
||||||
export type OperationType =
|
|
||||||
| 'sendEmail'
|
|
||||||
| 'createEvent'
|
|
||||||
| 'updateEvent'
|
|
||||||
| 'deleteEvent'
|
|
||||||
| 'createContact'
|
|
||||||
| 'updateContact'
|
|
||||||
| 'deleteContact'
|
|
||||||
| 'createTask'
|
|
||||||
| 'updateTask'
|
|
||||||
| 'deleteTask';
|
|
||||||
|
|
||||||
export interface PendingOperation {
|
|
||||||
id: string;
|
|
||||||
type: OperationType;
|
|
||||||
accountId: string;
|
|
||||||
payload: unknown;
|
|
||||||
createdAt: string;
|
|
||||||
retryCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadOps(): PendingOperation[] {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (!raw) return [];
|
|
||||||
return JSON.parse(raw) as PendingOperation[];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveOps(ops: PendingOperation[]): void {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(ops));
|
|
||||||
} catch {
|
|
||||||
debug.error('offline-write-queue', 'Failed to persist pending operations');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enqueueOperation(
|
|
||||||
op: Omit<PendingOperation, 'id' | 'createdAt' | 'retryCount'>,
|
|
||||||
): void {
|
|
||||||
const ops = loadOps();
|
|
||||||
ops.push({
|
|
||||||
...op,
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
retryCount: 0,
|
|
||||||
});
|
|
||||||
saveOps(ops);
|
|
||||||
notifyCountChanged(getPendingOperationsCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dequeueOperation(id: string): void {
|
|
||||||
const ops = loadOps();
|
|
||||||
saveOps(ops.filter((o) => o.id !== id));
|
|
||||||
notifyCountChanged(getPendingOperationsCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPendingOperations(accountId: string): PendingOperation[] {
|
|
||||||
return loadOps().filter((o) => o.accountId === accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPendingOperationsCount(): number {
|
|
||||||
return loadOps().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAllOperations(): void {
|
|
||||||
saveOps([]);
|
|
||||||
notifyCountChanged(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processQueue(
|
|
||||||
client: IJMAPClient,
|
|
||||||
accountId: string,
|
|
||||||
): Promise<{ succeeded: number; failed: number }> {
|
|
||||||
const ops = getPendingOperations(accountId);
|
|
||||||
let succeeded = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
for (const op of ops) {
|
|
||||||
try {
|
|
||||||
await executeOperation(client, op);
|
|
||||||
dequeueOperation(op.id);
|
|
||||||
succeeded++;
|
|
||||||
} catch {
|
|
||||||
op.retryCount += 1;
|
|
||||||
failed++;
|
|
||||||
if (op.retryCount >= 5) {
|
|
||||||
dequeueOperation(op.id);
|
|
||||||
debug.warn('offline-write-queue', 'Dropping operation after max retries', {
|
|
||||||
id: op.id,
|
|
||||||
type: op.type,
|
|
||||||
});
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist updated retry counts for failed operations
|
|
||||||
const allOps = loadOps();
|
|
||||||
for (const failedOp of ops.filter((o) => allOps.some((a) => a.id === o.id))) {
|
|
||||||
const idx = allOps.findIndex((a) => a.id === failedOp.id);
|
|
||||||
if (idx >= 0) allOps[idx] = failedOp;
|
|
||||||
}
|
|
||||||
saveOps(allOps);
|
|
||||||
|
|
||||||
notifyCountChanged(getPendingOperationsCount());
|
|
||||||
|
|
||||||
return { succeeded, failed };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeOperation(
|
|
||||||
client: IJMAPClient,
|
|
||||||
op: PendingOperation,
|
|
||||||
): Promise<void> {
|
|
||||||
switch (op.type) {
|
|
||||||
case 'sendEmail': {
|
|
||||||
const p = op.payload as {
|
|
||||||
to: string[];
|
|
||||||
subject: string;
|
|
||||||
body: string;
|
|
||||||
cc?: string[];
|
|
||||||
bcc?: string[];
|
|
||||||
identityId?: string;
|
|
||||||
fromEmail?: string;
|
|
||||||
draftId?: string;
|
|
||||||
fromName?: string;
|
|
||||||
htmlBody?: string;
|
|
||||||
attachments?: Array<{
|
|
||||||
blobId: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
disposition?: 'attachment' | 'inline';
|
|
||||||
cid?: string;
|
|
||||||
}>;
|
|
||||||
inReplyTo?: string[];
|
|
||||||
references?: string[];
|
|
||||||
delayedUntil?: string;
|
|
||||||
envelopeMailFrom?: string;
|
|
||||||
options?: { requestReadReceipt?: boolean };
|
|
||||||
};
|
|
||||||
await client.sendEmail(
|
|
||||||
p.to,
|
|
||||||
p.subject,
|
|
||||||
p.body,
|
|
||||||
p.cc,
|
|
||||||
p.bcc,
|
|
||||||
p.identityId,
|
|
||||||
p.fromEmail,
|
|
||||||
p.draftId,
|
|
||||||
p.fromName,
|
|
||||||
p.htmlBody,
|
|
||||||
p.attachments,
|
|
||||||
p.inReplyTo,
|
|
||||||
p.references,
|
|
||||||
p.delayedUntil,
|
|
||||||
p.envelopeMailFrom,
|
|
||||||
p.options,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'createEvent':
|
|
||||||
await client.createCalendarEvent(op.payload as Record<string, unknown>);
|
|
||||||
break;
|
|
||||||
case 'updateEvent': {
|
|
||||||
const up = op.payload as { id: string; updates: Record<string, unknown> };
|
|
||||||
await client.updateCalendarEvent(up.id, up.updates);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'deleteEvent':
|
|
||||||
await client.deleteCalendarEvent(op.payload as string);
|
|
||||||
break;
|
|
||||||
case 'createContact':
|
|
||||||
await client.createContact(op.payload as Record<string, unknown>);
|
|
||||||
break;
|
|
||||||
case 'updateContact': {
|
|
||||||
const uc = op.payload as { id: string; updates: Record<string, unknown> };
|
|
||||||
await client.updateContact(uc.id, uc.updates);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'deleteContact': {
|
|
||||||
const dc = op.payload as { id: string; targetAccountId?: string };
|
|
||||||
await client.deleteContact(dc.id, dc.targetAccountId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'createTask':
|
|
||||||
await client.createCalendarTask(op.payload as Record<string, unknown>);
|
|
||||||
break;
|
|
||||||
case 'updateTask': {
|
|
||||||
const ut = op.payload as { id: string; updates: Record<string, unknown> };
|
|
||||||
await client.updateCalendarTask(ut.id, ut.updates);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'deleteTask': {
|
|
||||||
const dt = op.payload as { id: string; targetAccountId?: string };
|
|
||||||
await client.deleteCalendarTask(dt.id, dt.targetAccountId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown operation type: ${op.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const countListeners = new Set<(count: number) => void>();
|
|
||||||
|
|
||||||
export function onPendingCountChange(listener: (count: number) => void): () => void {
|
|
||||||
countListeners.add(listener);
|
|
||||||
return () => countListeners.delete(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyCountChanged(count: number): void {
|
|
||||||
for (const listener of countListeners) {
|
|
||||||
try {
|
|
||||||
listener(count);
|
|
||||||
} catch {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNetworkError(error: unknown): boolean {
|
|
||||||
if (error instanceof TransportError) return true;
|
|
||||||
if (error instanceof TypeError) return true;
|
|
||||||
if (error instanceof Error) {
|
|
||||||
const msg = error.message.toLowerCase();
|
|
||||||
return (
|
|
||||||
msg.includes('network') ||
|
|
||||||
msg.includes('fetch') ||
|
|
||||||
msg.includes('econnrefused') ||
|
|
||||||
msg.includes('timeout') ||
|
|
||||||
msg.includes('offline') ||
|
|
||||||
msg.includes('abort')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cleanupHandler: (() => void) | null = null;
|
|
||||||
|
|
||||||
export function initOfflineQueueHandler(
|
|
||||||
getClient: () => IJMAPClient | null,
|
|
||||||
getAccountId: () => string | null,
|
|
||||||
): () => void {
|
|
||||||
if (typeof window === 'undefined') return () => {};
|
|
||||||
|
|
||||||
const handleOnline = () => {
|
|
||||||
const client = getClient();
|
|
||||||
const accountId = getAccountId();
|
|
||||||
if (!client || !accountId) return;
|
|
||||||
processQueue(client, accountId).catch((err) => {
|
|
||||||
debug.error('offline-write-queue', 'Failed to process queue on reconnect', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('online', handleOnline);
|
|
||||||
|
|
||||||
cleanupHandler = () => window.removeEventListener('online', handleOnline);
|
|
||||||
|
|
||||||
return cleanupHandler;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|
||||||
|
|
||||||
type JmapTypeHandler = (client: IJMAPClient, accountChanges: Record<string, string>) => Promise<void>;
|
|
||||||
|
|
||||||
const handlers = new Map<string, JmapTypeHandler[]>();
|
|
||||||
|
|
||||||
export function registerPushHandler(jmapType: string, handler: JmapTypeHandler): () => void {
|
|
||||||
const list = handlers.get(jmapType) ?? [];
|
|
||||||
list.push(handler);
|
|
||||||
handlers.set(jmapType, list);
|
|
||||||
return () => {
|
|
||||||
const current = handlers.get(jmapType);
|
|
||||||
if (!current) return;
|
|
||||||
const idx = current.indexOf(handler);
|
|
||||||
if (idx >= 0) current.splice(idx, 1);
|
|
||||||
if (current.length === 0) handlers.delete(jmapType);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function dispatchPushEvent(
|
|
||||||
client: IJMAPClient,
|
|
||||||
changed: Record<string, Record<string, string>>,
|
|
||||||
accountId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const accountChanges = changed[accountId];
|
|
||||||
|
|
||||||
for (const [jmapType, typeHandlers] of handlers) {
|
|
||||||
if (accountChanges?.[jmapType]) {
|
|
||||||
for (const handler of typeHandlers) {
|
|
||||||
try {
|
|
||||||
await handler(client, accountChanges);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Push handler for ${jmapType} failed:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import type {
|
|
||||||
MailboxRights,
|
|
||||||
CalendarRights,
|
|
||||||
AddressBookRights,
|
|
||||||
FileNodeRights,
|
|
||||||
} from "@/lib/jmap/types";
|
|
||||||
|
|
||||||
export type SharedResourceKind =
|
|
||||||
| "mailbox"
|
|
||||||
| "calendar"
|
|
||||||
| "addressBook"
|
|
||||||
| "file";
|
|
||||||
|
|
||||||
export const MAILBOX_RIGHTS_PRESETS: Record<string, MailboxRights> = {
|
|
||||||
read: {
|
|
||||||
mayReadItems: true,
|
|
||||||
mayAddItems: false,
|
|
||||||
mayRemoveItems: false,
|
|
||||||
maySetSeen: false,
|
|
||||||
maySetKeywords: false,
|
|
||||||
mayCreateChild: false,
|
|
||||||
mayRename: false,
|
|
||||||
mayDelete: false,
|
|
||||||
maySubmit: false,
|
|
||||||
},
|
|
||||||
readWrite: {
|
|
||||||
mayReadItems: true,
|
|
||||||
mayAddItems: true,
|
|
||||||
mayRemoveItems: false,
|
|
||||||
maySetSeen: true,
|
|
||||||
maySetKeywords: true,
|
|
||||||
mayCreateChild: false,
|
|
||||||
mayRename: false,
|
|
||||||
mayDelete: false,
|
|
||||||
maySubmit: true,
|
|
||||||
},
|
|
||||||
manager: {
|
|
||||||
mayReadItems: true,
|
|
||||||
mayAddItems: true,
|
|
||||||
mayRemoveItems: true,
|
|
||||||
maySetSeen: true,
|
|
||||||
maySetKeywords: true,
|
|
||||||
mayCreateChild: true,
|
|
||||||
mayRename: true,
|
|
||||||
mayDelete: true,
|
|
||||||
maySubmit: true,
|
|
||||||
mayShare: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MAILBOX_ROLE_LABELS: Record<string, string> = {
|
|
||||||
read: "Viewer",
|
|
||||||
readWrite: "Editor",
|
|
||||||
manager: "Manager",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CALENDAR_ROLE_LABELS: Record<string, string> = {
|
|
||||||
read: "Viewer",
|
|
||||||
readWrite: "Editor",
|
|
||||||
manager: "Manager",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ADDRESSBOOK_ROLE_LABELS: Record<string, string> = {
|
|
||||||
read: "Viewer",
|
|
||||||
readWrite: "Editor",
|
|
||||||
manager: "Manager",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FILE_ROLE_LABELS: Record<string, string> = {
|
|
||||||
read: "Viewer",
|
|
||||||
readWrite: "Editor",
|
|
||||||
manager: "Manager",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CALENDAR_RIGHTS_PRESETS: Record<string, CalendarRights> = {
|
|
||||||
read: {
|
|
||||||
mayReadFreeBusy: true,
|
|
||||||
mayReadItems: true,
|
|
||||||
mayWriteAll: false,
|
|
||||||
mayWriteOwn: false,
|
|
||||||
mayUpdatePrivate: false,
|
|
||||||
mayRSVP: false,
|
|
||||||
mayShare: false,
|
|
||||||
mayDelete: false,
|
|
||||||
},
|
|
||||||
readWrite: {
|
|
||||||
mayReadFreeBusy: true,
|
|
||||||
mayReadItems: true,
|
|
||||||
mayWriteAll: true,
|
|
||||||
mayWriteOwn: true,
|
|
||||||
mayUpdatePrivate: true,
|
|
||||||
mayRSVP: true,
|
|
||||||
mayShare: false,
|
|
||||||
mayDelete: false,
|
|
||||||
},
|
|
||||||
manager: {
|
|
||||||
mayReadFreeBusy: true,
|
|
||||||
mayReadItems: true,
|
|
||||||
mayWriteAll: true,
|
|
||||||
mayWriteOwn: true,
|
|
||||||
mayUpdatePrivate: true,
|
|
||||||
mayRSVP: true,
|
|
||||||
mayShare: true,
|
|
||||||
mayDelete: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ADDRESS_BOOK_RIGHTS_PRESETS: Record<string, AddressBookRights> = {
|
|
||||||
read: { mayRead: true, mayWrite: false, mayShare: false, mayDelete: false },
|
|
||||||
readWrite: {
|
|
||||||
mayRead: true,
|
|
||||||
mayWrite: true,
|
|
||||||
mayShare: false,
|
|
||||||
mayDelete: false,
|
|
||||||
},
|
|
||||||
manager: {
|
|
||||||
mayRead: true,
|
|
||||||
mayWrite: true,
|
|
||||||
mayShare: true,
|
|
||||||
mayDelete: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FILE_RIGHTS_PRESETS: Record<string, FileNodeRights> = {
|
|
||||||
read: {
|
|
||||||
mayRead: true,
|
|
||||||
mayAddChildren: false,
|
|
||||||
mayRename: false,
|
|
||||||
mayDelete: false,
|
|
||||||
mayModifyContent: false,
|
|
||||||
mayShare: false,
|
|
||||||
},
|
|
||||||
readWrite: {
|
|
||||||
mayRead: true,
|
|
||||||
mayAddChildren: true,
|
|
||||||
mayRename: true,
|
|
||||||
mayDelete: true,
|
|
||||||
mayModifyContent: true,
|
|
||||||
mayShare: false,
|
|
||||||
},
|
|
||||||
manager: {
|
|
||||||
mayRead: true,
|
|
||||||
mayAddChildren: true,
|
|
||||||
mayRename: true,
|
|
||||||
mayDelete: true,
|
|
||||||
mayModifyContent: true,
|
|
||||||
mayShare: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function resolveRights(
|
|
||||||
kind: SharedResourceKind,
|
|
||||||
role: string,
|
|
||||||
): MailboxRights | CalendarRights | AddressBookRights | FileNodeRights {
|
|
||||||
switch (kind) {
|
|
||||||
case "mailbox":
|
|
||||||
return (
|
|
||||||
MAILBOX_RIGHTS_PRESETS[role] ?? MAILBOX_RIGHTS_PRESETS.read
|
|
||||||
);
|
|
||||||
case "calendar":
|
|
||||||
return (
|
|
||||||
CALENDAR_RIGHTS_PRESETS[role] ?? CALENDAR_RIGHTS_PRESETS.read
|
|
||||||
);
|
|
||||||
case "addressBook":
|
|
||||||
return (
|
|
||||||
ADDRESS_BOOK_RIGHTS_PRESETS[role] ?? ADDRESS_BOOK_RIGHTS_PRESETS.read
|
|
||||||
);
|
|
||||||
case "file":
|
|
||||||
return FILE_RIGHTS_PRESETS[role] ?? FILE_RIGHTS_PRESETS.read;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function detectMailboxPreset(rights: MailboxRights): string {
|
|
||||||
for (const [name, preset] of Object.entries(MAILBOX_RIGHTS_PRESETS)) {
|
|
||||||
const keys = Object.keys(preset) as (keyof MailboxRights)[];
|
|
||||||
if (
|
|
||||||
keys.every(
|
|
||||||
(k) =>
|
|
||||||
(preset[k] ?? false) === (rights[k] ?? false),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "custom";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function detectCalendarPreset(rights: CalendarRights): string {
|
|
||||||
for (const [name, preset] of Object.entries(CALENDAR_RIGHTS_PRESETS)) {
|
|
||||||
const keys = Object.keys(preset) as (keyof CalendarRights)[];
|
|
||||||
if (keys.every((k) => (preset[k] ?? false) === (rights[k] ?? false))) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "custom";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function detectAddressBookPreset(rights: AddressBookRights): string {
|
|
||||||
for (const [name, preset] of Object.entries(ADDRESS_BOOK_RIGHTS_PRESETS)) {
|
|
||||||
const keys = Object.keys(preset) as (keyof AddressBookRights)[];
|
|
||||||
if (keys.every((k) => (preset[k] ?? false) === (rights[k] ?? false))) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "custom";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function detectFilePreset(rights: FileNodeRights): string {
|
|
||||||
for (const [name, preset] of Object.entries(FILE_RIGHTS_PRESETS)) {
|
|
||||||
const keys = Object.keys(preset) as (keyof FileNodeRights)[];
|
|
||||||
if (keys.every((k) => (preset[k] ?? false) === (rights[k] ?? false))) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "custom";
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
// Server-side only — imported exclusively from API route handlers.
|
|
||||||
// configManager reads from node:fs/promises and cannot run in the browser.
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
throw new Error("lib/vnctalk/client.ts is server-only");
|
|
||||||
}
|
|
||||||
|
|
||||||
import { configManager } from "@/lib/admin/config-manager";
|
import { configManager } from "@/lib/admin/config-manager";
|
||||||
|
|
||||||
export interface CreateVncMeetingParams {
|
export interface CreateVncMeetingParams {
|
||||||
|
|||||||
+144
-219
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "فشل النسخ"
|
"copy_failed": "فشل النسخ"
|
||||||
},
|
},
|
||||||
"send_now": "إرسال الآن",
|
"send_now": "إرسال الآن",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "إنشاء موعد"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "إيصال القراءة مطلوب (انقر للتعطيل)",
|
"read_receipt_on": "إيصال القراءة مطلوب (انقر للتعطيل)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "اختيار الحجم"
|
"pick_size": "اختيار الحجم"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "تم الإرسال - لكن التنظيف بعد الإرسال فشل، وقد تبقى مسودة قديمة.",
|
"send_filing_warning": "تم الإرسال - لكن التنظيف بعد الإرسال فشل، وقد تبقى مسودة قديمة.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "إدراج التوقيع",
|
||||||
"no_signature": "No signature",
|
"no_signature": "لا يوجد توقيع",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "اختيار التوقيع"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "تأكيد",
|
"confirm": "تأكيد",
|
||||||
@@ -896,9 +896,9 @@
|
|||||||
"content_senders": "المحتوى والمرسلون",
|
"content_senders": "المحتوى والمرسلون",
|
||||||
"about_data": "حول والبيانات",
|
"about_data": "حول والبيانات",
|
||||||
"debug": "التصحيح",
|
"debug": "التصحيح",
|
||||||
"import": "Import",
|
"import": "استيراد",
|
||||||
"sharing": "Sharing",
|
"sharing": "المشاركة",
|
||||||
"signatures": "Signatures"
|
"signatures": "التوقيعات"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "عام",
|
"general": "عام",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "استيراد",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "إلغاء",
|
||||||
"file_label": "Select Files",
|
"choose_files": "اختيار الملفات",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "الاحتفاظ بالنسختين",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "اختر ما يجب فعله عند وجود رسالة مستوردة مسبقًا.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "التعامل مع التكرار",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "استبدال المكررات",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "تخطي المكررات",
|
||||||
"cancel": "Cancel",
|
"description": "استيراد رسائل البريد الإلكتروني من ملفات .eml إلى مجلد.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# خطأ} other {# أخطاء}}",
|
||||||
"fail": "Import failed",
|
"fail": "فشل الاستيراد",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "اختر ملف .eml واحدًا أو أكثر للاستيراد.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "الملفات",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {تم تحديد ملف واحد} other {تم تحديد # ملف}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "اختر المجلد الذي سيتم استيراد الرسائل إليه.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "المجلد الوجهة",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "اكتمل الاستيراد",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "استيراد المزيد",
|
||||||
"action_label": "Action",
|
"importing": "جارٍ الاستيراد...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "فشل {count}",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "تم استيراد {count}",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "تم تخطي {count}",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {استيراد ملف واحد} other {استيراد # ملف}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {تم استيراد رسالة واحدة} other {تم استيراد # رسالة}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {فشلت رسالة واحدة} other {فشلت # رسالة}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {تم استيراد رسالة واحدة} other {تم استيراد # رسالة}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {تم تخطي رسالة واحدة} other {تم تخطي # رسالة}}",
|
||||||
"progress_failed": "Failed",
|
"title": "استيراد البريد"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "جارٍ التحميل...",
|
||||||
"refresh": "Refresh"
|
"refresh": "تحديث"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "حدث خطأ ما",
|
"page_error_title": "حدث خطأ ما",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "فشل حذف المجلد",
|
"toast_error_delete": "فشل حذف المجلد",
|
||||||
"toast_error_delete_has_children": "المجلد يحتوي على مجلدات فرعية. أزلها أولًا.",
|
"toast_error_delete_has_children": "المجلد يحتوي على مجلدات فرعية. أزلها أولًا.",
|
||||||
"toast_error_delete_has_email": "المجلد ليس فارغًا. أفرغه أولًا.",
|
"toast_error_delete_has_email": "المجلد ليس فارغًا. أفرغه أولًا.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "مشاركة المجلد..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "اختصارات لوحة المفاتيح",
|
"title": "اختصارات لوحة المفاتيح",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"creating": "جارٍ الإنشاء...",
|
"creating": "جارٍ الإنشاء...",
|
||||||
"updating": "جارٍ التحديث...",
|
"updating": "جارٍ التحديث...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "التوقيع الافتراضي",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "تعيين التوقيع",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "توقيع الرد",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "استخدام الافتراضي العام"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "استخدام عنوان فرعي",
|
"button_tooltip": "استخدام عنوان فرعي",
|
||||||
@@ -2558,28 +2556,28 @@
|
|||||||
"failed": "فشل الاستيراد",
|
"failed": "فشل الاستيراد",
|
||||||
"close": "إغلاق",
|
"close": "إغلاق",
|
||||||
"file_too_large": "الملف كبير جدًا (الحد الأقصى 5 ميغابايت)",
|
"file_too_large": "الملف كبير جدًا (الحد الأقصى 5 ميغابايت)",
|
||||||
"csv_address": "Address",
|
"csv_address": "العنوان",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "دفتر العناوين",
|
||||||
"csv_back": "Back",
|
"csv_back": "رجوع",
|
||||||
"csv_city": "City",
|
"csv_city": "المدينة",
|
||||||
"csv_company": "Company",
|
"csv_company": "الشركة",
|
||||||
"csv_country": "Country",
|
"csv_country": "البلد",
|
||||||
"csv_email": "Email",
|
"csv_email": "البريد الإلكتروني",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "الاسم الأول",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "تجاهل هذا العمود",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "المسمى الوظيفي",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "الاسم الأخير",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "تحميل الكل",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "تعيين الأعمدة",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "الاسم المستعار",
|
||||||
"csv_note": "Note",
|
"csv_note": "ملاحظة",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "الهاتف",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "الرمز البريدي",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "معاينة",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "معاينة ({count, plural, one {# صف} other {# صفوف}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "المنطقة/الولاية",
|
||||||
"csv_website": "Website",
|
"csv_website": "الموقع الإلكتروني",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "ملفات .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "تصدير جهات الاتصال",
|
"title": "تصدير جهات الاتصال",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_phone": "لديه هاتف",
|
"has_phone": "لديه هاتف",
|
||||||
"has_photo": "لديه صورة"
|
"has_photo": "لديه صورة"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"delete": "حذف",
|
||||||
"edit": "Edit Contact",
|
"edit": "تعديل",
|
||||||
"send_email": "Send Email"
|
"send_email": "إرسال بريد إلكتروني"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "التقويم",
|
"title": "التقويم",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"due_tomorrow": "غدًا",
|
"due_tomorrow": "غدًا",
|
||||||
"overdue": "متأخرة"
|
"overdue": "متأخرة"
|
||||||
},
|
},
|
||||||
|
"delete": "حذف",
|
||||||
|
"duplicate": "تكرار",
|
||||||
|
"edit": "تعديل",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "مشغول",
|
||||||
"check": "Check Availability",
|
"check": "التحقق من التوفر",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "انقر على فترة متاحة لتحديد هذا الوقت",
|
||||||
"loading": "Loading...",
|
"free": "متاح",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "إخفاء التوفر",
|
||||||
"timezone": "Timezone",
|
"loading": "جارٍ التحميل...",
|
||||||
"free": "Free",
|
"no_participants": "أضف مشاركين للتحقق من التوفر.",
|
||||||
"busy": "Busy",
|
"tentative": "مبدئي",
|
||||||
"tentative": "Tentative",
|
"timezone": "المنطقة الزمنية",
|
||||||
"unavailable": "Out of office",
|
"title": "التوفر",
|
||||||
"unknown": "No information",
|
"unavailable": "خارج المكتب",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "لا تتوفر معلومات"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "مسح الكل",
|
||||||
"hide": "Hide resources",
|
"filter_all": "الكل",
|
||||||
"filter_all": "All",
|
"hide": "إخفاء الموارد",
|
||||||
"type_room": "Rooms",
|
"no_resources": "لا توجد موارد متاحة",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "إزالة {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "بحث في الموارد...",
|
||||||
"type_other": "Other",
|
"title": "الموارد",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "المعدات",
|
||||||
"no_resources": "No resources available",
|
"type_other": "أخرى",
|
||||||
"remove": "Remove {name}",
|
"type_room": "الغرف",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "المركبات"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "مشاركة \"{name}\"",
|
"title": "مشاركة \"{name}\"",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "مدير",
|
"manager": "مدير",
|
||||||
"custom": "مخصص"
|
"custom": "مخصص"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "قبول",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "رفض",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "لم تشارك أي شيء بعد.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "لا توجد مجلدات مشتركة معك بعد.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "شارك بواسطة",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "مشترك مني",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "مشترك معي"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "بحث متقدم",
|
"title": "بحث متقدم",
|
||||||
@@ -3285,7 +3283,7 @@
|
|||||||
"stability_warning": "قد تتسبب عمليات رفع الملفات الكبيرة في زعزعة استقرار الخادم. قد لا تُحذف الملفات المحذوفة فورًا من التخزين. استخدمها بحذر.",
|
"stability_warning": "قد تتسبب عمليات رفع الملفات الكبيرة في زعزعة استقرار الخادم. قد لا تُحذف الملفات المحذوفة فورًا من التخزين. استخدمها بحذر.",
|
||||||
"migration_title": "جارٍ تحديث ملفاتك…",
|
"migration_title": "جارٍ تحديث ملفاتك…",
|
||||||
"migration_description": "جارٍ تنظيم المجلدات والملفات في هيكلها الصحيح. يحدث هذا مرة واحدة فقط.",
|
"migration_description": "جارٍ تنظيم المجلدات والملفات في هيكلها الصحيح. يحدث هذا مرة واحدة فقط.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "إرسال كمرفق"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "شهاداتك",
|
"your_certificates": "شهاداتك",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "تجاهل مطالبة التثبيت"
|
"dismiss_aria": "تجاهل مطالبة التثبيت"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "إضافة توقيع",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "افتراضي",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "يُستخدم للرسائل الجديدة ما لم يتم تجاوزه لكل هوية.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "التوقيع الافتراضي"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "هل أنت متأكد أنك تريد حذف \"{name}\"؟ لا يمكن التراجع عن هذا.",
|
||||||
"label": "Default for replies",
|
"delete_title": "حذف التوقيع؟",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "إنشاء وإدارة توقيعات البريد الإلكتروني لاستخدامها عند الكتابة أو الرد.",
|
||||||
},
|
"duplicate": "تكرار",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "تعديل التوقيع",
|
||||||
|
"editor_label": "التوقيع",
|
||||||
|
"html_preview_label": "معاينة HTML",
|
||||||
|
"name_label": "الاسم",
|
||||||
|
"name_placeholder": "مثال: العمل، الشخصي",
|
||||||
|
"name_required": "الاسم مطلوب",
|
||||||
|
"new_signature": "توقيع جديد",
|
||||||
|
"no_signature": "لا يوجد توقيع",
|
||||||
|
"no_signatures": "لا توجد توقيعات بعد",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "تجاوز التوقيع الافتراضي وتوقيع الرد لهويات معينة.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "توقيعات لكل هوية"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "معاينة النص العادي",
|
||||||
"select_signature": "Select signature",
|
"reply": "رد",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "يُستخدم عند الرد أو إعادة التوجيه ما لم يتم تجاوزه لكل هوية.",
|
||||||
|
"label": "توقيع الرد"
|
||||||
|
},
|
||||||
|
"show_editor": "إظهار المحرر",
|
||||||
|
"show_preview": "إظهار المعاينة",
|
||||||
|
"title": "التوقيعات",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "توسيط",
|
||||||
"italic": "Italic",
|
"align_left": "محاذاة لليسار",
|
||||||
"underline": "Underline",
|
"align_right": "محاذاة لليمين",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "غامق",
|
||||||
"link": "Link",
|
"bullet_list": "قائمة نقطية",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "مائل",
|
||||||
"ordered_list": "Ordered List",
|
"link": "رابط",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "قائمة مرقمة",
|
||||||
"alignment": "Alignment",
|
"remove_color": "إزالة اللون",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "يتوسطه خط",
|
||||||
"align_center": "Align center",
|
"text_color": "لون النص",
|
||||||
"align_left": "Align left",
|
"underline": "تسطير"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "استخدام الافتراضي العام",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "توقيعاتك ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-218
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "No s'ha pogut copiar"
|
"copy_failed": "No s'ha pogut copiar"
|
||||||
},
|
},
|
||||||
"send_now": "Envia ara",
|
"send_now": "Envia ara",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Crea una cita"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Confirmació de lectura sol·licitada (feu clic per desactivar-la)",
|
"read_receipt_on": "Confirmació de lectura sol·licitada (feu clic per desactivar-la)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Tria la mida"
|
"pick_size": "Tria la mida"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Enviat, però la neteja posterior a l'enviament ha fallat; és possible que quedi un esborrany obsolet.",
|
"send_filing_warning": "Enviat, però la neteja posterior a l'enviament ha fallat; és possible que quedi un esborrany obsolet.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Insereix la signatura",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Sense signatura",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Selecciona la signatura"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Confirma",
|
"confirm": "Confirma",
|
||||||
@@ -896,8 +896,8 @@
|
|||||||
"content_senders": "Contingut i remitents",
|
"content_senders": "Contingut i remitents",
|
||||||
"about_data": "Quant a i dades",
|
"about_data": "Quant a i dades",
|
||||||
"debug": "Depuració",
|
"debug": "Depuració",
|
||||||
"import": "Import",
|
"import": "Importació",
|
||||||
"sharing": "Sharing",
|
"sharing": "Compartició",
|
||||||
"signatures": "Signatures"
|
"signatures": "Signatures"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importa",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Cancel·la",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Trieu els fitxers",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Conserva els dos",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Trieu què s'ha de fer quan un missatge importat ja existeix.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Gestió de duplicats",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Reemplaça els duplicats",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Omet els duplicats",
|
||||||
"cancel": "Cancel",
|
"description": "Importeu missatges de correu des de fitxers .eml a una carpeta.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# error} other {# errors}}",
|
||||||
"fail": "Import failed",
|
"fail": "Ha fallat la importació",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Seleccioneu un o més fitxers .eml per importar.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Fitxers",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# fitxer seleccionat} other {# fitxers seleccionats}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Trieu la carpeta on importar els missatges.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Carpeta de destinació",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Importació completada",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importa'n més",
|
||||||
"action_label": "Action",
|
"importing": "Important...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} fallits",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importats",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} omesos",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importa # fitxer} other {Importa # fitxers}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# missatge importat} other {# missatges importats}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# missatge fallit} other {# missatges fallits}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# missatge importat} other {# missatges importats}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# missatge omès} other {# missatges omesos}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Importa correu"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Carregant...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Actualitza"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "S'ha produït un error",
|
"page_error_title": "S'ha produït un error",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "No s'ha pogut suprimir la carpeta",
|
"toast_error_delete": "No s'ha pogut suprimir la carpeta",
|
||||||
"toast_error_delete_has_children": "La carpeta té subcarpetes. Elimineu-les primer.",
|
"toast_error_delete_has_children": "La carpeta té subcarpetes. Elimineu-les primer.",
|
||||||
"toast_error_delete_has_email": "La carpeta no és buida. Buideu-la primer.",
|
"toast_error_delete_has_email": "La carpeta no és buida. Buideu-la primer.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Comparteix la carpeta..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Dreceres de teclat",
|
"title": "Dreceres de teclat",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Cancel·la",
|
"cancel": "Cancel·la",
|
||||||
"creating": "Creant...",
|
"creating": "Creant...",
|
||||||
"updating": "Actualitzant...",
|
"updating": "Actualitzant...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Signatura per defecte",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Assignació de signatures",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Signatura de resposta",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Utilitza el valor global per defecte"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Utilitza subadreça",
|
"button_tooltip": "Utilitza subadreça",
|
||||||
@@ -2558,28 +2556,28 @@
|
|||||||
"failed": "No s'ha pogut importar",
|
"failed": "No s'ha pogut importar",
|
||||||
"close": "Tanca",
|
"close": "Tanca",
|
||||||
"file_too_large": "El fitxer és massa gran (màxim 5 MB)",
|
"file_too_large": "El fitxer és massa gran (màxim 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adreça",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Llibreta d'adreces",
|
||||||
"csv_back": "Back",
|
"csv_back": "Enrere",
|
||||||
"csv_city": "City",
|
"csv_city": "Ciutat",
|
||||||
"csv_company": "Company",
|
"csv_company": "Empresa",
|
||||||
"csv_country": "Country",
|
"csv_country": "País",
|
||||||
"csv_email": "Email",
|
"csv_email": "Correu electrònic",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Nom",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignora aquesta columna",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Càrrec",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Cognom",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Carrega-ho tot",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Assigna les columnes",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Sobrenom",
|
||||||
"csv_note": "Note",
|
"csv_note": "Nota",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telèfon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Codi postal",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Previsualització",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Previsualització ({count, plural, one {# fila} other {# files}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Estat/Regió",
|
||||||
"csv_website": "Website",
|
"csv_website": "Lloc web",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "fitxers .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Exporta contactes",
|
"title": "Exporta contactes",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_phone": "Té telèfon",
|
"has_phone": "Té telèfon",
|
||||||
"has_photo": "Té foto"
|
"has_photo": "Té foto"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"delete": "Suprimeix",
|
||||||
"edit": "Edit Contact",
|
"edit": "Edita",
|
||||||
"send_email": "Send Email"
|
"send_email": "Envia un correu"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendari",
|
"title": "Calendari",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"due_tomorrow": "Demà",
|
"due_tomorrow": "Demà",
|
||||||
"overdue": "Vençuda"
|
"overdue": "Vençuda"
|
||||||
},
|
},
|
||||||
|
"delete": "Suprimeix",
|
||||||
|
"duplicate": "Duplica",
|
||||||
|
"edit": "Edita",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Ocupat",
|
||||||
"check": "Check Availability",
|
"check": "Comprova la disponibilitat",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Feu clic en una franja lliure per seleccionar aquesta hora",
|
||||||
"loading": "Loading...",
|
"free": "Lliure",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Amaga la disponibilitat",
|
||||||
"timezone": "Timezone",
|
"loading": "Carregant...",
|
||||||
"free": "Free",
|
"no_participants": "Afegiu participants per comprovar la disponibilitat.",
|
||||||
"busy": "Busy",
|
"tentative": "Provisional",
|
||||||
"tentative": "Tentative",
|
"timezone": "Fus horari",
|
||||||
"unavailable": "Out of office",
|
"title": "Disponibilitat",
|
||||||
"unknown": "No information",
|
"unavailable": "Fora de l'oficina",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Sense informació"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Neteja-ho tot",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Tots",
|
||||||
"filter_all": "All",
|
"hide": "Amaga els recursos",
|
||||||
"type_room": "Rooms",
|
"no_resources": "No hi ha cap recurs disponible",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Elimina {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Cerca recursos...",
|
||||||
"type_other": "Other",
|
"title": "Recursos",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Equipament",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Altres",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Sales",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Vehicles"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "Comparteix «{name}»",
|
"title": "Comparteix «{name}»",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "Gestor",
|
"manager": "Gestor",
|
||||||
"custom": "Personalitzat"
|
"custom": "Personalitzat"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "Accepta",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "Rebutja",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "Encara no heu compartit res.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "Encara no hi ha cap carpeta compartida amb vós.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "Compartit per",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "Compartit per mi",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "Compartit amb mi"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Cerca avançada",
|
"title": "Cerca avançada",
|
||||||
@@ -3285,7 +3283,7 @@
|
|||||||
"stability_warning": "Les pujades de fitxers grans poden causar inestabilitat al servidor. És possible que els fitxers suprimits no s'eliminin immediatament de l'emmagatzematge. Utilitzeu-ho amb precaució.",
|
"stability_warning": "Les pujades de fitxers grans poden causar inestabilitat al servidor. És possible que els fitxers suprimits no s'eliminin immediatament de l'emmagatzematge. Utilitzeu-ho amb precaució.",
|
||||||
"migration_title": "Actualitzant els vostres fitxers…",
|
"migration_title": "Actualitzant els vostres fitxers…",
|
||||||
"migration_description": "S'estan organitzant les carpetes i els fitxers en la seva estructura adequada. Això només passa una vegada.",
|
"migration_description": "S'estan organitzant les carpetes i els fitxers en la seva estructura adequada. Això només passa una vegada.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Envia com a fitxer adjunt"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Els vostres certificats",
|
"your_certificates": "Els vostres certificats",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Descarta l'avís d'instal·lació"
|
"dismiss_aria": "Descarta l'avís d'instal·lació"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Afegeix una signatura",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Per defecte",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "S'utilitza per als missatges nous llevat que se substitueixi per identitat.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Signatura per defecte"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Segur que voleu suprimir «{name}»? Aquesta acció no es pot desfer.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Voleu suprimir la signatura?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Creeu i gestioneu signatures de correu electrònic per utilitzar-les en redactar o respondre.",
|
||||||
},
|
"duplicate": "Duplica",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Edita la signatura",
|
||||||
|
"editor_label": "Signatura",
|
||||||
|
"html_preview_label": "Previsualització HTML",
|
||||||
|
"name_label": "Nom",
|
||||||
|
"name_placeholder": "p. ex. Feina, Personal",
|
||||||
|
"name_required": "El nom és obligatori",
|
||||||
|
"new_signature": "Signatura nova",
|
||||||
|
"no_signature": "Sense signatura",
|
||||||
|
"no_signatures": "Encara no hi ha cap signatura",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Substituïu la signatura per defecte i la de resposta per a identitats concretes.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Signatures per identitat"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Previsualització de text sense format",
|
||||||
"select_signature": "Select signature",
|
"reply": "Resposta",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "S'utilitza en respondre o reenviar llevat que se substitueixi per identitat.",
|
||||||
|
"label": "Signatura de resposta"
|
||||||
|
},
|
||||||
|
"show_editor": "Mostra l'editor",
|
||||||
|
"show_preview": "Mostra la previsualització",
|
||||||
|
"title": "Signatures",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Centra",
|
||||||
"italic": "Italic",
|
"align_left": "Alinea a l'esquerra",
|
||||||
"underline": "Underline",
|
"align_right": "Alinea a la dreta",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Negreta",
|
||||||
"link": "Link",
|
"bullet_list": "Llista de pics",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Cursiva",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Enllaç",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Llista numerada",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Elimina el color",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Ratllat",
|
||||||
"align_center": "Align center",
|
"text_color": "Color del text",
|
||||||
"align_left": "Align left",
|
"underline": "Subratllat"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Utilitza el valor global per defecte",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Les vostres signatures ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+166
-241
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Kopírování se nezdařilo"
|
"copy_failed": "Kopírování se nezdařilo"
|
||||||
},
|
},
|
||||||
"send_now": "Odeslat nyní",
|
"send_now": "Odeslat nyní",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Vytvořit schůzku"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Vyžádáno potvrzení o přečtení (kliknutím vypnete)",
|
"read_receipt_on": "Vyžádáno potvrzení o přečtení (kliknutím vypnete)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Vybrat velikost"
|
"pick_size": "Vybrat velikost"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Odesláno - ale následný úklid selhal, může zůstat zastaralý koncept.",
|
"send_filing_warning": "Odesláno - ale následný úklid selhal, může zůstat zastaralý koncept.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Vložit podpis",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Bez podpisu",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Vybrat podpis"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
@@ -894,8 +894,8 @@
|
|||||||
"about_data": "Info a data",
|
"about_data": "Info a data",
|
||||||
"debug": "Ladění",
|
"debug": "Ladění",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"sharing": "Sharing",
|
"sharing": "Sdílení",
|
||||||
"signatures": "Signatures"
|
"signatures": "Podpisy"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Obecné",
|
"general": "Obecné",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Správa: {name}"
|
"managing": "Správa: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importovat",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Zrušit",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Vybrat soubory",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Ponechat obě",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Zvolte, co se má stát, pokud importovaná zpráva již existuje.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Zpracování duplicit",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Nahradit duplicity",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Přeskočit duplicity",
|
||||||
"cancel": "Cancel",
|
"description": "Importovat e-mailové zprávy ze souborů .eml do složky.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# chyba} other {# chyb}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import selhal",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Vyberte jeden nebo více souborů .eml k importu.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Soubory",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# vybraný soubor} other {# vybraných souborů}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Vyberte složku, do které se mají zprávy importovat.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Cílová složka",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import dokončen",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importovat další",
|
||||||
"action_label": "Action",
|
"importing": "Importování...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} selhalo",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importováno",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} přeskočeno",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importovat # soubor} other {Importovat # souborů}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# zpráva importována} other {# zpráv importováno}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# zpráva selhala} other {# zpráv selhalo}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# zpráva importována} other {# zpráv importováno}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# zpráva přeskočena} other {# zpráv přeskočeno}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Import pošty"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Načítání...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Obnovit"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Něco se pokazilo",
|
"page_error_title": "Něco se pokazilo",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "Nepodařilo se smazat složku",
|
"toast_error_delete": "Nepodařilo se smazat složku",
|
||||||
"toast_error_delete_has_children": "Složka obsahuje podsložky. Nejprve je odstraňte.",
|
"toast_error_delete_has_children": "Složka obsahuje podsložky. Nejprve je odstraňte.",
|
||||||
"toast_error_delete_has_email": "Složka není prázdná. Nejprve ji vyprázdněte.",
|
"toast_error_delete_has_email": "Složka není prázdná. Nejprve ji vyprázdněte.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Sdílet složku..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Klávesové zkratky",
|
"title": "Klávesové zkratky",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"creating": "Vytváření...",
|
"creating": "Vytváření...",
|
||||||
"updating": "Aktualizování...",
|
"updating": "Aktualizování...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Výchozí podpis",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Mapování podpisů",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Podpis pro odpověď",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Použít globální výchozí nastavení"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Použít subadresu",
|
"button_tooltip": "Použít subadresu",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Import selhal",
|
"failed": "Import selhal",
|
||||||
"close": "Zavřít",
|
"close": "Zavřít",
|
||||||
"file_too_large": "Soubor je příliš velký (max. 5 MB)",
|
"file_too_large": "Soubor je příliš velký (max. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adresa",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Adresář",
|
||||||
"csv_back": "Back",
|
"csv_back": "Zpět",
|
||||||
"csv_city": "City",
|
"csv_city": "Město",
|
||||||
"csv_company": "Company",
|
"csv_company": "Společnost",
|
||||||
"csv_country": "Country",
|
"csv_country": "Země",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Jméno",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignorovat tento sloupec",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Pracovní pozice",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Příjmení",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Načíst vše",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Mapování sloupců",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Přezdívka",
|
||||||
"csv_note": "Note",
|
"csv_note": "Poznámka",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "PSČ",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Náhled",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Náhled ({count, plural, one {# řádek} other {# řádků}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Stát/kraj",
|
||||||
"csv_website": "Website",
|
"csv_website": "Webové stránky",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "soubory .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Exportovat kontakty",
|
"title": "Exportovat kontakty",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Má fotku"
|
"has_photo": "Má fotku"
|
||||||
},
|
},
|
||||||
"open_categories": "Otevřít kategorie",
|
"open_categories": "Otevřít kategorie",
|
||||||
"delete": "Delete Contact",
|
"delete": "Odstranit",
|
||||||
"edit": "Edit Contact",
|
"edit": "Upravit",
|
||||||
"send_email": "Send Email"
|
"send_email": "Odeslat e-mail"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalendář",
|
"title": "Kalendář",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Otevřít nabídku",
|
"nav_open_menu": "Otevřít nabídku",
|
||||||
|
"delete": "Odstranit",
|
||||||
|
"duplicate": "Duplikovat",
|
||||||
|
"edit": "Upravit",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Obsazeno",
|
||||||
"check": "Check Availability",
|
"check": "Zkontrolovat dostupnost",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Kliknutím na volný termín vyberte tento čas",
|
||||||
"loading": "Loading...",
|
"free": "Volno",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Skrýt dostupnost",
|
||||||
"timezone": "Timezone",
|
"loading": "Načítání...",
|
||||||
"free": "Free",
|
"no_participants": "Přidejte účastníky pro kontrolu dostupnosti.",
|
||||||
"busy": "Busy",
|
"tentative": "Nezávazně",
|
||||||
"tentative": "Tentative",
|
"timezone": "Časové pásmo",
|
||||||
"unavailable": "Out of office",
|
"title": "Dostupnost",
|
||||||
"unknown": "No information",
|
"unavailable": "Mimo kancelář",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Žádné informace"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Vymazat vše",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Vše",
|
||||||
"filter_all": "All",
|
"hide": "Skrýt zdroje",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Nejsou k dispozici žádné zdroje",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Odebrat {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Hledat zdroje...",
|
||||||
"type_other": "Other",
|
"title": "Zdroje",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Vybavení",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Jiné",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Místnosti",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Vozidla"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Sdílet „{name}\"",
|
|
||||||
"description": "Udělte přístup dalším uživatelům nebo skupinám na tomto serveru. Změny se projeví okamžitě.",
|
|
||||||
"no_shares": "Zatím nikomu nesdíleno.",
|
|
||||||
"add_person": "Přidat osobu nebo skupinu",
|
|
||||||
"search_placeholder": "Hledat podle jména nebo e-mailu…",
|
|
||||||
"loading_principals": "Načítání uživatelů…",
|
|
||||||
"no_principals": "Nenalezeni žádní další uživatelé ani skupiny.",
|
|
||||||
"no_match": "Žádné výsledky.",
|
|
||||||
"remove": "Odebrat přístup",
|
|
||||||
"group": "Skupina",
|
|
||||||
"share_added": "Přístup udělen",
|
|
||||||
"share_updated": "Přístup aktualizován",
|
|
||||||
"share_removed": "Přístup odebrán",
|
|
||||||
"share_failed": "Aktualizace sdílení selhala",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Pouze volno/zaneprázdněno",
|
|
||||||
"read": "Pouze čtení",
|
|
||||||
"readWrite": "Čtení a zápis",
|
|
||||||
"manager": "Správce",
|
|
||||||
"custom": "Vlastní"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Pokročilé hledání",
|
"title": "Pokročilé hledání",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Ostatní účty",
|
"other_accounts": "Ostatní účty",
|
||||||
"migration_title": "Aktualizace vašich souborů…",
|
"migration_title": "Aktualizace vašich souborů…",
|
||||||
"migration_description": "Uspořádání složek a souborů do správné struktury. Toto proběhne pouze jednou.",
|
"migration_description": "Uspořádání složek a souborů do správné struktury. Toto proběhne pouze jednou.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Odeslat jako přílohu"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Vaše certifikáty",
|
"your_certificates": "Vaše certifikáty",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "Vyhledávání není ve sjednoceném zobrazení k dispozici"
|
"search_unavailable": "Vyhledávání není ve sjednoceném zobrazení k dispozici"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Sdílet „{name}\"",
|
||||||
|
"description": "Udělte přístup dalším uživatelům nebo skupinám na tomto serveru. Změny se projeví okamžitě.",
|
||||||
|
"no_shares": "Zatím nikomu nesdíleno.",
|
||||||
|
"add_person": "Přidat osobu nebo skupinu",
|
||||||
|
"search_placeholder": "Hledat podle jména nebo e-mailu…",
|
||||||
|
"loading_principals": "Načítání uživatelů…",
|
||||||
|
"no_principals": "Nenalezeni žádní další uživatelé ani skupiny.",
|
||||||
|
"no_match": "Žádné výsledky.",
|
||||||
|
"remove": "Odebrat přístup",
|
||||||
|
"group": "Skupina",
|
||||||
|
"share_added": "Přístup udělen",
|
||||||
|
"share_updated": "Přístup aktualizován",
|
||||||
|
"share_removed": "Přístup odebrán",
|
||||||
|
"share_failed": "Aktualizace sdílení selhala",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Pouze volno/zaneprázdněno",
|
||||||
|
"read": "Pouze čtení",
|
||||||
|
"readWrite": "Čtení a zápis",
|
||||||
|
"manager": "Správce",
|
||||||
|
"custom": "Vlastní"
|
||||||
|
},
|
||||||
|
"accept": "Přijmout",
|
||||||
|
"decline": "Odmítnout",
|
||||||
|
"no_shares_by_me": "Zatím jste nic nesdíleli.",
|
||||||
|
"no_shares_with_me": "Zatím s vámi nikdo nesdílel žádné složky.",
|
||||||
|
"shared_by": "Sdílí",
|
||||||
|
"tab_shared_by_me": "Sdíleno mnou",
|
||||||
|
"tab_shared_with_me": "Sdíleno se mnou"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "Dne {date} napsal(a) {from}:",
|
"reply_line": "Dne {date} napsal(a) {from}:",
|
||||||
"forwarded_separator": "---------- Přeposlaná zpráva ----------",
|
"forwarded_separator": "---------- Přeposlaná zpráva ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Zavřít výzvu k instalaci"
|
"dismiss_aria": "Zavřít výzvu k instalaci"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Přidat podpis",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Výchozí",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Použije se pro nové zprávy, pokud není přepsáno pro danou identitu.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Výchozí podpis"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Opravdu chcete odstranit \"{name}\"? Tuto akci nelze vrátit zpět.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Odstranit podpis?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Vytvářejte a spravujte e-mailové podpisy pro psaní zpráv nebo odpovědi.",
|
||||||
},
|
"duplicate": "Duplikovat",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Upravit podpis",
|
||||||
|
"editor_label": "Podpis",
|
||||||
|
"html_preview_label": "Náhled HTML",
|
||||||
|
"name_label": "Název",
|
||||||
|
"name_placeholder": "např. Práce, Osobní",
|
||||||
|
"name_required": "Název je vyžadován",
|
||||||
|
"new_signature": "Nový podpis",
|
||||||
|
"no_signature": "Bez podpisu",
|
||||||
|
"no_signatures": "Zatím nejsou žádné podpisy",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Přepsat výchozí podpis a podpis pro odpověď pro jednotlivé identity.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Podpisy podle identity"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Náhled prostého textu",
|
||||||
"select_signature": "Select signature",
|
"reply": "Odpověď",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Použije se při odpovídání nebo přeposílání, pokud není přepsáno pro danou identitu.",
|
||||||
|
"label": "Podpis pro odpověď"
|
||||||
|
},
|
||||||
|
"show_editor": "Zobrazit editor",
|
||||||
|
"show_preview": "Zobrazit náhled",
|
||||||
|
"title": "Podpisy",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Na střed",
|
||||||
"italic": "Italic",
|
"align_left": "Zarovnat vlevo",
|
||||||
"underline": "Underline",
|
"align_right": "Zarovnat vpravo",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Tučné",
|
||||||
"link": "Link",
|
"bullet_list": "Odrážkový seznam",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Kurzíva",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Odkaz",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Číslovaný seznam",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Odebrat barvu",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Přeškrtnuté",
|
||||||
"align_center": "Align center",
|
"text_color": "Barva textu",
|
||||||
"align_left": "Align left",
|
"underline": "Podtržené"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Použít globální výchozí nastavení",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Vaše podpisy ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-170
@@ -2016,36 +2016,34 @@
|
|||||||
"managing": "Administrerer: {name}"
|
"managing": "Administrerer: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Import",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
|
||||||
"file_label": "Select Files",
|
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
|
||||||
"folder_label": "Import into Folder",
|
|
||||||
"conflict_label": "If Email Already Exists",
|
|
||||||
"start_import": "Start Import",
|
|
||||||
"importing": "Importing...",
|
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "Import successful",
|
"choose_files": "Choose files",
|
||||||
|
"conflict_copy": "Keep both",
|
||||||
|
"conflict_description": "Choose what to do when an imported message already exists.",
|
||||||
|
"conflict_label": "Duplicate handling",
|
||||||
|
"conflict_replace": "Replace duplicates",
|
||||||
|
"conflict_skip": "Skip duplicates",
|
||||||
|
"description": "Import email messages from .eml files into a folder.",
|
||||||
|
"error_details": "{count, plural, one {# error} other {# errors}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import failed",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Select one or more .eml files to import.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Files",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# file selected} other {# files selected}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Choose the folder to import messages into.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Destination folder",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import complete",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Import more",
|
||||||
"action_label": "Action",
|
"importing": "Importing...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} failed",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} imported",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} skipped",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Import # file} other {Import # files}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# message failed} other {# messages failed}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# message skipped} other {# messages skipped}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Import Mail"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
@@ -2227,8 +2225,8 @@
|
|||||||
"cancel": "Annuller",
|
"cancel": "Annuller",
|
||||||
"creating": "Opretter...",
|
"creating": "Opretter...",
|
||||||
"updating": "Opdaterer...",
|
"updating": "Opdaterer...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Default signature",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Signature mapping",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Reply signature",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Use global default"
|
||||||
},
|
},
|
||||||
@@ -2565,7 +2563,7 @@
|
|||||||
"csv_country": "Country",
|
"csv_country": "Country",
|
||||||
"csv_email": "Email",
|
"csv_email": "Email",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "First name",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignore this column",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Job title",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Last name",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Load all",
|
||||||
@@ -2575,8 +2573,8 @@
|
|||||||
"csv_phone": "Phone",
|
"csv_phone": "Phone",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Postal code",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Preview",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Preview ({count, plural, one {# row} other {# rows}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "State/Region",
|
||||||
"csv_website": "Website",
|
"csv_website": "Website",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv files"
|
||||||
},
|
},
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Har billede"
|
"has_photo": "Har billede"
|
||||||
},
|
},
|
||||||
"open_categories": "Åbn kategorier",
|
"open_categories": "Åbn kategorier",
|
||||||
"delete": "Delete Contact",
|
"delete": "Delete",
|
||||||
"edit": "Edit Contact",
|
"edit": "Edit",
|
||||||
"send_email": "Send Email"
|
"send_email": "Send email"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalender",
|
"title": "Kalender",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"overdue": "Forfalden"
|
"overdue": "Forfalden"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Åbn menu",
|
"nav_open_menu": "Åbn menu",
|
||||||
|
"delete": "Delete",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"edit": "Edit",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Busy",
|
||||||
"check": "Check Availability",
|
"check": "Check Availability",
|
||||||
|
"click_to_select": "Click a free slot to select this time",
|
||||||
|
"free": "Free",
|
||||||
"hide": "Hide Availability",
|
"hide": "Hide Availability",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"no_participants": "Add participants to check availability.",
|
"no_participants": "Add participants to check availability.",
|
||||||
"timezone": "Timezone",
|
|
||||||
"free": "Free",
|
|
||||||
"busy": "Busy",
|
|
||||||
"tentative": "Tentative",
|
"tentative": "Tentative",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"title": "Availability",
|
||||||
"unavailable": "Out of office",
|
"unavailable": "Out of office",
|
||||||
"unknown": "No information",
|
"unknown": "No information"
|
||||||
"click_to_select": "Click a free slot to select this time"
|
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Clear all",
|
||||||
"hide": "Hide resources",
|
|
||||||
"filter_all": "All",
|
"filter_all": "All",
|
||||||
"type_room": "Rooms",
|
"hide": "Hide resources",
|
||||||
"type_vehicle": "Vehicles",
|
|
||||||
"type_equipment": "Equipment",
|
|
||||||
"type_other": "Other",
|
|
||||||
"search_placeholder": "Search resources...",
|
|
||||||
"no_resources": "No resources available",
|
"no_resources": "No resources available",
|
||||||
"remove": "Remove {name}",
|
"remove": "Remove {name}",
|
||||||
"clear_all": "Clear all"
|
"search_placeholder": "Search resources...",
|
||||||
},
|
"title": "Resources",
|
||||||
"delete": "Delete Event",
|
"type_equipment": "Equipment",
|
||||||
"duplicate": "Duplicate Event",
|
"type_other": "Other",
|
||||||
"edit": "Edit Event"
|
"type_room": "Rooms",
|
||||||
|
"type_vehicle": "Vehicles"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "Del \"{name}\"",
|
"title": "Del \"{name}\"",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "Administrator",
|
"manager": "Administrator",
|
||||||
"custom": "Brugerdefineret"
|
"custom": "Brugerdefineret"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "Accept",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "Decline",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "You haven't shared anything yet.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "No folders shared with you yet.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "Shared by",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "Shared by me",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "Shared with me"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Avanceret søgning",
|
"title": "Avanceret søgning",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Afvis installationsprompt"
|
"dismiss_aria": "Afvis installationsprompt"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Add signature",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Default",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Used for new messages unless overridden per identity.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Default signature"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Delete signature?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Create and manage email signatures to use when composing or replying.",
|
||||||
},
|
"duplicate": "Duplicate",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Edit signature",
|
||||||
|
"editor_label": "Signature",
|
||||||
|
"html_preview_label": "HTML preview",
|
||||||
|
"name_label": "Name",
|
||||||
|
"name_placeholder": "e.g., Work, Personal",
|
||||||
|
"name_required": "Name is required",
|
||||||
|
"new_signature": "New signature",
|
||||||
|
"no_signature": "No signature",
|
||||||
|
"no_signatures": "No signatures yet",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Override the default and reply signature for individual identities.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Per-identity signatures"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Plain text preview",
|
||||||
"select_signature": "Select signature",
|
"reply": "Reply",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Used when replying or forwarding unless overridden per identity.",
|
||||||
|
"label": "Reply signature"
|
||||||
|
},
|
||||||
|
"show_editor": "Show editor",
|
||||||
|
"show_preview": "Show preview",
|
||||||
|
"title": "Signatures",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
|
||||||
"italic": "Italic",
|
|
||||||
"underline": "Underline",
|
|
||||||
"strikethrough": "Strikethrough",
|
|
||||||
"link": "Link",
|
|
||||||
"bullet_list": "Bullet List",
|
|
||||||
"ordered_list": "Ordered List",
|
|
||||||
"text_color": "Text Color",
|
|
||||||
"alignment": "Alignment",
|
|
||||||
"font_size": "Font Size",
|
|
||||||
"align_center": "Align center",
|
"align_center": "Align center",
|
||||||
"align_left": "Align left",
|
"align_left": "Align left",
|
||||||
"align_right": "Align right",
|
"align_right": "Align right",
|
||||||
"remove_color": "Remove color"
|
"bold": "Bold",
|
||||||
|
"bullet_list": "Bullet list",
|
||||||
|
"italic": "Italic",
|
||||||
|
"link": "Link",
|
||||||
|
"ordered_list": "Ordered list",
|
||||||
|
"remove_color": "Remove color",
|
||||||
|
"strikethrough": "Strikethrough",
|
||||||
|
"text_color": "Text color",
|
||||||
|
"underline": "Underline"
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Use global default",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Your signatures ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-240
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Kopieren fehlgeschlagen"
|
"copy_failed": "Kopieren fehlgeschlagen"
|
||||||
},
|
},
|
||||||
"send_now": "Jetzt senden",
|
"send_now": "Jetzt senden",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Termin erstellen"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Lesebestätigung angefordert (klicken zum Deaktivieren)",
|
"read_receipt_on": "Lesebestätigung angefordert (klicken zum Deaktivieren)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Größe wählen"
|
"pick_size": "Größe wählen"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Gesendet - aber das Aufräumen danach schlug fehl, evtl. bleibt ein alter Entwurf sichtbar.",
|
"send_filing_warning": "Gesendet - aber das Aufräumen danach schlug fehl, evtl. bleibt ein alter Entwurf sichtbar.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Signatur einfügen",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Keine Signatur",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Signatur auswählen"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
@@ -894,8 +894,8 @@
|
|||||||
"about_data": "Über & Daten",
|
"about_data": "Über & Daten",
|
||||||
"debug": "Debug",
|
"debug": "Debug",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"sharing": "Sharing",
|
"sharing": "Freigabe",
|
||||||
"signatures": "Signatures"
|
"signatures": "Signaturen"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Allgemein",
|
"general": "Allgemein",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Verwaltung: {name}"
|
"managing": "Verwaltung: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importieren",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Abbrechen",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Dateien auswählen",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Beide behalten",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Legen Sie fest, was geschehen soll, wenn eine importierte Nachricht bereits existiert.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Umgang mit Duplikaten",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Duplikate ersetzen",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Duplikate überspringen",
|
||||||
"cancel": "Cancel",
|
"description": "Importieren Sie E-Mail-Nachrichten aus .eml-Dateien in einen Ordner.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# Fehler} other {# Fehler}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import fehlgeschlagen",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Wählen Sie eine oder mehrere .eml-Dateien zum Importieren aus.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Dateien",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# Datei ausgewählt} other {# Dateien ausgewählt}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Wählen Sie den Ordner, in den die Nachrichten importiert werden sollen.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Zielordner",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import abgeschlossen",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Weitere importieren",
|
||||||
"action_label": "Action",
|
"importing": "Wird importiert...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} fehlgeschlagen",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importiert",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} übersprungen",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {# Datei importieren} other {# Dateien importieren}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# Nachricht importiert} other {# Nachrichten importiert}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# Nachricht fehlgeschlagen} other {# Nachrichten fehlgeschlagen}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# Nachricht importiert} other {# Nachrichten importiert}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# Nachricht übersprungen} other {# Nachrichten übersprungen}}",
|
||||||
"progress_failed": "Failed",
|
"title": "E-Mail importieren"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Lädt...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Aktualisieren"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Etwas ist schiefgelaufen",
|
"page_error_title": "Etwas ist schiefgelaufen",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "Ordner konnte nicht gelöscht werden",
|
"toast_error_delete": "Ordner konnte nicht gelöscht werden",
|
||||||
"toast_error_delete_has_children": "Ordner enthält Unterordner. Entferne diese zuerst.",
|
"toast_error_delete_has_children": "Ordner enthält Unterordner. Entferne diese zuerst.",
|
||||||
"toast_error_delete_has_email": "Ordner ist nicht leer. Leere ihn zuerst.",
|
"toast_error_delete_has_email": "Ordner ist nicht leer. Leere ihn zuerst.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Ordner freigeben..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Tastaturkürzel",
|
"title": "Tastaturkürzel",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"creating": "Wird erstellt...",
|
"creating": "Wird erstellt...",
|
||||||
"updating": "Wird aktualisiert...",
|
"updating": "Wird aktualisiert...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Standardsignatur",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Signaturzuordnung",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Antwortsignatur",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Globalen Standard verwenden"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Sub-Adresse verwenden",
|
"button_tooltip": "Sub-Adresse verwenden",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Import fehlgeschlagen",
|
"failed": "Import fehlgeschlagen",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"file_too_large": "Datei ist zu groß (max. 5 MB)",
|
"file_too_large": "Datei ist zu groß (max. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adresse",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Adressbuch",
|
||||||
"csv_back": "Back",
|
"csv_back": "Zurück",
|
||||||
"csv_city": "City",
|
"csv_city": "Stadt",
|
||||||
"csv_company": "Company",
|
"csv_company": "Firma",
|
||||||
"csv_country": "Country",
|
"csv_country": "Land",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-Mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Vorname",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Diese Spalte ignorieren",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Berufsbezeichnung",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Nachname",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Alle laden",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Spalten zuordnen",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Spitzname",
|
||||||
"csv_note": "Note",
|
"csv_note": "Notiz",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Postleitzahl",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Vorschau",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Vorschau ({count, plural, one {# Zeile} other {# Zeilen}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Bundesland/Region",
|
||||||
"csv_website": "Website",
|
"csv_website": "Webseite",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv-Dateien"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Kontakte exportieren",
|
"title": "Kontakte exportieren",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Mit Foto"
|
"has_photo": "Mit Foto"
|
||||||
},
|
},
|
||||||
"open_categories": "Kategorien öffnen",
|
"open_categories": "Kategorien öffnen",
|
||||||
"delete": "Delete Contact",
|
"delete": "Löschen",
|
||||||
"edit": "Edit Contact",
|
"edit": "Bearbeiten",
|
||||||
"send_email": "Send Email"
|
"send_email": "E-Mail senden"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalender",
|
"title": "Kalender",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Menü öffnen",
|
"nav_open_menu": "Menü öffnen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"duplicate": "Duplizieren",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Beschäftigt",
|
||||||
"check": "Check Availability",
|
"check": "Verfügbarkeit prüfen",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Klicken Sie auf einen freien Termin, um diese Zeit auszuwählen",
|
||||||
"loading": "Loading...",
|
"free": "Frei",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Verfügbarkeit ausblenden",
|
||||||
"timezone": "Timezone",
|
"loading": "Lädt...",
|
||||||
"free": "Free",
|
"no_participants": "Fügen Sie Teilnehmer hinzu, um die Verfügbarkeit zu prüfen.",
|
||||||
"busy": "Busy",
|
"tentative": "Vorläufig",
|
||||||
"tentative": "Tentative",
|
"timezone": "Zeitzone",
|
||||||
"unavailable": "Out of office",
|
"title": "Verfügbarkeit",
|
||||||
"unknown": "No information",
|
"unavailable": "Abwesend",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Keine Informationen"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Alle entfernen",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Alle",
|
||||||
"filter_all": "All",
|
"hide": "Ressourcen ausblenden",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Keine Ressourcen verfügbar",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "{name} entfernen",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Ressourcen suchen...",
|
||||||
"type_other": "Other",
|
"title": "Ressourcen",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Ausrüstung",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Sonstige",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Räume",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Fahrzeuge"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "„{name}\" freigeben",
|
|
||||||
"description": "Anderen Benutzern oder Gruppen auf diesem Server Zugriff gewähren. Änderungen werden sofort wirksam.",
|
|
||||||
"no_shares": "Noch nicht freigegeben.",
|
|
||||||
"add_person": "Person oder Gruppe hinzufügen",
|
|
||||||
"search_placeholder": "Nach Name oder E-Mail suchen…",
|
|
||||||
"loading_principals": "Benutzer werden geladen…",
|
|
||||||
"no_principals": "Keine weiteren Benutzer oder Gruppen gefunden.",
|
|
||||||
"no_match": "Keine Treffer.",
|
|
||||||
"remove": "Zugriff entfernen",
|
|
||||||
"group": "Gruppe",
|
|
||||||
"share_added": "Zugriff erteilt",
|
|
||||||
"share_updated": "Zugriff aktualisiert",
|
|
||||||
"share_removed": "Zugriff entfernt",
|
|
||||||
"share_failed": "Freigabe konnte nicht aktualisiert werden",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Nur Frei/Belegt",
|
|
||||||
"read": "Nur lesen",
|
|
||||||
"readWrite": "Lesen & schreiben",
|
|
||||||
"manager": "Verwalten",
|
|
||||||
"custom": "Benutzerdefiniert"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Erweiterte Suche",
|
"title": "Erweiterte Suche",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Andere Konten",
|
"other_accounts": "Andere Konten",
|
||||||
"migration_title": "Ihre Dateien werden aktualisiert…",
|
"migration_title": "Ihre Dateien werden aktualisiert…",
|
||||||
"migration_description": "Ordner und Dateien werden in die richtige Struktur gebracht. Dies geschieht nur einmal.",
|
"migration_description": "Ordner und Dateien werden in die richtige Struktur gebracht. Dies geschieht nur einmal.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Als Anhang senden"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Ihre Zertifikate",
|
"your_certificates": "Ihre Zertifikate",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "Die Suche ist in der vereinheitlichten Ansicht nicht verfügbar"
|
"search_unavailable": "Die Suche ist in der vereinheitlichten Ansicht nicht verfügbar"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "„{name}\" freigeben",
|
||||||
|
"description": "Anderen Benutzern oder Gruppen auf diesem Server Zugriff gewähren. Änderungen werden sofort wirksam.",
|
||||||
|
"no_shares": "Noch nicht freigegeben.",
|
||||||
|
"add_person": "Person oder Gruppe hinzufügen",
|
||||||
|
"search_placeholder": "Nach Name oder E-Mail suchen…",
|
||||||
|
"loading_principals": "Benutzer werden geladen…",
|
||||||
|
"no_principals": "Keine weiteren Benutzer oder Gruppen gefunden.",
|
||||||
|
"no_match": "Keine Treffer.",
|
||||||
|
"remove": "Zugriff entfernen",
|
||||||
|
"group": "Gruppe",
|
||||||
|
"share_added": "Zugriff erteilt",
|
||||||
|
"share_updated": "Zugriff aktualisiert",
|
||||||
|
"share_removed": "Zugriff entfernt",
|
||||||
|
"share_failed": "Freigabe konnte nicht aktualisiert werden",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Nur Frei/Belegt",
|
||||||
|
"read": "Nur lesen",
|
||||||
|
"readWrite": "Lesen & schreiben",
|
||||||
|
"manager": "Verwalten",
|
||||||
|
"custom": "Benutzerdefiniert"
|
||||||
|
},
|
||||||
|
"accept": "Annehmen",
|
||||||
|
"decline": "Ablehnen",
|
||||||
|
"no_shares_by_me": "Sie haben noch nichts freigegeben.",
|
||||||
|
"no_shares_with_me": "Es wurden Ihnen noch keine Ordner freigegeben.",
|
||||||
|
"shared_by": "Freigegeben von",
|
||||||
|
"tab_shared_by_me": "Von mir freigegeben",
|
||||||
|
"tab_shared_with_me": "Für mich freigegeben"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "Am {date} schrieb {from}:",
|
"reply_line": "Am {date} schrieb {from}:",
|
||||||
"forwarded_separator": "---------- Weitergeleitete Nachricht ----------",
|
"forwarded_separator": "---------- Weitergeleitete Nachricht ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Installationshinweis schließen"
|
"dismiss_aria": "Installationshinweis schließen"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Signatur hinzufügen",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Standard",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Wird für neue Nachrichten verwendet, sofern nicht pro Identität überschrieben.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Standardsignatur"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Möchten Sie \"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Signatur löschen?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Erstellen und verwalten Sie E-Mail-Signaturen zum Verfassen und Antworten.",
|
||||||
},
|
"duplicate": "Duplizieren",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Signatur bearbeiten",
|
||||||
|
"editor_label": "Signatur",
|
||||||
|
"html_preview_label": "HTML-Vorschau",
|
||||||
|
"name_label": "Name",
|
||||||
|
"name_placeholder": "z. B. Arbeit, Privat",
|
||||||
|
"name_required": "Name ist erforderlich",
|
||||||
|
"new_signature": "Neue Signatur",
|
||||||
|
"no_signature": "Keine Signatur",
|
||||||
|
"no_signatures": "Noch keine Signaturen vorhanden",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Überschreiben Sie die Standard- und Antwortsignatur für einzelne Identitäten.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Signaturen pro Identität"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Nur-Text-Vorschau",
|
||||||
"select_signature": "Select signature",
|
"reply": "Antwort",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Wird beim Antworten oder Weiterleiten verwendet, sofern nicht pro Identität überschrieben.",
|
||||||
|
"label": "Antwortsignatur"
|
||||||
|
},
|
||||||
|
"show_editor": "Editor anzeigen",
|
||||||
|
"show_preview": "Vorschau anzeigen",
|
||||||
|
"title": "Signaturen",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Zentriert",
|
||||||
"italic": "Italic",
|
"align_left": "Linksbündig",
|
||||||
"underline": "Underline",
|
"align_right": "Rechtsbündig",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Fett",
|
||||||
|
"bullet_list": "Aufzählung",
|
||||||
|
"italic": "Kursiv",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"bullet_list": "Bullet List",
|
"ordered_list": "Nummerierte Liste",
|
||||||
"ordered_list": "Ordered List",
|
"remove_color": "Farbe entfernen",
|
||||||
"text_color": "Text Color",
|
"strikethrough": "Durchgestrichen",
|
||||||
"alignment": "Alignment",
|
"text_color": "Textfarbe",
|
||||||
"font_size": "Font Size",
|
"underline": "Unterstrichen"
|
||||||
"align_center": "Align center",
|
|
||||||
"align_left": "Align left",
|
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Globalen Standard verwenden",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Ihre Signaturen ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-162
@@ -897,8 +897,8 @@
|
|||||||
"about_data": "About & Data",
|
"about_data": "About & Data",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"sharing": "Sharing",
|
"sharing": "Sharing",
|
||||||
"signatures": "Signatures",
|
"debug": "Debug",
|
||||||
"debug": "Debug"
|
"signatures": "Signatures"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@@ -2015,40 +2015,38 @@
|
|||||||
"label": "Preview"
|
"label": "Preview"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"loading": "Loading...",
|
||||||
|
"refresh": "Refresh",
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"title": "Import Mail",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"description": "Import email messages from .eml files into a folder.",
|
||||||
"file_label": "Select Files",
|
"file_label": "Files",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"file_description": "Select one or more .eml files to import.",
|
||||||
"folder_label": "Import into Folder",
|
"choose_files": "Choose files",
|
||||||
"conflict_label": "If Email Already Exists",
|
"files_selected": "{count, plural, one {# file selected} other {# files selected}}",
|
||||||
"start_import": "Start Import",
|
"folder_label": "Destination folder",
|
||||||
|
"folder_description": "Choose the folder to import messages into.",
|
||||||
|
"conflict_label": "Duplicate handling",
|
||||||
|
"conflict_description": "Choose what to do when an imported message already exists.",
|
||||||
|
"conflict_skip": "Skip duplicates",
|
||||||
|
"conflict_replace": "Replace duplicates",
|
||||||
|
"conflict_copy": "Keep both",
|
||||||
|
"action_label": "Import",
|
||||||
|
"start_import": "{count, plural, one {Import # file} other {Import # files}}",
|
||||||
"importing": "Importing...",
|
"importing": "Importing...",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "Import successful",
|
"progress_imported": "{count} imported",
|
||||||
|
"progress_skipped": "{count} skipped",
|
||||||
|
"progress_failed": "{count} failed",
|
||||||
|
"import_complete": "Import complete",
|
||||||
|
"summary_imported": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
|
"summary_skipped": "{count, plural, one {# message skipped} other {# messages skipped}}",
|
||||||
|
"summary_failed": "{count, plural, one {# message failed} other {# messages failed}}",
|
||||||
|
"error_details": "{count, plural, one {# error} other {# errors}}",
|
||||||
|
"import_more": "Import more",
|
||||||
"fail": "Import failed",
|
"fail": "Import failed",
|
||||||
"import_complete": "Import Complete",
|
"success": "{count, plural, one {# message imported} other {# messages imported}}"
|
||||||
"summary_imported": "{count} imported",
|
}
|
||||||
"summary_skipped": "{count} skipped",
|
|
||||||
"summary_failed": "{count} failed",
|
|
||||||
"error_details": "Error Details",
|
|
||||||
"import_more": "Import More Files",
|
|
||||||
"progress_title": "Import Progress",
|
|
||||||
"action_label": "Action",
|
|
||||||
"choose_files": "Choose Files",
|
|
||||||
"conflict_copy": "Duplicate",
|
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
|
||||||
"conflict_replace": "Replace",
|
|
||||||
"conflict_skip": "Skip",
|
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
|
||||||
"files_selected": "{count} file(s) selected",
|
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
|
||||||
"progress_failed": "Failed",
|
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
|
||||||
"loading": "Loading...",
|
|
||||||
"refresh": "Refresh"
|
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Something went wrong",
|
"page_error_title": "Something went wrong",
|
||||||
@@ -2227,8 +2225,8 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"creating": "Creating...",
|
"creating": "Creating...",
|
||||||
"updating": "Updating...",
|
"updating": "Updating...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_mapping": "Signature mapping",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_default": "Default signature",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Reply signature",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Use global default"
|
||||||
},
|
},
|
||||||
@@ -2558,27 +2556,27 @@
|
|||||||
"failed": "Import failed",
|
"failed": "Import failed",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"file_too_large": "File is too large (max 5 MB)",
|
"file_too_large": "File is too large (max 5 MB)",
|
||||||
"csv_address": "Address",
|
|
||||||
"csv_address_book": "Address book",
|
|
||||||
"csv_back": "Back",
|
|
||||||
"csv_city": "City",
|
|
||||||
"csv_company": "Company",
|
|
||||||
"csv_country": "Country",
|
|
||||||
"csv_email": "Email",
|
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "First name",
|
||||||
"csv_ignore": "Ignore",
|
|
||||||
"csv_job_title": "Job title",
|
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Last name",
|
||||||
"csv_load_all": "Load all",
|
"csv_email": "Email",
|
||||||
"csv_map_columns": "Map columns",
|
|
||||||
"csv_nickname": "Nickname",
|
|
||||||
"csv_note": "Note",
|
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Phone",
|
||||||
|
"csv_company": "Company",
|
||||||
|
"csv_job_title": "Job title",
|
||||||
|
"csv_address": "Address",
|
||||||
|
"csv_city": "City",
|
||||||
|
"csv_region": "State/Region",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Postal code",
|
||||||
"csv_preview": "Preview",
|
"csv_country": "Country",
|
||||||
"csv_preview_title": "Preview",
|
|
||||||
"csv_region": "State / Region",
|
|
||||||
"csv_website": "Website",
|
"csv_website": "Website",
|
||||||
|
"csv_note": "Note",
|
||||||
|
"csv_nickname": "Nickname",
|
||||||
|
"csv_map_columns": "Map columns",
|
||||||
|
"csv_ignore": "Ignore this column",
|
||||||
|
"csv_address_book": "Address book",
|
||||||
|
"csv_preview": "Preview",
|
||||||
|
"csv_preview_title": "Preview ({count, plural, one {# row} other {# rows}})",
|
||||||
|
"csv_back": "Back",
|
||||||
|
"csv_load_all": "Load all",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv files"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_phone": "Has phone",
|
"has_phone": "Has phone",
|
||||||
"has_photo": "Has photo"
|
"has_photo": "Has photo"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"edit": "Edit",
|
||||||
"edit": "Edit Contact",
|
"delete": "Delete",
|
||||||
"send_email": "Send Email"
|
"send_email": "Send email"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendar",
|
"title": "Calendar",
|
||||||
@@ -3087,9 +3085,9 @@
|
|||||||
"remove": "Remove {name}",
|
"remove": "Remove {name}",
|
||||||
"clear_all": "Clear all"
|
"clear_all": "Clear all"
|
||||||
},
|
},
|
||||||
"delete": "Delete Event",
|
"edit": "Edit",
|
||||||
"duplicate": "Duplicate Event",
|
"delete": "Delete",
|
||||||
"edit": "Edit Event"
|
"duplicate": "Duplicate"
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "Share \"{name}\"",
|
"title": "Share \"{name}\"",
|
||||||
@@ -3436,125 +3434,52 @@
|
|||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"title": "Signatures",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"description": "Create and manage email signatures to use when composing or replying.",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"label": "Default signature",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"description": "Used for new messages unless overridden per identity."
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"reply_signature": {
|
||||||
"label": "Default for replies",
|
"label": "Reply signature",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Used when replying or forwarding unless overridden per identity."
|
||||||
},
|
},
|
||||||
"no_signatures_available": "No signatures available",
|
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"label": "Per-identity signatures",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"description": "Override the default and reply signature for individual identities."
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"use_global_default": "Use global default",
|
||||||
"select_signature": "Select signature",
|
"default": "Default",
|
||||||
"cancel": "Cancel",
|
"reply": "Reply",
|
||||||
"save": "Save Signature",
|
"your_signatures": "Your signatures ({count})",
|
||||||
|
"add_signature": "Add signature",
|
||||||
|
"no_signatures": "No signatures yet",
|
||||||
|
"no_signature": "No signature",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"delete_title": "Delete signature?",
|
||||||
|
"delete_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
|
||||||
|
"new_signature": "New signature",
|
||||||
|
"edit_signature": "Edit signature",
|
||||||
|
"name_label": "Name",
|
||||||
|
"name_placeholder": "e.g., Work, Personal",
|
||||||
|
"name_required": "Name is required",
|
||||||
|
"editor_label": "Signature",
|
||||||
|
"show_editor": "Show editor",
|
||||||
|
"show_preview": "Show preview",
|
||||||
|
"html_preview_label": "HTML preview",
|
||||||
|
"plain_text_preview_label": "Plain text preview",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
"italic": "Italic",
|
"italic": "Italic",
|
||||||
"underline": "Underline",
|
"underline": "Underline",
|
||||||
"strikethrough": "Strikethrough",
|
"strikethrough": "Strikethrough",
|
||||||
"link": "Link",
|
"text_color": "Text color",
|
||||||
"bullet_list": "Bullet List",
|
"remove_color": "Remove color",
|
||||||
"ordered_list": "Ordered List",
|
"bullet_list": "Bullet list",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Ordered list",
|
||||||
"alignment": "Alignment",
|
|
||||||
"font_size": "Font Size",
|
|
||||||
"align_center": "Align center",
|
|
||||||
"align_left": "Align left",
|
"align_left": "Align left",
|
||||||
|
"align_center": "Align center",
|
||||||
"align_right": "Align right",
|
"align_right": "Align right",
|
||||||
"remove_color": "Remove color"
|
"link": "Link"
|
||||||
},
|
|
||||||
"default": "Default",
|
|
||||||
"no_signatures": "No signatures yet",
|
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-196
@@ -2016,36 +2016,34 @@
|
|||||||
"managing": "Gestionando: {name}"
|
"managing": "Gestionando: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Import",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
|
||||||
"file_label": "Select Files",
|
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
|
||||||
"folder_label": "Import into Folder",
|
|
||||||
"conflict_label": "If Email Already Exists",
|
|
||||||
"start_import": "Start Import",
|
|
||||||
"importing": "Importing...",
|
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "Import successful",
|
"choose_files": "Choose files",
|
||||||
|
"conflict_copy": "Keep both",
|
||||||
|
"conflict_description": "Choose what to do when an imported message already exists.",
|
||||||
|
"conflict_label": "Duplicate handling",
|
||||||
|
"conflict_replace": "Replace duplicates",
|
||||||
|
"conflict_skip": "Skip duplicates",
|
||||||
|
"description": "Import email messages from .eml files into a folder.",
|
||||||
|
"error_details": "{count, plural, one {# error} other {# errors}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import failed",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Select one or more .eml files to import.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Files",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# file selected} other {# files selected}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Choose the folder to import messages into.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Destination folder",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import complete",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Import more",
|
||||||
"action_label": "Action",
|
"importing": "Importing...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} failed",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} imported",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} skipped",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Import # file} other {Import # files}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# message failed} other {# messages failed}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# message skipped} other {# messages skipped}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Import Mail"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
@@ -2227,8 +2225,8 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"creating": "Creando...",
|
"creating": "Creando...",
|
||||||
"updating": "Actualizando...",
|
"updating": "Actualizando...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Default signature",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Signature mapping",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Reply signature",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Use global default"
|
||||||
},
|
},
|
||||||
@@ -2565,7 +2563,7 @@
|
|||||||
"csv_country": "Country",
|
"csv_country": "Country",
|
||||||
"csv_email": "Email",
|
"csv_email": "Email",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "First name",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignore this column",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Job title",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Last name",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Load all",
|
||||||
@@ -2575,8 +2573,8 @@
|
|||||||
"csv_phone": "Phone",
|
"csv_phone": "Phone",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Postal code",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Preview",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Preview ({count, plural, one {# row} other {# rows}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "State/Region",
|
||||||
"csv_website": "Website",
|
"csv_website": "Website",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv files"
|
||||||
},
|
},
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Con foto"
|
"has_photo": "Con foto"
|
||||||
},
|
},
|
||||||
"open_categories": "Abrir categorías",
|
"open_categories": "Abrir categorías",
|
||||||
"delete": "Delete Contact",
|
"delete": "Delete",
|
||||||
"edit": "Edit Contact",
|
"edit": "Edit",
|
||||||
"send_email": "Send Email"
|
"send_email": "Send email"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendario",
|
"title": "Calendario",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Abrir menú",
|
"nav_open_menu": "Abrir menú",
|
||||||
|
"delete": "Delete",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"edit": "Edit",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Busy",
|
||||||
"check": "Check Availability",
|
"check": "Check Availability",
|
||||||
|
"click_to_select": "Click a free slot to select this time",
|
||||||
|
"free": "Free",
|
||||||
"hide": "Hide Availability",
|
"hide": "Hide Availability",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"no_participants": "Add participants to check availability.",
|
"no_participants": "Add participants to check availability.",
|
||||||
"timezone": "Timezone",
|
|
||||||
"free": "Free",
|
|
||||||
"busy": "Busy",
|
|
||||||
"tentative": "Tentative",
|
"tentative": "Tentative",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"title": "Availability",
|
||||||
"unavailable": "Out of office",
|
"unavailable": "Out of office",
|
||||||
"unknown": "No information",
|
"unknown": "No information"
|
||||||
"click_to_select": "Click a free slot to select this time"
|
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Clear all",
|
||||||
"hide": "Hide resources",
|
|
||||||
"filter_all": "All",
|
"filter_all": "All",
|
||||||
"type_room": "Rooms",
|
"hide": "Hide resources",
|
||||||
"type_vehicle": "Vehicles",
|
|
||||||
"type_equipment": "Equipment",
|
|
||||||
"type_other": "Other",
|
|
||||||
"search_placeholder": "Search resources...",
|
|
||||||
"no_resources": "No resources available",
|
"no_resources": "No resources available",
|
||||||
"remove": "Remove {name}",
|
"remove": "Remove {name}",
|
||||||
"clear_all": "Clear all"
|
"search_placeholder": "Search resources...",
|
||||||
},
|
"title": "Resources",
|
||||||
"delete": "Delete Event",
|
"type_equipment": "Equipment",
|
||||||
"duplicate": "Duplicate Event",
|
"type_other": "Other",
|
||||||
"edit": "Edit Event"
|
"type_room": "Rooms",
|
||||||
},
|
"type_vehicle": "Vehicles"
|
||||||
"sharing": {
|
}
|
||||||
"title": "Compartir «{name}»",
|
|
||||||
"description": "Concede acceso a otros usuarios o grupos de este servidor. Los cambios surten efecto inmediatamente.",
|
|
||||||
"no_shares": "Aún no se ha compartido con nadie.",
|
|
||||||
"add_person": "Añadir persona o grupo",
|
|
||||||
"search_placeholder": "Buscar por nombre o correo…",
|
|
||||||
"loading_principals": "Cargando usuarios…",
|
|
||||||
"no_principals": "No se han encontrado otros usuarios ni grupos.",
|
|
||||||
"no_match": "Sin resultados.",
|
|
||||||
"remove": "Quitar acceso",
|
|
||||||
"group": "Grupo",
|
|
||||||
"share_added": "Acceso concedido",
|
|
||||||
"share_updated": "Acceso actualizado",
|
|
||||||
"share_removed": "Acceso retirado",
|
|
||||||
"share_failed": "No se pudo actualizar el uso compartido",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Solo disponibilidad",
|
|
||||||
"read": "Solo lectura",
|
|
||||||
"readWrite": "Lectura y escritura",
|
|
||||||
"manager": "Administrador",
|
|
||||||
"custom": "Personalizado"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Búsqueda avanzada",
|
"title": "Búsqueda avanzada",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "La búsqueda no está disponible en la vista unificada"
|
"search_unavailable": "La búsqueda no está disponible en la vista unificada"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Compartir «{name}»",
|
||||||
|
"description": "Concede acceso a otros usuarios o grupos de este servidor. Los cambios surten efecto inmediatamente.",
|
||||||
|
"no_shares": "Aún no se ha compartido con nadie.",
|
||||||
|
"add_person": "Añadir persona o grupo",
|
||||||
|
"search_placeholder": "Buscar por nombre o correo…",
|
||||||
|
"loading_principals": "Cargando usuarios…",
|
||||||
|
"no_principals": "No se han encontrado otros usuarios ni grupos.",
|
||||||
|
"no_match": "Sin resultados.",
|
||||||
|
"remove": "Quitar acceso",
|
||||||
|
"group": "Grupo",
|
||||||
|
"share_added": "Acceso concedido",
|
||||||
|
"share_updated": "Acceso actualizado",
|
||||||
|
"share_removed": "Acceso retirado",
|
||||||
|
"share_failed": "No se pudo actualizar el uso compartido",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Solo disponibilidad",
|
||||||
|
"read": "Solo lectura",
|
||||||
|
"readWrite": "Lectura y escritura",
|
||||||
|
"manager": "Administrador",
|
||||||
|
"custom": "Personalizado"
|
||||||
|
},
|
||||||
|
"accept": "Accept",
|
||||||
|
"decline": "Decline",
|
||||||
|
"no_shares_by_me": "You haven't shared anything yet.",
|
||||||
|
"no_shares_with_me": "No folders shared with you yet.",
|
||||||
|
"shared_by": "Shared by",
|
||||||
|
"tab_shared_by_me": "Shared by me",
|
||||||
|
"tab_shared_with_me": "Shared with me"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "El {date}, {from} escribió:",
|
"reply_line": "El {date}, {from} escribió:",
|
||||||
"forwarded_separator": "---------- Mensaje reenviado ----------",
|
"forwarded_separator": "---------- Mensaje reenviado ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Cerrar aviso de instalación"
|
"dismiss_aria": "Cerrar aviso de instalación"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Add signature",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Default",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Used for new messages unless overridden per identity.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Default signature"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Delete signature?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Create and manage email signatures to use when composing or replying.",
|
||||||
},
|
"duplicate": "Duplicate",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Edit signature",
|
||||||
|
"editor_label": "Signature",
|
||||||
|
"html_preview_label": "HTML preview",
|
||||||
|
"name_label": "Name",
|
||||||
|
"name_placeholder": "e.g., Work, Personal",
|
||||||
|
"name_required": "Name is required",
|
||||||
|
"new_signature": "New signature",
|
||||||
|
"no_signature": "No signature",
|
||||||
|
"no_signatures": "No signatures yet",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Override the default and reply signature for individual identities.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Per-identity signatures"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Plain text preview",
|
||||||
"select_signature": "Select signature",
|
"reply": "Reply",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Used when replying or forwarding unless overridden per identity.",
|
||||||
|
"label": "Reply signature"
|
||||||
|
},
|
||||||
|
"show_editor": "Show editor",
|
||||||
|
"show_preview": "Show preview",
|
||||||
|
"title": "Signatures",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
|
||||||
"italic": "Italic",
|
|
||||||
"underline": "Underline",
|
|
||||||
"strikethrough": "Strikethrough",
|
|
||||||
"link": "Link",
|
|
||||||
"bullet_list": "Bullet List",
|
|
||||||
"ordered_list": "Ordered List",
|
|
||||||
"text_color": "Text Color",
|
|
||||||
"alignment": "Alignment",
|
|
||||||
"font_size": "Font Size",
|
|
||||||
"align_center": "Align center",
|
"align_center": "Align center",
|
||||||
"align_left": "Align left",
|
"align_left": "Align left",
|
||||||
"align_right": "Align right",
|
"align_right": "Align right",
|
||||||
"remove_color": "Remove color"
|
"bold": "Bold",
|
||||||
|
"bullet_list": "Bullet list",
|
||||||
|
"italic": "Italic",
|
||||||
|
"link": "Link",
|
||||||
|
"ordered_list": "Ordered list",
|
||||||
|
"remove_color": "Remove color",
|
||||||
|
"strikethrough": "Strikethrough",
|
||||||
|
"text_color": "Text color",
|
||||||
|
"underline": "Underline"
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Use global default",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Your signatures ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-170
@@ -2016,36 +2016,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Import",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
|
||||||
"file_label": "Select Files",
|
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
|
||||||
"folder_label": "Import into Folder",
|
|
||||||
"conflict_label": "If Email Already Exists",
|
|
||||||
"start_import": "Start Import",
|
|
||||||
"importing": "Importing...",
|
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "Import successful",
|
"choose_files": "Choose files",
|
||||||
|
"conflict_copy": "Keep both",
|
||||||
|
"conflict_description": "Choose what to do when an imported message already exists.",
|
||||||
|
"conflict_label": "Duplicate handling",
|
||||||
|
"conflict_replace": "Replace duplicates",
|
||||||
|
"conflict_skip": "Skip duplicates",
|
||||||
|
"description": "Import email messages from .eml files into a folder.",
|
||||||
|
"error_details": "{count, plural, one {# error} other {# errors}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import failed",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Select one or more .eml files to import.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Files",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# file selected} other {# files selected}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Choose the folder to import messages into.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Destination folder",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import complete",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Import more",
|
||||||
"action_label": "Action",
|
"importing": "Importing...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} failed",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} imported",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} skipped",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Import # file} other {Import # files}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# message failed} other {# messages failed}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# message skipped} other {# messages skipped}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Import Mail"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
@@ -2227,8 +2225,8 @@
|
|||||||
"cancel": "انصراف",
|
"cancel": "انصراف",
|
||||||
"creating": "در حال ایجاد...",
|
"creating": "در حال ایجاد...",
|
||||||
"updating": "در حال بهروزرسانی...",
|
"updating": "در حال بهروزرسانی...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Default signature",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Signature mapping",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Reply signature",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Use global default"
|
||||||
},
|
},
|
||||||
@@ -2566,7 +2564,7 @@
|
|||||||
"csv_country": "Country",
|
"csv_country": "Country",
|
||||||
"csv_email": "Email",
|
"csv_email": "Email",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "First name",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignore this column",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Job title",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Last name",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Load all",
|
||||||
@@ -2576,8 +2574,8 @@
|
|||||||
"csv_phone": "Phone",
|
"csv_phone": "Phone",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Postal code",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Preview",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Preview ({count, plural, one {# row} other {# rows}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "State/Region",
|
||||||
"csv_website": "Website",
|
"csv_website": "Website",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv files"
|
||||||
},
|
},
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_phone": "دارای تلفن",
|
"has_phone": "دارای تلفن",
|
||||||
"has_photo": "دارای عکس"
|
"has_photo": "دارای عکس"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"delete": "Delete",
|
||||||
"edit": "Edit Contact",
|
"edit": "Edit",
|
||||||
"send_email": "Send Email"
|
"send_email": "Send email"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "تقویم",
|
"title": "تقویم",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"due_tomorrow": "فردا",
|
"due_tomorrow": "فردا",
|
||||||
"overdue": "عقبافتاده"
|
"overdue": "عقبافتاده"
|
||||||
},
|
},
|
||||||
|
"delete": "Delete",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"edit": "Edit",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Busy",
|
||||||
"check": "Check Availability",
|
"check": "Check Availability",
|
||||||
|
"click_to_select": "Click a free slot to select this time",
|
||||||
|
"free": "Free",
|
||||||
"hide": "Hide Availability",
|
"hide": "Hide Availability",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"no_participants": "Add participants to check availability.",
|
"no_participants": "Add participants to check availability.",
|
||||||
"timezone": "Timezone",
|
|
||||||
"free": "Free",
|
|
||||||
"busy": "Busy",
|
|
||||||
"tentative": "Tentative",
|
"tentative": "Tentative",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"title": "Availability",
|
||||||
"unavailable": "Out of office",
|
"unavailable": "Out of office",
|
||||||
"unknown": "No information",
|
"unknown": "No information"
|
||||||
"click_to_select": "Click a free slot to select this time"
|
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Clear all",
|
||||||
"hide": "Hide resources",
|
|
||||||
"filter_all": "All",
|
"filter_all": "All",
|
||||||
"type_room": "Rooms",
|
"hide": "Hide resources",
|
||||||
"type_vehicle": "Vehicles",
|
|
||||||
"type_equipment": "Equipment",
|
|
||||||
"type_other": "Other",
|
|
||||||
"search_placeholder": "Search resources...",
|
|
||||||
"no_resources": "No resources available",
|
"no_resources": "No resources available",
|
||||||
"remove": "Remove {name}",
|
"remove": "Remove {name}",
|
||||||
"clear_all": "Clear all"
|
"search_placeholder": "Search resources...",
|
||||||
},
|
"title": "Resources",
|
||||||
"delete": "Delete Event",
|
"type_equipment": "Equipment",
|
||||||
"duplicate": "Duplicate Event",
|
"type_other": "Other",
|
||||||
"edit": "Edit Event"
|
"type_room": "Rooms",
|
||||||
|
"type_vehicle": "Vehicles"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "اشتراکگذاری \"{name}\"",
|
"title": "اشتراکگذاری \"{name}\"",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "مدیر",
|
"manager": "مدیر",
|
||||||
"custom": "سفارشی"
|
"custom": "سفارشی"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "Accept",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "Decline",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "You haven't shared anything yet.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "No folders shared with you yet.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "Shared by",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "Shared by me",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "Shared with me"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "جستجوی پیشرفته",
|
"title": "جستجوی پیشرفته",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "رد کردن پیشنهاد نصب"
|
"dismiss_aria": "رد کردن پیشنهاد نصب"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Add signature",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Default",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Used for new messages unless overridden per identity.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Default signature"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Delete signature?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Create and manage email signatures to use when composing or replying.",
|
||||||
},
|
"duplicate": "Duplicate",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Edit signature",
|
||||||
|
"editor_label": "Signature",
|
||||||
|
"html_preview_label": "HTML preview",
|
||||||
|
"name_label": "Name",
|
||||||
|
"name_placeholder": "e.g., Work, Personal",
|
||||||
|
"name_required": "Name is required",
|
||||||
|
"new_signature": "New signature",
|
||||||
|
"no_signature": "No signature",
|
||||||
|
"no_signatures": "No signatures yet",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Override the default and reply signature for individual identities.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Per-identity signatures"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Plain text preview",
|
||||||
"select_signature": "Select signature",
|
"reply": "Reply",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Used when replying or forwarding unless overridden per identity.",
|
||||||
|
"label": "Reply signature"
|
||||||
|
},
|
||||||
|
"show_editor": "Show editor",
|
||||||
|
"show_preview": "Show preview",
|
||||||
|
"title": "Signatures",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
|
||||||
"italic": "Italic",
|
|
||||||
"underline": "Underline",
|
|
||||||
"strikethrough": "Strikethrough",
|
|
||||||
"link": "Link",
|
|
||||||
"bullet_list": "Bullet List",
|
|
||||||
"ordered_list": "Ordered List",
|
|
||||||
"text_color": "Text Color",
|
|
||||||
"alignment": "Alignment",
|
|
||||||
"font_size": "Font Size",
|
|
||||||
"align_center": "Align center",
|
"align_center": "Align center",
|
||||||
"align_left": "Align left",
|
"align_left": "Align left",
|
||||||
"align_right": "Align right",
|
"align_right": "Align right",
|
||||||
"remove_color": "Remove color"
|
"bold": "Bold",
|
||||||
|
"bullet_list": "Bullet list",
|
||||||
|
"italic": "Italic",
|
||||||
|
"link": "Link",
|
||||||
|
"ordered_list": "Ordered list",
|
||||||
|
"remove_color": "Remove color",
|
||||||
|
"strikethrough": "Strikethrough",
|
||||||
|
"text_color": "Text color",
|
||||||
|
"underline": "Underline"
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Use global default",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Your signatures ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-240
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Échec de la copie"
|
"copy_failed": "Échec de la copie"
|
||||||
},
|
},
|
||||||
"send_now": "Envoyer maintenant",
|
"send_now": "Envoyer maintenant",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Créer un rendez-vous"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Accusé de lecture demandé (cliquez pour désactiver)",
|
"read_receipt_on": "Accusé de lecture demandé (cliquez pour désactiver)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Choisir la taille"
|
"pick_size": "Choisir la taille"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Envoyé - mais le nettoyage après envoi a échoué, un ancien brouillon peut subsister.",
|
"send_filing_warning": "Envoyé - mais le nettoyage après envoi a échoué, un ancien brouillon peut subsister.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Insérer une signature",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Aucune signature",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Sélectionner une signature"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Confirmer",
|
"confirm": "Confirmer",
|
||||||
@@ -893,8 +893,8 @@
|
|||||||
"content_senders": "Contenu et expéditeurs",
|
"content_senders": "Contenu et expéditeurs",
|
||||||
"about_data": "À propos et données",
|
"about_data": "À propos et données",
|
||||||
"debug": "Débogage",
|
"debug": "Débogage",
|
||||||
"import": "Import",
|
"import": "Importation",
|
||||||
"sharing": "Sharing",
|
"sharing": "Partage",
|
||||||
"signatures": "Signatures"
|
"signatures": "Signatures"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Gestion : {name}"
|
"managing": "Gestion : {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importer",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Annuler",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Choisir des fichiers",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Conserver les deux",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Choisissez l'action à effectuer lorsqu'un message importé existe déjà.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Gestion des doublons",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Remplacer les doublons",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Ignorer les doublons",
|
||||||
"cancel": "Cancel",
|
"description": "Importez des messages e-mail à partir de fichiers .eml vers un dossier.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# erreur} other {# erreurs}}",
|
||||||
"fail": "Import failed",
|
"fail": "Échec de l'importation",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Sélectionnez un ou plusieurs fichiers .eml à importer.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Fichiers",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# fichier sélectionné} other {# fichiers sélectionnés}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Choisissez le dossier dans lequel importer les messages.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Dossier de destination",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Importation terminée",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importer d'autres fichiers",
|
||||||
"action_label": "Action",
|
"importing": "Importation en cours...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} échoués",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importés",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} ignorés",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importer # fichier} other {Importer # fichiers}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# message importé} other {# messages importés}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# message en échec} other {# messages en échec}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# message importé} other {# messages importés}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# message ignoré} other {# messages ignorés}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Importation de courrier"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Chargement...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Actualiser"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Une erreur s'est produite",
|
"page_error_title": "Une erreur s'est produite",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Nom du dossier",
|
"placeholder_folder_name": "Nom du dossier",
|
||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
"rename_confirm": "Renommer",
|
"rename_confirm": "Renommer",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Partager le dossier..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Raccourcis clavier",
|
"title": "Raccourcis clavier",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"creating": "Création...",
|
"creating": "Création...",
|
||||||
"updating": "Mise à jour...",
|
"updating": "Mise à jour...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Signature par défaut",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Association de signatures",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Signature de réponse",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Utiliser la valeur par défaut globale"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Utiliser le sous-adressage",
|
"button_tooltip": "Utiliser le sous-adressage",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Échec de l'importation",
|
"failed": "Échec de l'importation",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"file_too_large": "Fichier trop volumineux (max 5 Mo)",
|
"file_too_large": "Fichier trop volumineux (max 5 Mo)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adresse",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Carnet d'adresses",
|
||||||
"csv_back": "Back",
|
"csv_back": "Retour",
|
||||||
"csv_city": "City",
|
"csv_city": "Ville",
|
||||||
"csv_company": "Company",
|
"csv_company": "Société",
|
||||||
"csv_country": "Country",
|
"csv_country": "Pays",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Prénom",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignorer cette colonne",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Fonction",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Nom",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Tout charger",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Associer les colonnes",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Surnom",
|
||||||
"csv_note": "Note",
|
"csv_note": "Note",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Téléphone",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Code postal",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Aperçu",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Aperçu ({count, plural, one {# ligne} other {# lignes}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "État/Région",
|
||||||
"csv_website": "Website",
|
"csv_website": "Site web",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "fichiers .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Exporter les contacts",
|
"title": "Exporter les contacts",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Avec photo"
|
"has_photo": "Avec photo"
|
||||||
},
|
},
|
||||||
"open_categories": "Ouvrir les catégories",
|
"open_categories": "Ouvrir les catégories",
|
||||||
"delete": "Delete Contact",
|
"delete": "Supprimer",
|
||||||
"edit": "Edit Contact",
|
"edit": "Modifier",
|
||||||
"send_email": "Send Email"
|
"send_email": "Envoyer un e-mail"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendrier",
|
"title": "Calendrier",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"overdue": "En retard"
|
"overdue": "En retard"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Ouvrir le menu",
|
"nav_open_menu": "Ouvrir le menu",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"duplicate": "Dupliquer",
|
||||||
|
"edit": "Modifier",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Occupé",
|
||||||
"check": "Check Availability",
|
"check": "Vérifier la disponibilité",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Cliquez sur un créneau libre pour sélectionner cette heure",
|
||||||
"loading": "Loading...",
|
"free": "Libre",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Masquer la disponibilité",
|
||||||
"timezone": "Timezone",
|
"loading": "Chargement...",
|
||||||
"free": "Free",
|
"no_participants": "Ajoutez des participants pour vérifier la disponibilité.",
|
||||||
"busy": "Busy",
|
"tentative": "Provisoire",
|
||||||
"tentative": "Tentative",
|
"timezone": "Fuseau horaire",
|
||||||
"unavailable": "Out of office",
|
"title": "Disponibilité",
|
||||||
"unknown": "No information",
|
"unavailable": "Absent du bureau",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Aucune information"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Tout effacer",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Toutes",
|
||||||
"filter_all": "All",
|
"hide": "Masquer les ressources",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Aucune ressource disponible",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Retirer {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Rechercher des ressources...",
|
||||||
"type_other": "Other",
|
"title": "Ressources",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Équipement",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Autre",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Salles",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Véhicules"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Partager « {name} »",
|
|
||||||
"description": "Accordez l'accès à d'autres utilisateurs ou groupes de ce serveur. Les modifications sont immédiates.",
|
|
||||||
"no_shares": "Pas encore partagé.",
|
|
||||||
"add_person": "Ajouter une personne ou un groupe",
|
|
||||||
"search_placeholder": "Rechercher par nom ou e-mail…",
|
|
||||||
"loading_principals": "Chargement des utilisateurs…",
|
|
||||||
"no_principals": "Aucun autre utilisateur ou groupe trouvé.",
|
|
||||||
"no_match": "Aucun résultat.",
|
|
||||||
"remove": "Révoquer l'accès",
|
|
||||||
"group": "Groupe",
|
|
||||||
"share_added": "Accès accordé",
|
|
||||||
"share_updated": "Accès mis à jour",
|
|
||||||
"share_removed": "Accès révoqué",
|
|
||||||
"share_failed": "Échec de la mise à jour du partage",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Disponibilité uniquement",
|
|
||||||
"read": "Lecture seule",
|
|
||||||
"readWrite": "Lecture & écriture",
|
|
||||||
"manager": "Gestionnaire",
|
|
||||||
"custom": "Personnalisé"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Recherche avancée",
|
"title": "Recherche avancée",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Autres comptes",
|
"other_accounts": "Autres comptes",
|
||||||
"migration_title": "Mise à jour de vos fichiers…",
|
"migration_title": "Mise à jour de vos fichiers…",
|
||||||
"migration_description": "Organisation des dossiers et fichiers dans leur structure appropriée. Cela ne se produit qu'une seule fois.",
|
"migration_description": "Organisation des dossiers et fichiers dans leur structure appropriée. Cela ne se produit qu'une seule fois.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Envoyer en pièce jointe"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Vos certificats",
|
"your_certificates": "Vos certificats",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "La recherche n'est pas disponible dans la vue unifiée"
|
"search_unavailable": "La recherche n'est pas disponible dans la vue unifiée"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Partager « {name} »",
|
||||||
|
"description": "Accordez l'accès à d'autres utilisateurs ou groupes de ce serveur. Les modifications sont immédiates.",
|
||||||
|
"no_shares": "Pas encore partagé.",
|
||||||
|
"add_person": "Ajouter une personne ou un groupe",
|
||||||
|
"search_placeholder": "Rechercher par nom ou e-mail…",
|
||||||
|
"loading_principals": "Chargement des utilisateurs…",
|
||||||
|
"no_principals": "Aucun autre utilisateur ou groupe trouvé.",
|
||||||
|
"no_match": "Aucun résultat.",
|
||||||
|
"remove": "Révoquer l'accès",
|
||||||
|
"group": "Groupe",
|
||||||
|
"share_added": "Accès accordé",
|
||||||
|
"share_updated": "Accès mis à jour",
|
||||||
|
"share_removed": "Accès révoqué",
|
||||||
|
"share_failed": "Échec de la mise à jour du partage",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Disponibilité uniquement",
|
||||||
|
"read": "Lecture seule",
|
||||||
|
"readWrite": "Lecture & écriture",
|
||||||
|
"manager": "Gestionnaire",
|
||||||
|
"custom": "Personnalisé"
|
||||||
|
},
|
||||||
|
"accept": "Accepter",
|
||||||
|
"decline": "Refuser",
|
||||||
|
"no_shares_by_me": "Vous n'avez encore rien partagé.",
|
||||||
|
"no_shares_with_me": "Aucun dossier n'a encore été partagé avec vous.",
|
||||||
|
"shared_by": "Partagé par",
|
||||||
|
"tab_shared_by_me": "Partagé par moi",
|
||||||
|
"tab_shared_with_me": "Partagé avec moi"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "Le {date}, {from} a écrit :",
|
"reply_line": "Le {date}, {from} a écrit :",
|
||||||
"forwarded_separator": "---------- Message transféré ----------",
|
"forwarded_separator": "---------- Message transféré ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Fermer l'invite d'installation"
|
"dismiss_aria": "Fermer l'invite d'installation"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Ajouter une signature",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Par défaut",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Utilisée pour les nouveaux messages, sauf si remplacée par identité.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Signature par défaut"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Êtes-vous sûr de vouloir supprimer \"{name}\" ? Cette action est irréversible.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Supprimer la signature ?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Créez et gérez des signatures e-mail à utiliser lors de la rédaction ou de la réponse.",
|
||||||
},
|
"duplicate": "Dupliquer",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Modifier la signature",
|
||||||
|
"editor_label": "Signature",
|
||||||
|
"html_preview_label": "Aperçu HTML",
|
||||||
|
"name_label": "Nom",
|
||||||
|
"name_placeholder": "p. ex. Travail, Personnel",
|
||||||
|
"name_required": "Le nom est requis",
|
||||||
|
"new_signature": "Nouvelle signature",
|
||||||
|
"no_signature": "Aucune signature",
|
||||||
|
"no_signatures": "Aucune signature pour le moment",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Remplacez la signature par défaut et de réponse pour des identités individuelles.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Signatures par identité"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Aperçu en texte brut",
|
||||||
"select_signature": "Select signature",
|
"reply": "Réponse",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Utilisée lors d'une réponse ou d'un transfert, sauf si remplacée par identité.",
|
||||||
|
"label": "Signature de réponse"
|
||||||
|
},
|
||||||
|
"show_editor": "Afficher l'éditeur",
|
||||||
|
"show_preview": "Afficher l'aperçu",
|
||||||
|
"title": "Signatures",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Centrer",
|
||||||
"italic": "Italic",
|
"align_left": "Aligner à gauche",
|
||||||
"underline": "Underline",
|
"align_right": "Aligner à droite",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Gras",
|
||||||
"link": "Link",
|
"bullet_list": "Liste à puces",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Italique",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Lien",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Liste numérotée",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Supprimer la couleur",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Barré",
|
||||||
"align_center": "Align center",
|
"text_color": "Couleur du texte",
|
||||||
"align_left": "Align left",
|
"underline": "Souligné"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Utiliser la valeur par défaut globale",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Vos signatures ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+240
-315
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"meta_description": "לקוח webmail מינימליסטי באמצעות פרוטוקול JMAP",
|
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "דוא״ל",
|
"username_label": "דוא״ל",
|
||||||
@@ -144,40 +143,6 @@
|
|||||||
"remove_account": "הסרת חשבון",
|
"remove_account": "הסרת חשבון",
|
||||||
"remove_account_confirm": "להסיר את {account} מהמכשיר הזה? ניתן להוסיף אותו שוב מאוחר יותר."
|
"remove_account_confirm": "להסיר את {account} מהמכשיר הזה? ניתן להוסיף אותו שוב מאוחר יותר."
|
||||||
},
|
},
|
||||||
"protocol_handlers": {
|
|
||||||
"title": "יישומים ברירת מחדל",
|
|
||||||
"description": "בחר אם קישורי דוא״ל ולוח שנה ייפתחו ב־VNCmail+. מבחינה טכנית, VNCmail+ רשום כטיפול בפרוטוקול עבור קישורי mailto: ו־webcal:.",
|
|
||||||
"unsupported": "דפדפן זה או חיבור זה לא תומך בהרשמה ידנית של טיפול בפרוטוקול. ייתכן שעדיין תוכל להשתמש ב־PWA המותקנת דרך הדפדפן או הגדרות מערכת ההפעלה.",
|
|
||||||
"mailto_label": "קישורי דוא״ל",
|
|
||||||
"mailto_description": "פתח קישורי mailto: ב־VNCmail+ עם מחבר שמלא מראש.",
|
|
||||||
"protocol_open_mode_label": "בעת פתיחת קישורי פרוטוקול",
|
|
||||||
"protocol_open_mode_description": "בחר אם VNCmail+ יפתח קישורי mailto: ו־webcal: בלשונית חדשה או ישתמש בחיבור פתוח. אפשרות הישיבה הפעילה דורשת הרשאת עדכון כדי שתוכל ללחוץ על עדכון נופל כדי להביא את VNCmail+ לחזית אם הדפדפן חוסם מיקוד.",
|
|
||||||
"protocol_open_mode_active_session": "פתח בישיבה פעילה אם אפשר",
|
|
||||||
"protocol_open_mode_new_tab": "פתח תמיד לשונית חדשה",
|
|
||||||
"focus_notification_title": "פתח את VNCmail+",
|
|
||||||
"focus_notification_body": "הקישור נפתח ב־VNCmail+. לחץ כדי להביא את החלון לחזית.",
|
|
||||||
"webcal_label": "קישורי לוח שנה",
|
|
||||||
"webcal_description": "פתח קישורי webcal: ב־VNCmail+ עם תיבת דו־שיח מלא מראש להרשמה ללוח שנה.",
|
|
||||||
"register_mailto": "רשום יישום דוא״ל",
|
|
||||||
"register_webcal": "רשום יישום לוח שנה",
|
|
||||||
"mailto_registered": "בקש הרשמה של מטפל דוא״ל",
|
|
||||||
"webcal_registered": "בקש הרשמה של מטפל לוח שנה",
|
|
||||||
"registration_failed": "הרשמת טיפול בפרוטוקול נכשלה",
|
|
||||||
"opening_mailto": "פתיחת מחבר…",
|
|
||||||
"opening_webcal": "פתיחת לוח שנה…",
|
|
||||||
"browser_note": "הדפדפן או מערכת ההפעלה שלך עשויים לבקש ממך לאשר זאת ועשויים לדרוש שVNCmail+ יותקן לפני שניתן לבחור אותו כיישום ברירת המחדל.",
|
|
||||||
"select_account_title": "בחר חשבון",
|
|
||||||
"select_mailto_account": "בחר איזה חשבון צריך לפתוח קישור דוא״ל זה.",
|
|
||||||
"select_webcal_account": "בחר איזה חשבון צריך לפתוח קישור לוח שנה זה.",
|
|
||||||
"select_account_note": "זה חל רק על קישור פרוטוקול זה.",
|
|
||||||
"detail_to": "אל",
|
|
||||||
"detail_subject": "נושא",
|
|
||||||
"detail_no_subject": "אין נושא",
|
|
||||||
"detail_calendar": "לוח שנה",
|
|
||||||
"detail_source": "מקור",
|
|
||||||
"active_account": "פעיל",
|
|
||||||
"switching_account": "החלפת חשבון…"
|
|
||||||
},
|
|
||||||
"sidebar_apps": {
|
"sidebar_apps": {
|
||||||
"modal_title": "אפליקציות בסרגל הצד",
|
"modal_title": "אפליקציות בסרגל הצד",
|
||||||
"add_new": "הוסף אפליקציה",
|
"add_new": "הוסף אפליקציה",
|
||||||
@@ -568,7 +533,7 @@
|
|||||||
"copy_failed": "העתקה נכשלה"
|
"copy_failed": "העתקה נכשלה"
|
||||||
},
|
},
|
||||||
"send_now": "שלח עכשיו",
|
"send_now": "שלח עכשיו",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "צור פגישה"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"new_message": "הודעה חדשה",
|
"new_message": "הודעה חדשה",
|
||||||
@@ -714,9 +679,9 @@
|
|||||||
"pick_size": "בחירת גודל"
|
"pick_size": "בחירת גודל"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "נשלח - אך הניקוי שלאחר השליחה נכשל, ייתכן שתישאר טיוטה ישנה.",
|
"send_filing_warning": "נשלח - אך הניקוי שלאחר השליחה נכשל, ייתכן שתישאר טיוטה ישנה.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "הוסף חתימה",
|
||||||
"no_signature": "No signature",
|
"no_signature": "ללא חתימה",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "בחר חתימה"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "אשר",
|
"confirm": "אשר",
|
||||||
@@ -893,9 +858,9 @@
|
|||||||
"content_senders": "תוכן ושולחים",
|
"content_senders": "תוכן ושולחים",
|
||||||
"about_data": "בערך וגדול",
|
"about_data": "בערך וגדול",
|
||||||
"debug": "ניפוי שגיאות",
|
"debug": "ניפוי שגיאות",
|
||||||
"import": "Import",
|
"import": "ייבוא",
|
||||||
"sharing": "Sharing",
|
"sharing": "שיתוף",
|
||||||
"signatures": "Signatures"
|
"signatures": "חתימות"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "כללי",
|
"general": "כללי",
|
||||||
@@ -2017,39 +1982,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "ייבוא",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "ביטול",
|
||||||
"file_label": "Select Files",
|
"choose_files": "בחר קבצים",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "שמור את שניהם",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "בחר מה לעשות כאשר הודעה מיובאת כבר קיימת.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "טיפול בכפילויות",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "החלף כפילויות",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "דלג על כפילויות",
|
||||||
"cancel": "Cancel",
|
"description": "ייבא הודעות דוא״ל מקבצי .eml לתוך תיקייה.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {שגיאה אחת} other {# שגיאות}}",
|
||||||
"fail": "Import failed",
|
"fail": "הייבוא נכשל",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "בחר קובץ .eml אחד או יותר לייבוא.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "קבצים",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {קובץ אחד נבחר} other {# קבצים נבחרו}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "בחר את התיקייה לייבוא ההודעות אליה.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "תיקיית יעד",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "הייבוא הושלם",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "ייבא עוד",
|
||||||
"action_label": "Action",
|
"importing": "מייבא...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} נכשלו",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} יובאו",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} דולגו",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {ייבא קובץ אחד} other {ייבא # קבצים}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {הודעה אחת יובאה} other {# הודעות יובאו}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {הודעה אחת נכשלה} other {# הודעות נכשלו}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {הודעה אחת יובאה} other {# הודעות יובאו}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {הודעה אחת דולגה} other {# הודעות דולגו}}",
|
||||||
"progress_failed": "Failed",
|
"title": "ייבוא דואר"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "טוען...",
|
||||||
"refresh": "Refresh"
|
"refresh": "רענן"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "משהו השתבש",
|
"page_error_title": "משהו השתבש",
|
||||||
@@ -2091,45 +2054,6 @@
|
|||||||
"cancel_and_edit": "בטל וערוך",
|
"cancel_and_edit": "בטל וערוך",
|
||||||
"cancel_and_compose_again": "בטל והרכיב שוב"
|
"cancel_and_compose_again": "בטל והרכיב שוב"
|
||||||
},
|
},
|
||||||
"mailbox_context_menu": {
|
|
||||||
"mark_folder_read": "סמן תיקייה כקרויה",
|
|
||||||
"mark_folder_tree_read": "סמן תיקייה ותת־תיקיות כקרויות",
|
|
||||||
"mark_all_folders_read": "סמן את כל התיקיות כקרויות",
|
|
||||||
"new_subfolder": "תת־תיקייה חדשה…",
|
|
||||||
"new_folder": "תיקייה חדשה…",
|
|
||||||
"rename": "שנה שם…",
|
|
||||||
"import_email": "ייבא .eml או .zip…",
|
|
||||||
"empty_folder": "תיקייה ריקה",
|
|
||||||
"empty_folder_generic": "תיקייה ריקה",
|
|
||||||
"delete_folder": "מחק תיקייה",
|
|
||||||
"refresh": "רענן",
|
|
||||||
"mark_all_confirm_title": "סמן את כל התיקיות כקרויות",
|
|
||||||
"mark_all_confirm_message": "סמן כל הודעה שלא קרויה בחשבון האישי שלך כקרויה?",
|
|
||||||
"delete_confirm_title": "מחק תיקייה",
|
|
||||||
"delete_confirm_message": "מחק לצמיתות את התיקייה \"{name}\"? לא ניתן לבטל פעולה זו.",
|
|
||||||
"prompt_new_subfolder": "הזן שם לתת־תיקייה החדשה.",
|
|
||||||
"prompt_new_folder": "הזן שם לתיקייה החדשה.",
|
|
||||||
"prompt_rename": "הזן שם חדש לתיקייה זו.",
|
|
||||||
"placeholder_folder_name": "שם תיקייה",
|
|
||||||
"create": "צור",
|
|
||||||
"rename_confirm": "שנה שם",
|
|
||||||
"toast_marked_read": "התיקייה סומנה כקרויה",
|
|
||||||
"toast_marked_read_count": "סומנו {count, plural, one {הודעה 1} other {# הודעות}} כקרויות",
|
|
||||||
"toast_already_read": "אין הודעות שלא קרויות",
|
|
||||||
"toast_marked_all_read": "כל התיקיות סומנו כקרויות",
|
|
||||||
"toast_emptied": "התיקייה התרוקנה",
|
|
||||||
"toast_folder_created": "תיקייה נוצרה",
|
|
||||||
"toast_folder_renamed": "שם התיקייה שונה",
|
|
||||||
"toast_folder_deleted": "התיקייה נמחקה",
|
|
||||||
"toast_error_mark_read": "נכשל בסימון כקרויה",
|
|
||||||
"toast_error_empty": "נכשל בתרוקנון תיקייה",
|
|
||||||
"toast_error_create": "נכשל ביצירת תיקייה",
|
|
||||||
"toast_error_rename": "נכשל בשינוי שם תיקייה",
|
|
||||||
"toast_error_delete": "נכשל במחיקת תיקייה",
|
|
||||||
"toast_error_delete_has_children": "לתיקייה יש תת־תיקיות. הסר אותן קודם.",
|
|
||||||
"toast_error_delete_has_email": "התיקייה אינה ריקה. תרוקנה קודם.",
|
|
||||||
"share_folder": "Share Folder..."
|
|
||||||
},
|
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "קיצורי מקלדת",
|
"title": "קיצורי מקלדת",
|
||||||
"tip": "לחץ על? בכל עת כדי להראות את העזרה הזו",
|
"tip": "לחץ על? בכל עת כדי להראות את העזרה הזו",
|
||||||
@@ -2228,10 +2152,10 @@
|
|||||||
"updating": "מעדכן...",
|
"updating": "מעדכן...",
|
||||||
"signature_byte_counter": "{bytes} / {max} בתים",
|
"signature_byte_counter": "{bytes} / {max} בתים",
|
||||||
"signature_byte_limit_reached": "הגבול של השרת הושג",
|
"signature_byte_limit_reached": "הגבול של השרת הושג",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "חתימת ברירת מחדל",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "מיפוי חתימות",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "חתימת תשובה",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "השתמש בברירת המחדל הגלובלית"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "השתמש בכתובת משנה",
|
"button_tooltip": "השתמש בכתובת משנה",
|
||||||
@@ -2544,28 +2468,28 @@
|
|||||||
"failed": "הייבוא נכשל",
|
"failed": "הייבוא נכשל",
|
||||||
"close": "לִסְגוֹר",
|
"close": "לִסְגוֹר",
|
||||||
"file_too_large": "הקובץ גדול מדי (מקסימום 5MB)",
|
"file_too_large": "הקובץ גדול מדי (מקסימום 5MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "כתובת",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "ספר כתובות",
|
||||||
"csv_back": "Back",
|
"csv_back": "חזרה",
|
||||||
"csv_city": "City",
|
"csv_city": "עיר",
|
||||||
"csv_company": "Company",
|
"csv_company": "חברה",
|
||||||
"csv_country": "Country",
|
"csv_country": "מדינה",
|
||||||
"csv_email": "Email",
|
"csv_email": "דוא״ל",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "שם פרטי",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "התעלם מעמודה זו",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "תפקיד עבודה",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "שם משפחה",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "טען הכל",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "מיפוי עמודות",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "כינוי",
|
||||||
"csv_note": "Note",
|
"csv_note": "הערה",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "טלפון",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "מיקוד",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "תצוגה מקדימה",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "תצוגה מקדימה ({count, plural, one {שורה אחת} other {# שורות}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "מדינה/אזור",
|
||||||
"csv_website": "Website",
|
"csv_website": "אתר",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "קבצי .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "ייצוא אנשי קשר",
|
"title": "ייצוא אנשי קשר",
|
||||||
@@ -2639,9 +2563,9 @@
|
|||||||
"has_phone": "יש טלפון",
|
"has_phone": "יש טלפון",
|
||||||
"has_photo": "יש תמונה"
|
"has_photo": "יש תמונה"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"delete": "מחק",
|
||||||
"edit": "Edit Contact",
|
"edit": "ערוך",
|
||||||
"send_email": "Send Email"
|
"send_email": "שלח דוא״ל"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "לוח שנה",
|
"title": "לוח שנה",
|
||||||
@@ -3061,66 +2985,36 @@
|
|||||||
"subscribe_description": "שמור את הלוח שנה הזה בסנכרון אוטומטי כלוח שנה נפרד.",
|
"subscribe_description": "שמור את הלוח שנה הזה בסנכרון אוטומטי כלוח שנה נפרד.",
|
||||||
"cancel": "בטל"
|
"cancel": "בטל"
|
||||||
},
|
},
|
||||||
|
"delete": "מחק",
|
||||||
|
"duplicate": "שכפל",
|
||||||
|
"edit": "ערוך",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "תפוס",
|
||||||
"check": "Check Availability",
|
"check": "בדוק זמינות",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "לחץ על משבצת פנויה כדי לבחור את השעה הזו",
|
||||||
"loading": "Loading...",
|
"free": "חופשי",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "הסתר זמינות",
|
||||||
"timezone": "Timezone",
|
"loading": "טוען...",
|
||||||
"free": "Free",
|
"no_participants": "הוסף משתתפים כדי לבדוק זמינות.",
|
||||||
"busy": "Busy",
|
"tentative": "טנטטיבי",
|
||||||
"tentative": "Tentative",
|
"timezone": "אזור זמן",
|
||||||
"unavailable": "Out of office",
|
"title": "זמינות",
|
||||||
"unknown": "No information",
|
"unavailable": "מחוץ למשרד",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "אין מידע"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "נקה הכל",
|
||||||
"hide": "Hide resources",
|
"filter_all": "הכל",
|
||||||
"filter_all": "All",
|
"hide": "הסתר משאבים",
|
||||||
"type_room": "Rooms",
|
"no_resources": "אין משאבים זמינים",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "הסר {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "חיפוש משאבים...",
|
||||||
"type_other": "Other",
|
"title": "משאבים",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "ציוד",
|
||||||
"no_resources": "No resources available",
|
"type_other": "אחר",
|
||||||
"remove": "Remove {name}",
|
"type_room": "חדרים",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "כלי רכב"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "שתף \"{name}\"",
|
|
||||||
"description": "הענק גישה למשתמשים או קבוצות אחרות בשרת זה. השינויים יופעלו מיד.",
|
|
||||||
"no_shares": "לא משותף עם מישהו עדיין.",
|
|
||||||
"add_person": "הוסף אדם או קבוצה",
|
|
||||||
"search_placeholder": "חפש לפי שם או דוא״ל…",
|
|
||||||
"loading_principals": "טעינת משתמשים…",
|
|
||||||
"no_principals": "לא נמצאו משתמשים או קבוצות אחרים.",
|
|
||||||
"no_match": "אין התאמות.",
|
|
||||||
"remove": "הסר גישה",
|
|
||||||
"group": "קבוצה",
|
|
||||||
"share_added": "גישה ניתנה",
|
|
||||||
"share_updated": "גישה עודכנה",
|
|
||||||
"share_removed": "גישה הוסרה",
|
|
||||||
"share_failed": "נכשל בעדכון שיתוף",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "חופשי/תפוס בלבד",
|
|
||||||
"read": "קריאה בלבד",
|
|
||||||
"readWrite": "קרא וכתוב",
|
|
||||||
"manager": "מנהל",
|
|
||||||
"custom": "מותאם אישית"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "חיפוש מתקדם",
|
"title": "חיפוש מתקדם",
|
||||||
@@ -3286,7 +3180,7 @@
|
|||||||
"open_folder_tree": "פתח עץ תיקייה",
|
"open_folder_tree": "פתח עץ תיקייה",
|
||||||
"migration_title": "עדכון הקבצים שלך…",
|
"migration_title": "עדכון הקבצים שלך…",
|
||||||
"migration_description": "ארגון תיקיות וקבצים לתוך המבנה הנכון שלהם. זה קורה רק פעם אחת.",
|
"migration_description": "ארגון תיקיות וקבצים לתוך המבנה הנכון שלהם. זה קורה רק פעם אחת.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "שלח כקובץ מצורף"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "התעודות שלך",
|
"your_certificates": "התעודות שלך",
|
||||||
@@ -3417,6 +3311,110 @@
|
|||||||
"show_on_new_devices_title": "הצג בהתקנים חדשים",
|
"show_on_new_devices_title": "הצג בהתקנים חדשים",
|
||||||
"show_on_new_devices_desc": "הפעל מחדש את בר הברכה ושיוך את הסיור בפעם הראשונה שאתה נכנס בהתקן חדש, גם אם כבר השלמת אותו במקום אחר"
|
"show_on_new_devices_desc": "הפעל מחדש את בר הברכה ושיוך את הסיור בפעם הראשונה שאתה נכנס בהתקן חדש, גם אם כבר השלמת אותו במקום אחר"
|
||||||
},
|
},
|
||||||
|
"meta_description": "לקוח webmail מינימליסטי באמצעות פרוטוקול JMAP",
|
||||||
|
"protocol_handlers": {
|
||||||
|
"title": "יישומים ברירת מחדל",
|
||||||
|
"description": "בחר אם קישורי דוא״ל ולוח שנה ייפתחו ב־VNCmail+. מבחינה טכנית, VNCmail+ רשום כטיפול בפרוטוקול עבור קישורי mailto: ו־webcal:.",
|
||||||
|
"unsupported": "דפדפן זה או חיבור זה לא תומך בהרשמה ידנית של טיפול בפרוטוקול. ייתכן שעדיין תוכל להשתמש ב־PWA המותקנת דרך הדפדפן או הגדרות מערכת ההפעלה.",
|
||||||
|
"mailto_label": "קישורי דוא״ל",
|
||||||
|
"mailto_description": "פתח קישורי mailto: ב־VNCmail+ עם מחבר שמלא מראש.",
|
||||||
|
"protocol_open_mode_label": "בעת פתיחת קישורי פרוטוקול",
|
||||||
|
"protocol_open_mode_description": "בחר אם VNCmail+ יפתח קישורי mailto: ו־webcal: בלשונית חדשה או ישתמש בחיבור פתוח. אפשרות הישיבה הפעילה דורשת הרשאת עדכון כדי שתוכל ללחוץ על עדכון נופל כדי להביא את VNCmail+ לחזית אם הדפדפן חוסם מיקוד.",
|
||||||
|
"protocol_open_mode_active_session": "פתח בישיבה פעילה אם אפשר",
|
||||||
|
"protocol_open_mode_new_tab": "פתח תמיד לשונית חדשה",
|
||||||
|
"focus_notification_title": "פתח את VNCmail+",
|
||||||
|
"focus_notification_body": "הקישור נפתח ב־VNCmail+. לחץ כדי להביא את החלון לחזית.",
|
||||||
|
"webcal_label": "קישורי לוח שנה",
|
||||||
|
"webcal_description": "פתח קישורי webcal: ב־VNCmail+ עם תיבת דו־שיח מלא מראש להרשמה ללוח שנה.",
|
||||||
|
"register_mailto": "רשום יישום דוא״ל",
|
||||||
|
"register_webcal": "רשום יישום לוח שנה",
|
||||||
|
"mailto_registered": "בקש הרשמה של מטפל דוא״ל",
|
||||||
|
"webcal_registered": "בקש הרשמה של מטפל לוח שנה",
|
||||||
|
"registration_failed": "הרשמת טיפול בפרוטוקול נכשלה",
|
||||||
|
"opening_mailto": "פתיחת מחבר…",
|
||||||
|
"opening_webcal": "פתיחת לוח שנה…",
|
||||||
|
"browser_note": "הדפדפן או מערכת ההפעלה שלך עשויים לבקש ממך לאשר זאת ועשויים לדרוש שVNCmail+ יותקן לפני שניתן לבחור אותו כיישום ברירת המחדל.",
|
||||||
|
"select_account_title": "בחר חשבון",
|
||||||
|
"select_mailto_account": "בחר איזה חשבון צריך לפתוח קישור דוא״ל זה.",
|
||||||
|
"select_webcal_account": "בחר איזה חשבון צריך לפתוח קישור לוח שנה זה.",
|
||||||
|
"select_account_note": "זה חל רק על קישור פרוטוקול זה.",
|
||||||
|
"detail_to": "אל",
|
||||||
|
"detail_subject": "נושא",
|
||||||
|
"detail_no_subject": "אין נושא",
|
||||||
|
"detail_calendar": "לוח שנה",
|
||||||
|
"detail_source": "מקור",
|
||||||
|
"active_account": "פעיל",
|
||||||
|
"switching_account": "החלפת חשבון…"
|
||||||
|
},
|
||||||
|
"mailbox_context_menu": {
|
||||||
|
"mark_folder_read": "סמן תיקייה כקרויה",
|
||||||
|
"mark_folder_tree_read": "סמן תיקייה ותת־תיקיות כקרויות",
|
||||||
|
"mark_all_folders_read": "סמן את כל התיקיות כקרויות",
|
||||||
|
"new_subfolder": "תת־תיקייה חדשה…",
|
||||||
|
"new_folder": "תיקייה חדשה…",
|
||||||
|
"rename": "שנה שם…",
|
||||||
|
"import_email": "ייבא .eml או .zip…",
|
||||||
|
"empty_folder": "תיקייה ריקה",
|
||||||
|
"empty_folder_generic": "תיקייה ריקה",
|
||||||
|
"delete_folder": "מחק תיקייה",
|
||||||
|
"refresh": "רענן",
|
||||||
|
"mark_all_confirm_title": "סמן את כל התיקיות כקרויות",
|
||||||
|
"mark_all_confirm_message": "סמן כל הודעה שלא קרויה בחשבון האישי שלך כקרויה?",
|
||||||
|
"delete_confirm_title": "מחק תיקייה",
|
||||||
|
"delete_confirm_message": "מחק לצמיתות את התיקייה \"{name}\"? לא ניתן לבטל פעולה זו.",
|
||||||
|
"prompt_new_subfolder": "הזן שם לתת־תיקייה החדשה.",
|
||||||
|
"prompt_new_folder": "הזן שם לתיקייה החדשה.",
|
||||||
|
"prompt_rename": "הזן שם חדש לתיקייה זו.",
|
||||||
|
"placeholder_folder_name": "שם תיקייה",
|
||||||
|
"create": "צור",
|
||||||
|
"rename_confirm": "שנה שם",
|
||||||
|
"toast_marked_read": "התיקייה סומנה כקרויה",
|
||||||
|
"toast_marked_read_count": "סומנו {count, plural, one {הודעה 1} other {# הודעות}} כקרויות",
|
||||||
|
"toast_already_read": "אין הודעות שלא קרויות",
|
||||||
|
"toast_marked_all_read": "כל התיקיות סומנו כקרויות",
|
||||||
|
"toast_emptied": "התיקייה התרוקנה",
|
||||||
|
"toast_folder_created": "תיקייה נוצרה",
|
||||||
|
"toast_folder_renamed": "שם התיקייה שונה",
|
||||||
|
"toast_folder_deleted": "התיקייה נמחקה",
|
||||||
|
"toast_error_mark_read": "נכשל בסימון כקרויה",
|
||||||
|
"toast_error_empty": "נכשל בתרוקנון תיקייה",
|
||||||
|
"toast_error_create": "נכשל ביצירת תיקייה",
|
||||||
|
"toast_error_rename": "נכשל בשינוי שם תיקייה",
|
||||||
|
"toast_error_delete": "נכשל במחיקת תיקייה",
|
||||||
|
"toast_error_delete_has_children": "לתיקייה יש תת־תיקיות. הסר אותן קודם.",
|
||||||
|
"toast_error_delete_has_email": "התיקייה אינה ריקה. תרוקנה קודם.",
|
||||||
|
"share_folder": "שתף תיקייה..."
|
||||||
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "שתף \"{name}\"",
|
||||||
|
"description": "הענק גישה למשתמשים או קבוצות אחרות בשרת זה. השינויים יופעלו מיד.",
|
||||||
|
"no_shares": "לא משותף עם מישהו עדיין.",
|
||||||
|
"add_person": "הוסף אדם או קבוצה",
|
||||||
|
"search_placeholder": "חפש לפי שם או דוא״ל…",
|
||||||
|
"loading_principals": "טעינת משתמשים…",
|
||||||
|
"no_principals": "לא נמצאו משתמשים או קבוצות אחרים.",
|
||||||
|
"no_match": "אין התאמות.",
|
||||||
|
"remove": "הסר גישה",
|
||||||
|
"group": "קבוצה",
|
||||||
|
"share_added": "גישה ניתנה",
|
||||||
|
"share_updated": "גישה עודכנה",
|
||||||
|
"share_removed": "גישה הוסרה",
|
||||||
|
"share_failed": "נכשל בעדכון שיתוף",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "חופשי/תפוס בלבד",
|
||||||
|
"read": "קריאה בלבד",
|
||||||
|
"readWrite": "קרא וכתוב",
|
||||||
|
"manager": "מנהל",
|
||||||
|
"custom": "מותאם אישית"
|
||||||
|
},
|
||||||
|
"accept": "קבל",
|
||||||
|
"decline": "דחה",
|
||||||
|
"no_shares_by_me": "עדיין לא שיתפת שום דבר.",
|
||||||
|
"no_shares_with_me": "עדיין אין תיקיות ששותפו איתך.",
|
||||||
|
"shared_by": "משותף על ידי",
|
||||||
|
"tab_shared_by_me": "השיתופים שלי",
|
||||||
|
"tab_shared_with_me": "משותף איתי"
|
||||||
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "חיפוש אינו זמין בתצוגה המאוחדת"
|
"search_unavailable": "חיפוש אינו זמין בתצוגה המאוחדת"
|
||||||
},
|
},
|
||||||
@@ -3436,126 +3434,53 @@
|
|||||||
"dismiss_aria": "בטל הודעת התקנה"
|
"dismiss_aria": "בטל הודעת התקנה"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "הוסף חתימה",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "ברירת מחדל",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "משמש עבור הודעות חדשות, אלא אם נעקף עבור זהות ספציפית.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "חתימת ברירת מחדל"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "האם אתה בטוח שברצונך למחוק את \"{name}\"? לא ניתן לבטל פעולה זו.",
|
||||||
"label": "Default for replies",
|
"delete_title": "למחוק חתימה?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "צור ונהל חתימות דוא״ל לשימוש בעת כתיבה או מענה.",
|
||||||
},
|
"duplicate": "שכפל",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "ערוך חתימה",
|
||||||
|
"editor_label": "חתימה",
|
||||||
|
"html_preview_label": "תצוגה מקדימה של HTML",
|
||||||
|
"name_label": "שם",
|
||||||
|
"name_placeholder": "למשל, עבודה, אישי",
|
||||||
|
"name_required": "נדרש שם",
|
||||||
|
"new_signature": "חתימה חדשה",
|
||||||
|
"no_signature": "ללא חתימה",
|
||||||
|
"no_signatures": "עדיין אין חתימות",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "עקוף את חתימת ברירת המחדל וחתימת התשובה עבור זהויות בודדות.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "חתימות לפי זהות"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "תצוגה מקדימה של טקסט רגיל",
|
||||||
"select_signature": "Select signature",
|
"reply": "תשובה",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "משמש בעת מענה או העברה, אלא אם נעקף עבור זהות ספציפית.",
|
||||||
|
"label": "חתימת תשובה"
|
||||||
|
},
|
||||||
|
"show_editor": "הצג עורך",
|
||||||
|
"show_preview": "הצג תצוגה מקדימה",
|
||||||
|
"title": "חתימות",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "מרכוז",
|
||||||
"italic": "Italic",
|
"align_left": "יישור לשמאל",
|
||||||
"underline": "Underline",
|
"align_right": "יישור לימין",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "מודגש",
|
||||||
"link": "Link",
|
"bullet_list": "רשימת תבליטים",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "נטוי",
|
||||||
"ordered_list": "Ordered List",
|
"link": "קישור",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "רשימה ממוספרת",
|
||||||
"alignment": "Alignment",
|
"remove_color": "הסרת צבע",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "קו חוצה",
|
||||||
"align_center": "Align center",
|
"text_color": "צבע טקסט",
|
||||||
"align_left": "Align left",
|
"underline": "קו תחתון"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "השתמש בברירת המחדל הגלובלית",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "החתימות שלך ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+144
-219
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "A másolás nem sikerült"
|
"copy_failed": "A másolás nem sikerült"
|
||||||
},
|
},
|
||||||
"send_now": "Küldés most",
|
"send_now": "Küldés most",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Találkozó létrehozása"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Olvasási visszaigazolás kérve (kattints a letiltáshoz)",
|
"read_receipt_on": "Olvasási visszaigazolás kérve (kattints a letiltáshoz)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Méret kiválasztása"
|
"pick_size": "Méret kiválasztása"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Elküldve - de az utólagos rendrakás nem sikerült, egy elavult piszkozat megmaradhat.",
|
"send_filing_warning": "Elküldve - de az utólagos rendrakás nem sikerült, egy elavult piszkozat megmaradhat.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Aláírás beszúrása",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Nincs aláírás",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Aláírás kiválasztása"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Megerősítés",
|
"confirm": "Megerősítés",
|
||||||
@@ -896,9 +896,9 @@
|
|||||||
"content_senders": "Tartalom és feladók",
|
"content_senders": "Tartalom és feladók",
|
||||||
"about_data": "Névjegy és adatok",
|
"about_data": "Névjegy és adatok",
|
||||||
"debug": "Hibakeresés",
|
"debug": "Hibakeresés",
|
||||||
"import": "Import",
|
"import": "Importálás",
|
||||||
"sharing": "Sharing",
|
"sharing": "Megosztás",
|
||||||
"signatures": "Signatures"
|
"signatures": "Aláírások"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Általános",
|
"general": "Általános",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Kezelés: {name}"
|
"managing": "Kezelés: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importálás",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Mégse",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Fájlok kiválasztása",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Mindkettő megtartása",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Válaszd ki, mi történjen, ha egy importált üzenet már létezik.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Duplikátumok kezelése",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Duplikátumok cseréje",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Duplikátumok kihagyása",
|
||||||
"cancel": "Cancel",
|
"description": "E-mail üzenetek importálása .eml fájlokból egy mappába.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# hiba} other {# hiba}}",
|
||||||
"fail": "Import failed",
|
"fail": "Importálás sikertelen",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Válassz ki egy vagy több .eml fájlt az importáláshoz.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Fájlok",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# fájl kijelölve} other {# fájl kijelölve}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Válaszd ki a mappát, amelybe az üzeneteket importálni szeretnéd.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Célmappa",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Importálás befejezve",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "További importálás",
|
||||||
"action_label": "Action",
|
"importing": "Importálás...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} sikertelen",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importálva",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} kihagyva",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {# fájl importálása} other {# fájl importálása}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# üzenet importálva} other {# üzenet importálva}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# üzenet sikertelen} other {# üzenet sikertelen}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# üzenet importálva} other {# üzenet importálva}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# üzenet kihagyva} other {# üzenet kihagyva}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Levelek importálása"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Betöltés...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Frissítés"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Valami hiba történt",
|
"page_error_title": "Valami hiba történt",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "Nem sikerült törölni a mappát",
|
"toast_error_delete": "Nem sikerült törölni a mappát",
|
||||||
"toast_error_delete_has_children": "A mappának almappái vannak. Távolítsd el azokat először.",
|
"toast_error_delete_has_children": "A mappának almappái vannak. Távolítsd el azokat először.",
|
||||||
"toast_error_delete_has_email": "A mappa nem üres. Ürítsd ki először.",
|
"toast_error_delete_has_email": "A mappa nem üres. Ürítsd ki először.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Mappa megosztása..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Billentyűparancsok",
|
"title": "Billentyűparancsok",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"creating": "Létrehozás...",
|
"creating": "Létrehozás...",
|
||||||
"updating": "Frissítés...",
|
"updating": "Frissítés...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Alapértelmezett aláírás",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Aláírás-hozzárendelés",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Válasz aláírás",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Globális alapértelmezett használata"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Alcím használata",
|
"button_tooltip": "Alcím használata",
|
||||||
@@ -2558,28 +2556,28 @@
|
|||||||
"failed": "Importálás sikertelen",
|
"failed": "Importálás sikertelen",
|
||||||
"close": "Bezárás",
|
"close": "Bezárás",
|
||||||
"file_too_large": "A fájl túl nagy (max 5 MB)",
|
"file_too_large": "A fájl túl nagy (max 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Cím",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Címjegyzék",
|
||||||
"csv_back": "Back",
|
"csv_back": "Vissza",
|
||||||
"csv_city": "City",
|
"csv_city": "Város",
|
||||||
"csv_company": "Company",
|
"csv_company": "Cég",
|
||||||
"csv_country": "Country",
|
"csv_country": "Ország",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Keresztnév",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Oszlop figyelmen kívül hagyása",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Beosztás",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Vezetéknév",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Összes betöltése",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Oszlopok megfeleltetése",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Becenév",
|
||||||
"csv_note": "Note",
|
"csv_note": "Jegyzet",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Irányítószám",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Előnézet",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Előnézet ({count, plural, one {# sor} other {# sor}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Állam/Régió",
|
||||||
"csv_website": "Website",
|
"csv_website": "Weboldal",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv fájlok"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Névjegyek exportálása",
|
"title": "Névjegyek exportálása",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_phone": "Van telefon",
|
"has_phone": "Van telefon",
|
||||||
"has_photo": "Van fotó"
|
"has_photo": "Van fotó"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"delete": "Törlés",
|
||||||
"edit": "Edit Contact",
|
"edit": "Szerkesztés",
|
||||||
"send_email": "Send Email"
|
"send_email": "E-mail küldése"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Naptár",
|
"title": "Naptár",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"due_tomorrow": "Holnap",
|
"due_tomorrow": "Holnap",
|
||||||
"overdue": "Lejárt"
|
"overdue": "Lejárt"
|
||||||
},
|
},
|
||||||
|
"delete": "Törlés",
|
||||||
|
"duplicate": "Duplikálás",
|
||||||
|
"edit": "Szerkesztés",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Elfoglalt",
|
||||||
"check": "Check Availability",
|
"check": "Elérhetőség ellenőrzése",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Kattints egy szabad időpontra ennek az időpontnak a kiválasztásához",
|
||||||
"loading": "Loading...",
|
"free": "Szabad",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Elérhetőség elrejtése",
|
||||||
"timezone": "Timezone",
|
"loading": "Betöltés...",
|
||||||
"free": "Free",
|
"no_participants": "Adj hozzá résztvevőket az elérhetőség ellenőrzéséhez.",
|
||||||
"busy": "Busy",
|
"tentative": "Előzetes",
|
||||||
"tentative": "Tentative",
|
"timezone": "Időzóna",
|
||||||
"unavailable": "Out of office",
|
"title": "Elérhetőség",
|
||||||
"unknown": "No information",
|
"unavailable": "Házon kívül",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Nincs információ"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Összes törlése",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Összes",
|
||||||
"filter_all": "All",
|
"hide": "Erőforrások elrejtése",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Nincs elérhető erőforrás",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "{name} eltávolítása",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Erőforrások keresése...",
|
||||||
"type_other": "Other",
|
"title": "Erőforrások",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Berendezés",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Egyéb",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Termek",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Járművek"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "\"{name}\" megosztása",
|
"title": "\"{name}\" megosztása",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "Kezelő",
|
"manager": "Kezelő",
|
||||||
"custom": "Egyéni"
|
"custom": "Egyéni"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "Elfogadás",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "Elutasítás",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "Még nem osztottál meg semmit.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "Még nincs veled megosztott mappa.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "Megosztotta",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "Általam megosztott",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "Velem megosztott"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Speciális keresés",
|
"title": "Speciális keresés",
|
||||||
@@ -3285,7 +3283,7 @@
|
|||||||
"stability_warning": "Nagyméretű fájlok feltöltése szerver instabilitást okozhat. A törölt fájlok nem feltétlenül kerülnek azonnal eltávolításra a tárolóból. Használat óvatosan.",
|
"stability_warning": "Nagyméretű fájlok feltöltése szerver instabilitást okozhat. A törölt fájlok nem feltétlenül kerülnek azonnal eltávolításra a tárolóból. Használat óvatosan.",
|
||||||
"migration_title": "Fájlok frissítése…",
|
"migration_title": "Fájlok frissítése…",
|
||||||
"migration_description": "A mappák és fájlok a megfelelő struktúrába rendeződnek. Ez csak egyszer történik meg.",
|
"migration_description": "A mappák és fájlok a megfelelő struktúrába rendeződnek. Ez csak egyszer történik meg.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Küldés csatolmányként"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Tanúsítványaid",
|
"your_certificates": "Tanúsítványaid",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Telepítési ablak elutasítása"
|
"dismiss_aria": "Telepítési ablak elutasítása"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Aláírás hozzáadása",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Alapértelmezett",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Új üzenetekhez használatos, hacsak nincs felülbírálva azonosságonként.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Alapértelmezett aláírás"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Biztosan törölni szeretnéd a(z) \"{name}\" aláírást? Ez nem vonható vissza.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Aláírás törlése?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "E-mail aláírások létrehozása és kezelése levélíráshoz vagy válaszadáshoz.",
|
||||||
},
|
"duplicate": "Duplikálás",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Aláírás szerkesztése",
|
||||||
|
"editor_label": "Aláírás",
|
||||||
|
"html_preview_label": "HTML előnézet",
|
||||||
|
"name_label": "Név",
|
||||||
|
"name_placeholder": "pl. Munka, Személyes",
|
||||||
|
"name_required": "Név megadása kötelező",
|
||||||
|
"new_signature": "Új aláírás",
|
||||||
|
"no_signature": "Nincs aláírás",
|
||||||
|
"no_signatures": "Még nincsenek aláírások",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Az alapértelmezett és a válasz aláírás felülbírálása az egyes azonosságoknál.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Azonosságonkénti aláírások"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Egyszerű szöveges előnézet",
|
||||||
"select_signature": "Select signature",
|
"reply": "Válasz",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Válaszadáskor vagy továbbításkor használatos, hacsak nincs felülbírálva azonosságonként.",
|
||||||
|
"label": "Válasz aláírás"
|
||||||
|
},
|
||||||
|
"show_editor": "Szerkesztő megjelenítése",
|
||||||
|
"show_preview": "Előnézet megjelenítése",
|
||||||
|
"title": "Aláírások",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Középre igazítás",
|
||||||
"italic": "Italic",
|
"align_left": "Balra igazítás",
|
||||||
"underline": "Underline",
|
"align_right": "Jobbra igazítás",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Félkövér",
|
||||||
"link": "Link",
|
"bullet_list": "Felsorolás",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Dőlt",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Hivatkozás",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Számozott lista",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Szín eltávolítása",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Áthúzott",
|
||||||
"align_center": "Align center",
|
"text_color": "Betűszín",
|
||||||
"align_left": "Align left",
|
"underline": "Aláhúzott"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Globális alapértelmezett használata",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Aláírásaid ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-240
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Copia non riuscita"
|
"copy_failed": "Copia non riuscita"
|
||||||
},
|
},
|
||||||
"send_now": "Invia ora",
|
"send_now": "Invia ora",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Crea appuntamento"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Conferma di lettura richiesta (clicca per disattivare)",
|
"read_receipt_on": "Conferma di lettura richiesta (clicca per disattivare)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Scegli dimensione"
|
"pick_size": "Scegli dimensione"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Inviato - ma la pulizia successiva non è riuscita, potrebbe restare una bozza obsoleta.",
|
"send_filing_warning": "Inviato - ma la pulizia successiva non è riuscita, potrebbe restare una bozza obsoleta.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Inserisci firma",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Nessuna firma",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Seleziona firma"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Conferma",
|
"confirm": "Conferma",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "Contenuto e mittenti",
|
"content_senders": "Contenuto e mittenti",
|
||||||
"about_data": "Informazioni e dati",
|
"about_data": "Informazioni e dati",
|
||||||
"debug": "Debug",
|
"debug": "Debug",
|
||||||
"import": "Import",
|
"import": "Importa",
|
||||||
"sharing": "Sharing",
|
"sharing": "Condivisione",
|
||||||
"signatures": "Signatures"
|
"signatures": "Firme"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Generale",
|
"general": "Generale",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Gestione: {name}"
|
"managing": "Gestione: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importa",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Annulla",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Scegli file",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Mantieni entrambi",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Scegli cosa fare quando un messaggio importato esiste già.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Gestione dei duplicati",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Sostituisci i duplicati",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Salta i duplicati",
|
||||||
"cancel": "Cancel",
|
"description": "Importa messaggi email da file .eml in una cartella.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# errore} other {# errori}}",
|
||||||
"fail": "Import failed",
|
"fail": "Importazione non riuscita",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Seleziona uno o più file .eml da importare.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "File",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# file selezionato} other {# file selezionati}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Scegli la cartella in cui importare i messaggi.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Cartella di destinazione",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Importazione completata",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importa altro",
|
||||||
"action_label": "Action",
|
"importing": "Importazione in corso...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} non riusciti",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importati",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} saltati",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importa # file} other {Importa # file}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# messaggio importato} other {# messaggi importati}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# messaggio non riuscito} other {# messaggi non riusciti}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# messaggio importato} other {# messaggi importati}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# messaggio saltato} other {# messaggi saltati}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Importa posta"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Caricamento...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Aggiorna"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Qualcosa è andato storto",
|
"page_error_title": "Qualcosa è andato storto",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Nome cartella",
|
"placeholder_folder_name": "Nome cartella",
|
||||||
"create": "Crea",
|
"create": "Crea",
|
||||||
"rename_confirm": "Rinomina",
|
"rename_confirm": "Rinomina",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Condividi cartella..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Scorciatoie da tastiera",
|
"title": "Scorciatoie da tastiera",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Annulla",
|
"cancel": "Annulla",
|
||||||
"creating": "Creazione...",
|
"creating": "Creazione...",
|
||||||
"updating": "Aggiornamento...",
|
"updating": "Aggiornamento...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Firma predefinita",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Mappatura firma",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Firma di risposta",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Usa predefinito globale"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Usa sotto-indirizzo",
|
"button_tooltip": "Usa sotto-indirizzo",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Importazione fallita",
|
"failed": "Importazione fallita",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
"file_too_large": "Il file è troppo grande (max 5 MB)",
|
"file_too_large": "Il file è troppo grande (max 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Indirizzo",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Rubrica",
|
||||||
"csv_back": "Back",
|
"csv_back": "Indietro",
|
||||||
"csv_city": "City",
|
"csv_city": "Città",
|
||||||
"csv_company": "Company",
|
"csv_company": "Azienda",
|
||||||
"csv_country": "Country",
|
"csv_country": "Paese",
|
||||||
"csv_email": "Email",
|
"csv_email": "Email",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Nome",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignora questa colonna",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Titolo professionale",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Cognome",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Carica tutto",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Mappa colonne",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Soprannome",
|
||||||
"csv_note": "Note",
|
"csv_note": "Nota",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefono",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Codice postale",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Anteprima",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Anteprima ({count, plural, one {# riga} other {# righe}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Stato / Regione",
|
||||||
"csv_website": "Website",
|
"csv_website": "Sito web",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "File .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Esporta contatti",
|
"title": "Esporta contatti",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Con foto"
|
"has_photo": "Con foto"
|
||||||
},
|
},
|
||||||
"open_categories": "Apri categorie",
|
"open_categories": "Apri categorie",
|
||||||
"delete": "Delete Contact",
|
"delete": "Elimina",
|
||||||
"edit": "Edit Contact",
|
"edit": "Modifica",
|
||||||
"send_email": "Send Email"
|
"send_email": "Invia email"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendario",
|
"title": "Calendario",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Apri menu",
|
"nav_open_menu": "Apri menu",
|
||||||
|
"delete": "Elimina",
|
||||||
|
"duplicate": "Duplica",
|
||||||
|
"edit": "Modifica",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Occupato",
|
||||||
"check": "Check Availability",
|
"check": "Verifica disponibilità",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Fai clic su uno slot libero per selezionare questo orario",
|
||||||
"loading": "Loading...",
|
"free": "Libero",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Nascondi disponibilità",
|
||||||
"timezone": "Timezone",
|
"loading": "Caricamento...",
|
||||||
"free": "Free",
|
"no_participants": "Aggiungi partecipanti per verificare la disponibilità.",
|
||||||
"busy": "Busy",
|
"tentative": "Provvisorio",
|
||||||
"tentative": "Tentative",
|
"timezone": "Fuso orario",
|
||||||
"unavailable": "Out of office",
|
"title": "Disponibilità",
|
||||||
"unknown": "No information",
|
"unavailable": "Fuori ufficio",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Nessuna informazione"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Cancella tutto",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Tutte",
|
||||||
"filter_all": "All",
|
"hide": "Nascondi risorse",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Nessuna risorsa disponibile",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Rimuovi {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Cerca risorse...",
|
||||||
"type_other": "Other",
|
"title": "Risorse",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Attrezzature",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Altro",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Sale",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Veicoli"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Condividi \"{name}\"",
|
|
||||||
"description": "Concedi l'accesso ad altri utenti o gruppi su questo server. Le modifiche hanno effetto immediato.",
|
|
||||||
"no_shares": "Non ancora condiviso.",
|
|
||||||
"add_person": "Aggiungi persona o gruppo",
|
|
||||||
"search_placeholder": "Cerca per nome o email…",
|
|
||||||
"loading_principals": "Caricamento utenti…",
|
|
||||||
"no_principals": "Nessun altro utente o gruppo trovato.",
|
|
||||||
"no_match": "Nessun risultato.",
|
|
||||||
"remove": "Rimuovi accesso",
|
|
||||||
"group": "Gruppo",
|
|
||||||
"share_added": "Accesso concesso",
|
|
||||||
"share_updated": "Accesso aggiornato",
|
|
||||||
"share_removed": "Accesso rimosso",
|
|
||||||
"share_failed": "Impossibile aggiornare la condivisione",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Solo libero/occupato",
|
|
||||||
"read": "Sola lettura",
|
|
||||||
"readWrite": "Lettura e scrittura",
|
|
||||||
"manager": "Gestore",
|
|
||||||
"custom": "Personalizzato"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Ricerca avanzata",
|
"title": "Ricerca avanzata",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Altri account",
|
"other_accounts": "Altri account",
|
||||||
"migration_title": "Aggiornamento dei tuoi file…",
|
"migration_title": "Aggiornamento dei tuoi file…",
|
||||||
"migration_description": "Organizzazione di cartelle e file nella loro struttura corretta. Questo avviene solo una volta.",
|
"migration_description": "Organizzazione di cartelle e file nella loro struttura corretta. Questo avviene solo una volta.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Invia come allegato"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "I tuoi certificati",
|
"your_certificates": "I tuoi certificati",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "La ricerca non è disponibile nella vista unificata"
|
"search_unavailable": "La ricerca non è disponibile nella vista unificata"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Condividi \"{name}\"",
|
||||||
|
"description": "Concedi l'accesso ad altri utenti o gruppi su questo server. Le modifiche hanno effetto immediato.",
|
||||||
|
"no_shares": "Non ancora condiviso.",
|
||||||
|
"add_person": "Aggiungi persona o gruppo",
|
||||||
|
"search_placeholder": "Cerca per nome o email…",
|
||||||
|
"loading_principals": "Caricamento utenti…",
|
||||||
|
"no_principals": "Nessun altro utente o gruppo trovato.",
|
||||||
|
"no_match": "Nessun risultato.",
|
||||||
|
"remove": "Rimuovi accesso",
|
||||||
|
"group": "Gruppo",
|
||||||
|
"share_added": "Accesso concesso",
|
||||||
|
"share_updated": "Accesso aggiornato",
|
||||||
|
"share_removed": "Accesso rimosso",
|
||||||
|
"share_failed": "Impossibile aggiornare la condivisione",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Solo libero/occupato",
|
||||||
|
"read": "Sola lettura",
|
||||||
|
"readWrite": "Lettura e scrittura",
|
||||||
|
"manager": "Gestore",
|
||||||
|
"custom": "Personalizzato"
|
||||||
|
},
|
||||||
|
"accept": "Accetta",
|
||||||
|
"decline": "Rifiuta",
|
||||||
|
"no_shares_by_me": "Non hai ancora condiviso nulla.",
|
||||||
|
"no_shares_with_me": "Nessuna cartella condivisa con te per ora.",
|
||||||
|
"shared_by": "Condiviso da",
|
||||||
|
"tab_shared_by_me": "Condivisi da me",
|
||||||
|
"tab_shared_with_me": "Condivisi con me"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "Il {date}, {from} ha scritto:",
|
"reply_line": "Il {date}, {from} ha scritto:",
|
||||||
"forwarded_separator": "---------- Messaggio inoltrato ----------",
|
"forwarded_separator": "---------- Messaggio inoltrato ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Chiudi avviso di installazione"
|
"dismiss_aria": "Chiudi avviso di installazione"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Aggiungi firma",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Predefinita",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Utilizzata per i nuovi messaggi salvo diversa impostazione per identità.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Firma predefinita"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Sei sicuro di voler eliminare \"{name}\"? Questa azione non può essere annullata.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Eliminare la firma?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Crea e gestisci le firme email da usare quando componi o rispondi.",
|
||||||
},
|
"duplicate": "Duplica",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Modifica firma",
|
||||||
|
"editor_label": "Firma",
|
||||||
|
"html_preview_label": "Anteprima HTML",
|
||||||
|
"name_label": "Nome",
|
||||||
|
"name_placeholder": "es. Lavoro, Personale",
|
||||||
|
"name_required": "Il nome è obbligatorio",
|
||||||
|
"new_signature": "Nuova firma",
|
||||||
|
"no_signature": "Nessuna firma",
|
||||||
|
"no_signatures": "Nessuna firma ancora",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Sovrascrivi la firma predefinita e di risposta per le singole identità.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Firme per identità"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Anteprima testo semplice",
|
||||||
"select_signature": "Select signature",
|
"reply": "Di risposta",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Utilizzata quando rispondi o inoltri, salvo diversa impostazione per identità.",
|
||||||
|
"label": "Firma di risposta"
|
||||||
|
},
|
||||||
|
"show_editor": "Mostra editor",
|
||||||
|
"show_preview": "Mostra anteprima",
|
||||||
|
"title": "Firme",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Centra",
|
||||||
"italic": "Italic",
|
"align_left": "Allinea a sinistra",
|
||||||
"underline": "Underline",
|
"align_right": "Allinea a destra",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Grassetto",
|
||||||
|
"bullet_list": "Elenco puntato",
|
||||||
|
"italic": "Corsivo",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"bullet_list": "Bullet List",
|
"ordered_list": "Elenco numerato",
|
||||||
"ordered_list": "Ordered List",
|
"remove_color": "Rimuovi colore",
|
||||||
"text_color": "Text Color",
|
"strikethrough": "Barrato",
|
||||||
"alignment": "Alignment",
|
"text_color": "Colore del testo",
|
||||||
"font_size": "Font Size",
|
"underline": "Sottolineato"
|
||||||
"align_center": "Align center",
|
|
||||||
"align_left": "Align left",
|
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Usa predefinito globale",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Le tue firme ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+167
-242
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "コピーに失敗しました"
|
"copy_failed": "コピーに失敗しました"
|
||||||
},
|
},
|
||||||
"send_now": "今すぐ送信",
|
"send_now": "今すぐ送信",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "予定を作成"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "開封確認を要求中(クリックで無効化)",
|
"read_receipt_on": "開封確認を要求中(クリックで無効化)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "サイズを選択"
|
"pick_size": "サイズを選択"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "送信されましたが、送信後の整理に失敗しました。古い下書きが残る場合があります。",
|
"send_filing_warning": "送信されましたが、送信後の整理に失敗しました。古い下書きが残る場合があります。",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "署名を挿入",
|
||||||
"no_signature": "No signature",
|
"no_signature": "署名なし",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "署名を選択"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "コンテンツと送信者",
|
"content_senders": "コンテンツと送信者",
|
||||||
"about_data": "情報とデータ",
|
"about_data": "情報とデータ",
|
||||||
"debug": "デバッグ",
|
"debug": "デバッグ",
|
||||||
"import": "Import",
|
"import": "インポート",
|
||||||
"sharing": "Sharing",
|
"sharing": "共有",
|
||||||
"signatures": "Signatures"
|
"signatures": "署名"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "一般",
|
"general": "一般",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "管理中: {name}"
|
"managing": "管理中: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "インポート",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "キャンセル",
|
||||||
"file_label": "Select Files",
|
"choose_files": "ファイルを選択",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "両方を保持",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "インポートするメッセージがすでに存在する場合の処理方法を選択してください。",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "重複の処理",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "重複を置き換え",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "重複をスキップ",
|
||||||
"cancel": "Cancel",
|
"description": ".emlファイルからメールメッセージをフォルダーにインポートします。",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, other {#件のエラー}}",
|
||||||
"fail": "Import failed",
|
"fail": "インポートに失敗しました",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "インポートする.emlファイルを1つ以上選択してください。",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "ファイル",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, other {#件のファイルを選択}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "メッセージのインポート先フォルダーを選択してください。",
|
||||||
"error_details": "Error Details",
|
"folder_label": "インポート先フォルダー",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "インポート完了",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "さらにインポート",
|
||||||
"action_label": "Action",
|
"importing": "インポート中...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count}件失敗",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count}件インポート済み",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count}件スキップ",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, other {#件のファイルをインポート}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, other {#件のメッセージをインポートしました}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, other {#件のメッセージが失敗しました}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, other {#件のメッセージをインポートしました}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, other {#件のメッセージをスキップしました}}",
|
||||||
"progress_failed": "Failed",
|
"title": "メールをインポート"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "読み込み中...",
|
||||||
"refresh": "Refresh"
|
"refresh": "更新"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "問題が発生しました",
|
"page_error_title": "問題が発生しました",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "フォルダー名",
|
"placeholder_folder_name": "フォルダー名",
|
||||||
"create": "作成",
|
"create": "作成",
|
||||||
"rename_confirm": "名前を変更",
|
"rename_confirm": "名前を変更",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "フォルダーを共有..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "キーボードショートカット",
|
"title": "キーボードショートカット",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"creating": "作成中...",
|
"creating": "作成中...",
|
||||||
"updating": "更新中...",
|
"updating": "更新中...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "デフォルト署名",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "署名のマッピング",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "返信署名",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "全体のデフォルトを使用"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "サブアドレスを使用",
|
"button_tooltip": "サブアドレスを使用",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "インポートに失敗しました",
|
"failed": "インポートに失敗しました",
|
||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"file_too_large": "ファイルが大きすぎます(最大5 MB)",
|
"file_too_large": "ファイルが大きすぎます(最大5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "住所",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "アドレス帳",
|
||||||
"csv_back": "Back",
|
"csv_back": "戻る",
|
||||||
"csv_city": "City",
|
"csv_city": "市区町村",
|
||||||
"csv_company": "Company",
|
"csv_company": "会社名",
|
||||||
"csv_country": "Country",
|
"csv_country": "国",
|
||||||
"csv_email": "Email",
|
"csv_email": "メール",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "名",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "この列を無視",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "役職",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "姓",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "すべて読み込む",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "列のマッピング",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "ニックネーム",
|
||||||
"csv_note": "Note",
|
"csv_note": "メモ",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "電話",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "郵便番号",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "プレビュー",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "プレビュー({count, plural, other {#行}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "都道府県",
|
||||||
"csv_website": "Website",
|
"csv_website": "ウェブサイト",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv ファイル"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "連絡先をエクスポート",
|
"title": "連絡先をエクスポート",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "写真あり"
|
"has_photo": "写真あり"
|
||||||
},
|
},
|
||||||
"open_categories": "カテゴリを開く",
|
"open_categories": "カテゴリを開く",
|
||||||
"delete": "Delete Contact",
|
"delete": "削除",
|
||||||
"edit": "Edit Contact",
|
"edit": "編集",
|
||||||
"send_email": "Send Email"
|
"send_email": "メールを送信"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "カレンダー",
|
"title": "カレンダー",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "メニューを開く",
|
"nav_open_menu": "メニューを開く",
|
||||||
|
"delete": "削除",
|
||||||
|
"duplicate": "複製",
|
||||||
|
"edit": "編集",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "予定あり",
|
||||||
"check": "Check Availability",
|
"check": "空き状況を確認",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "この時間を選択するには、空いている枠をクリックしてください",
|
||||||
"loading": "Loading...",
|
"free": "空き",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "空き状況を非表示",
|
||||||
"timezone": "Timezone",
|
"loading": "読み込み中...",
|
||||||
"free": "Free",
|
"no_participants": "空き状況を確認するには参加者を追加してください。",
|
||||||
"busy": "Busy",
|
"tentative": "仮",
|
||||||
"tentative": "Tentative",
|
"timezone": "タイムゾーン",
|
||||||
"unavailable": "Out of office",
|
"title": "空き状況",
|
||||||
"unknown": "No information",
|
"unavailable": "不在",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "情報なし"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "すべてクリア",
|
||||||
"hide": "Hide resources",
|
"filter_all": "すべて",
|
||||||
"filter_all": "All",
|
"hide": "リソースを非表示",
|
||||||
"type_room": "Rooms",
|
"no_resources": "利用可能なリソースがありません",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "{name}を削除",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "リソースを検索...",
|
||||||
"type_other": "Other",
|
"title": "リソース",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "備品",
|
||||||
"no_resources": "No resources available",
|
"type_other": "その他",
|
||||||
"remove": "Remove {name}",
|
"type_room": "会議室",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "車両"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "「{name}」を共有",
|
|
||||||
"description": "このサーバー上の他のユーザーまたはグループにアクセス権を付与します。変更はすぐに反映されます。",
|
|
||||||
"no_shares": "まだ誰にも共有されていません。",
|
|
||||||
"add_person": "ユーザーまたはグループを追加",
|
|
||||||
"search_placeholder": "名前またはメールで検索…",
|
|
||||||
"loading_principals": "ユーザーを読み込み中…",
|
|
||||||
"no_principals": "他のユーザーまたはグループは見つかりません。",
|
|
||||||
"no_match": "一致する項目がありません。",
|
|
||||||
"remove": "アクセス権を削除",
|
|
||||||
"group": "グループ",
|
|
||||||
"share_added": "アクセス権を付与しました",
|
|
||||||
"share_updated": "アクセス権を更新しました",
|
|
||||||
"share_removed": "アクセス権を削除しました",
|
|
||||||
"share_failed": "共有の更新に失敗しました",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "空き時間情報のみ",
|
|
||||||
"read": "読み取り専用",
|
|
||||||
"readWrite": "読み取り・書き込み",
|
|
||||||
"manager": "管理者",
|
|
||||||
"custom": "カスタム"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "詳細検索",
|
"title": "詳細検索",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "その他のアカウント",
|
"other_accounts": "その他のアカウント",
|
||||||
"migration_title": "ファイルを更新しています…",
|
"migration_title": "ファイルを更新しています…",
|
||||||
"migration_description": "フォルダーとファイルを適切な構造に整理しています。これは一度だけ行われます。",
|
"migration_description": "フォルダーとファイルを適切な構造に整理しています。これは一度だけ行われます。",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "添付ファイルとして送信"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "あなたの証明書",
|
"your_certificates": "あなたの証明書",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "統合ビューでは検索を利用できません"
|
"search_unavailable": "統合ビューでは検索を利用できません"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "「{name}」を共有",
|
||||||
|
"description": "このサーバー上の他のユーザーまたはグループにアクセス権を付与します。変更はすぐに反映されます。",
|
||||||
|
"no_shares": "まだ誰にも共有されていません。",
|
||||||
|
"add_person": "ユーザーまたはグループを追加",
|
||||||
|
"search_placeholder": "名前またはメールで検索…",
|
||||||
|
"loading_principals": "ユーザーを読み込み中…",
|
||||||
|
"no_principals": "他のユーザーまたはグループは見つかりません。",
|
||||||
|
"no_match": "一致する項目がありません。",
|
||||||
|
"remove": "アクセス権を削除",
|
||||||
|
"group": "グループ",
|
||||||
|
"share_added": "アクセス権を付与しました",
|
||||||
|
"share_updated": "アクセス権を更新しました",
|
||||||
|
"share_removed": "アクセス権を削除しました",
|
||||||
|
"share_failed": "共有の更新に失敗しました",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "空き時間情報のみ",
|
||||||
|
"read": "読み取り専用",
|
||||||
|
"readWrite": "読み取り・書き込み",
|
||||||
|
"manager": "管理者",
|
||||||
|
"custom": "カスタム"
|
||||||
|
},
|
||||||
|
"accept": "承諾",
|
||||||
|
"decline": "辞退",
|
||||||
|
"no_shares_by_me": "まだ何も共有していません。",
|
||||||
|
"no_shares_with_me": "共有されているフォルダーはまだありません。",
|
||||||
|
"shared_by": "共有者",
|
||||||
|
"tab_shared_by_me": "自分が共有",
|
||||||
|
"tab_shared_with_me": "自分と共有"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "{date}に{from}が書きました:",
|
"reply_line": "{date}に{from}が書きました:",
|
||||||
"forwarded_separator": "---------- 転送メッセージ ----------",
|
"forwarded_separator": "---------- 転送メッセージ ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "インストールプロンプトを閉じる"
|
"dismiss_aria": "インストールプロンプトを閉じる"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "署名を追加",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "デフォルト",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "個々の送信者情報で上書きしない限り、新規メッセージに使用されます。",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "デフォルト署名"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "\"{name}\"を削除してもよろしいですか?この操作は元に戻せません。",
|
||||||
"label": "Default for replies",
|
"delete_title": "署名を削除",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "作成や返信で使用するメール署名を作成・管理します。",
|
||||||
},
|
"duplicate": "複製",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "署名を編集",
|
||||||
|
"editor_label": "署名",
|
||||||
|
"html_preview_label": "HTMLプレビュー",
|
||||||
|
"name_label": "名前",
|
||||||
|
"name_placeholder": "例: 仕事用、個人用",
|
||||||
|
"name_required": "名前は必須です",
|
||||||
|
"new_signature": "新しい署名",
|
||||||
|
"no_signature": "署名なし",
|
||||||
|
"no_signatures": "署名はまだありません",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "個々の送信者情報について、デフォルトおよび返信の署名を上書きします。",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "送信者情報ごとの署名"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "プレーンテキストプレビュー",
|
||||||
"select_signature": "Select signature",
|
"reply": "返信",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "個々の送信者情報で上書きしない限り、返信または転送時に使用されます。",
|
||||||
|
"label": "返信署名"
|
||||||
|
},
|
||||||
|
"show_editor": "エディターを表示",
|
||||||
|
"show_preview": "プレビューを表示",
|
||||||
|
"title": "署名",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "中央揃え",
|
||||||
"italic": "Italic",
|
"align_left": "左揃え",
|
||||||
"underline": "Underline",
|
"align_right": "右揃え",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "太字",
|
||||||
"link": "Link",
|
"bullet_list": "箇条書き",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "斜体",
|
||||||
"ordered_list": "Ordered List",
|
"link": "リンク",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "番号付きリスト",
|
||||||
"alignment": "Alignment",
|
"remove_color": "色を解除",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "取り消し線",
|
||||||
"align_center": "Align center",
|
"text_color": "文字色",
|
||||||
"align_left": "Align left",
|
"underline": "下線"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "全体のデフォルトを使用",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "署名({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+167
-242
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "복사하지 못했습니다"
|
"copy_failed": "복사하지 못했습니다"
|
||||||
},
|
},
|
||||||
"send_now": "지금 보내기",
|
"send_now": "지금 보내기",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "일정 만들기"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "읽음 확인 요청됨 (클릭하여 해제)",
|
"read_receipt_on": "읽음 확인 요청됨 (클릭하여 해제)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "크기 선택"
|
"pick_size": "크기 선택"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "보냈지만 전송 후 정리에 실패했습니다. 오래된 임시 보관 메일이 남아 있을 수 있습니다.",
|
"send_filing_warning": "보냈지만 전송 후 정리에 실패했습니다. 오래된 임시 보관 메일이 남아 있을 수 있습니다.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "서명 삽입",
|
||||||
"no_signature": "No signature",
|
"no_signature": "서명 없음",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "서명 선택"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "확인",
|
"confirm": "확인",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "콘텐츠 및 발신자",
|
"content_senders": "콘텐츠 및 발신자",
|
||||||
"about_data": "정보 및 데이터",
|
"about_data": "정보 및 데이터",
|
||||||
"debug": "디버그",
|
"debug": "디버그",
|
||||||
"import": "Import",
|
"import": "가져오기",
|
||||||
"sharing": "Sharing",
|
"sharing": "공유",
|
||||||
"signatures": "Signatures"
|
"signatures": "서명"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "일반",
|
"general": "일반",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "관리 중: {name}"
|
"managing": "관리 중: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "가져오기",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "취소",
|
||||||
"file_label": "Select Files",
|
"choose_files": "파일 선택",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "둘 다 유지",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "가져올 메시지가 이미 있을 때 어떻게 처리할지 선택해 주세요.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "중복 처리",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "중복 항목 바꾸기",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "중복 항목 건너뛰기",
|
||||||
"cancel": "Cancel",
|
"description": ".eml 파일에서 이메일 메시지를 폴더로 가져와요.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {오류 1개} other {오류 #개}}",
|
||||||
"fail": "Import failed",
|
"fail": "가져오기 실패",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "가져올 .eml 파일을 하나 이상 선택해 주세요.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "파일",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {파일 1개 선택됨} other {파일 #개 선택됨}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "메시지를 가져올 폴더를 선택해 주세요.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "대상 폴더",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "가져오기 완료",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "더 가져오기",
|
||||||
"action_label": "Action",
|
"importing": "가져오는 중...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count}개 실패",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count}개 가져옴",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count}개 건너뜀",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {파일 1개 가져오기} other {파일 #개 가져오기}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {메시지 1개 가져옴} other {메시지 #개 가져옴}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {메시지 1개 실패} other {메시지 #개 실패}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {메시지 1개 가져옴} other {메시지 #개 가져옴}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {메시지 1개 건너뜀} other {메시지 #개 건너뜀}}",
|
||||||
"progress_failed": "Failed",
|
"title": "메일 가져오기"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "불러오는 중...",
|
||||||
"refresh": "Refresh"
|
"refresh": "새로고침"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "문제가 발생했어요",
|
"page_error_title": "문제가 발생했어요",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "폴더 이름",
|
"placeholder_folder_name": "폴더 이름",
|
||||||
"create": "만들기",
|
"create": "만들기",
|
||||||
"rename_confirm": "이름 바꾸기",
|
"rename_confirm": "이름 바꾸기",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "폴더 공유..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "단축키",
|
"title": "단축키",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"creating": "만드는 중...",
|
"creating": "만드는 중...",
|
||||||
"updating": "업데이트 중...",
|
"updating": "업데이트 중...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "기본 서명",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "서명 매핑",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "답장 서명",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "전역 기본값 사용"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "서브 어드레스 사용",
|
"button_tooltip": "서브 어드레스 사용",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "가져오기 실패",
|
"failed": "가져오기 실패",
|
||||||
"close": "닫기",
|
"close": "닫기",
|
||||||
"file_too_large": "파일이 너무 커요 (최대 5MB)",
|
"file_too_large": "파일이 너무 커요 (최대 5MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "주소",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "주소록",
|
||||||
"csv_back": "Back",
|
"csv_back": "뒤로",
|
||||||
"csv_city": "City",
|
"csv_city": "도시",
|
||||||
"csv_company": "Company",
|
"csv_company": "회사",
|
||||||
"csv_country": "Country",
|
"csv_country": "국가",
|
||||||
"csv_email": "Email",
|
"csv_email": "이메일",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "이름",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "이 열 무시",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "직책",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "성",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "전체 불러오기",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "열 매핑",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "별명",
|
||||||
"csv_note": "Note",
|
"csv_note": "메모",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "전화번호",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "우편번호",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "미리보기",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "미리보기 ({count, plural, one {1행} other {#행}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "주/지역",
|
||||||
"csv_website": "Website",
|
"csv_website": "웹사이트",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv 파일"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "연락처 내보내기",
|
"title": "연락처 내보내기",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "사진 있음"
|
"has_photo": "사진 있음"
|
||||||
},
|
},
|
||||||
"open_categories": "카테고리 열기",
|
"open_categories": "카테고리 열기",
|
||||||
"delete": "Delete Contact",
|
"delete": "삭제",
|
||||||
"edit": "Edit Contact",
|
"edit": "수정",
|
||||||
"send_email": "Send Email"
|
"send_email": "이메일 보내기"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "캘린더",
|
"title": "캘린더",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "메뉴 열기",
|
"nav_open_menu": "메뉴 열기",
|
||||||
|
"delete": "삭제",
|
||||||
|
"duplicate": "복제",
|
||||||
|
"edit": "수정",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "바쁨",
|
||||||
"check": "Check Availability",
|
"check": "가능 여부 확인",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "빈 시간을 클릭해서 이 시간을 선택하세요.",
|
||||||
"loading": "Loading...",
|
"free": "한가함",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "가능 여부 숨기기",
|
||||||
"timezone": "Timezone",
|
"loading": "불러오는 중...",
|
||||||
"free": "Free",
|
"no_participants": "참석자를 추가하면 가능 여부를 확인할 수 있어요.",
|
||||||
"busy": "Busy",
|
"tentative": "미정",
|
||||||
"tentative": "Tentative",
|
"timezone": "시간대",
|
||||||
"unavailable": "Out of office",
|
"title": "가능 여부",
|
||||||
"unknown": "No information",
|
"unavailable": "부재중",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "정보 없음"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "모두 지우기",
|
||||||
"hide": "Hide resources",
|
"filter_all": "전체",
|
||||||
"filter_all": "All",
|
"hide": "리소스 숨기기",
|
||||||
"type_room": "Rooms",
|
"no_resources": "사용 가능한 리소스가 없어요",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "{name} 제거",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "리소스 검색...",
|
||||||
"type_other": "Other",
|
"title": "리소스",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "장비",
|
||||||
"no_resources": "No resources available",
|
"type_other": "기타",
|
||||||
"remove": "Remove {name}",
|
"type_room": "회의실",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "차량"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "\"{name}\" 공유",
|
|
||||||
"description": "이 서버의 다른 사용자나 그룹에 액세스 권한을 부여합니다. 변경 사항은 즉시 적용됩니다.",
|
|
||||||
"no_shares": "아직 공유되지 않았습니다.",
|
|
||||||
"add_person": "사용자 또는 그룹 추가",
|
|
||||||
"search_placeholder": "이름 또는 이메일로 검색…",
|
|
||||||
"loading_principals": "사용자 불러오는 중…",
|
|
||||||
"no_principals": "다른 사용자나 그룹을 찾을 수 없습니다.",
|
|
||||||
"no_match": "일치하는 항목이 없습니다.",
|
|
||||||
"remove": "액세스 권한 제거",
|
|
||||||
"group": "그룹",
|
|
||||||
"share_added": "액세스 권한이 부여되었습니다",
|
|
||||||
"share_updated": "액세스 권한이 업데이트되었습니다",
|
|
||||||
"share_removed": "액세스 권한이 제거되었습니다",
|
|
||||||
"share_failed": "공유 업데이트에 실패했습니다",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "한가함/바쁨만",
|
|
||||||
"read": "읽기 전용",
|
|
||||||
"readWrite": "읽기 및 쓰기",
|
|
||||||
"manager": "관리자",
|
|
||||||
"custom": "사용자 지정"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "상세 검색",
|
"title": "상세 검색",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "다른 계정",
|
"other_accounts": "다른 계정",
|
||||||
"migration_title": "파일 업데이트 중…",
|
"migration_title": "파일 업데이트 중…",
|
||||||
"migration_description": "폴더와 파일을 올바른 구조로 정리하고 있습니다. 이 작업은 한 번만 수행됩니다.",
|
"migration_description": "폴더와 파일을 올바른 구조로 정리하고 있습니다. 이 작업은 한 번만 수행됩니다.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "첨부 파일로 보내기"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "내 인증서",
|
"your_certificates": "내 인증서",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "통합 보기에서는 검색을 사용할 수 없습니다"
|
"search_unavailable": "통합 보기에서는 검색을 사용할 수 없습니다"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "\"{name}\" 공유",
|
||||||
|
"description": "이 서버의 다른 사용자나 그룹에 액세스 권한을 부여합니다. 변경 사항은 즉시 적용됩니다.",
|
||||||
|
"no_shares": "아직 공유되지 않았습니다.",
|
||||||
|
"add_person": "사용자 또는 그룹 추가",
|
||||||
|
"search_placeholder": "이름 또는 이메일로 검색…",
|
||||||
|
"loading_principals": "사용자 불러오는 중…",
|
||||||
|
"no_principals": "다른 사용자나 그룹을 찾을 수 없습니다.",
|
||||||
|
"no_match": "일치하는 항목이 없습니다.",
|
||||||
|
"remove": "액세스 권한 제거",
|
||||||
|
"group": "그룹",
|
||||||
|
"share_added": "액세스 권한이 부여되었습니다",
|
||||||
|
"share_updated": "액세스 권한이 업데이트되었습니다",
|
||||||
|
"share_removed": "액세스 권한이 제거되었습니다",
|
||||||
|
"share_failed": "공유 업데이트에 실패했습니다",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "한가함/바쁨만",
|
||||||
|
"read": "읽기 전용",
|
||||||
|
"readWrite": "읽기 및 쓰기",
|
||||||
|
"manager": "관리자",
|
||||||
|
"custom": "사용자 지정"
|
||||||
|
},
|
||||||
|
"accept": "수락",
|
||||||
|
"decline": "거절",
|
||||||
|
"no_shares_by_me": "아직 공유한 항목이 없습니다.",
|
||||||
|
"no_shares_with_me": "아직 공유받은 폴더가 없습니다.",
|
||||||
|
"shared_by": "공유한 사람",
|
||||||
|
"tab_shared_by_me": "내가 공유함",
|
||||||
|
"tab_shared_with_me": "나와 공유됨"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "{date}에 {from}님이 작성:",
|
"reply_line": "{date}에 {from}님이 작성:",
|
||||||
"forwarded_separator": "---------- 전달된 메시지 ----------",
|
"forwarded_separator": "---------- 전달된 메시지 ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "설치 프롬프트 닫기"
|
"dismiss_aria": "설치 프롬프트 닫기"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "서명 추가",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "기본",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "발신자별로 다르게 설정하지 않으면 새 메시지에 사용돼요.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "기본 서명"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "정말 \"{name}\"을(를) 삭제할까요? 이 작업은 되돌릴 수 없어요.",
|
||||||
"label": "Default for replies",
|
"delete_title": "서명을 삭제할까요?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "메일을 작성하거나 답장할 때 사용할 서명을 만들고 관리해 보세요.",
|
||||||
},
|
"duplicate": "복제",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "서명 수정",
|
||||||
|
"editor_label": "서명",
|
||||||
|
"html_preview_label": "HTML 미리보기",
|
||||||
|
"name_label": "이름",
|
||||||
|
"name_placeholder": "예: 업무, 개인",
|
||||||
|
"name_required": "이름을 입력해 주세요",
|
||||||
|
"new_signature": "새 서명",
|
||||||
|
"no_signature": "서명 없음",
|
||||||
|
"no_signatures": "아직 서명이 없어요",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "발신자별로 기본 서명과 답장 서명을 다르게 설정할 수 있어요.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "발신자별 서명"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "일반 텍스트 미리보기",
|
||||||
"select_signature": "Select signature",
|
"reply": "답장",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "발신자별로 다르게 설정하지 않으면 답장하거나 전달할 때 사용돼요.",
|
||||||
|
"label": "답장 서명"
|
||||||
|
},
|
||||||
|
"show_editor": "편집기 표시",
|
||||||
|
"show_preview": "미리보기 표시",
|
||||||
|
"title": "서명",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "가운데 정렬",
|
||||||
"italic": "Italic",
|
"align_left": "왼쪽 정렬",
|
||||||
"underline": "Underline",
|
"align_right": "오른쪽 정렬",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "굵게",
|
||||||
"link": "Link",
|
"bullet_list": "글머리 기호 목록",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "기울임꼴",
|
||||||
"ordered_list": "Ordered List",
|
"link": "링크",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "번호 매기기 목록",
|
||||||
"alignment": "Alignment",
|
"remove_color": "색 제거",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "취소선",
|
||||||
"align_center": "Align center",
|
"text_color": "글자 색",
|
||||||
"align_left": "Align left",
|
"underline": "밑줄"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "전역 기본값 사용",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "내 서명 ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+167
-242
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Neizdevās nokopēt"
|
"copy_failed": "Neizdevās nokopēt"
|
||||||
},
|
},
|
||||||
"send_now": "Sūtīt tagad",
|
"send_now": "Sūtīt tagad",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Izveidot pasākumu"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Pieprasīts lasīšanas apstiprinājums (noklikšķiniet, lai atspējotu)",
|
"read_receipt_on": "Pieprasīts lasīšanas apstiprinājums (noklikšķiniet, lai atspējotu)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Izvēlēties izmēru"
|
"pick_size": "Izvēlēties izmēru"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Nosūtīts - bet pēcapstrāde neizdevās, var palikt novecojis melnraksts.",
|
"send_filing_warning": "Nosūtīts - bet pēcapstrāde neizdevās, var palikt novecojis melnraksts.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Ievietot parakstu",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Nav paraksta",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Izvēlēties parakstu"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Apstiprināt",
|
"confirm": "Apstiprināt",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "Saturs un sūtītāji",
|
"content_senders": "Saturs un sūtītāji",
|
||||||
"about_data": "Par un dati",
|
"about_data": "Par un dati",
|
||||||
"debug": "Atkļūdošana",
|
"debug": "Atkļūdošana",
|
||||||
"import": "Import",
|
"import": "Imports",
|
||||||
"sharing": "Sharing",
|
"sharing": "Koplietošana",
|
||||||
"signatures": "Signatures"
|
"signatures": "Paraksti"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Vispārīgi",
|
"general": "Vispārīgi",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Pārvalda: {name}"
|
"managing": "Pārvalda: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importēt",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Atcelt",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Izvēlēties failus",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Saglabāt abus",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Izvēlieties, kas jādara, ja importētais ziņojums jau pastāv.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Dublikātu apstrāde",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Aizstāt dublikātus",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Izlaist dublikātus",
|
||||||
"cancel": "Cancel",
|
"description": "Importējiet e-pasta ziņojumus no .eml failiem izvēlētajā mapē.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# kļūda} other {# kļūdas}}",
|
||||||
"fail": "Import failed",
|
"fail": "Imports neizdevās",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Izvēlieties vienu vai vairākus .eml failus importēšanai.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Faili",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {Izvēlēts # fails} other {Izvēlēti # faili}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Izvēlieties mapi, kurā importēt ziņojumus.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Mērķa mape",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Imports pabeigts",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importēt vēl",
|
||||||
"action_label": "Action",
|
"importing": "Importē...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} neizdevās",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importēti",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} izlaisti",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importēt # failu} other {Importēt # failus}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# ziņojums importēts} other {# ziņojumi importēti}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# ziņojums neizdevās} other {# ziņojumi neizdevās}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# ziņojums importēts} other {# ziņojumi importēti}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# ziņojums izlaists} other {# ziņojumi izlaisti}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Importēt pastu"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Ielādē...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Atsvaidzināt"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Kaut kas nogāja griezi",
|
"page_error_title": "Kaut kas nogāja griezi",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Mapes nosaukums",
|
"placeholder_folder_name": "Mapes nosaukums",
|
||||||
"create": "Izveidot",
|
"create": "Izveidot",
|
||||||
"rename_confirm": "Pārsaukt",
|
"rename_confirm": "Pārsaukt",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Kopīgot mapi..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Īsinājumtaustiņi",
|
"title": "Īsinājumtaustiņi",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Atcelt",
|
"cancel": "Atcelt",
|
||||||
"creating": "Izveido...",
|
"creating": "Izveido...",
|
||||||
"updating": "Atjaunina...",
|
"updating": "Atjaunina...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Noklusējuma paraksts",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Paraksta piesaiste",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Atbildes paraksts",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Izmantot globālo noklusējumu"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Izmantot apakšadresi",
|
"button_tooltip": "Izmantot apakšadresi",
|
||||||
@@ -2553,28 +2551,28 @@
|
|||||||
"failed": "Imports neizdevās",
|
"failed": "Imports neizdevās",
|
||||||
"close": "Aizvērt",
|
"close": "Aizvērt",
|
||||||
"file_too_large": "Fails ir pārāk liels (maks. 5 MB)",
|
"file_too_large": "Fails ir pārāk liels (maks. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adrese",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Adrešu grāmata",
|
||||||
"csv_back": "Back",
|
"csv_back": "Atpakaļ",
|
||||||
"csv_city": "City",
|
"csv_city": "Pilsēta",
|
||||||
"csv_company": "Company",
|
"csv_company": "Uzņēmums",
|
||||||
"csv_country": "Country",
|
"csv_country": "Valsts",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-pasts",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Vārds",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignorēt šo kolonnu",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Amats",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Uzvārds",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Ielādēt visu",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Piesaistīt kolonnas",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Segvārds",
|
||||||
"csv_note": "Note",
|
"csv_note": "Piezīme",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Tālrunis",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Pasta indekss",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Priekšskatījums",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Priekšskatījums ({count, plural, one {# rinda} other {# rindas}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Novads/reģions",
|
||||||
"csv_website": "Website",
|
"csv_website": "Tīmekļa vietne",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv faili"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Kontaktu eksports",
|
"title": "Kontaktu eksports",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Ar foto"
|
"has_photo": "Ar foto"
|
||||||
},
|
},
|
||||||
"open_categories": "Atvērt kategorijas",
|
"open_categories": "Atvērt kategorijas",
|
||||||
"delete": "Delete Contact",
|
"delete": "Dzēst",
|
||||||
"edit": "Edit Contact",
|
"edit": "Rediģēt",
|
||||||
"send_email": "Send Email"
|
"send_email": "Sūtīt e-pastu"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalendārs",
|
"title": "Kalendārs",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Atvērt izvēlni",
|
"nav_open_menu": "Atvērt izvēlni",
|
||||||
|
"delete": "Dzēst",
|
||||||
|
"duplicate": "Dublēt",
|
||||||
|
"edit": "Rediģēt",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Aizņemts",
|
||||||
"check": "Check Availability",
|
"check": "Pārbaudīt pieejamību",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Noklikšķiniet uz brīva laika, lai izvēlētos šo laiku",
|
||||||
"loading": "Loading...",
|
"free": "Brīvs",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Slēpt pieejamību",
|
||||||
"timezone": "Timezone",
|
"loading": "Ielādē...",
|
||||||
"free": "Free",
|
"no_participants": "Pievienojiet dalībniekus, lai pārbaudītu pieejamību.",
|
||||||
"busy": "Busy",
|
"tentative": "Pagaidām",
|
||||||
"tentative": "Tentative",
|
"timezone": "Laika josla",
|
||||||
"unavailable": "Out of office",
|
"title": "Pieejamība",
|
||||||
"unknown": "No information",
|
"unavailable": "Prombūtnē",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Nav informācijas"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Notīrīt visu",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Visi",
|
||||||
"filter_all": "All",
|
"hide": "Slēpt resursus",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Resursi nav pieejami",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Noņemt {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Meklēt resursus...",
|
||||||
"type_other": "Other",
|
"title": "Resursi",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Aprīkojums",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Cits",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Telpas",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Transportlīdzekļi"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Kopīgot \"{name}\"",
|
|
||||||
"description": "Piešķiriet piekļuvi citiem lietotājiem vai grupām šajā serverī. Izmaiņas stājas spēkā nekavējoties.",
|
|
||||||
"no_shares": "Vēl nav kopīgots.",
|
|
||||||
"add_person": "Pievienot personu vai grupu",
|
|
||||||
"search_placeholder": "Meklēt pēc vārda vai e-pasta…",
|
|
||||||
"loading_principals": "Ielādē lietotājus…",
|
|
||||||
"no_principals": "Citi lietotāji vai grupas nav atrastas.",
|
|
||||||
"no_match": "Nav atbilstību.",
|
|
||||||
"remove": "Noņemt piekļuvi",
|
|
||||||
"group": "Grupa",
|
|
||||||
"share_added": "Piekļuve piešķirta",
|
|
||||||
"share_updated": "Piekļuve atjaunināta",
|
|
||||||
"share_removed": "Piekļuve noņemta",
|
|
||||||
"share_failed": "Neizdevās atjaunināt kopīgošanu",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Tikai brīvs/aizņemts",
|
|
||||||
"read": "Tikai lasīšana",
|
|
||||||
"readWrite": "Lasīšana un rakstīšana",
|
|
||||||
"manager": "Pārvaldnieks",
|
|
||||||
"custom": "Pielāgots"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Izvērstā meklēšana",
|
"title": "Izvērstā meklēšana",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Citi konti",
|
"other_accounts": "Citi konti",
|
||||||
"migration_title": "Notiek jūsu failu atjaunināšana…",
|
"migration_title": "Notiek jūsu failu atjaunināšana…",
|
||||||
"migration_description": "Mapes un faili tiek sakārtoti pareizajā struktūrā. Tas notiek tikai vienu reizi.",
|
"migration_description": "Mapes un faili tiek sakārtoti pareizajā struktūrā. Tas notiek tikai vienu reizi.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Nosūtīt kā pielikumu"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Jūsu sertifikāti",
|
"your_certificates": "Jūsu sertifikāti",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "Meklēšana nav pieejama apvienotajā skatā"
|
"search_unavailable": "Meklēšana nav pieejama apvienotajā skatā"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Kopīgot \"{name}\"",
|
||||||
|
"description": "Piešķiriet piekļuvi citiem lietotājiem vai grupām šajā serverī. Izmaiņas stājas spēkā nekavējoties.",
|
||||||
|
"no_shares": "Vēl nav kopīgots.",
|
||||||
|
"add_person": "Pievienot personu vai grupu",
|
||||||
|
"search_placeholder": "Meklēt pēc vārda vai e-pasta…",
|
||||||
|
"loading_principals": "Ielādē lietotājus…",
|
||||||
|
"no_principals": "Citi lietotāji vai grupas nav atrastas.",
|
||||||
|
"no_match": "Nav atbilstību.",
|
||||||
|
"remove": "Noņemt piekļuvi",
|
||||||
|
"group": "Grupa",
|
||||||
|
"share_added": "Piekļuve piešķirta",
|
||||||
|
"share_updated": "Piekļuve atjaunināta",
|
||||||
|
"share_removed": "Piekļuve noņemta",
|
||||||
|
"share_failed": "Neizdevās atjaunināt kopīgošanu",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Tikai brīvs/aizņemts",
|
||||||
|
"read": "Tikai lasīšana",
|
||||||
|
"readWrite": "Lasīšana un rakstīšana",
|
||||||
|
"manager": "Pārvaldnieks",
|
||||||
|
"custom": "Pielāgots"
|
||||||
|
},
|
||||||
|
"accept": "Pieņemt",
|
||||||
|
"decline": "Noraidīt",
|
||||||
|
"no_shares_by_me": "Jūs vēl neko neesat kopīgojis.",
|
||||||
|
"no_shares_with_me": "Ar jums vēl nav kopīgota neviena mape.",
|
||||||
|
"shared_by": "Kopīgoja",
|
||||||
|
"tab_shared_by_me": "Manis kopīgots",
|
||||||
|
"tab_shared_with_me": "Kopīgots ar mani"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "{date} {from} rakstīja:",
|
"reply_line": "{date} {from} rakstīja:",
|
||||||
"forwarded_separator": "---------- Pārsūtītā ziņa ----------",
|
"forwarded_separator": "---------- Pārsūtītā ziņa ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Aizvērt instalēšanas paziņojumu"
|
"dismiss_aria": "Aizvērt instalēšanas paziņojumu"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Pievienot parakstu",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Noklusējuma",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Tiek izmantots jauniem ziņojumiem, ja vien tas nav pārrakstīts katrai identitātei atsevišķi.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Noklusējuma paraksts"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Vai tiešām vēlaties dzēst \"{name}\"? Šo darbību nevar atcelt.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Dzēst parakstu?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Izveidojiet un pārvaldiet e-pasta parakstus, ko izmantot, rakstot vai atbildot uz vēstulēm.",
|
||||||
},
|
"duplicate": "Dublēt",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Rediģēt parakstu",
|
||||||
|
"editor_label": "Paraksts",
|
||||||
|
"html_preview_label": "HTML priekšskatījums",
|
||||||
|
"name_label": "Nosaukums",
|
||||||
|
"name_placeholder": "piem., Darbs, Personīgi",
|
||||||
|
"name_required": "Nosaukums ir obligāts",
|
||||||
|
"new_signature": "Jauns paraksts",
|
||||||
|
"no_signature": "Nav paraksta",
|
||||||
|
"no_signatures": "Paraksti vēl nav izveidoti",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Pārrakstiet noklusējuma un atbildes parakstu atsevišķām identitātēm.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Paraksti pa identitātēm"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Vienkāršā teksta priekšskatījums",
|
||||||
"select_signature": "Select signature",
|
"reply": "Atbildes",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Tiek izmantots, atbildot vai pārsūtot, ja vien tas nav pārrakstīts katrai identitātei atsevišķi.",
|
||||||
|
"label": "Atbildes paraksts"
|
||||||
|
},
|
||||||
|
"show_editor": "Rādīt redaktoru",
|
||||||
|
"show_preview": "Rādīt priekšskatījumu",
|
||||||
|
"title": "Paraksti",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Centrēt",
|
||||||
"italic": "Italic",
|
"align_left": "Līdzināt pa kreisi",
|
||||||
"underline": "Underline",
|
"align_right": "Līdzināt pa labi",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Treknraksts",
|
||||||
"link": "Link",
|
"bullet_list": "Aizzīmju saraksts",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Kursīvs",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Saite",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Numurēts saraksts",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Noņemt krāsu",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Pārsvītrots",
|
||||||
"align_center": "Align center",
|
"text_color": "Teksta krāsa",
|
||||||
"align_left": "Align left",
|
"underline": "Pasvītrots"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Izmantot globālo noklusējumu",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Jūsu paraksti ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-240
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Kopiëren mislukt"
|
"copy_failed": "Kopiëren mislukt"
|
||||||
},
|
},
|
||||||
"send_now": "Nu verzenden",
|
"send_now": "Nu verzenden",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Afspraak maken"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Leesbevestiging aangevraagd (klik om uit te schakelen)",
|
"read_receipt_on": "Leesbevestiging aangevraagd (klik om uit te schakelen)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Grootte kiezen"
|
"pick_size": "Grootte kiezen"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Verzonden - maar het opruimen daarna is mislukt, mogelijk blijft een oud concept staan.",
|
"send_filing_warning": "Verzonden - maar het opruimen daarna is mislukt, mogelijk blijft een oud concept staan.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Handtekening invoegen",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Geen handtekening",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Handtekening selecteren"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Bevestigen",
|
"confirm": "Bevestigen",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "Inhoud en afzenders",
|
"content_senders": "Inhoud en afzenders",
|
||||||
"about_data": "Over en gegevens",
|
"about_data": "Over en gegevens",
|
||||||
"debug": "Debuggen",
|
"debug": "Debuggen",
|
||||||
"import": "Import",
|
"import": "Importeren",
|
||||||
"sharing": "Sharing",
|
"sharing": "Delen",
|
||||||
"signatures": "Signatures"
|
"signatures": "Handtekeningen"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Algemeen",
|
"general": "Algemeen",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Beheren: {name}"
|
"managing": "Beheren: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importeren",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Annuleren",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Bestanden kiezen",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Beide behouden",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Kies wat er moet gebeuren als een geïmporteerd bericht al bestaat.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Omgaan met duplicaten",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Duplicaten vervangen",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Duplicaten overslaan",
|
||||||
"cancel": "Cancel",
|
"description": "Importeer e-mailberichten uit .eml-bestanden in een map.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# fout} other {# fouten}}",
|
||||||
"fail": "Import failed",
|
"fail": "Importeren mislukt",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Selecteer een of meer .eml-bestanden om te importeren.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Bestanden",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# bestand geselecteerd} other {# bestanden geselecteerd}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Kies de map waarin de berichten worden geïmporteerd.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Doelmap",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Importeren voltooid",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Meer importeren",
|
||||||
"action_label": "Action",
|
"importing": "Importeren...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} mislukt",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} geïmporteerd",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} overgeslagen",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {# bestand importeren} other {# bestanden importeren}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# bericht geïmporteerd} other {# berichten geïmporteerd}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# bericht mislukt} other {# berichten mislukt}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# bericht geïmporteerd} other {# berichten geïmporteerd}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# bericht overgeslagen} other {# berichten overgeslagen}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Mail importeren"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Laden...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Vernieuwen"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Er is iets misgegaan",
|
"page_error_title": "Er is iets misgegaan",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Mapnaam",
|
"placeholder_folder_name": "Mapnaam",
|
||||||
"create": "Aanmaken",
|
"create": "Aanmaken",
|
||||||
"rename_confirm": "Hernoemen",
|
"rename_confirm": "Hernoemen",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Map delen..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Sneltoetsen",
|
"title": "Sneltoetsen",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
"creating": "Aanmaken...",
|
"creating": "Aanmaken...",
|
||||||
"updating": "Bijwerken...",
|
"updating": "Bijwerken...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Standaardhandtekening",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Handtekeningtoewijzing",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Antwoordhandtekening",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Algemene standaardinstelling gebruiken"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Sub-adres gebruiken",
|
"button_tooltip": "Sub-adres gebruiken",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Import mislukt",
|
"failed": "Import mislukt",
|
||||||
"close": "Sluiten",
|
"close": "Sluiten",
|
||||||
"file_too_large": "Bestand is te groot (max 5 MB)",
|
"file_too_large": "Bestand is te groot (max 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adres",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Adresboek",
|
||||||
"csv_back": "Back",
|
"csv_back": "Terug",
|
||||||
"csv_city": "City",
|
"csv_city": "Plaats",
|
||||||
"csv_company": "Company",
|
"csv_company": "Bedrijf",
|
||||||
"csv_country": "Country",
|
"csv_country": "Land",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Voornaam",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Deze kolom negeren",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Functietitel",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Achternaam",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Alles laden",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Kolommen koppelen",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Bijnaam",
|
||||||
"csv_note": "Note",
|
"csv_note": "Notitie",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefoon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Postcode",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Voorbeeld",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Voorbeeld ({count, plural, one {# rij} other {# rijen}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Staat/Regio",
|
||||||
"csv_website": "Website",
|
"csv_website": "Website",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv-bestanden"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Contacten exporteren",
|
"title": "Contacten exporteren",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Met foto"
|
"has_photo": "Met foto"
|
||||||
},
|
},
|
||||||
"open_categories": "Categorieën openen",
|
"open_categories": "Categorieën openen",
|
||||||
"delete": "Delete Contact",
|
"delete": "Verwijderen",
|
||||||
"edit": "Edit Contact",
|
"edit": "Bewerken",
|
||||||
"send_email": "Send Email"
|
"send_email": "E-mail verzenden"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Agenda",
|
"title": "Agenda",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Menu openen",
|
"nav_open_menu": "Menu openen",
|
||||||
|
"delete": "Verwijderen",
|
||||||
|
"duplicate": "Dupliceren",
|
||||||
|
"edit": "Bewerken",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Bezet",
|
||||||
"check": "Check Availability",
|
"check": "Beschikbaarheid controleren",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Klik op een vrij tijdslot om deze tijd te selecteren",
|
||||||
"loading": "Loading...",
|
"free": "Vrij",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Beschikbaarheid verbergen",
|
||||||
"timezone": "Timezone",
|
"loading": "Laden...",
|
||||||
"free": "Free",
|
"no_participants": "Voeg deelnemers toe om de beschikbaarheid te controleren.",
|
||||||
"busy": "Busy",
|
"tentative": "Voorlopig",
|
||||||
"tentative": "Tentative",
|
"timezone": "Tijdzone",
|
||||||
"unavailable": "Out of office",
|
"title": "Beschikbaarheid",
|
||||||
"unknown": "No information",
|
"unavailable": "Afwezig",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Geen informatie"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Alles wissen",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Alle",
|
||||||
"filter_all": "All",
|
"hide": "Hulpbronnen verbergen",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Geen hulpbronnen beschikbaar",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "{name} verwijderen",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Hulpbronnen zoeken...",
|
||||||
"type_other": "Other",
|
"title": "Hulpbronnen",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Apparatuur",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Overig",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Ruimtes",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Voertuigen"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "\"{name}\" delen",
|
|
||||||
"description": "Geef andere gebruikers of groepen op deze server toegang. Wijzigingen zijn direct van kracht.",
|
|
||||||
"no_shares": "Nog niet gedeeld.",
|
|
||||||
"add_person": "Persoon of groep toevoegen",
|
|
||||||
"search_placeholder": "Zoeken op naam of e-mail…",
|
|
||||||
"loading_principals": "Gebruikers laden…",
|
|
||||||
"no_principals": "Geen andere gebruikers of groepen gevonden.",
|
|
||||||
"no_match": "Geen overeenkomsten.",
|
|
||||||
"remove": "Toegang intrekken",
|
|
||||||
"group": "Groep",
|
|
||||||
"share_added": "Toegang verleend",
|
|
||||||
"share_updated": "Toegang bijgewerkt",
|
|
||||||
"share_removed": "Toegang ingetrokken",
|
|
||||||
"share_failed": "Delen kon niet worden bijgewerkt",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Alleen vrij/bezet",
|
|
||||||
"read": "Alleen lezen",
|
|
||||||
"readWrite": "Lezen en schrijven",
|
|
||||||
"manager": "Beheerder",
|
|
||||||
"custom": "Aangepast"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Geavanceerd zoeken",
|
"title": "Geavanceerd zoeken",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Andere accounts",
|
"other_accounts": "Andere accounts",
|
||||||
"migration_title": "Je bestanden worden bijgewerkt…",
|
"migration_title": "Je bestanden worden bijgewerkt…",
|
||||||
"migration_description": "Mappen en bestanden worden in de juiste structuur georganiseerd. Dit gebeurt slechts één keer.",
|
"migration_description": "Mappen en bestanden worden in de juiste structuur georganiseerd. Dit gebeurt slechts één keer.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Als bijlage verzenden"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Uw certificaten",
|
"your_certificates": "Uw certificaten",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "Zoeken is niet beschikbaar in de gecombineerde weergave"
|
"search_unavailable": "Zoeken is niet beschikbaar in de gecombineerde weergave"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "\"{name}\" delen",
|
||||||
|
"description": "Geef andere gebruikers of groepen op deze server toegang. Wijzigingen zijn direct van kracht.",
|
||||||
|
"no_shares": "Nog niet gedeeld.",
|
||||||
|
"add_person": "Persoon of groep toevoegen",
|
||||||
|
"search_placeholder": "Zoeken op naam of e-mail…",
|
||||||
|
"loading_principals": "Gebruikers laden…",
|
||||||
|
"no_principals": "Geen andere gebruikers of groepen gevonden.",
|
||||||
|
"no_match": "Geen overeenkomsten.",
|
||||||
|
"remove": "Toegang intrekken",
|
||||||
|
"group": "Groep",
|
||||||
|
"share_added": "Toegang verleend",
|
||||||
|
"share_updated": "Toegang bijgewerkt",
|
||||||
|
"share_removed": "Toegang ingetrokken",
|
||||||
|
"share_failed": "Delen kon niet worden bijgewerkt",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Alleen vrij/bezet",
|
||||||
|
"read": "Alleen lezen",
|
||||||
|
"readWrite": "Lezen en schrijven",
|
||||||
|
"manager": "Beheerder",
|
||||||
|
"custom": "Aangepast"
|
||||||
|
},
|
||||||
|
"accept": "Accepteren",
|
||||||
|
"decline": "Weigeren",
|
||||||
|
"no_shares_by_me": "Je hebt nog niets gedeeld.",
|
||||||
|
"no_shares_with_me": "Nog geen mappen met je gedeeld.",
|
||||||
|
"shared_by": "Gedeeld door",
|
||||||
|
"tab_shared_by_me": "Gedeeld door mij",
|
||||||
|
"tab_shared_with_me": "Gedeeld met mij"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "Op {date} schreef {from}:",
|
"reply_line": "Op {date} schreef {from}:",
|
||||||
"forwarded_separator": "---------- Doorgestuurd bericht ----------",
|
"forwarded_separator": "---------- Doorgestuurd bericht ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Installatiemelding sluiten"
|
"dismiss_aria": "Installatiemelding sluiten"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Handtekening toevoegen",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Standaard",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Gebruikt voor nieuwe berichten, tenzij dit per identiteit is overschreven.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Standaardhandtekening"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Weet je zeker dat je \"{name}\" wilt verwijderen? Dit kan niet ongedaan worden gemaakt.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Handtekening verwijderen?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Maak en beheer e-mailhandtekeningen om te gebruiken bij het opstellen of beantwoorden van berichten.",
|
||||||
},
|
"duplicate": "Dupliceren",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Handtekening bewerken",
|
||||||
|
"editor_label": "Handtekening",
|
||||||
|
"html_preview_label": "HTML-voorbeeld",
|
||||||
|
"name_label": "Naam",
|
||||||
|
"name_placeholder": "bijv. Werk, Persoonlijk",
|
||||||
|
"name_required": "Naam is vereist",
|
||||||
|
"new_signature": "Nieuwe handtekening",
|
||||||
|
"no_signature": "Geen handtekening",
|
||||||
|
"no_signatures": "Nog geen handtekeningen",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Overschrijf de standaard- en antwoordhandtekening voor individuele identiteiten.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Handtekeningen per identiteit"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Voorbeeld platte tekst",
|
||||||
"select_signature": "Select signature",
|
"reply": "Antwoord",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Gebruikt bij het beantwoorden of doorsturen, tenzij dit per identiteit is overschreven.",
|
||||||
|
"label": "Antwoordhandtekening"
|
||||||
|
},
|
||||||
|
"show_editor": "Editor tonen",
|
||||||
|
"show_preview": "Voorbeeld tonen",
|
||||||
|
"title": "Handtekeningen",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Centreren",
|
||||||
"italic": "Italic",
|
"align_left": "Links uitlijnen",
|
||||||
"underline": "Underline",
|
"align_right": "Rechts uitlijnen",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Vet",
|
||||||
|
"bullet_list": "Opsommingslijst",
|
||||||
|
"italic": "Cursief",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"bullet_list": "Bullet List",
|
"ordered_list": "Genummerde lijst",
|
||||||
"ordered_list": "Ordered List",
|
"remove_color": "Kleur verwijderen",
|
||||||
"text_color": "Text Color",
|
"strikethrough": "Doorhalen",
|
||||||
"alignment": "Alignment",
|
"text_color": "Tekstkleur",
|
||||||
"font_size": "Font Size",
|
"underline": "Onderstrepen"
|
||||||
"align_center": "Align center",
|
|
||||||
"align_left": "Align left",
|
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Algemene standaardinstelling gebruiken",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Jouw handtekeningen ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-240
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Nie udało się skopiować"
|
"copy_failed": "Nie udało się skopiować"
|
||||||
},
|
},
|
||||||
"send_now": "Wyślij teraz",
|
"send_now": "Wyślij teraz",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Utwórz wydarzenie"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Zażądano potwierdzenia przeczytania (kliknij, aby wyłączyć)",
|
"read_receipt_on": "Zażądano potwierdzenia przeczytania (kliknij, aby wyłączyć)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Wybierz rozmiar"
|
"pick_size": "Wybierz rozmiar"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Wysłano - ale późniejsze porządkowanie nie powiodło się, może pozostać nieaktualna wersja robocza.",
|
"send_filing_warning": "Wysłano - ale późniejsze porządkowanie nie powiodło się, może pozostać nieaktualna wersja robocza.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Wstaw podpis",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Brak podpisu",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Wybierz podpis"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Potwierdź",
|
"confirm": "Potwierdź",
|
||||||
@@ -894,8 +894,8 @@
|
|||||||
"about_data": "O programie i dane",
|
"about_data": "O programie i dane",
|
||||||
"debug": "Debugowanie",
|
"debug": "Debugowanie",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"sharing": "Sharing",
|
"sharing": "Udostępnianie",
|
||||||
"signatures": "Signatures"
|
"signatures": "Podpisy"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Ogólne",
|
"general": "Ogólne",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Zarządzanie: {name}"
|
"managing": "Zarządzanie: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importuj",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Anuluj",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Wybierz pliki",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Zachowaj oba",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Wybierz, co zrobić, gdy importowana wiadomość już istnieje.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Obsługa duplikatów",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Zastąp duplikaty",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Pomiń duplikaty",
|
||||||
"cancel": "Cancel",
|
"description": "Importuj wiadomości e-mail z plików .eml do folderu.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# błąd} other {# błędów}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import nie powiódł się",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Wybierz jeden lub więcej plików .eml do zaimportowania.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Pliki",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# plik wybrany} other {# plików wybranych}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Wybierz folder, do którego mają zostać zaimportowane wiadomości.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Folder docelowy",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import zakończony",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importuj więcej",
|
||||||
"action_label": "Action",
|
"importing": "Importowanie...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} niepowodzeń",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} zaimportowanych",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} pominiętych",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importuj # plik} other {Importuj # plików}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# wiadomość zaimportowana} other {# wiadomości zaimportowanych}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# wiadomość nieudana} other {# wiadomości nieudanych}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# wiadomość zaimportowana} other {# wiadomości zaimportowanych}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# wiadomość pominięta} other {# wiadomości pominiętych}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Import poczty"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Ładowanie...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Odśwież"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Coś poszło nie tak",
|
"page_error_title": "Coś poszło nie tak",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Nazwa folderu",
|
"placeholder_folder_name": "Nazwa folderu",
|
||||||
"create": "Utwórz",
|
"create": "Utwórz",
|
||||||
"rename_confirm": "Zmień nazwę",
|
"rename_confirm": "Zmień nazwę",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Udostępnij folder..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Skróty klawiszowe",
|
"title": "Skróty klawiszowe",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"creating": "Tworzenie...",
|
"creating": "Tworzenie...",
|
||||||
"updating": "Aktualizowanie...",
|
"updating": "Aktualizowanie...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Domyślny podpis",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Przypisanie podpisów",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Podpis odpowiedzi",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Użyj globalnego ustawienia domyślnego"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Użyj podadresu",
|
"button_tooltip": "Użyj podadresu",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Import nie powiódł się",
|
"failed": "Import nie powiódł się",
|
||||||
"close": "Zamknij",
|
"close": "Zamknij",
|
||||||
"file_too_large": "Plik jest za duży (maks. 5 MB)",
|
"file_too_large": "Plik jest za duży (maks. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adres",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Książka adresowa",
|
||||||
"csv_back": "Back",
|
"csv_back": "Wstecz",
|
||||||
"csv_city": "City",
|
"csv_city": "Miasto",
|
||||||
"csv_company": "Company",
|
"csv_company": "Firma",
|
||||||
"csv_country": "Country",
|
"csv_country": "Kraj",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Imię",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignoruj tę kolumnę",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Stanowisko",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Nazwisko",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Wczytaj wszystkie",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Mapuj kolumny",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Pseudonim",
|
||||||
"csv_note": "Note",
|
"csv_note": "Notatka",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Kod pocztowy",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Podgląd",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Podgląd ({count, plural, one {# wiersz} other {# wierszy}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Stan / region",
|
||||||
"csv_website": "Website",
|
"csv_website": "Strona internetowa",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "pliki .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Eksportuj kontakty",
|
"title": "Eksportuj kontakty",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Ze zdjęciem"
|
"has_photo": "Ze zdjęciem"
|
||||||
},
|
},
|
||||||
"open_categories": "Otwórz kategorie",
|
"open_categories": "Otwórz kategorie",
|
||||||
"delete": "Delete Contact",
|
"delete": "Usuń",
|
||||||
"edit": "Edit Contact",
|
"edit": "Edytuj",
|
||||||
"send_email": "Send Email"
|
"send_email": "Wyślij e-mail"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalendarz",
|
"title": "Kalendarz",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Otwórz menu",
|
"nav_open_menu": "Otwórz menu",
|
||||||
|
"delete": "Usuń",
|
||||||
|
"duplicate": "Duplikuj",
|
||||||
|
"edit": "Edytuj",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Zajęty",
|
||||||
"check": "Check Availability",
|
"check": "Sprawdź dostępność",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Kliknij wolny termin, aby wybrać tę godzinę",
|
||||||
"loading": "Loading...",
|
"free": "Wolny",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Ukryj dostępność",
|
||||||
"timezone": "Timezone",
|
"loading": "Ładowanie...",
|
||||||
"free": "Free",
|
"no_participants": "Dodaj uczestników, aby sprawdzić dostępność.",
|
||||||
"busy": "Busy",
|
"tentative": "Wstępnie",
|
||||||
"tentative": "Tentative",
|
"timezone": "Strefa czasowa",
|
||||||
"unavailable": "Out of office",
|
"title": "Dostępność",
|
||||||
"unknown": "No information",
|
"unavailable": "Poza biurem",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Brak informacji"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Wyczyść wszystko",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Wszystkie",
|
||||||
"filter_all": "All",
|
"hide": "Ukryj zasoby",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Brak dostępnych zasobów",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Usuń {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Szukaj zasobów...",
|
||||||
"type_other": "Other",
|
"title": "Zasoby",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Sprzęt",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Inne",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Sale",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Pojazdy"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Udostępnij „{name}\"",
|
|
||||||
"description": "Udziel dostępu innym użytkownikom lub grupom na tym serwerze. Zmiany są natychmiastowe.",
|
|
||||||
"no_shares": "Jeszcze nie udostępniono.",
|
|
||||||
"add_person": "Dodaj osobę lub grupę",
|
|
||||||
"search_placeholder": "Szukaj po imieniu lub e-mailu…",
|
|
||||||
"loading_principals": "Ładowanie użytkowników…",
|
|
||||||
"no_principals": "Nie znaleziono innych użytkowników ani grup.",
|
|
||||||
"no_match": "Brak wyników.",
|
|
||||||
"remove": "Usuń dostęp",
|
|
||||||
"group": "Grupa",
|
|
||||||
"share_added": "Dostęp przyznany",
|
|
||||||
"share_updated": "Dostęp zaktualizowany",
|
|
||||||
"share_removed": "Dostęp usunięty",
|
|
||||||
"share_failed": "Nie udało się zaktualizować udostępniania",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Tylko dostępność",
|
|
||||||
"read": "Tylko do odczytu",
|
|
||||||
"readWrite": "Odczyt i zapis",
|
|
||||||
"manager": "Menedżer",
|
|
||||||
"custom": "Niestandardowe"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Wyszukiwanie zaawansowane",
|
"title": "Wyszukiwanie zaawansowane",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Inne konta",
|
"other_accounts": "Inne konta",
|
||||||
"migration_title": "Aktualizowanie plików…",
|
"migration_title": "Aktualizowanie plików…",
|
||||||
"migration_description": "Porządkowanie folderów i plików w odpowiedniej strukturze. Dzieje się to tylko raz.",
|
"migration_description": "Porządkowanie folderów i plików w odpowiedniej strukturze. Dzieje się to tylko raz.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Wyślij jako załącznik"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Twoje certyfikaty",
|
"your_certificates": "Twoje certyfikaty",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "Wyszukiwanie jest niedostępne w widoku ujednoliconym"
|
"search_unavailable": "Wyszukiwanie jest niedostępne w widoku ujednoliconym"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Udostępnij „{name}\"",
|
||||||
|
"description": "Udziel dostępu innym użytkownikom lub grupom na tym serwerze. Zmiany są natychmiastowe.",
|
||||||
|
"no_shares": "Jeszcze nie udostępniono.",
|
||||||
|
"add_person": "Dodaj osobę lub grupę",
|
||||||
|
"search_placeholder": "Szukaj po imieniu lub e-mailu…",
|
||||||
|
"loading_principals": "Ładowanie użytkowników…",
|
||||||
|
"no_principals": "Nie znaleziono innych użytkowników ani grup.",
|
||||||
|
"no_match": "Brak wyników.",
|
||||||
|
"remove": "Usuń dostęp",
|
||||||
|
"group": "Grupa",
|
||||||
|
"share_added": "Dostęp przyznany",
|
||||||
|
"share_updated": "Dostęp zaktualizowany",
|
||||||
|
"share_removed": "Dostęp usunięty",
|
||||||
|
"share_failed": "Nie udało się zaktualizować udostępniania",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Tylko dostępność",
|
||||||
|
"read": "Tylko do odczytu",
|
||||||
|
"readWrite": "Odczyt i zapis",
|
||||||
|
"manager": "Menedżer",
|
||||||
|
"custom": "Niestandardowe"
|
||||||
|
},
|
||||||
|
"accept": "Akceptuj",
|
||||||
|
"decline": "Odrzuć",
|
||||||
|
"no_shares_by_me": "Nie udostępniono jeszcze niczego.",
|
||||||
|
"no_shares_with_me": "Nie udostępniono Ci jeszcze żadnych folderów.",
|
||||||
|
"shared_by": "Udostępnione przez",
|
||||||
|
"tab_shared_by_me": "Udostępnione przeze mnie",
|
||||||
|
"tab_shared_with_me": "Udostępnione ze mną"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "{date}, {from} napisał(a):",
|
"reply_line": "{date}, {from} napisał(a):",
|
||||||
"forwarded_separator": "---------- Wiadomość przekazana ----------",
|
"forwarded_separator": "---------- Wiadomość przekazana ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Zamknij monit instalacji"
|
"dismiss_aria": "Zamknij monit instalacji"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Dodaj podpis",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Domyślny",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Używany w nowych wiadomościach, chyba że zostanie zastąpiony dla danej tożsamości.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Domyślny podpis"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Czy na pewno chcesz usunąć \"{name}\"? Tej operacji nie można cofnąć.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Usunąć podpis?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Twórz i zarządzaj podpisami e-mail używanymi podczas pisania wiadomości lub odpowiadania na nie.",
|
||||||
},
|
"duplicate": "Duplikuj",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Edytuj podpis",
|
||||||
|
"editor_label": "Podpis",
|
||||||
|
"html_preview_label": "Podgląd HTML",
|
||||||
|
"name_label": "Nazwa",
|
||||||
|
"name_placeholder": "np. Służbowy, Prywatny",
|
||||||
|
"name_required": "Nazwa jest wymagana",
|
||||||
|
"new_signature": "Nowy podpis",
|
||||||
|
"no_signature": "Brak podpisu",
|
||||||
|
"no_signatures": "Nie ma jeszcze żadnych podpisów",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Zastąp domyślny podpis i podpis odpowiedzi dla poszczególnych tożsamości.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Podpisy dla poszczególnych tożsamości"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Podgląd tekstu",
|
||||||
"select_signature": "Select signature",
|
"reply": "Odpowiedź",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Używany podczas odpowiadania lub przekazywania wiadomości dalej, chyba że zostanie zastąpiony dla danej tożsamości.",
|
||||||
|
"label": "Podpis odpowiedzi"
|
||||||
|
},
|
||||||
|
"show_editor": "Pokaż edytor",
|
||||||
|
"show_preview": "Pokaż podgląd",
|
||||||
|
"title": "Podpisy",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Wyśrodkuj",
|
||||||
"italic": "Italic",
|
"align_left": "Wyrównaj do lewej",
|
||||||
"underline": "Underline",
|
"align_right": "Wyrównaj do prawej",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Pogrubienie",
|
||||||
|
"bullet_list": "Lista punktowana",
|
||||||
|
"italic": "Kursywa",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"bullet_list": "Bullet List",
|
"ordered_list": "Lista numerowana",
|
||||||
"ordered_list": "Ordered List",
|
"remove_color": "Usuń kolor",
|
||||||
"text_color": "Text Color",
|
"strikethrough": "Przekreślenie",
|
||||||
"alignment": "Alignment",
|
"text_color": "Kolor tekstu",
|
||||||
"font_size": "Font Size",
|
"underline": "Podkreślenie"
|
||||||
"align_center": "Align center",
|
|
||||||
"align_left": "Align left",
|
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Użyj globalnego ustawienia domyślnego",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Twoje podpisy ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+166
-241
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Falha ao copiar"
|
"copy_failed": "Falha ao copiar"
|
||||||
},
|
},
|
||||||
"send_now": "Enviar agora",
|
"send_now": "Enviar agora",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Criar evento"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Confirmação de leitura solicitada (clique para desativar)",
|
"read_receipt_on": "Confirmação de leitura solicitada (clique para desativar)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Escolher tamanho"
|
"pick_size": "Escolher tamanho"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Enviado - mas a limpeza posterior falhou, um rascunho antigo pode permanecer.",
|
"send_filing_warning": "Enviado - mas a limpeza posterior falhou, um rascunho antigo pode permanecer.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Inserir assinatura",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Sem assinatura",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Selecionar assinatura"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Confirmar",
|
"confirm": "Confirmar",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "Conteúdo e remetentes",
|
"content_senders": "Conteúdo e remetentes",
|
||||||
"about_data": "Sobre e dados",
|
"about_data": "Sobre e dados",
|
||||||
"debug": "Depuração",
|
"debug": "Depuração",
|
||||||
"import": "Import",
|
"import": "Importar",
|
||||||
"sharing": "Sharing",
|
"sharing": "Compartilhamento",
|
||||||
"signatures": "Signatures"
|
"signatures": "Assinaturas"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Geral",
|
"general": "Geral",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Gerenciando: {name}"
|
"managing": "Gerenciando: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importar",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Cancelar",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Escolher arquivos",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Manter ambos",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Escolha o que fazer quando uma mensagem importada já existir.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Tratamento de duplicados",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Substituir duplicados",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Ignorar duplicados",
|
||||||
"cancel": "Cancel",
|
"description": "Importe mensagens de e-mail de arquivos .eml para uma pasta.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# erro} other {# erros}}",
|
||||||
"fail": "Import failed",
|
"fail": "Falha na importação",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Selecione um ou mais arquivos .eml para importar.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Arquivos",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# arquivo selecionado} other {# arquivos selecionados}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Escolha a pasta para a qual importar as mensagens.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Pasta de destino",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Importação concluída",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importar mais",
|
||||||
"action_label": "Action",
|
"importing": "Importando...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} com falha",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importados",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} ignorados",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importar # arquivo} other {Importar # arquivos}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# mensagem importada} other {# mensagens importadas}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# mensagem falhou} other {# mensagens falharam}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# mensagem importada} other {# mensagens importadas}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# mensagem ignorada} other {# mensagens ignoradas}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Importar E-mail"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Carregando...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Atualizar"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Algo deu errado",
|
"page_error_title": "Algo deu errado",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Nome da pasta",
|
"placeholder_folder_name": "Nome da pasta",
|
||||||
"create": "Criar",
|
"create": "Criar",
|
||||||
"rename_confirm": "Renomear",
|
"rename_confirm": "Renomear",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Compartilhar pasta..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Atalhos de Teclado",
|
"title": "Atalhos de Teclado",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"creating": "Criando...",
|
"creating": "Criando...",
|
||||||
"updating": "Atualizando...",
|
"updating": "Atualizando...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Assinatura padrão",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Mapeamento de assinatura",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Assinatura de resposta",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Usar padrão global"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Usar sub-endereço",
|
"button_tooltip": "Usar sub-endereço",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Falha na importação",
|
"failed": "Falha na importação",
|
||||||
"close": "Fechar",
|
"close": "Fechar",
|
||||||
"file_too_large": "Arquivo muito grande (máx. 5 MB)",
|
"file_too_large": "Arquivo muito grande (máx. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Endereço",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Catálogo de endereços",
|
||||||
"csv_back": "Back",
|
"csv_back": "Voltar",
|
||||||
"csv_city": "City",
|
"csv_city": "Cidade",
|
||||||
"csv_company": "Company",
|
"csv_company": "Empresa",
|
||||||
"csv_country": "Country",
|
"csv_country": "País",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Nome",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignorar esta coluna",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Cargo",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Sobrenome",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Carregar tudo",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Mapear colunas",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Apelido",
|
||||||
"csv_note": "Note",
|
"csv_note": "Nota",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefone",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Código postal",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Pré-visualização",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Pré-visualização ({count, plural, one {# linha} other {# linhas}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Estado/Região",
|
||||||
"csv_website": "Website",
|
"csv_website": "Site",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "Arquivos .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Exportar contatos",
|
"title": "Exportar contatos",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Com foto"
|
"has_photo": "Com foto"
|
||||||
},
|
},
|
||||||
"open_categories": "Abrir categorias",
|
"open_categories": "Abrir categorias",
|
||||||
"delete": "Delete Contact",
|
"delete": "Excluir",
|
||||||
"edit": "Edit Contact",
|
"edit": "Editar",
|
||||||
"send_email": "Send Email"
|
"send_email": "Enviar e-mail"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendário",
|
"title": "Calendário",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"overdue": "Atrasada"
|
"overdue": "Atrasada"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Abrir menu",
|
"nav_open_menu": "Abrir menu",
|
||||||
|
"delete": "Excluir",
|
||||||
|
"duplicate": "Duplicar",
|
||||||
|
"edit": "Editar",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Ocupado",
|
||||||
"check": "Check Availability",
|
"check": "Verificar disponibilidade",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Clique em um horário livre para selecionar este horário",
|
||||||
"loading": "Loading...",
|
"free": "Livre",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Ocultar disponibilidade",
|
||||||
"timezone": "Timezone",
|
"loading": "Carregando...",
|
||||||
"free": "Free",
|
"no_participants": "Adicione participantes para verificar a disponibilidade.",
|
||||||
"busy": "Busy",
|
"tentative": "Provisório",
|
||||||
"tentative": "Tentative",
|
"timezone": "Fuso horário",
|
||||||
"unavailable": "Out of office",
|
"title": "Disponibilidade",
|
||||||
"unknown": "No information",
|
"unavailable": "Fora do escritório",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Sem informação"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Limpar tudo",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Todos",
|
||||||
"filter_all": "All",
|
"hide": "Ocultar recursos",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Nenhum recurso disponível",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Remover {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Pesquisar recursos...",
|
||||||
"type_other": "Other",
|
"title": "Recursos",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Equipamento",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Outro",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Salas",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Veículos"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Compartilhar \"{name}\"",
|
|
||||||
"description": "Conceda acesso a outros usuários ou grupos neste servidor. As alterações têm efeito imediato.",
|
|
||||||
"no_shares": "Ainda não compartilhado.",
|
|
||||||
"add_person": "Adicionar pessoa ou grupo",
|
|
||||||
"search_placeholder": "Buscar por nome ou e-mail…",
|
|
||||||
"loading_principals": "Carregando usuários…",
|
|
||||||
"no_principals": "Nenhum outro usuário ou grupo encontrado.",
|
|
||||||
"no_match": "Sem resultados.",
|
|
||||||
"remove": "Remover acesso",
|
|
||||||
"group": "Grupo",
|
|
||||||
"share_added": "Acesso concedido",
|
|
||||||
"share_updated": "Acesso atualizado",
|
|
||||||
"share_removed": "Acesso removido",
|
|
||||||
"share_failed": "Falha ao atualizar o compartilhamento",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Apenas disponibilidade",
|
|
||||||
"read": "Somente leitura",
|
|
||||||
"readWrite": "Leitura e escrita",
|
|
||||||
"manager": "Gerente",
|
|
||||||
"custom": "Personalizado"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Pesquisa avançada",
|
"title": "Pesquisa avançada",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Outras contas",
|
"other_accounts": "Outras contas",
|
||||||
"migration_title": "Atualizando seus arquivos…",
|
"migration_title": "Atualizando seus arquivos…",
|
||||||
"migration_description": "Organizando pastas e arquivos em sua estrutura adequada. Isso acontece apenas uma vez.",
|
"migration_description": "Organizando pastas e arquivos em sua estrutura adequada. Isso acontece apenas uma vez.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Enviar como anexo"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Seus certificados",
|
"your_certificates": "Seus certificados",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "A pesquisa não está disponível na vista unificada"
|
"search_unavailable": "A pesquisa não está disponível na vista unificada"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Compartilhar \"{name}\"",
|
||||||
|
"description": "Conceda acesso a outros usuários ou grupos neste servidor. As alterações têm efeito imediato.",
|
||||||
|
"no_shares": "Ainda não compartilhado.",
|
||||||
|
"add_person": "Adicionar pessoa ou grupo",
|
||||||
|
"search_placeholder": "Buscar por nome ou e-mail…",
|
||||||
|
"loading_principals": "Carregando usuários…",
|
||||||
|
"no_principals": "Nenhum outro usuário ou grupo encontrado.",
|
||||||
|
"no_match": "Sem resultados.",
|
||||||
|
"remove": "Remover acesso",
|
||||||
|
"group": "Grupo",
|
||||||
|
"share_added": "Acesso concedido",
|
||||||
|
"share_updated": "Acesso atualizado",
|
||||||
|
"share_removed": "Acesso removido",
|
||||||
|
"share_failed": "Falha ao atualizar o compartilhamento",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Apenas disponibilidade",
|
||||||
|
"read": "Somente leitura",
|
||||||
|
"readWrite": "Leitura e escrita",
|
||||||
|
"manager": "Gerente",
|
||||||
|
"custom": "Personalizado"
|
||||||
|
},
|
||||||
|
"accept": "Aceitar",
|
||||||
|
"decline": "Recusar",
|
||||||
|
"no_shares_by_me": "Você ainda não compartilhou nada.",
|
||||||
|
"no_shares_with_me": "Nenhuma pasta foi compartilhada com você ainda.",
|
||||||
|
"shared_by": "Compartilhado por",
|
||||||
|
"tab_shared_by_me": "Compartilhado por mim",
|
||||||
|
"tab_shared_with_me": "Compartilhado comigo"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "Em {date}, {from} escreveu:",
|
"reply_line": "Em {date}, {from} escreveu:",
|
||||||
"forwarded_separator": "---------- Mensagem encaminhada ----------",
|
"forwarded_separator": "---------- Mensagem encaminhada ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Dispensar aviso de instalação"
|
"dismiss_aria": "Dispensar aviso de instalação"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Adicionar assinatura",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Padrão",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Usada em novas mensagens, a menos que seja substituída por identidade.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Assinatura padrão"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Tem certeza de que deseja excluir \"{name}\"? Esta ação não pode ser desfeita.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Excluir assinatura?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Crie e gerencie assinaturas de e-mail para usar ao redigir ou responder.",
|
||||||
},
|
"duplicate": "Duplicar",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Editar assinatura",
|
||||||
|
"editor_label": "Assinatura",
|
||||||
|
"html_preview_label": "Pré-visualização HTML",
|
||||||
|
"name_label": "Nome",
|
||||||
|
"name_placeholder": "ex. Trabalho, Pessoal",
|
||||||
|
"name_required": "Nome é obrigatório",
|
||||||
|
"new_signature": "Nova assinatura",
|
||||||
|
"no_signature": "Sem assinatura",
|
||||||
|
"no_signatures": "Ainda não há assinaturas",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Substitua a assinatura padrão e a de resposta para identidades específicas.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Assinaturas por identidade"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Pré-visualização em texto simples",
|
||||||
"select_signature": "Select signature",
|
"reply": "Resposta",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Usada ao responder ou encaminhar, a menos que seja substituída por identidade.",
|
||||||
|
"label": "Assinatura de resposta"
|
||||||
|
},
|
||||||
|
"show_editor": "Mostrar editor",
|
||||||
|
"show_preview": "Mostrar pré-visualização",
|
||||||
|
"title": "Assinaturas",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Centralizar",
|
||||||
"italic": "Italic",
|
"align_left": "Alinhar à esquerda",
|
||||||
"underline": "Underline",
|
"align_right": "Alinhar à direita",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Negrito",
|
||||||
|
"bullet_list": "Lista com marcadores",
|
||||||
|
"italic": "Itálico",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"bullet_list": "Bullet List",
|
"ordered_list": "Lista numerada",
|
||||||
"ordered_list": "Ordered List",
|
"remove_color": "Remover cor",
|
||||||
"text_color": "Text Color",
|
"strikethrough": "Tachado",
|
||||||
"alignment": "Alignment",
|
"text_color": "Cor do texto",
|
||||||
"font_size": "Font Size",
|
"underline": "Sublinhado"
|
||||||
"align_center": "Align center",
|
|
||||||
"align_left": "Align left",
|
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Usar padrão global",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Suas assinaturas ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-217
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Copierea a eșuat"
|
"copy_failed": "Copierea a eșuat"
|
||||||
},
|
},
|
||||||
"send_now": "Trimite acum",
|
"send_now": "Trimite acum",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Creați o programare"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Solicitare confirmare de citire (faceți clic pentru a dezactiva)",
|
"read_receipt_on": "Solicitare confirmare de citire (faceți clic pentru a dezactiva)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Alege dimensiunea"
|
"pick_size": "Alege dimensiunea"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Trimis - dar curățarea ulterioară a eșuat, poate rămâne o ciornă veche.",
|
"send_filing_warning": "Trimis - dar curățarea ulterioară a eșuat, poate rămâne o ciornă veche.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Inserează semnătura",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Fără semnătură",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Selectați semnătura"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Confirmare",
|
"confirm": "Confirmare",
|
||||||
@@ -897,8 +897,8 @@
|
|||||||
"about_data": "Despre & Date",
|
"about_data": "Despre & Date",
|
||||||
"debug": "Depanare",
|
"debug": "Depanare",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"sharing": "Sharing",
|
"sharing": "Partajare",
|
||||||
"signatures": "Signatures"
|
"signatures": "Semnături"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Generalități",
|
"general": "Generalități",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Import",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Anulează",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Alegeți fișierele",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Păstrează ambele",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Alegeți ce se întâmplă atunci când un mesaj importat există deja.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Gestionarea duplicatelor",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Înlocuiește duplicatele",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Omite duplicatele",
|
||||||
"cancel": "Cancel",
|
"description": "Importați mesaje de e-mail din fișiere .eml într-un dosar.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# eroare} other {# erori}}",
|
||||||
"fail": "Import failed",
|
"fail": "Importul a eșuat",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Selectați unul sau mai multe fișiere .eml pentru a le importa.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Fișiere",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# fișier selectat} other {# fișiere selectate}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Alegeți dosarul în care se vor importa mesajele.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Dosar de destinație",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import finalizat",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importă mai multe",
|
||||||
"action_label": "Action",
|
"importing": "Se importă...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} eșuate",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importate",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} omise",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importă # fișier} other {Importă # fișiere}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# mesaj importat} other {# mesaje importate}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# mesaj eșuat} other {# mesaje eșuate}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# mesaj importat} other {# mesaje importate}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# mesaj omis} other {# mesaje omise}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Import mesaje"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Se încarcă...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Reîmprospătează"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "A apărut o eroare",
|
"page_error_title": "A apărut o eroare",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "Nu s-a putut șterge folderul",
|
"toast_error_delete": "Nu s-a putut șterge folderul",
|
||||||
"toast_error_delete_has_children": "Dosarul conține subdosare. Ștergeți-le mai întâi.",
|
"toast_error_delete_has_children": "Dosarul conține subdosare. Ștergeți-le mai întâi.",
|
||||||
"toast_error_delete_has_email": "Dosarul nu este gol. Goliți-l mai întâi.",
|
"toast_error_delete_has_email": "Dosarul nu este gol. Goliți-l mai întâi.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Partajare dosar..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Comenzi rapide de la tastatură",
|
"title": "Comenzi rapide de la tastatură",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Anulează",
|
"cancel": "Anulează",
|
||||||
"creating": "Se creează...",
|
"creating": "Se creează...",
|
||||||
"updating": "Se actualizează...",
|
"updating": "Se actualizează...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Semnătură implicită",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Mapare semnături",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Semnătură pentru răspuns",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Utilizați valoarea implicită globală"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Utilizați subadrese",
|
"button_tooltip": "Utilizați subadrese",
|
||||||
@@ -2558,28 +2556,28 @@
|
|||||||
"failed": "Importul a eșuat",
|
"failed": "Importul a eșuat",
|
||||||
"close": "Închide",
|
"close": "Închide",
|
||||||
"file_too_large": "Fișierul este prea mare (max. 5 MB)",
|
"file_too_large": "Fișierul este prea mare (max. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adresă",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Agendă",
|
||||||
"csv_back": "Back",
|
"csv_back": "Înapoi",
|
||||||
"csv_city": "City",
|
"csv_city": "Oraș",
|
||||||
"csv_company": "Company",
|
"csv_company": "Companie",
|
||||||
"csv_country": "Country",
|
"csv_country": "Țară",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Prenume",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignoră această coloană",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Funcție",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Nume de familie",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Încarcă tot",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Mapare coloane",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Pseudonim",
|
||||||
"csv_note": "Note",
|
"csv_note": "Notă",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Cod poștal",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Previzualizare",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Previzualizare ({count, plural, one {# rând} other {# rânduri}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Stat/Regiune",
|
||||||
"csv_website": "Website",
|
"csv_website": "Site web",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "fișiere .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Exportați contactele",
|
"title": "Exportați contactele",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_phone": "Are telefon",
|
"has_phone": "Are telefon",
|
||||||
"has_photo": "Are fotografie"
|
"has_photo": "Are fotografie"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"delete": "Șterge",
|
||||||
"edit": "Edit Contact",
|
"edit": "Editare",
|
||||||
"send_email": "Send Email"
|
"send_email": "Trimite e-mail"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendar",
|
"title": "Calendar",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"due_tomorrow": "Mâine",
|
"due_tomorrow": "Mâine",
|
||||||
"overdue": "Restant"
|
"overdue": "Restant"
|
||||||
},
|
},
|
||||||
|
"delete": "Șterge",
|
||||||
|
"duplicate": "Duplică",
|
||||||
|
"edit": "Editare",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Ocupat",
|
||||||
"check": "Check Availability",
|
"check": "Verifică disponibilitatea",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Faceți clic pe un interval liber pentru a selecta această oră",
|
||||||
"loading": "Loading...",
|
"free": "Liber",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Ascunde disponibilitatea",
|
||||||
"timezone": "Timezone",
|
"loading": "Se încarcă...",
|
||||||
"free": "Free",
|
"no_participants": "Adăugați participanți pentru a verifica disponibilitatea.",
|
||||||
"busy": "Busy",
|
"tentative": "Provizoriu",
|
||||||
"tentative": "Tentative",
|
"timezone": "Fus orar",
|
||||||
"unavailable": "Out of office",
|
"title": "Disponibilitate",
|
||||||
"unknown": "No information",
|
"unavailable": "În afara biroului",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Fără informații"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Șterge tot",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Toate",
|
||||||
"filter_all": "All",
|
"hide": "Ascunde resursele",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Nu sunt resurse disponibile",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Elimină {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Căutare resurse...",
|
||||||
"type_other": "Other",
|
"title": "Resurse",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Echipamente",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Altele",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Săli",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Vehicule"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "Distribuie „{name}”",
|
"title": "Distribuie „{name}”",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "Manager",
|
"manager": "Manager",
|
||||||
"custom": "Personalizat"
|
"custom": "Personalizat"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "Acceptă",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "Refuză",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "Nu ați partajat încă nimic.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "Niciun dosar partajat cu dvs. încă.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "Partajat de",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "Partajate de mine",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "Partajate cu mine"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Căutare avansată",
|
"title": "Căutare avansată",
|
||||||
@@ -3285,7 +3283,7 @@
|
|||||||
"stability_warning": "Încărcarea fișierelor de dimensiuni mari poate provoca instabilitatea serverului. Este posibil ca fișierele șterse să nu fie eliminate imediat din spațiul de stocare. Utilizați cu precauție.",
|
"stability_warning": "Încărcarea fișierelor de dimensiuni mari poate provoca instabilitatea serverului. Este posibil ca fișierele șterse să nu fie eliminate imediat din spațiul de stocare. Utilizați cu precauție.",
|
||||||
"migration_title": "Se actualizează fișierele...",
|
"migration_title": "Se actualizează fișierele...",
|
||||||
"migration_description": "Organizarea dosarelor și a fișierelor în structura corespunzătoare. Această operațiune se efectuează o singură dată.",
|
"migration_description": "Organizarea dosarelor și a fișierelor în structura corespunzătoare. Această operațiune se efectuează o singură dată.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Trimite ca atașament"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Certificatele dvs.",
|
"your_certificates": "Certificatele dvs.",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Ignorați solicitarea de instalare"
|
"dismiss_aria": "Ignorați solicitarea de instalare"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Adăugați semnătură",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Implicit",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Utilizată pentru mesajele noi, cu excepția cazului în care este suprascrisă pentru o anumită identitate.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Semnătură implicită"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Sunteți sigur că doriți să ștergeți \"{name}\"? Această acțiune nu poate fi anulată.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Ștergeți semnătura?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Creați și gestionați semnături de e-mail pentru a le utiliza la redactare sau răspuns.",
|
||||||
},
|
"duplicate": "Duplică",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Editați semnătura",
|
||||||
|
"editor_label": "Semnătură",
|
||||||
|
"html_preview_label": "Previzualizare HTML",
|
||||||
|
"name_label": "Nume",
|
||||||
|
"name_placeholder": "de ex. Serviciu, Personal",
|
||||||
|
"name_required": "Numele este obligatoriu",
|
||||||
|
"new_signature": "Semnătură nouă",
|
||||||
|
"no_signature": "Fără semnătură",
|
||||||
|
"no_signatures": "Nicio semnătură încă",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Suprascrieți semnătura implicită și cea de răspuns pentru identități individuale.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Semnături pe identitate"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Previzualizare text simplu",
|
||||||
"select_signature": "Select signature",
|
"reply": "Răspuns",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Utilizată la răspuns sau redirecționare, cu excepția cazului în care este suprascrisă pentru o anumită identitate.",
|
||||||
|
"label": "Semnătură pentru răspuns"
|
||||||
|
},
|
||||||
|
"show_editor": "Afișează editorul",
|
||||||
|
"show_preview": "Afișează previzualizarea",
|
||||||
|
"title": "Semnături",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Centrare",
|
||||||
"italic": "Italic",
|
"align_left": "Aliniere la stânga",
|
||||||
"underline": "Underline",
|
"align_right": "Aliniere la dreapta",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Aldin",
|
||||||
|
"bullet_list": "Listă cu marcatori",
|
||||||
|
"italic": "Cursiv",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"bullet_list": "Bullet List",
|
"ordered_list": "Listă numerotată",
|
||||||
"ordered_list": "Ordered List",
|
"remove_color": "Elimină culoarea",
|
||||||
"text_color": "Text Color",
|
"strikethrough": "Tăiat",
|
||||||
"alignment": "Alignment",
|
"text_color": "Culoarea textului",
|
||||||
"font_size": "Font Size",
|
"underline": "Subliniat"
|
||||||
"align_center": "Align center",
|
|
||||||
"align_left": "Align left",
|
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Utilizați valoarea implicită globală",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Semnăturile dvs. ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+166
-241
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Не удалось скопировать"
|
"copy_failed": "Не удалось скопировать"
|
||||||
},
|
},
|
||||||
"send_now": "Отправить сейчас",
|
"send_now": "Отправить сейчас",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Создать встречу"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Запрошено уведомление о прочтении (нажмите, чтобы отключить)",
|
"read_receipt_on": "Запрошено уведомление о прочтении (нажмите, чтобы отключить)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Выбрать размер"
|
"pick_size": "Выбрать размер"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Отправлено - но последующая очистка не удалась, может остаться устаревший черновик.",
|
"send_filing_warning": "Отправлено - но последующая очистка не удалась, может остаться устаревший черновик.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Вставить подпись",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Без подписи",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Выбрать подпись"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Подтвердить",
|
"confirm": "Подтвердить",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "Содержимое и отправители",
|
"content_senders": "Содержимое и отправители",
|
||||||
"about_data": "О программе и данные",
|
"about_data": "О программе и данные",
|
||||||
"debug": "Отладка",
|
"debug": "Отладка",
|
||||||
"import": "Import",
|
"import": "Импорт",
|
||||||
"sharing": "Sharing",
|
"sharing": "Общий доступ",
|
||||||
"signatures": "Signatures"
|
"signatures": "Подписи"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Управление: {name}"
|
"managing": "Управление: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Импорт",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Отмена",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Выбрать файлы",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Сохранить оба",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Выберите, что делать, если импортируемое сообщение уже существует.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Обработка дубликатов",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Заменить дубликаты",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Пропустить дубликаты",
|
||||||
"cancel": "Cancel",
|
"description": "Импортируйте сообщения электронной почты из файлов .eml в папку.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# ошибка} other {# ошибок}}",
|
||||||
"fail": "Import failed",
|
"fail": "Не удалось выполнить импорт",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Выберите один или несколько файлов .eml для импорта.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Файлы",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# файл выбран} other {# файлов выбрано}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Выберите папку, в которую нужно импортировать сообщения.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Папка назначения",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Импорт завершён",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Импортировать ещё",
|
||||||
"action_label": "Action",
|
"importing": "Импортирование...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} не удалось",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} импортировано",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} пропущено",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Импортировать # файл} other {Импортировать # файлов}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# сообщение импортировано} other {# сообщений импортировано}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# сообщение не импортировано} other {# сообщений не импортировано}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# сообщение импортировано} other {# сообщений импортировано}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# сообщение пропущено} other {# сообщений пропущено}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Импорт почты"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Загрузка...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Обновить"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Что-то пошло не так",
|
"page_error_title": "Что-то пошло не так",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Имя папки",
|
"placeholder_folder_name": "Имя папки",
|
||||||
"create": "Создать",
|
"create": "Создать",
|
||||||
"rename_confirm": "Переименовать",
|
"rename_confirm": "Переименовать",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Поделиться папкой..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Сочетания клавиш",
|
"title": "Сочетания клавиш",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"creating": "Создание...",
|
"creating": "Создание...",
|
||||||
"updating": "Обновление...",
|
"updating": "Обновление...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Подпись по умолчанию",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Сопоставление подписей",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Подпись для ответа",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Использовать значение по умолчанию"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Использовать суб-адрес",
|
"button_tooltip": "Использовать суб-адрес",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Импорт не выполнен",
|
"failed": "Импорт не выполнен",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"file_too_large": "Файл слишком большой (макс. 5 МБ)",
|
"file_too_large": "Файл слишком большой (макс. 5 МБ)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Адрес",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Адресная книга",
|
||||||
"csv_back": "Back",
|
"csv_back": "Назад",
|
||||||
"csv_city": "City",
|
"csv_city": "Город",
|
||||||
"csv_company": "Company",
|
"csv_company": "Компания",
|
||||||
"csv_country": "Country",
|
"csv_country": "Страна",
|
||||||
"csv_email": "Email",
|
"csv_email": "Email",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Имя",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Игнорировать этот столбец",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Должность",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Фамилия",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Загрузить все",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Сопоставить столбцы",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Псевдоним",
|
||||||
"csv_note": "Note",
|
"csv_note": "Заметка",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Телефон",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Почтовый индекс",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Предпросмотр",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Предпросмотр ({count, plural, one {# строка} other {# строк}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Область/регион",
|
||||||
"csv_website": "Website",
|
"csv_website": "Веб-сайт",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "Файлы .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Экспорт контактов",
|
"title": "Экспорт контактов",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "С фото"
|
"has_photo": "С фото"
|
||||||
},
|
},
|
||||||
"open_categories": "Открыть категории",
|
"open_categories": "Открыть категории",
|
||||||
"delete": "Delete Contact",
|
"delete": "Удалить",
|
||||||
"edit": "Edit Contact",
|
"edit": "Редактировать",
|
||||||
"send_email": "Send Email"
|
"send_email": "Отправить письмо"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Календарь",
|
"title": "Календарь",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Открыть меню",
|
"nav_open_menu": "Открыть меню",
|
||||||
|
"delete": "Удалить",
|
||||||
|
"duplicate": "Дублировать",
|
||||||
|
"edit": "Редактировать",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Занято",
|
||||||
"check": "Check Availability",
|
"check": "Проверить доступность",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Нажмите на свободный слот, чтобы выбрать это время",
|
||||||
"loading": "Loading...",
|
"free": "Свободно",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Скрыть доступность",
|
||||||
"timezone": "Timezone",
|
"loading": "Загрузка...",
|
||||||
"free": "Free",
|
"no_participants": "Добавьте участников, чтобы проверить доступность.",
|
||||||
"busy": "Busy",
|
"tentative": "Предварительно",
|
||||||
"tentative": "Tentative",
|
"timezone": "Часовой пояс",
|
||||||
"unavailable": "Out of office",
|
"title": "Доступность",
|
||||||
"unknown": "No information",
|
"unavailable": "Отсутствует",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Нет данных"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Очистить всё",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Все",
|
||||||
"filter_all": "All",
|
"hide": "Скрыть ресурсы",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Нет доступных ресурсов",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Удалить {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Поиск ресурсов...",
|
||||||
"type_other": "Other",
|
"title": "Ресурсы",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Оборудование",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Другое",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Помещения",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Транспорт"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Поделиться «{name}»",
|
|
||||||
"description": "Предоставьте доступ другим пользователям или группам на этом сервере. Изменения вступают в силу немедленно.",
|
|
||||||
"no_shares": "Пока никому не предоставлен доступ.",
|
|
||||||
"add_person": "Добавить пользователя или группу",
|
|
||||||
"search_placeholder": "Искать по имени или email…",
|
|
||||||
"loading_principals": "Загрузка пользователей…",
|
|
||||||
"no_principals": "Других пользователей или групп не найдено.",
|
|
||||||
"no_match": "Нет совпадений.",
|
|
||||||
"remove": "Отозвать доступ",
|
|
||||||
"group": "Группа",
|
|
||||||
"share_added": "Доступ предоставлен",
|
|
||||||
"share_updated": "Доступ обновлён",
|
|
||||||
"share_removed": "Доступ отозван",
|
|
||||||
"share_failed": "Не удалось обновить общий доступ",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Только занятость",
|
|
||||||
"read": "Только чтение",
|
|
||||||
"readWrite": "Чтение и запись",
|
|
||||||
"manager": "Управляющий",
|
|
||||||
"custom": "Пользовательский"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Расширенный поиск",
|
"title": "Расширенный поиск",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Другие учётные записи",
|
"other_accounts": "Другие учётные записи",
|
||||||
"migration_title": "Обновление ваших файлов…",
|
"migration_title": "Обновление ваших файлов…",
|
||||||
"migration_description": "Папки и файлы упорядочиваются в правильную структуру. Это происходит только один раз.",
|
"migration_description": "Папки и файлы упорядочиваются в правильную структуру. Это происходит только один раз.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Отправить как вложение"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Ваши сертификаты",
|
"your_certificates": "Ваши сертификаты",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "Поиск недоступен в объединённом представлении"
|
"search_unavailable": "Поиск недоступен в объединённом представлении"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Поделиться «{name}»",
|
||||||
|
"description": "Предоставьте доступ другим пользователям или группам на этом сервере. Изменения вступают в силу немедленно.",
|
||||||
|
"no_shares": "Пока никому не предоставлен доступ.",
|
||||||
|
"add_person": "Добавить пользователя или группу",
|
||||||
|
"search_placeholder": "Искать по имени или email…",
|
||||||
|
"loading_principals": "Загрузка пользователей…",
|
||||||
|
"no_principals": "Других пользователей или групп не найдено.",
|
||||||
|
"no_match": "Нет совпадений.",
|
||||||
|
"remove": "Отозвать доступ",
|
||||||
|
"group": "Группа",
|
||||||
|
"share_added": "Доступ предоставлен",
|
||||||
|
"share_updated": "Доступ обновлён",
|
||||||
|
"share_removed": "Доступ отозван",
|
||||||
|
"share_failed": "Не удалось обновить общий доступ",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Только занятость",
|
||||||
|
"read": "Только чтение",
|
||||||
|
"readWrite": "Чтение и запись",
|
||||||
|
"manager": "Управляющий",
|
||||||
|
"custom": "Пользовательский"
|
||||||
|
},
|
||||||
|
"accept": "Принять",
|
||||||
|
"decline": "Отклонить",
|
||||||
|
"no_shares_by_me": "Вы пока ничего не предоставили в общий доступ.",
|
||||||
|
"no_shares_with_me": "Пока нет папок, к которым вам предоставлен доступ.",
|
||||||
|
"shared_by": "Предоставлено",
|
||||||
|
"tab_shared_by_me": "Предоставлено мной",
|
||||||
|
"tab_shared_with_me": "Предоставлено мне"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "{date}, {from} написал:",
|
"reply_line": "{date}, {from} написал:",
|
||||||
"forwarded_separator": "---------- Пересланное сообщение ----------",
|
"forwarded_separator": "---------- Пересланное сообщение ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Закрыть запрос на установку"
|
"dismiss_aria": "Закрыть запрос на установку"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Добавить подпись",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "По умолчанию",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Используется для новых сообщений, если не переопределено для отдельной идентификации.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Подпись по умолчанию"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Вы уверены, что хотите удалить \"{name}\"? Это действие нельзя отменить.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Удалить подпись?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Создавайте и управляйте подписями электронной почты для использования при написании писем или ответе на них.",
|
||||||
},
|
"duplicate": "Дублировать",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Редактировать подпись",
|
||||||
|
"editor_label": "Подпись",
|
||||||
|
"html_preview_label": "Просмотр HTML",
|
||||||
|
"name_label": "Имя",
|
||||||
|
"name_placeholder": "напр., Работа, Личное",
|
||||||
|
"name_required": "Имя обязательно",
|
||||||
|
"new_signature": "Новая подпись",
|
||||||
|
"no_signature": "Без подписи",
|
||||||
|
"no_signatures": "Подписей пока нет",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Переопределите подпись по умолчанию и подпись для ответа для отдельных идентификаций.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Подписи для отдельных идентификаций"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Просмотр в виде обычного текста",
|
||||||
"select_signature": "Select signature",
|
"reply": "Ответ",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Используется при ответе или пересылке, если не переопределено для отдельной идентификации.",
|
||||||
|
"label": "Подпись для ответа"
|
||||||
|
},
|
||||||
|
"show_editor": "Показать редактор",
|
||||||
|
"show_preview": "Показать предпросмотр",
|
||||||
|
"title": "Подписи",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "По центру",
|
||||||
"italic": "Italic",
|
"align_left": "По левому краю",
|
||||||
"underline": "Underline",
|
"align_right": "По правому краю",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Жирный",
|
||||||
"link": "Link",
|
"bullet_list": "Маркированный список",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Курсив",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Ссылка",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Нумерованный список",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Убрать цвет",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Зачёркнутый",
|
||||||
"align_center": "Align center",
|
"text_color": "Цвет текста",
|
||||||
"align_left": "Align left",
|
"underline": "Подчёркнутый"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Использовать значение по умолчанию",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Ваши подписи ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-218
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Kopírovanie zlyhalo"
|
"copy_failed": "Kopírovanie zlyhalo"
|
||||||
},
|
},
|
||||||
"send_now": "Odoslať teraz",
|
"send_now": "Odoslať teraz",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Vytvoriť stretnutie"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Potvrdenie o prečítaní požiadané (kliknutím vypnete)",
|
"read_receipt_on": "Potvrdenie o prečítaní požiadané (kliknutím vypnete)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Vybrať veľkosť"
|
"pick_size": "Vybrať veľkosť"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Odoslané - ale následné upratovanie zlyhalo, môže zostať zastaraný koncept.",
|
"send_filing_warning": "Odoslané - ale následné upratovanie zlyhalo, môže zostať zastaraný koncept.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Vložiť podpis",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Bez podpisu",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Vybrať podpis"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Potvrdiť",
|
"confirm": "Potvrdiť",
|
||||||
@@ -897,8 +897,8 @@
|
|||||||
"about_data": "Info a dáta",
|
"about_data": "Info a dáta",
|
||||||
"debug": "Ladenie",
|
"debug": "Ladenie",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"sharing": "Sharing",
|
"sharing": "Zdieľanie",
|
||||||
"signatures": "Signatures"
|
"signatures": "Podpisy"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Všeobecné",
|
"general": "Všeobecné",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Importovať",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Zrušiť",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Vybrať súbory",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Ponechať obe",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Vyberte, čo sa má stať, keď importovaná správa už existuje.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Spracovanie duplicít",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Nahradiť duplicity",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Preskočiť duplicity",
|
||||||
"cancel": "Cancel",
|
"description": "Importujte e-mailové správy zo súborov .eml do priečinka.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# chyba} other {# chýb}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import zlyhal",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Vyberte jeden alebo viac súborov .eml na import.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Súbory",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# vybraný súbor} other {# vybraných súborov}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Vyberte priečinok, do ktorého sa majú správy importovať.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Cieľový priečinok",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import dokončený",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Importovať ďalšie",
|
||||||
"action_label": "Action",
|
"importing": "Importovanie...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} zlyhaných",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} importovaných",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} preskočených",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Importovať # súbor} other {Importovať # súborov}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# správa importovaná} other {# správ importovaných}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# správa zlyhaná} other {# správ zlyhaných}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# správa importovaná} other {# správ importovaných}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# správa preskočená} other {# správ preskočených}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Importovať poštu"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Načítavanie...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Obnoviť"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Niečo sa pokazilo",
|
"page_error_title": "Niečo sa pokazilo",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "Nepodarilo sa zmazať priečinok",
|
"toast_error_delete": "Nepodarilo sa zmazať priečinok",
|
||||||
"toast_error_delete_has_children": "Priečinok obsahuje podpriečinky. Najprv ich odstráňte.",
|
"toast_error_delete_has_children": "Priečinok obsahuje podpriečinky. Najprv ich odstráňte.",
|
||||||
"toast_error_delete_has_email": "Priečinok nie je prázdny. Najprv ho vyprázdnite.",
|
"toast_error_delete_has_email": "Priečinok nie je prázdny. Najprv ho vyprázdnite.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Zdieľať priečinok..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Klávesové skratky",
|
"title": "Klávesové skratky",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Zrušiť",
|
"cancel": "Zrušiť",
|
||||||
"creating": "Vytváranie...",
|
"creating": "Vytváranie...",
|
||||||
"updating": "Aktualizovanie...",
|
"updating": "Aktualizovanie...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Predvolený podpis",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Priradenie podpisov",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Podpis pre odpoveď",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Použiť globálne predvolené"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Použiť podadresu",
|
"button_tooltip": "Použiť podadresu",
|
||||||
@@ -2558,28 +2556,28 @@
|
|||||||
"failed": "Import zlyhal",
|
"failed": "Import zlyhal",
|
||||||
"close": "Zavrieť",
|
"close": "Zavrieť",
|
||||||
"file_too_large": "Súbor je príliš veľký (max. 5 MB)",
|
"file_too_large": "Súbor je príliš veľký (max. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adresa",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Adresár",
|
||||||
"csv_back": "Back",
|
"csv_back": "Späť",
|
||||||
"csv_city": "City",
|
"csv_city": "Mesto",
|
||||||
"csv_company": "Company",
|
"csv_company": "Spoločnosť",
|
||||||
"csv_country": "Country",
|
"csv_country": "Krajina",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-mail",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Krstné meno",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignorovať tento stĺpec",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Pozícia",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Priezvisko",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Načítať všetko",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Priradenie stĺpcov",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Prezývka",
|
||||||
"csv_note": "Note",
|
"csv_note": "Poznámka",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefón",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "PSČ",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Náhľad",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Náhľad ({count, plural, one {# riadok} other {# riadkov}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Štát / Kraj",
|
||||||
"csv_website": "Website",
|
"csv_website": "Webová stránka",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "súbory .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Exportovať kontakty",
|
"title": "Exportovať kontakty",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_phone": "Má telefón",
|
"has_phone": "Má telefón",
|
||||||
"has_photo": "Má fotku"
|
"has_photo": "Má fotku"
|
||||||
},
|
},
|
||||||
"delete": "Delete Contact",
|
"delete": "Odstrániť",
|
||||||
"edit": "Edit Contact",
|
"edit": "Upraviť",
|
||||||
"send_email": "Send Email"
|
"send_email": "Odoslať e-mail"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalendár",
|
"title": "Kalendár",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"due_tomorrow": "Zajtra",
|
"due_tomorrow": "Zajtra",
|
||||||
"overdue": "Po termíne"
|
"overdue": "Po termíne"
|
||||||
},
|
},
|
||||||
|
"delete": "Odstrániť",
|
||||||
|
"duplicate": "Duplikovať",
|
||||||
|
"edit": "Upraviť",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Obsadený",
|
||||||
"check": "Check Availability",
|
"check": "Skontrolovať dostupnosť",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Kliknutím na voľný termín vyberiete tento čas",
|
||||||
"loading": "Loading...",
|
"free": "Voľný",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Skryť dostupnosť",
|
||||||
"timezone": "Timezone",
|
"loading": "Načítavanie...",
|
||||||
"free": "Free",
|
"no_participants": "Pridajte účastníkov na kontrolu dostupnosti.",
|
||||||
"busy": "Busy",
|
"tentative": "Nezáväzne",
|
||||||
"tentative": "Tentative",
|
"timezone": "Časové pásmo",
|
||||||
"unavailable": "Out of office",
|
"title": "Dostupnosť",
|
||||||
"unknown": "No information",
|
"unavailable": "Mimo kancelárie",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Žiadne informácie"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Vymazať všetko",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Všetky",
|
||||||
"filter_all": "All",
|
"hide": "Skryť zdroje",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Žiadne dostupné zdroje",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Odstrániť {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Hľadať zdroje...",
|
||||||
"type_other": "Other",
|
"title": "Zdroje",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Vybavenie",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Ostatné",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Miestnosti",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Vozidlá"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "Zdieľať \"{name}\"",
|
"title": "Zdieľať \"{name}\"",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "Správca",
|
"manager": "Správca",
|
||||||
"custom": "Vlastné"
|
"custom": "Vlastné"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "Prijať",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "Odmietnuť",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "Zatiaľ ste nič nezdieľali.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "Zatiaľ s vami neboli zdieľané žiadne priečinky.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "Zdieľané od",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "Zdieľané mnou",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "Zdieľané so mnou"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Pokročilé hľadanie",
|
"title": "Pokročilé hľadanie",
|
||||||
@@ -3285,7 +3283,7 @@
|
|||||||
"stability_warning": "Nahrávanie veľkých súborov môže spôsobiť nestabilitu servera. Používajte s opatrnosťou.",
|
"stability_warning": "Nahrávanie veľkých súborov môže spôsobiť nestabilitu servera. Používajte s opatrnosťou.",
|
||||||
"migration_title": "Aktualizácia vašich súborov…",
|
"migration_title": "Aktualizácia vašich súborov…",
|
||||||
"migration_description": "Usporiadanie priečinkov a súborov do správnej štruktúry. Toto prebehne iba raz.",
|
"migration_description": "Usporiadanie priečinkov a súborov do správnej štruktúry. Toto prebehne iba raz.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Odoslať ako prílohu"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Vaše certifikáty",
|
"your_certificates": "Vaše certifikáty",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Zavrieť výzvu na inštaláciu"
|
"dismiss_aria": "Zavrieť výzvu na inštaláciu"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Pridať podpis",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Predvolený",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Použije sa pre nové správy, pokiaľ nie je pre danú identitu nastavený iný.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Predvolený podpis"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Naozaj chcete odstrániť \"{name}\"? Túto akciu nie je možné vrátiť.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Odstrániť podpis?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Vytvárajte a spravujte e-mailové podpisy na použitie pri písaní alebo odpovedaní.",
|
||||||
},
|
"duplicate": "Duplikovať",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Upraviť podpis",
|
||||||
|
"editor_label": "Podpis",
|
||||||
|
"html_preview_label": "HTML náhľad",
|
||||||
|
"name_label": "Názov",
|
||||||
|
"name_placeholder": "napr. Práca, Osobné",
|
||||||
|
"name_required": "Názov je povinný",
|
||||||
|
"new_signature": "Nový podpis",
|
||||||
|
"no_signature": "Bez podpisu",
|
||||||
|
"no_signatures": "Zatiaľ žiadne podpisy",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Prepíšte predvolený podpis a podpis pre odpoveď pre jednotlivé identity.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Podpisy podľa identity"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Textový náhľad",
|
||||||
"select_signature": "Select signature",
|
"reply": "Odpoveď",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Použije sa pri odpovedaní alebo preposielaní, pokiaľ nie je pre danú identitu nastavený iný.",
|
||||||
|
"label": "Podpis pre odpoveď"
|
||||||
|
},
|
||||||
|
"show_editor": "Zobraziť editor",
|
||||||
|
"show_preview": "Zobraziť náhľad",
|
||||||
|
"title": "Podpisy",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Na stred",
|
||||||
"italic": "Italic",
|
"align_left": "Zarovnať doľava",
|
||||||
"underline": "Underline",
|
"align_right": "Zarovnať doprava",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Tučné",
|
||||||
"link": "Link",
|
"bullet_list": "Odrážkový zoznam",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Kurzíva",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Odkaz",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Číslovaný zoznam",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Odstrániť farbu",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Prečiarknuté",
|
||||||
"align_center": "Align center",
|
"text_color": "Farba textu",
|
||||||
"align_left": "Align left",
|
"underline": "Podčiarknuté"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Použiť globálne predvolené",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Vaše podpisy ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+144
-219
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Kopyalanamadı"
|
"copy_failed": "Kopyalanamadı"
|
||||||
},
|
},
|
||||||
"send_now": "Şimdi gönder",
|
"send_now": "Şimdi gönder",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Randevu Oluştur"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Okundu bilgisi istendi (devre dışı bırakmak için tıklayın)",
|
"read_receipt_on": "Okundu bilgisi istendi (devre dışı bırakmak için tıklayın)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Boyut seç"
|
"pick_size": "Boyut seç"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Gönderildi - ancak sonrasındaki temizleme başarısız oldu, eski bir taslak kalabilir.",
|
"send_filing_warning": "Gönderildi - ancak sonrasındaki temizleme başarısız oldu, eski bir taslak kalabilir.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "İmza ekle",
|
||||||
"no_signature": "No signature",
|
"no_signature": "İmza yok",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "İmza seç"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Onayla",
|
"confirm": "Onayla",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "İçerik ve Göndericiler",
|
"content_senders": "İçerik ve Göndericiler",
|
||||||
"about_data": "Hakkında ve Veriler",
|
"about_data": "Hakkında ve Veriler",
|
||||||
"debug": "Hata Ayıklama",
|
"debug": "Hata Ayıklama",
|
||||||
"import": "Import",
|
"import": "İçe Aktar",
|
||||||
"sharing": "Sharing",
|
"sharing": "Paylaşım",
|
||||||
"signatures": "Signatures"
|
"signatures": "İmzalar"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Genel",
|
"general": "Genel",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Yönetiliyor: {name}"
|
"managing": "Yönetiliyor: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "İçe Aktar",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "İptal",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Dosya seç",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "İkisini de sakla",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "İçe aktarılan bir ileti zaten mevcut olduğunda ne yapılacağını seçin.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Yinelenen işleme",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Yinelenenleri değiştir",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Yinelenenleri atla",
|
||||||
"cancel": "Cancel",
|
"description": ".eml dosyalarından bir klasöre e-posta iletileri içe aktarın.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# hata} other {# hata}}",
|
||||||
"fail": "Import failed",
|
"fail": "İçe aktarma başarısız",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "İçe aktarmak için bir veya daha fazla .eml dosyası seçin.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Dosyalar",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# dosya seçildi} other {# dosya seçildi}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "İletilerin içe aktarılacağı klasörü seçin.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Hedef klasör",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "İçe aktarma tamamlandı",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Daha fazla içe aktar",
|
||||||
"action_label": "Action",
|
"importing": "İçe aktarılıyor...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} başarısız",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} içe aktarıldı",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} atlandı",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {# dosyayı içe aktar} other {# dosyayı içe aktar}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# ileti içe aktarıldı} other {# ileti içe aktarıldı}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# ileti başarısız oldu} other {# ileti başarısız oldu}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# ileti içe aktarıldı} other {# ileti içe aktarıldı}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# ileti atlandı} other {# ileti atlandı}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Postayı İçe Aktar"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Yükleniyor...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Yenile"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Bir şeyler ters gitti",
|
"page_error_title": "Bir şeyler ters gitti",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"toast_error_delete": "Klasör silinemedi",
|
"toast_error_delete": "Klasör silinemedi",
|
||||||
"toast_error_delete_has_children": "Klasörde alt klasörler var. Önce onları kaldırın.",
|
"toast_error_delete_has_children": "Klasörde alt klasörler var. Önce onları kaldırın.",
|
||||||
"toast_error_delete_has_email": "Klasör boş değil. Önce boşaltın.",
|
"toast_error_delete_has_email": "Klasör boş değil. Önce boşaltın.",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Klasörü paylaş..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Klavye Kısayolları",
|
"title": "Klavye Kısayolları",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "İptal",
|
"cancel": "İptal",
|
||||||
"creating": "Oluşturuluyor...",
|
"creating": "Oluşturuluyor...",
|
||||||
"updating": "Güncelleniyor...",
|
"updating": "Güncelleniyor...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Varsayılan imza",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "İmza eşleştirmesi",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Yanıt imzası",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Genel varsayılanı kullan"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Alt adres kullan",
|
"button_tooltip": "Alt adres kullan",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "İçe aktarma başarısız",
|
"failed": "İçe aktarma başarısız",
|
||||||
"close": "Kapat",
|
"close": "Kapat",
|
||||||
"file_too_large": "Dosya çok büyük (maks. 5 MB)",
|
"file_too_large": "Dosya çok büyük (maks. 5 MB)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Adres",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Adres defteri",
|
||||||
"csv_back": "Back",
|
"csv_back": "Geri",
|
||||||
"csv_city": "City",
|
"csv_city": "Şehir",
|
||||||
"csv_company": "Company",
|
"csv_company": "Şirket",
|
||||||
"csv_country": "Country",
|
"csv_country": "Ülke",
|
||||||
"csv_email": "Email",
|
"csv_email": "E-posta",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Ad",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Bu sütunu yoksay",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "İş unvanı",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Soyadı",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Tümünü yükle",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Sütunları eşleştir",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "Takma ad",
|
||||||
"csv_note": "Note",
|
"csv_note": "Not",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Telefon",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Posta kodu",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Önizleme",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Önizleme ({count, plural, one {# satır} other {# satır}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "İl / Bölge",
|
||||||
"csv_website": "Website",
|
"csv_website": "Web sitesi",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv dosyaları"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Kişileri Dışa Aktar",
|
"title": "Kişileri Dışa Aktar",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "Fotoğrafı var"
|
"has_photo": "Fotoğrafı var"
|
||||||
},
|
},
|
||||||
"open_categories": "Kategorileri aç",
|
"open_categories": "Kategorileri aç",
|
||||||
"delete": "Delete Contact",
|
"delete": "Sil",
|
||||||
"edit": "Edit Contact",
|
"edit": "Düzenle",
|
||||||
"send_email": "Send Email"
|
"send_email": "E-posta gönder"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Takvim",
|
"title": "Takvim",
|
||||||
@@ -3060,36 +3058,36 @@
|
|||||||
"overdue": "Gecikmiş"
|
"overdue": "Gecikmiş"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Menüyü aç",
|
"nav_open_menu": "Menüyü aç",
|
||||||
|
"delete": "Sil",
|
||||||
|
"duplicate": "Çoğalt",
|
||||||
|
"edit": "Düzenle",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Meşgul",
|
||||||
"check": "Check Availability",
|
"check": "Müsaitliği kontrol et",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Bu saati seçmek için boş bir aralığa tıklayın",
|
||||||
"loading": "Loading...",
|
"free": "Boş",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Müsaitliği gizle",
|
||||||
"timezone": "Timezone",
|
"loading": "Yükleniyor...",
|
||||||
"free": "Free",
|
"no_participants": "Müsaitliği kontrol etmek için katılımcı ekleyin.",
|
||||||
"busy": "Busy",
|
"tentative": "Geçici",
|
||||||
"tentative": "Tentative",
|
"timezone": "Saat Dilimi",
|
||||||
"unavailable": "Out of office",
|
"title": "Müsaitlik",
|
||||||
"unknown": "No information",
|
"unavailable": "Ofis dışında",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Bilgi yok"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Tümünü temizle",
|
||||||
"hide": "Hide resources",
|
"filter_all": "Tümü",
|
||||||
"filter_all": "All",
|
"hide": "Kaynakları gizle",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Kullanılabilir kaynak yok",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "{name} öğesini kaldır",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Kaynaklarda ara...",
|
||||||
"type_other": "Other",
|
"title": "Kaynaklar",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Ekipman",
|
||||||
"no_resources": "No resources available",
|
"type_other": "Diğer",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Odalar",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Araçlar"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"title": "\"{name}\" paylaş",
|
"title": "\"{name}\" paylaş",
|
||||||
@@ -3113,13 +3111,13 @@
|
|||||||
"manager": "Yönetici",
|
"manager": "Yönetici",
|
||||||
"custom": "Özel"
|
"custom": "Özel"
|
||||||
},
|
},
|
||||||
"tab_shared_by_me": "Shared by me",
|
"accept": "Kabul et",
|
||||||
"tab_shared_with_me": "Shared with me",
|
"decline": "Reddet",
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
"no_shares_by_me": "Henüz kimseyle paylaşım yapmadınız.",
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
"no_shares_with_me": "Sizinle henüz paylaşılan klasör yok.",
|
||||||
"shared_by": "Shared by",
|
"shared_by": "Paylaşan",
|
||||||
"accept": "Accept",
|
"tab_shared_by_me": "Benim paylaştıklarım",
|
||||||
"decline": "Decline"
|
"tab_shared_with_me": "Benimle paylaşılanlar"
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Gelişmiş Arama",
|
"title": "Gelişmiş Arama",
|
||||||
@@ -3285,7 +3283,7 @@
|
|||||||
"other_accounts": "Diğer hesaplar",
|
"other_accounts": "Diğer hesaplar",
|
||||||
"migration_title": "Dosyalarınız güncelleniyor…",
|
"migration_title": "Dosyalarınız güncelleniyor…",
|
||||||
"migration_description": "Klasörler ve dosyalar doğru yapıya göre düzenleniyor. Bu yalnızca bir kez gerçekleşir.",
|
"migration_description": "Klasörler ve dosyalar doğru yapıya göre düzenleniyor. Bu yalnızca bir kez gerçekleşir.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Ek Olarak Gönder"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Sertifikalarınız",
|
"your_certificates": "Sertifikalarınız",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Yükleme istemini kapat"
|
"dismiss_aria": "Yükleme istemini kapat"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "İmza ekle",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Varsayılan",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Kimlik başına geçersiz kılınmadığı sürece yeni iletiler için kullanılır.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Varsayılan imza"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "\"{name}\" imzasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||||
"label": "Default for replies",
|
"delete_title": "İmza silinsin mi?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Yazarken veya yanıtlarken kullanmak üzere e-posta imzaları oluşturun ve yönetin.",
|
||||||
},
|
"duplicate": "Çoğalt",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "İmzayı düzenle",
|
||||||
|
"editor_label": "İmza",
|
||||||
|
"html_preview_label": "HTML önizleme",
|
||||||
|
"name_label": "Ad",
|
||||||
|
"name_placeholder": "ör. İş, Kişisel",
|
||||||
|
"name_required": "Ad gerekli",
|
||||||
|
"new_signature": "Yeni imza",
|
||||||
|
"no_signature": "İmza yok",
|
||||||
|
"no_signatures": "Henüz imza yok",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Bireysel kimlikler için varsayılan ve yanıt imzasını geçersiz kılın.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Kimlik başına imzalar"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Düz metin önizleme",
|
||||||
"select_signature": "Select signature",
|
"reply": "Yanıt",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Kimlik başına geçersiz kılınmadığı sürece yanıtlarken veya iletirken kullanılır.",
|
||||||
|
"label": "Yanıt imzası"
|
||||||
|
},
|
||||||
|
"show_editor": "Düzenleyiciyi göster",
|
||||||
|
"show_preview": "Önizlemeyi göster",
|
||||||
|
"title": "İmzalar",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "Ortala",
|
||||||
"italic": "Italic",
|
"align_left": "Sola hizala",
|
||||||
"underline": "Underline",
|
"align_right": "Sağa hizala",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Kalın",
|
||||||
"link": "Link",
|
"bullet_list": "Madde işaretli liste",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "İtalik",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Bağlantı",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Numaralı liste",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Rengi kaldır",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Üstü çizili",
|
||||||
"align_center": "Align center",
|
"text_color": "Metin rengi",
|
||||||
"align_left": "Align left",
|
"underline": "Altı çizili"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Genel varsayılanı kullan",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "İmzalarınız ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+167
-242
@@ -568,7 +568,7 @@
|
|||||||
"copy_failed": "Не вдалося скопіювати"
|
"copy_failed": "Не вдалося скопіювати"
|
||||||
},
|
},
|
||||||
"send_now": "Надіслати зараз",
|
"send_now": "Надіслати зараз",
|
||||||
"create_appointment": "Create Appointment"
|
"create_appointment": "Створити зустріч"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
"read_receipt_on": "Запитано сповіщення про прочитання (натисніть, щоб вимкнути)",
|
"read_receipt_on": "Запитано сповіщення про прочитання (натисніть, щоб вимкнути)",
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
"pick_size": "Вибрати розмір"
|
"pick_size": "Вибрати розмір"
|
||||||
},
|
},
|
||||||
"send_filing_warning": "Надіслано - але подальше очищення не вдалося, може залишитися застаріла чернетка.",
|
"send_filing_warning": "Надіслано - але подальше очищення не вдалося, може залишитися застаріла чернетка.",
|
||||||
"insert_signature": "Insert signature",
|
"insert_signature": "Вставити підпис",
|
||||||
"no_signature": "No signature",
|
"no_signature": "Без підпису",
|
||||||
"select_signature": "Select signature"
|
"select_signature": "Виберіть підпис"
|
||||||
},
|
},
|
||||||
"confirm_dialog": {
|
"confirm_dialog": {
|
||||||
"confirm": "Підтвердити",
|
"confirm": "Підтвердити",
|
||||||
@@ -893,9 +893,9 @@
|
|||||||
"content_senders": "Вміст і відправники",
|
"content_senders": "Вміст і відправники",
|
||||||
"about_data": "Про програму та дані",
|
"about_data": "Про програму та дані",
|
||||||
"debug": "Налагодження",
|
"debug": "Налагодження",
|
||||||
"import": "Import",
|
"import": "Імпорт",
|
||||||
"sharing": "Sharing",
|
"sharing": "Спільний доступ",
|
||||||
"signatures": "Signatures"
|
"signatures": "Підписи"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Загальний",
|
"general": "Загальний",
|
||||||
@@ -2016,39 +2016,37 @@
|
|||||||
"managing": "Керування: {name}"
|
"managing": "Керування: {name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Імпорт",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
"cancel": "Скасувати",
|
||||||
"file_label": "Select Files",
|
"choose_files": "Вибрати файли",
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
"conflict_copy": "Зберегти обидва",
|
||||||
"folder_label": "Import into Folder",
|
"conflict_description": "Виберіть, що робити, якщо імпортоване повідомлення вже існує.",
|
||||||
"conflict_label": "If Email Already Exists",
|
"conflict_label": "Обробка дублікатів",
|
||||||
"start_import": "Start Import",
|
"conflict_replace": "Замінювати дублікати",
|
||||||
"importing": "Importing...",
|
"conflict_skip": "Пропускати дублікати",
|
||||||
"cancel": "Cancel",
|
"description": "Імпортуйте повідомлення електронної пошти з файлів .eml до папки.",
|
||||||
"success": "Import successful",
|
"error_details": "{count, plural, one {# помилка} other {# помилок}}",
|
||||||
"fail": "Import failed",
|
"fail": "Не вдалося імпортувати",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Виберіть один або кілька файлів .eml для імпорту.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Файли",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# файл вибрано} other {# файлів вибрано}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Виберіть папку, до якої імпортувати повідомлення.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Папка призначення",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Імпорт завершено",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Імпортувати ще",
|
||||||
"action_label": "Action",
|
"importing": "Імпорт...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "Помилок: {count}",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "Імпортовано: {count}",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "Пропущено: {count}",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Імпортувати # файл} other {Імпортувати # файлів}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# повідомлення імпортовано} other {# повідомлень імпортовано}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# повідомлення не вдалося імпортувати} other {# повідомлень не вдалося імпортувати}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# повідомлення імпортовано} other {# повідомлень імпортовано}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# повідомлення пропущено} other {# повідомлень пропущено}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Імпортувати пошту"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Завантаження...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Оновити"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"page_error_title": "Щось пішло не так",
|
"page_error_title": "Щось пішло не так",
|
||||||
@@ -2127,7 +2125,7 @@
|
|||||||
"placeholder_folder_name": "Ім'я папки",
|
"placeholder_folder_name": "Ім'я папки",
|
||||||
"create": "Створити",
|
"create": "Створити",
|
||||||
"rename_confirm": "Перейменувати",
|
"rename_confirm": "Перейменувати",
|
||||||
"share_folder": "Share Folder..."
|
"share_folder": "Поділитися папкою..."
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Комбінації клавіш",
|
"title": "Комбінації клавіш",
|
||||||
@@ -2227,10 +2225,10 @@
|
|||||||
"cancel": "Скасувати",
|
"cancel": "Скасувати",
|
||||||
"creating": "Створення...",
|
"creating": "Створення...",
|
||||||
"updating": "Оновлення...",
|
"updating": "Оновлення...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Підпис за замовчуванням",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Зіставлення підписів",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Підпис для відповіді",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Використовувати загальне значення за замовчуванням"
|
||||||
},
|
},
|
||||||
"sub_address": {
|
"sub_address": {
|
||||||
"button_tooltip": "Використовуйте допоміжну адресу",
|
"button_tooltip": "Використовуйте допоміжну адресу",
|
||||||
@@ -2557,28 +2555,28 @@
|
|||||||
"failed": "Помилка імпорту",
|
"failed": "Помилка імпорту",
|
||||||
"close": "Закрити",
|
"close": "Закрити",
|
||||||
"file_too_large": "Файл завеликий (макс. 5 МБ)",
|
"file_too_large": "Файл завеликий (макс. 5 МБ)",
|
||||||
"csv_address": "Address",
|
"csv_address": "Адреса",
|
||||||
"csv_address_book": "Address book",
|
"csv_address_book": "Адресна книга",
|
||||||
"csv_back": "Back",
|
"csv_back": "Назад",
|
||||||
"csv_city": "City",
|
"csv_city": "Місто",
|
||||||
"csv_company": "Company",
|
"csv_company": "Компанія",
|
||||||
"csv_country": "Country",
|
"csv_country": "Країна",
|
||||||
"csv_email": "Email",
|
"csv_email": "Електронна пошта",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "Ім'я",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ігнорувати цей стовпець",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Назва посади",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Прізвище",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Завантажити все",
|
||||||
"csv_map_columns": "Map columns",
|
"csv_map_columns": "Зіставлення стовпців",
|
||||||
"csv_nickname": "Nickname",
|
"csv_nickname": "псевдонім",
|
||||||
"csv_note": "Note",
|
"csv_note": "Примітка",
|
||||||
"csv_phone": "Phone",
|
"csv_phone": "Телефон",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Поштовий індекс",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Попередній перегляд",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Попередній перегляд ({count, plural, one {# рядок} other {# рядків}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "Штат / Регіон",
|
||||||
"csv_website": "Website",
|
"csv_website": "Веб-сайт",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": "файли .csv"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Експортувати контакти",
|
"title": "Експортувати контакти",
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "З фото"
|
"has_photo": "З фото"
|
||||||
},
|
},
|
||||||
"open_categories": "Відкрити категорії",
|
"open_categories": "Відкрити категорії",
|
||||||
"delete": "Delete Contact",
|
"delete": "Видалити",
|
||||||
"edit": "Edit Contact",
|
"edit": "Редагувати",
|
||||||
"send_email": "Send Email"
|
"send_email": "Надіслати лист"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Календар",
|
"title": "Календар",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "Відкрити меню",
|
"nav_open_menu": "Відкрити меню",
|
||||||
|
"delete": "Видалити",
|
||||||
|
"duplicate": "Дублювати",
|
||||||
|
"edit": "Редагувати",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Зайнято",
|
||||||
"check": "Check Availability",
|
"check": "Перевірити доступність",
|
||||||
"hide": "Hide Availability",
|
"click_to_select": "Натисніть на вільний проміжок часу, щоб вибрати цей час",
|
||||||
"loading": "Loading...",
|
"free": "Вільно",
|
||||||
"no_participants": "Add participants to check availability.",
|
"hide": "Приховати доступність",
|
||||||
"timezone": "Timezone",
|
"loading": "Завантаження...",
|
||||||
"free": "Free",
|
"no_participants": "Додайте учасників, щоб перевірити доступність.",
|
||||||
"busy": "Busy",
|
"tentative": "Орієнтовний",
|
||||||
"tentative": "Tentative",
|
"timezone": "Часовий пояс",
|
||||||
"unavailable": "Out of office",
|
"title": "Доступність",
|
||||||
"unknown": "No information",
|
"unavailable": "Немає на місці",
|
||||||
"click_to_select": "Click a free slot to select this time"
|
"unknown": "Немає інформації"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Очистити все",
|
||||||
"hide": "Hide resources",
|
"filter_all": "все",
|
||||||
"filter_all": "All",
|
"hide": "Приховати ресурси",
|
||||||
"type_room": "Rooms",
|
"no_resources": "Немає доступних ресурсів",
|
||||||
"type_vehicle": "Vehicles",
|
"remove": "Видалити {name}",
|
||||||
"type_equipment": "Equipment",
|
"search_placeholder": "Пошук ресурсів...",
|
||||||
"type_other": "Other",
|
"title": "Ресурси",
|
||||||
"search_placeholder": "Search resources...",
|
"type_equipment": "Обладнання",
|
||||||
"no_resources": "No resources available",
|
"type_other": "інше",
|
||||||
"remove": "Remove {name}",
|
"type_room": "Кімнати",
|
||||||
"clear_all": "Clear all"
|
"type_vehicle": "Транспортні засоби"
|
||||||
},
|
}
|
||||||
"delete": "Delete Event",
|
|
||||||
"duplicate": "Duplicate Event",
|
|
||||||
"edit": "Edit Event"
|
|
||||||
},
|
|
||||||
"sharing": {
|
|
||||||
"title": "Поділитися «{name}»",
|
|
||||||
"description": "Надайте доступ іншим користувачам або групам на цьому сервері. Зміни набувають чинності негайно.",
|
|
||||||
"no_shares": "Поки що ні з ким не поділено.",
|
|
||||||
"add_person": "Додати людину або групу",
|
|
||||||
"search_placeholder": "Шукати за іменем або email…",
|
|
||||||
"loading_principals": "Завантаження користувачів…",
|
|
||||||
"no_principals": "Інших користувачів або груп не знайдено.",
|
|
||||||
"no_match": "Збігів немає.",
|
|
||||||
"remove": "Видалити доступ",
|
|
||||||
"group": "Група",
|
|
||||||
"share_added": "Доступ надано",
|
|
||||||
"share_updated": "Доступ оновлено",
|
|
||||||
"share_removed": "Доступ видалено",
|
|
||||||
"share_failed": "Не вдалося оновити спільний доступ",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "Лише зайнятість",
|
|
||||||
"read": "Лише читання",
|
|
||||||
"readWrite": "Читання та запис",
|
|
||||||
"manager": "Керівник",
|
|
||||||
"custom": "Власне"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "Розширений пошук",
|
"title": "Розширений пошук",
|
||||||
@@ -3285,7 +3253,7 @@
|
|||||||
"other_accounts": "Інші облікові записи",
|
"other_accounts": "Інші облікові записи",
|
||||||
"migration_title": "Оновлення ваших файлів…",
|
"migration_title": "Оновлення ваших файлів…",
|
||||||
"migration_description": "Папки та файли впорядковуються у правильну структуру. Це відбувається лише один раз.",
|
"migration_description": "Папки та файли впорядковуються у правильну структуру. Це відбувається лише один раз.",
|
||||||
"send_as_attachment": "Send as Attachment"
|
"send_as_attachment": "Надіслати як вкладення"
|
||||||
},
|
},
|
||||||
"smime": {
|
"smime": {
|
||||||
"your_certificates": "Ваші сертифікати",
|
"your_certificates": "Ваші сертифікати",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "Пошук недоступний в об'єднаному перегляді"
|
"search_unavailable": "Пошук недоступний в об'єднаному перегляді"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "Поділитися «{name}»",
|
||||||
|
"description": "Надайте доступ іншим користувачам або групам на цьому сервері. Зміни набувають чинності негайно.",
|
||||||
|
"no_shares": "Поки що ні з ким не поділено.",
|
||||||
|
"add_person": "Додати людину або групу",
|
||||||
|
"search_placeholder": "Шукати за іменем або email…",
|
||||||
|
"loading_principals": "Завантаження користувачів…",
|
||||||
|
"no_principals": "Інших користувачів або груп не знайдено.",
|
||||||
|
"no_match": "Збігів немає.",
|
||||||
|
"remove": "Видалити доступ",
|
||||||
|
"group": "Група",
|
||||||
|
"share_added": "Доступ надано",
|
||||||
|
"share_updated": "Доступ оновлено",
|
||||||
|
"share_removed": "Доступ видалено",
|
||||||
|
"share_failed": "Не вдалося оновити спільний доступ",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "Лише зайнятість",
|
||||||
|
"read": "Лише читання",
|
||||||
|
"readWrite": "Читання та запис",
|
||||||
|
"manager": "Керівник",
|
||||||
|
"custom": "Власне"
|
||||||
|
},
|
||||||
|
"accept": "Прийняти",
|
||||||
|
"decline": "Відхилити",
|
||||||
|
"no_shares_by_me": "Ви ще нічим не поділилися.",
|
||||||
|
"no_shares_with_me": "Поки що ніхто не поділився з вами папками.",
|
||||||
|
"shared_by": "Надав доступ",
|
||||||
|
"tab_shared_by_me": "Надані мною",
|
||||||
|
"tab_shared_with_me": "Надані мені"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "{date}, {from} написав:",
|
"reply_line": "{date}, {from} написав:",
|
||||||
"forwarded_separator": "---------- Переслане повідомлення ----------",
|
"forwarded_separator": "---------- Переслане повідомлення ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "Закрити запит на встановлення"
|
"dismiss_aria": "Закрити запит на встановлення"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Додати підпис",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "За замовчуванням",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Використовується для нових повідомлень, якщо не перевизначено для окремої ідентичності.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Підпис за замовчуванням"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Ви впевнені, що хочете видалити \"{name}\"? Це неможливо скасувати.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Видалити підпис?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Створюйте підписи електронної пошти та керуйте ними для використання під час написання чи відповіді.",
|
||||||
},
|
"duplicate": "Дублювати",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Редагувати підпис",
|
||||||
|
"editor_label": "Підпис",
|
||||||
|
"html_preview_label": "Попередній перегляд HTML",
|
||||||
|
"name_label": "Назва",
|
||||||
|
"name_placeholder": "наприклад, Робота, Особисте",
|
||||||
|
"name_required": "Потрібно вказати назву",
|
||||||
|
"new_signature": "Новий підпис",
|
||||||
|
"no_signature": "Без підпису",
|
||||||
|
"no_signatures": "Підписів ще немає",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Перевизначте підпис за замовчуванням і підпис для відповіді для окремих ідентичностей.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Підписи для окремих ідентичностей"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Попередній перегляд простого тексту",
|
||||||
"select_signature": "Select signature",
|
"reply": "Відповідь",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Використовується під час відповіді чи пересилання, якщо не перевизначено для окремої ідентичності.",
|
||||||
|
"label": "Підпис для відповіді"
|
||||||
|
},
|
||||||
|
"show_editor": "Показати редактор",
|
||||||
|
"show_preview": "Показати попередній перегляд",
|
||||||
|
"title": "Підписи",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
"align_center": "По центру",
|
||||||
"italic": "Italic",
|
"align_left": "По лівому краю",
|
||||||
"underline": "Underline",
|
"align_right": "По правому краю",
|
||||||
"strikethrough": "Strikethrough",
|
"bold": "Жирний",
|
||||||
"link": "Link",
|
"bullet_list": "Маркований список",
|
||||||
"bullet_list": "Bullet List",
|
"italic": "Курсив",
|
||||||
"ordered_list": "Ordered List",
|
"link": "Посилання",
|
||||||
"text_color": "Text Color",
|
"ordered_list": "Нумерований список",
|
||||||
"alignment": "Alignment",
|
"remove_color": "Прибрати колір",
|
||||||
"font_size": "Font Size",
|
"strikethrough": "Закреслений",
|
||||||
"align_center": "Align center",
|
"text_color": "Колір тексту",
|
||||||
"align_left": "Align left",
|
"underline": "Підкреслений"
|
||||||
"align_right": "Align right",
|
|
||||||
"remove_color": "Remove color"
|
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Використовувати загальне значення за замовчуванням",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Ваші підписи ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-196
@@ -2016,36 +2016,34 @@
|
|||||||
"managing": "管理:{name}"
|
"managing": "管理:{name}"
|
||||||
},
|
},
|
||||||
"importer": {
|
"importer": {
|
||||||
"title": "Import Data",
|
"action_label": "Import",
|
||||||
"description": "Import emails from .eml files or .zip/.tgz archives.",
|
|
||||||
"file_label": "Select Files",
|
|
||||||
"file_placeholder": "Choose .eml, .zip, or .tgz files",
|
|
||||||
"folder_label": "Import into Folder",
|
|
||||||
"conflict_label": "If Email Already Exists",
|
|
||||||
"start_import": "Start Import",
|
|
||||||
"importing": "Importing...",
|
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success": "Import successful",
|
"choose_files": "Choose files",
|
||||||
|
"conflict_copy": "Keep both",
|
||||||
|
"conflict_description": "Choose what to do when an imported message already exists.",
|
||||||
|
"conflict_label": "Duplicate handling",
|
||||||
|
"conflict_replace": "Replace duplicates",
|
||||||
|
"conflict_skip": "Skip duplicates",
|
||||||
|
"description": "Import email messages from .eml files into a folder.",
|
||||||
|
"error_details": "{count, plural, one {# error} other {# errors}}",
|
||||||
"fail": "Import failed",
|
"fail": "Import failed",
|
||||||
"import_complete": "Import Complete",
|
"file_description": "Select one or more .eml files to import.",
|
||||||
"summary_imported": "{count} imported",
|
"file_label": "Files",
|
||||||
"summary_skipped": "{count} skipped",
|
"files_selected": "{count, plural, one {# file selected} other {# files selected}}",
|
||||||
"summary_failed": "{count} failed",
|
"folder_description": "Choose the folder to import messages into.",
|
||||||
"error_details": "Error Details",
|
"folder_label": "Destination folder",
|
||||||
"import_more": "Import More Files",
|
"import_complete": "Import complete",
|
||||||
"progress_title": "Import Progress",
|
"import_more": "Import more",
|
||||||
"action_label": "Action",
|
"importing": "Importing...",
|
||||||
"choose_files": "Choose Files",
|
"progress_failed": "{count} failed",
|
||||||
"conflict_copy": "Duplicate",
|
"progress_imported": "{count} imported",
|
||||||
"conflict_description": "What to do when importing an email that already exists in the target folder.",
|
"progress_skipped": "{count} skipped",
|
||||||
"conflict_replace": "Replace",
|
"start_import": "{count, plural, one {Import # file} other {Import # files}}",
|
||||||
"conflict_skip": "Skip",
|
"success": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"file_description": "Select .eml, .zip, or .tgz files containing emails to import.",
|
"summary_failed": "{count, plural, one {# message failed} other {# messages failed}}",
|
||||||
"files_selected": "{count} file(s) selected",
|
"summary_imported": "{count, plural, one {# message imported} other {# messages imported}}",
|
||||||
"folder_description": "Choose which folder the imported emails go into.",
|
"summary_skipped": "{count, plural, one {# message skipped} other {# messages skipped}}",
|
||||||
"progress_failed": "Failed",
|
"title": "Import Mail"
|
||||||
"progress_imported": "Imported",
|
|
||||||
"progress_skipped": "Skipped"
|
|
||||||
},
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
@@ -2227,8 +2225,8 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"creating": "创建中...",
|
"creating": "创建中...",
|
||||||
"updating": "更新中...",
|
"updating": "更新中...",
|
||||||
"signature_store_default": "Use default signature",
|
"signature_store_default": "Default signature",
|
||||||
"signature_store_mapping": "Choose signature",
|
"signature_store_mapping": "Signature mapping",
|
||||||
"signature_store_reply": "Reply signature",
|
"signature_store_reply": "Reply signature",
|
||||||
"use_global_default": "Use global default"
|
"use_global_default": "Use global default"
|
||||||
},
|
},
|
||||||
@@ -2565,7 +2563,7 @@
|
|||||||
"csv_country": "Country",
|
"csv_country": "Country",
|
||||||
"csv_email": "Email",
|
"csv_email": "Email",
|
||||||
"csv_first_name": "First name",
|
"csv_first_name": "First name",
|
||||||
"csv_ignore": "Ignore",
|
"csv_ignore": "Ignore this column",
|
||||||
"csv_job_title": "Job title",
|
"csv_job_title": "Job title",
|
||||||
"csv_last_name": "Last name",
|
"csv_last_name": "Last name",
|
||||||
"csv_load_all": "Load all",
|
"csv_load_all": "Load all",
|
||||||
@@ -2575,8 +2573,8 @@
|
|||||||
"csv_phone": "Phone",
|
"csv_phone": "Phone",
|
||||||
"csv_postcode": "Postal code",
|
"csv_postcode": "Postal code",
|
||||||
"csv_preview": "Preview",
|
"csv_preview": "Preview",
|
||||||
"csv_preview_title": "Preview",
|
"csv_preview_title": "Preview ({count, plural, one {# row} other {# rows}})",
|
||||||
"csv_region": "State / Region",
|
"csv_region": "State/Region",
|
||||||
"csv_website": "Website",
|
"csv_website": "Website",
|
||||||
"file_types_csv": ".csv files"
|
"file_types_csv": ".csv files"
|
||||||
},
|
},
|
||||||
@@ -2638,9 +2636,9 @@
|
|||||||
"has_photo": "有照片"
|
"has_photo": "有照片"
|
||||||
},
|
},
|
||||||
"open_categories": "打开分类",
|
"open_categories": "打开分类",
|
||||||
"delete": "Delete Contact",
|
"delete": "Delete",
|
||||||
"edit": "Edit Contact",
|
"edit": "Edit",
|
||||||
"send_email": "Send Email"
|
"send_email": "Send email"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "日历",
|
"title": "日历",
|
||||||
@@ -3060,66 +3058,36 @@
|
|||||||
"esf": "Esfand"
|
"esf": "Esfand"
|
||||||
},
|
},
|
||||||
"nav_open_menu": "打开菜单",
|
"nav_open_menu": "打开菜单",
|
||||||
|
"delete": "Delete",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"edit": "Edit",
|
||||||
"freeBusy": {
|
"freeBusy": {
|
||||||
"title": "Availability",
|
"busy": "Busy",
|
||||||
"check": "Check Availability",
|
"check": "Check Availability",
|
||||||
|
"click_to_select": "Click a free slot to select this time",
|
||||||
|
"free": "Free",
|
||||||
"hide": "Hide Availability",
|
"hide": "Hide Availability",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"no_participants": "Add participants to check availability.",
|
"no_participants": "Add participants to check availability.",
|
||||||
"timezone": "Timezone",
|
|
||||||
"free": "Free",
|
|
||||||
"busy": "Busy",
|
|
||||||
"tentative": "Tentative",
|
"tentative": "Tentative",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"title": "Availability",
|
||||||
"unavailable": "Out of office",
|
"unavailable": "Out of office",
|
||||||
"unknown": "No information",
|
"unknown": "No information"
|
||||||
"click_to_select": "Click a free slot to select this time"
|
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"title": "Resources",
|
"clear_all": "Clear all",
|
||||||
"hide": "Hide resources",
|
|
||||||
"filter_all": "All",
|
"filter_all": "All",
|
||||||
"type_room": "Rooms",
|
"hide": "Hide resources",
|
||||||
"type_vehicle": "Vehicles",
|
|
||||||
"type_equipment": "Equipment",
|
|
||||||
"type_other": "Other",
|
|
||||||
"search_placeholder": "Search resources...",
|
|
||||||
"no_resources": "No resources available",
|
"no_resources": "No resources available",
|
||||||
"remove": "Remove {name}",
|
"remove": "Remove {name}",
|
||||||
"clear_all": "Clear all"
|
"search_placeholder": "Search resources...",
|
||||||
},
|
"title": "Resources",
|
||||||
"delete": "Delete Event",
|
"type_equipment": "Equipment",
|
||||||
"duplicate": "Duplicate Event",
|
"type_other": "Other",
|
||||||
"edit": "Edit Event"
|
"type_room": "Rooms",
|
||||||
},
|
"type_vehicle": "Vehicles"
|
||||||
"sharing": {
|
}
|
||||||
"title": "共享「{name}」",
|
|
||||||
"description": "向此服务器上的其他用户或群组授予访问权限。更改会立即生效。",
|
|
||||||
"no_shares": "尚未共享。",
|
|
||||||
"add_person": "添加用户或群组",
|
|
||||||
"search_placeholder": "按姓名或邮箱搜索…",
|
|
||||||
"loading_principals": "正在加载用户…",
|
|
||||||
"no_principals": "未找到其他用户或群组。",
|
|
||||||
"no_match": "无匹配项。",
|
|
||||||
"remove": "取消访问",
|
|
||||||
"group": "群组",
|
|
||||||
"share_added": "已授予访问权限",
|
|
||||||
"share_updated": "已更新访问权限",
|
|
||||||
"share_removed": "已取消访问权限",
|
|
||||||
"share_failed": "更新共享失败",
|
|
||||||
"preset": {
|
|
||||||
"freeBusy": "仅显示忙/闲",
|
|
||||||
"read": "只读",
|
|
||||||
"readWrite": "读写",
|
|
||||||
"manager": "管理员",
|
|
||||||
"custom": "自定义"
|
|
||||||
},
|
|
||||||
"tab_shared_by_me": "Shared by me",
|
|
||||||
"tab_shared_with_me": "Shared with me",
|
|
||||||
"no_shares_by_me": "You haven't shared anything yet.",
|
|
||||||
"no_shares_with_me": "No folders shared with you yet.",
|
|
||||||
"shared_by": "Shared by",
|
|
||||||
"accept": "Accept",
|
|
||||||
"decline": "Decline"
|
|
||||||
},
|
},
|
||||||
"advanced_search": {
|
"advanced_search": {
|
||||||
"title": "高级搜索",
|
"title": "高级搜索",
|
||||||
@@ -3419,6 +3387,36 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"search_unavailable": "统一视图中无法使用搜索"
|
"search_unavailable": "统一视图中无法使用搜索"
|
||||||
},
|
},
|
||||||
|
"sharing": {
|
||||||
|
"title": "共享「{name}」",
|
||||||
|
"description": "向此服务器上的其他用户或群组授予访问权限。更改会立即生效。",
|
||||||
|
"no_shares": "尚未共享。",
|
||||||
|
"add_person": "添加用户或群组",
|
||||||
|
"search_placeholder": "按姓名或邮箱搜索…",
|
||||||
|
"loading_principals": "正在加载用户…",
|
||||||
|
"no_principals": "未找到其他用户或群组。",
|
||||||
|
"no_match": "无匹配项。",
|
||||||
|
"remove": "取消访问",
|
||||||
|
"group": "群组",
|
||||||
|
"share_added": "已授予访问权限",
|
||||||
|
"share_updated": "已更新访问权限",
|
||||||
|
"share_removed": "已取消访问权限",
|
||||||
|
"share_failed": "更新共享失败",
|
||||||
|
"preset": {
|
||||||
|
"freeBusy": "仅显示忙/闲",
|
||||||
|
"read": "只读",
|
||||||
|
"readWrite": "读写",
|
||||||
|
"manager": "管理员",
|
||||||
|
"custom": "自定义"
|
||||||
|
},
|
||||||
|
"accept": "Accept",
|
||||||
|
"decline": "Decline",
|
||||||
|
"no_shares_by_me": "You haven't shared anything yet.",
|
||||||
|
"no_shares_with_me": "No folders shared with you yet.",
|
||||||
|
"shared_by": "Shared by",
|
||||||
|
"tab_shared_by_me": "Shared by me",
|
||||||
|
"tab_shared_with_me": "Shared with me"
|
||||||
|
},
|
||||||
"quote_header": {
|
"quote_header": {
|
||||||
"reply_line": "在 {date},{from} 写道:",
|
"reply_line": "在 {date},{from} 写道:",
|
||||||
"forwarded_separator": "---------- 转发邮件 ----------",
|
"forwarded_separator": "---------- 转发邮件 ----------",
|
||||||
@@ -3435,126 +3433,53 @@
|
|||||||
"dismiss_aria": "关闭安装提示"
|
"dismiss_aria": "关闭安装提示"
|
||||||
},
|
},
|
||||||
"signatures": {
|
"signatures": {
|
||||||
"title": "Signatures",
|
"add_signature": "Add signature",
|
||||||
"description": "Create and manage email signatures. Assign default signatures for new messages and replies.",
|
"default": "Default",
|
||||||
"no_signature": "No signatures created yet.",
|
|
||||||
"add_signature": "Add Signature",
|
|
||||||
"duplicate": "Duplicate",
|
|
||||||
"your_signatures": "Your Signatures",
|
|
||||||
"delete_title": "Delete Signature",
|
|
||||||
"delete_message": "Are you sure you want to delete \"{name}\"?",
|
|
||||||
"edit_signature": "Edit Signature",
|
|
||||||
"new_signature": "New Signature",
|
|
||||||
"name_required": "Signature name is required",
|
|
||||||
"name_label": "Signature Name",
|
|
||||||
"name_placeholder": "e.g., Work, Personal, Legal",
|
|
||||||
"editor_label": "Signature Content",
|
|
||||||
"show_preview": "Preview",
|
|
||||||
"show_editor": "Editor",
|
|
||||||
"html_preview_label": "HTML Preview",
|
|
||||||
"plain_text_preview_label": "Plain Text Preview",
|
|
||||||
"default_signature": {
|
"default_signature": {
|
||||||
"label": "Default for new messages",
|
"description": "Used for new messages unless overridden per identity.",
|
||||||
"description": "Automatically insert this signature when composing a new message."
|
"label": "Default signature"
|
||||||
},
|
},
|
||||||
"reply_signature": {
|
"delete_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
|
||||||
"label": "Default for replies",
|
"delete_title": "Delete signature?",
|
||||||
"description": "Automatically insert this signature when replying or forwarding."
|
"description": "Create and manage email signatures to use when composing or replying.",
|
||||||
},
|
"duplicate": "Duplicate",
|
||||||
"no_signatures_available": "No signatures available",
|
"edit_signature": "Edit signature",
|
||||||
|
"editor_label": "Signature",
|
||||||
|
"html_preview_label": "HTML preview",
|
||||||
|
"name_label": "Name",
|
||||||
|
"name_placeholder": "e.g., Work, Personal",
|
||||||
|
"name_required": "Name is required",
|
||||||
|
"new_signature": "New signature",
|
||||||
|
"no_signature": "No signature",
|
||||||
|
"no_signatures": "No signatures yet",
|
||||||
"per_identity_signatures": {
|
"per_identity_signatures": {
|
||||||
"label": "Per-Identity Signature Overrides",
|
"description": "Override the default and reply signature for individual identities.",
|
||||||
"description": "Override the default signature for individual sending identities."
|
"label": "Per-identity signatures"
|
||||||
},
|
},
|
||||||
"per_identity_description": "Assign different signatures to specific identities.",
|
"plain_text_preview_label": "Plain text preview",
|
||||||
"select_signature": "Select signature",
|
"reply": "Reply",
|
||||||
"cancel": "Cancel",
|
"reply_signature": {
|
||||||
"save": "Save Signature",
|
"description": "Used when replying or forwarding unless overridden per identity.",
|
||||||
|
"label": "Reply signature"
|
||||||
|
},
|
||||||
|
"show_editor": "Show editor",
|
||||||
|
"show_preview": "Show preview",
|
||||||
|
"title": "Signatures",
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Bold",
|
|
||||||
"italic": "Italic",
|
|
||||||
"underline": "Underline",
|
|
||||||
"strikethrough": "Strikethrough",
|
|
||||||
"link": "Link",
|
|
||||||
"bullet_list": "Bullet List",
|
|
||||||
"ordered_list": "Ordered List",
|
|
||||||
"text_color": "Text Color",
|
|
||||||
"alignment": "Alignment",
|
|
||||||
"font_size": "Font Size",
|
|
||||||
"align_center": "Align center",
|
"align_center": "Align center",
|
||||||
"align_left": "Align left",
|
"align_left": "Align left",
|
||||||
"align_right": "Align right",
|
"align_right": "Align right",
|
||||||
"remove_color": "Remove color"
|
"bold": "Bold",
|
||||||
|
"bullet_list": "Bullet list",
|
||||||
|
"italic": "Italic",
|
||||||
|
"link": "Link",
|
||||||
|
"ordered_list": "Ordered list",
|
||||||
|
"remove_color": "Remove color",
|
||||||
|
"strikethrough": "Strikethrough",
|
||||||
|
"text_color": "Text color",
|
||||||
|
"underline": "Underline"
|
||||||
},
|
},
|
||||||
"default": "Default",
|
"use_global_default": "Use global default",
|
||||||
"no_signatures": "No signatures yet",
|
"your_signatures": "Your signatures ({count})"
|
||||||
"reply": "Replies",
|
|
||||||
"use_global_default": "Use global default"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"vncdirectory": {
|
|
||||||
"title": "VNCdirectory",
|
|
||||||
"description": "Centralized identity and directory integration (SAML, LDAP, 2FA)",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save configuration",
|
|
||||||
"saving": "Saving...",
|
|
||||||
"saved": "VNCdirectory configuration saved.",
|
|
||||||
"save_error": "Failed to save",
|
|
||||||
"enable_section": "Enable VNCdirectory Integration",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"enabled_description": "Turn on VNCdirectory integration for identity management, SSO, and directory services",
|
|
||||||
"connection": "Connection",
|
|
||||||
"url": "VNCdirectory URL",
|
|
||||||
"url_placeholder": "https://vncdirectory.example.com",
|
|
||||||
"api_key": "API Key",
|
|
||||||
"api_key_placeholder": "Enter API key",
|
|
||||||
"saml": "SAML / Identity Provider",
|
|
||||||
"saml_enabled": "SAML Enabled",
|
|
||||||
"saml_enabled_description": "Enable SAML single sign-on via VNCdirectory",
|
|
||||||
"idp_url": "Identity Provider URL",
|
|
||||||
"idp_url_placeholder": "https://idp.example.com/saml2/idp",
|
|
||||||
"issuer": "Issuer Name (Entity ID)",
|
|
||||||
"issuer_placeholder": "urn:example:vncmail",
|
|
||||||
"sp_cert": "Service Provider Certificate (X.509)",
|
|
||||||
"sp_cert_placeholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
|
|
||||||
"ldap": "LDAP Directory",
|
|
||||||
"ldap_enabled": "LDAP Enabled",
|
|
||||||
"ldap_enabled_description": "Query user directory via LDAP for contact lookups and authentication",
|
|
||||||
"ldap_uri": "LDAP Server URI",
|
|
||||||
"ldap_uri_placeholder": "ldaps://ldap.example.com:636",
|
|
||||||
"bind_dn": "Bind DN",
|
|
||||||
"bind_dn_placeholder": "cn=readonly,dc=example,dc=com",
|
|
||||||
"bind_password": "Bind Password",
|
|
||||||
"bind_password_placeholder": "Enter LDAP bind password",
|
|
||||||
"search_base": "Search Base",
|
|
||||||
"search_base_placeholder": "ou=users,dc=example,dc=com",
|
|
||||||
"ldap_type": "LDAP Type",
|
|
||||||
"ldap_type_openldap": "OpenLDAP",
|
|
||||||
"ldap_type_msad": "Microsoft Active Directory",
|
|
||||||
"auth_section": "Authentication",
|
|
||||||
"require_2fa": "Enforce 2FA/TOTP",
|
|
||||||
"require_2fa_description": "Require two-factor authentication for all users",
|
|
||||||
"oidc_section": "OpenID Connect (OIDC)",
|
|
||||||
"oidc_section_description": "Enable OIDC login alongside or instead of SAML",
|
|
||||||
"oidc_client_id": "OIDC Client ID",
|
|
||||||
"oidc_client_id_placeholder": "vncmail-client",
|
|
||||||
"oidc_discovery_url": "OIDC Discovery URL",
|
|
||||||
"oidc_discovery_url_placeholder": "https://idp.example.com/.well-known/openid-configuration",
|
|
||||||
"session_ttl": "Session TTL (seconds)",
|
|
||||||
"session_ttl_description": "How long SSO sessions remain valid. Default: 8 hours (28800)",
|
|
||||||
"federated": "Federated Applications",
|
|
||||||
"federated_description": "Configure SSO redirect URLs for other VNC applications. Users signed into one app will be transparently authenticated when navigating to another.",
|
|
||||||
"add_app": "Add federated app",
|
|
||||||
"app_name_placeholder": "App name (e.g. vnctalk)",
|
|
||||||
"app_url_placeholder": "https://vnc.example.com/auth/sso",
|
|
||||||
"remove_app": "Remove {name}",
|
|
||||||
"add": "Add",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"app_name_error": "Enter an application name",
|
|
||||||
"app_name_format_error": "Name must contain only letters, numbers, hyphens, and underscores",
|
|
||||||
"app_exists_error": "An app with this name already exists",
|
|
||||||
"app_url_error": "Enter an SSO URL",
|
|
||||||
"saved_password_hint": "Saved - type to replace"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
calendar.delete Delete
|
||||||
|
calendar.duplicate Duplicate
|
||||||
|
calendar.edit Edit
|
||||||
|
calendar.freeBusy.busy Busy
|
||||||
|
calendar.freeBusy.check Check Availability
|
||||||
|
calendar.freeBusy.click_to_select Click a free slot to select this time
|
||||||
|
calendar.freeBusy.free Free
|
||||||
|
calendar.freeBusy.hide Hide Availability
|
||||||
|
calendar.freeBusy.loading Loading...
|
||||||
|
calendar.freeBusy.no_participants Add participants to check availability.
|
||||||
|
calendar.freeBusy.tentative Tentative
|
||||||
|
calendar.freeBusy.timezone Timezone
|
||||||
|
calendar.freeBusy.title Availability
|
||||||
|
calendar.freeBusy.unavailable Out of office
|
||||||
|
calendar.freeBusy.unknown No information
|
||||||
|
calendar.resources.clear_all Clear all
|
||||||
|
calendar.resources.filter_all All
|
||||||
|
calendar.resources.hide Hide resources
|
||||||
|
calendar.resources.no_resources No resources available
|
||||||
|
calendar.resources.remove Remove {name}
|
||||||
|
calendar.resources.search_placeholder Search resources...
|
||||||
|
calendar.resources.title Resources
|
||||||
|
calendar.resources.type_equipment Equipment
|
||||||
|
calendar.resources.type_other Other
|
||||||
|
calendar.resources.type_room Rooms
|
||||||
|
calendar.resources.type_vehicle Vehicles
|
||||||
|
contacts.delete Delete
|
||||||
|
contacts.edit Edit
|
||||||
|
contacts.import.csv_address Address
|
||||||
|
contacts.import.csv_address_book Address book
|
||||||
|
contacts.import.csv_back Back
|
||||||
|
contacts.import.csv_city City
|
||||||
|
contacts.import.csv_company Company
|
||||||
|
contacts.import.csv_country Country
|
||||||
|
contacts.import.csv_email Email
|
||||||
|
contacts.import.csv_first_name First name
|
||||||
|
contacts.import.csv_ignore Ignore this column
|
||||||
|
contacts.import.csv_job_title Job title
|
||||||
|
contacts.import.csv_last_name Last name
|
||||||
|
contacts.import.csv_load_all Load all
|
||||||
|
contacts.import.csv_map_columns Map columns
|
||||||
|
contacts.import.csv_nickname Nickname
|
||||||
|
contacts.import.csv_note Note
|
||||||
|
contacts.import.csv_phone Phone
|
||||||
|
contacts.import.csv_postcode Postal code
|
||||||
|
contacts.import.csv_preview Preview
|
||||||
|
contacts.import.csv_preview_title Preview ({count, plural, one {# row} other {# rows}})
|
||||||
|
contacts.import.csv_region State/Region
|
||||||
|
contacts.import.csv_website Website
|
||||||
|
contacts.import.file_types_csv .csv files
|
||||||
|
contacts.send_email Send email
|
||||||
|
email_composer.insert_signature Insert signature
|
||||||
|
email_composer.no_signature No signature
|
||||||
|
email_composer.select_signature Select signature
|
||||||
|
email_viewer.create_appointment Create Appointment
|
||||||
|
files.send_as_attachment Send as Attachment
|
||||||
|
identities.form.signature_store_default Default signature
|
||||||
|
identities.form.signature_store_mapping Signature mapping
|
||||||
|
identities.form.signature_store_reply Reply signature
|
||||||
|
identities.form.use_global_default Use global default
|
||||||
|
mailbox_context_menu.share_folder Share Folder...
|
||||||
|
settings.importer.action_label Import
|
||||||
|
settings.importer.cancel Cancel
|
||||||
|
settings.importer.choose_files Choose files
|
||||||
|
settings.importer.conflict_copy Keep both
|
||||||
|
settings.importer.conflict_description Choose what to do when an imported message already exists.
|
||||||
|
settings.importer.conflict_label Duplicate handling
|
||||||
|
settings.importer.conflict_replace Replace duplicates
|
||||||
|
settings.importer.conflict_skip Skip duplicates
|
||||||
|
settings.importer.description Import email messages from .eml files into a folder.
|
||||||
|
settings.importer.error_details {count, plural, one {# error} other {# errors}}
|
||||||
|
settings.importer.fail Import failed
|
||||||
|
settings.importer.file_description Select one or more .eml files to import.
|
||||||
|
settings.importer.file_label Files
|
||||||
|
settings.importer.files_selected {count, plural, one {# file selected} other {# files selected}}
|
||||||
|
settings.importer.folder_description Choose the folder to import messages into.
|
||||||
|
settings.importer.folder_label Destination folder
|
||||||
|
settings.importer.import_complete Import complete
|
||||||
|
settings.importer.import_more Import more
|
||||||
|
settings.importer.importing Importing...
|
||||||
|
settings.importer.progress_failed {count} failed
|
||||||
|
settings.importer.progress_imported {count} imported
|
||||||
|
settings.importer.progress_skipped {count} skipped
|
||||||
|
settings.importer.start_import {count, plural, one {Import # file} other {Import # files}}
|
||||||
|
settings.importer.success {count, plural, one {# message imported} other {# messages imported}}
|
||||||
|
settings.importer.summary_failed {count, plural, one {# message failed} other {# messages failed}}
|
||||||
|
settings.importer.summary_imported {count, plural, one {# message imported} other {# messages imported}}
|
||||||
|
settings.importer.summary_skipped {count, plural, one {# message skipped} other {# messages skipped}}
|
||||||
|
settings.importer.title Import Mail
|
||||||
|
settings.loading Loading...
|
||||||
|
settings.refresh Refresh
|
||||||
|
settings.tabs.import Import
|
||||||
|
settings.tabs.sharing Sharing
|
||||||
|
settings.tabs.signatures Signatures
|
||||||
|
sharing.accept Accept
|
||||||
|
sharing.decline Decline
|
||||||
|
sharing.no_shares_by_me You haven't shared anything yet.
|
||||||
|
sharing.no_shares_with_me No folders shared with you yet.
|
||||||
|
sharing.shared_by Shared by
|
||||||
|
sharing.tab_shared_by_me Shared by me
|
||||||
|
sharing.tab_shared_with_me Shared with me
|
||||||
|
signatures.add_signature Add signature
|
||||||
|
signatures.default Default
|
||||||
|
signatures.default_signature.description Used for new messages unless overridden per identity.
|
||||||
|
signatures.default_signature.label Default signature
|
||||||
|
signatures.delete_message Are you sure you want to delete "{name}"? This cannot be undone.
|
||||||
|
signatures.delete_title Delete signature?
|
||||||
|
signatures.description Create and manage email signatures to use when composing or replying.
|
||||||
|
signatures.duplicate Duplicate
|
||||||
|
signatures.edit_signature Edit signature
|
||||||
|
signatures.editor_label Signature
|
||||||
|
signatures.html_preview_label HTML preview
|
||||||
|
signatures.name_label Name
|
||||||
|
signatures.name_placeholder e.g., Work, Personal
|
||||||
|
signatures.name_required Name is required
|
||||||
|
signatures.new_signature New signature
|
||||||
|
signatures.no_signature No signature
|
||||||
|
signatures.no_signatures No signatures yet
|
||||||
|
signatures.per_identity_signatures.description Override the default and reply signature for individual identities.
|
||||||
|
signatures.per_identity_signatures.label Per-identity signatures
|
||||||
|
signatures.plain_text_preview_label Plain text preview
|
||||||
|
signatures.reply Reply
|
||||||
|
signatures.reply_signature.description Used when replying or forwarding unless overridden per identity.
|
||||||
|
signatures.reply_signature.label Reply signature
|
||||||
|
signatures.show_editor Show editor
|
||||||
|
signatures.show_preview Show preview
|
||||||
|
signatures.title Signatures
|
||||||
|
signatures.toolbar.align_center Align center
|
||||||
|
signatures.toolbar.align_left Align left
|
||||||
|
signatures.toolbar.align_right Align right
|
||||||
|
signatures.toolbar.bold Bold
|
||||||
|
signatures.toolbar.bullet_list Bullet list
|
||||||
|
signatures.toolbar.italic Italic
|
||||||
|
signatures.toolbar.link Link
|
||||||
|
signatures.toolbar.ordered_list Ordered list
|
||||||
|
signatures.toolbar.remove_color Remove color
|
||||||
|
signatures.toolbar.strikethrough Strikethrough
|
||||||
|
signatures.toolbar.text_color Text color
|
||||||
|
signatures.toolbar.underline Underline
|
||||||
|
signatures.use_global_default Use global default
|
||||||
|
signatures.your_signatures Your signatures ({count})
|
||||||
|
Can't render this file because it contains an unexpected character in line 106 and column 59.
|
Generated
-33
@@ -48,7 +48,6 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"webcrypto-liner": "^1.4.3",
|
"webcrypto-liner": "^1.4.3",
|
||||||
"ws": "^8.21.3",
|
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -62,7 +61,6 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/ws": "^8.18.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||||
"@typescript-eslint/parser": "^8.59.0",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
@@ -4731,16 +4729,6 @@
|
|||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/ws": {
|
|
||||||
"version": "8.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
|
||||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.59.0",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||||
@@ -12870,27 +12858,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.21.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
|
||||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
|||||||
@@ -84,7 +84,6 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"webcrypto-liner": "^1.4.3",
|
"webcrypto-liner": "^1.4.3",
|
||||||
"ws": "^8.21.3",
|
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
@@ -101,7 +100,6 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/ws": "^8.18.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||||
"@typescript-eslint/parser": "^8.59.0",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,128 +0,0 @@
|
|||||||
# Phase 2 QA Report — v1.7.9 → v1.8.0
|
|
||||||
|
|
||||||
**Date:** 2026-08-07
|
|
||||||
**Branch:** feat/phase2-signatures-sharing → main
|
|
||||||
**Sandbox:** https://vncmail.sandbox.vnc.de (ArgoCD `vncmail-dev`)
|
|
||||||
**Scope:** All 14 Phase 2 features (59 files, +7,767/-122 lines)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Build Status
|
|
||||||
|
|
||||||
| # | Feature | Built | QA Status |
|
|
||||||
|---|---------|:-----:|-----------|
|
|
||||||
| P2.1 | Extended Signatures | ✅ | 4 issues (1 CRITICAL **FIXED**, 3 MEDIUM) |
|
|
||||||
| P2.2 | Create Appointment from Email | ✅ | 0 issues |
|
|
||||||
| P2.3 | Folder Sharing | ✅ | 4 issues (1 CRITICAL **FIXED**, 3 HIGH) |
|
|
||||||
| P2.4 | Calendar Dashlet | ✅ | 1 issue (LOW) |
|
|
||||||
| P2.5 | Email Import | ✅ | 3 issues (1 CRITICAL **FIXED**, 2 LOW) |
|
|
||||||
| P2.6 | Contact Import | ✅ | 0 issues |
|
|
||||||
| P2.7 | Free/Busy View | ✅ | 3 issues (1 HIGH, 2 MEDIUM) |
|
|
||||||
| P2.8 | Resources/Equipment Booking | ✅ | 5 issues (2 HIGH, 2 MEDIUM, 1 LOW) |
|
|
||||||
| P2.9 | VNCtalk Video Meeting | ✅ | 1 issue (MEDIUM) |
|
|
||||||
| P2.10 | Collabora Online Editing | ✅ | 1 issue (MEDIUM) |
|
|
||||||
| P2.11 | Calendar Enhancements | ✅ | 0 issues |
|
|
||||||
| P2.12 | Action Wheel Radial Menu | ✅ | 1 issue (MEDIUM) |
|
|
||||||
| P2.13 | VNCdirectory IDP Admin | ✅ | 3 issues (2 HIGH, 1 MEDIUM) |
|
|
||||||
| P2.14 | Share Files by Email | ✅ | 0 issues |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CRITICAL Issues (4 found, 4 fixed)
|
|
||||||
|
|
||||||
### C1 — Missing `signatures` translation namespace ✅ FIXED
|
|
||||||
- **Files:** `signature-settings.tsx:21`, `signature-editor-modal.tsx:104`
|
|
||||||
- **Impact:** All UI strings rendered as raw key strings (e.g., `signatures.title`)
|
|
||||||
- **Fix:** Added `"signatures"` namespace with 27 keys to `locales/en/common.json`
|
|
||||||
|
|
||||||
### C2 — Missing `settings.tabs.signatures` translation key ✅ FIXED
|
|
||||||
- **File:** `app/(main)/[locale]/settings/page.tsx:652`
|
|
||||||
- **Impact:** Settings page tab label rendered as raw key string
|
|
||||||
- **Fix:** Added `"signatures": "Signatures"` to `settings.tabs` section
|
|
||||||
|
|
||||||
### C3 — Missing `settings.importer` translation namespace ✅ FIXED
|
|
||||||
- **File:** `components/settings/import-settings.tsx:16`
|
|
||||||
- **Impact:** All import UI strings rendered as raw key strings
|
|
||||||
- **Fix:** Added `"importer"` namespace with 18 keys under `"settings"`
|
|
||||||
|
|
||||||
### C4 — `sharedWithMe` never populated in sharing-store ✅ FIXED
|
|
||||||
- **File:** `stores/sharing-store.ts:237`
|
|
||||||
- **Impact:** "Shared with me" tab permanently empty — accept/decline workflow dead
|
|
||||||
- **Fix:** Added discovery logic for incoming mail/calendar/addressBook shares by checking `isShared` + `myRights` properties
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## HIGH Issues (7 remaining)
|
|
||||||
|
|
||||||
### H1 — VNCdirectory admin tab has no internationalization
|
|
||||||
- **File:** `app/(main)/admin/_tabs/vncdirectory.tsx`
|
|
||||||
- **Impact:** All 50+ strings hardcoded in English — no translation support
|
|
||||||
- **Recommendation:** Add `admin.vncdirectory.*` translation keys
|
|
||||||
|
|
||||||
### H2 — VNCdirectory admin `handleSave` has no try/catch
|
|
||||||
- **File:** `app/(main)/admin/_tabs/vncdirectory.tsx:104-123`
|
|
||||||
- **Impact:** Network failure on save crashes admin UI silently
|
|
||||||
- **Recommendation:** Wrap in try/catch, show error toast
|
|
||||||
|
|
||||||
### H3 — Free/busy `queryAllCalendarEvents` queries all accounts indiscriminately
|
|
||||||
- **File:** `lib/calendar-freebusy.ts:140-143`
|
|
||||||
- **Impact:** Free/busy results mix events from all connected accounts
|
|
||||||
- **Recommendation:** Accept an `accountId` parameter to scope the query
|
|
||||||
|
|
||||||
### H4 — `cancelEventBookings` ignores `_eventId` parameter
|
|
||||||
- **File:** `stores/resource-store.ts:139-153`
|
|
||||||
- **Impact:** Cancelling a single event's bookings removes ALL resource bookings
|
|
||||||
- **Recommendation:** Filter by `eventId` before cancelling
|
|
||||||
|
|
||||||
### H5 — Resource picker dynamic import in hot loop
|
|
||||||
- **File:** `components/calendar/resource-picker.tsx:75`
|
|
||||||
- **Impact:** `apiFetch` imported once per resource item — N× network chunk requests
|
|
||||||
- **Recommendation:** Import at module top level
|
|
||||||
|
|
||||||
### H6 — Hardcoded English toast messages in sharing-store
|
|
||||||
- **File:** `stores/sharing-store.ts:374,398,421,430,437`
|
|
||||||
- **Impact:** Toast notifications always in English regardless of user locale
|
|
||||||
- **Recommendation:** Pass translation keys or use `useToastStore` with i18n
|
|
||||||
|
|
||||||
### H7 — `roleLabel` only handles mailbox kind
|
|
||||||
- **File:** `stores/sharing-store.ts:69-72`
|
|
||||||
- **Impact:** Calendar/addressBook/file share roles show raw internal strings instead of labels
|
|
||||||
- **Recommendation:** Add label mappings for all resource types
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MEDIUM Issues (11 remaining)
|
|
||||||
|
|
||||||
1. `identitySignatureMap` not cleaned up on signature delete — stale references
|
|
||||||
2. Duplicated rights detection logic between sharing-store and API route
|
|
||||||
3. Free/busy "now" line absolute positioning without relative parent
|
|
||||||
4. Radial menu keyboard nav skips disabled items but can land on disabled
|
|
||||||
5. Radar menu re-registers event listener on every `activeIndex` change
|
|
||||||
6. `configManager` import pattern in VNCtalk client may not be safe server-side
|
|
||||||
7. Collabora uses direct `process.env` access instead of configManager
|
|
||||||
8. `CONFIG_ENV_MAP` missing most VNCdirectory fields for env var overrides
|
|
||||||
9. `SENSITIVE_CONFIG_KEYS` field name mismatch between types.ts and vncdirectory-config.ts
|
|
||||||
10. `cancelBooking` silently fails on missing booking ID
|
|
||||||
11. `PasswordRow` sentinel value `'••••••'` is a design smell
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LOW Issues (7 remaining)
|
|
||||||
|
|
||||||
1. Unused imports: `Mailbox` in email-import.ts, `isArchiveName` in eml-import.ts, `toBase64` in email-import.ts
|
|
||||||
2. Missing `aria-label` on close buttons in signature editor and import settings
|
|
||||||
3. Resource picker spinner missing `role="status"` and `aria-label`
|
|
||||||
4. Free/busy `slot!` non-null assertion is fragile
|
|
||||||
5. `parseDurationMs` duplicates existing duration parsing logic
|
|
||||||
6. Mini-calendar dashlet uses imperative `fetchEvents` outside reactive lifecycle
|
|
||||||
7. Search input in resource picker missing `aria-label`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Release Recommendation
|
|
||||||
|
|
||||||
**APPROVED with noted issues.** The 4 CRITICAL bugs are fixed. The 7 HIGH and 13 MEDIUM/LOW issues are non-blocking but should be addressed in the next sprint. All 14 features are functional and code-complete.
|
|
||||||
|
|
||||||
**Test URL:** https://vncmail.sandbox.vnc.de (ArgoCD syncs from `dev` branch)
|
|
||||||
|
|
||||||
**Commit:** `42a7b67e` (main)
|
|
||||||
@@ -19,7 +19,7 @@ const shared = {
|
|||||||
// stays external so electron-builder ships it from node_modules as a
|
// stays external so electron-builder ships it from node_modules as a
|
||||||
// normal production dependency instead of us re-bundling its native-ish
|
// normal production dependency instead of us re-bundling its native-ish
|
||||||
// internals (see electron-builder.config.js's file collection).
|
// internals (see electron-builder.config.js's file collection).
|
||||||
external: ["electron", "electron-updater", "ws"],
|
external: ["electron", "electron-updater"],
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { encryptedStorage } from '@/stores/encrypted-storage';
|
|
||||||
import { generateAccountId, generateAvatarColor, getMaxAccounts } from '@/lib/account-utils';
|
import { generateAccountId, generateAvatarColor, getMaxAccounts } from '@/lib/account-utils';
|
||||||
|
|
||||||
export interface AccountEntry {
|
export interface AccountEntry {
|
||||||
@@ -219,7 +218,6 @@ export const useAccountStore = create<AccountState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'account-registry',
|
name: 'account-registry',
|
||||||
storage: createJSONStorage(() => encryptedStorage),
|
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
accounts: state.accounts,
|
accounts: state.accounts,
|
||||||
activeAccountId: state.activeAccountId,
|
activeAccountId: state.activeAccountId,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { encryptedStorage } from '@/stores/encrypted-storage';
|
|
||||||
import { JMAPClient, RateLimitError } from '@/lib/jmap/client';
|
import { JMAPClient, RateLimitError } from '@/lib/jmap/client';
|
||||||
import { withOfflineFallback } from '@/lib/offline-fallback-client';
|
import { withOfflineFallback } from '@/lib/offline-fallback-client';
|
||||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
||||||
@@ -2011,7 +2010,6 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'auth-storage',
|
name: 'auth-storage',
|
||||||
storage: createJSONStorage(() => encryptedStorage),
|
|
||||||
partialize: (state) => {
|
partialize: (state) => {
|
||||||
// Don't persist unauthenticated state - prevents resurrecting stale sessions
|
// Don't persist unauthenticated state - prevents resurrecting stale sessions
|
||||||
if (!state.isAuthenticated) return {};
|
if (!state.isAuthenticated) return {};
|
||||||
|
|||||||
+18
-88
@@ -12,8 +12,6 @@ import { generateUUID } from '@/lib/utils';
|
|||||||
import { apiFetch } from '@/lib/browser-navigation';
|
import { apiFetch } from '@/lib/browser-navigation';
|
||||||
import { BIRTHDAY_CALENDAR_ID } from '@/lib/birthday-calendar';
|
import { BIRTHDAY_CALENDAR_ID } from '@/lib/birthday-calendar';
|
||||||
import { getClientByLocalAccountId } from './client-registry';
|
import { getClientByLocalAccountId } from './client-registry';
|
||||||
import { enqueueOperation, isNetworkError } from '@/lib/offline-write-queue';
|
|
||||||
import { useAccountStore } from '@/stores/account-store';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the Pro shell aggregates calendars/events from every connected
|
* When the Pro shell aggregates calendars/events from every connected
|
||||||
@@ -411,13 +409,13 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
|
|
||||||
createEvent: async (client, event, sendSchedulingMessages) => {
|
createEvent: async (client, event, sendSchedulingMessages) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
let targetAccountId: string | undefined = event.accountId;
|
|
||||||
const cleanEvent = sanitizeOutgoingCalendarEventData({ ...event });
|
|
||||||
try {
|
try {
|
||||||
// Resolve shared calendar context from calendarIds. Also pin the
|
// Resolve shared calendar context from calendarIds. Also pin the
|
||||||
// local account from the calendar so we route through that
|
// local account from the calendar so we route through that
|
||||||
// server's client when in multi-account Pro mode.
|
// server's client when in multi-account Pro mode.
|
||||||
|
let targetAccountId = event.accountId;
|
||||||
let localAccountId = event.localAccountId;
|
let localAccountId = event.localAccountId;
|
||||||
|
const cleanEvent = sanitizeOutgoingCalendarEventData({ ...event });
|
||||||
if (event.calendarIds) {
|
if (event.calendarIds) {
|
||||||
const remapped: Record<string, boolean> = {};
|
const remapped: Record<string, boolean> = {};
|
||||||
for (const calId of Object.keys(event.calendarIds)) {
|
for (const calId of Object.keys(event.calendarIds)) {
|
||||||
@@ -487,25 +485,12 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({ events: [...state.events, mappedCreated] }));
|
set((state) => ({ events: [...state.events, mappedCreated] }));
|
||||||
// Send invitation emails (iTIP REQUEST) to participants. Stalwart
|
// Invitation emails are sent by the server: `sendSchedulingMessages`
|
||||||
// 0.16 does not reliably queue these server-side via
|
// on CalendarEvent/set makes Stalwart queue the iTIP REQUEST itself.
|
||||||
// `sendSchedulingMessages`, so fall back to a client-side iMIP send.
|
// Sending a client-side iMIP copy here produced duplicate emails.
|
||||||
if (sendSchedulingMessages && created.participants) {
|
|
||||||
try {
|
|
||||||
await client.sendImipInvitation(created);
|
|
||||||
} catch (e) {
|
|
||||||
debug.error('Failed to send invitation emails:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mappedCreated;
|
return mappedCreated;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to create event:', error);
|
debug.error('Failed to create event:', error);
|
||||||
if (isNetworkError(error)) {
|
|
||||||
const accountId = targetAccountId || useAccountStore.getState().activeAccountId;
|
|
||||||
if (accountId) {
|
|
||||||
enqueueOperation({ type: 'createEvent', accountId, payload: cleanEvent });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set({ error: 'Failed to create event' });
|
set({ error: 'Failed to create event' });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -513,12 +498,11 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
|
|
||||||
updateEvent: async (client, id, updates, sendSchedulingMessages) => {
|
updateEvent: async (client, id, updates, sendSchedulingMessages) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
const storeEvent = get().events.find(e => e.id === id);
|
|
||||||
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
|
||||||
const targetAccountId = storeEvent?.accountId;
|
|
||||||
const cleanUpdates = sanitizeOutgoingCalendarEventData({ ...updates });
|
|
||||||
try {
|
try {
|
||||||
// Resolve shared event IDs and client-side expanded occurrence IDs
|
// Resolve shared event IDs and client-side expanded occurrence IDs
|
||||||
|
const storeEvent = get().events.find(e => e.id === id);
|
||||||
|
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
||||||
|
const targetAccountId = storeEvent?.accountId;
|
||||||
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
||||||
debug.log('calendar', 'Calendar updateEvent', {
|
debug.log('calendar', 'Calendar updateEvent', {
|
||||||
storeId: id,
|
storeId: id,
|
||||||
@@ -529,6 +513,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
updateKeys: Object.keys(updates),
|
updateKeys: Object.keys(updates),
|
||||||
});
|
});
|
||||||
// Remap namespaced calendarIds back to original IDs
|
// Remap namespaced calendarIds back to original IDs
|
||||||
|
const cleanUpdates = sanitizeOutgoingCalendarEventData({ ...updates });
|
||||||
if (cleanUpdates.calendarIds) {
|
if (cleanUpdates.calendarIds) {
|
||||||
const remapped: Record<string, boolean> = {};
|
const remapped: Record<string, boolean> = {};
|
||||||
for (const [calId, v] of Object.entries(cleanUpdates.calendarIds)) {
|
for (const [calId, v] of Object.entries(cleanUpdates.calendarIds)) {
|
||||||
@@ -566,31 +551,11 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
return merged;
|
return merged;
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
// Send invitation emails (iTIP REQUEST) when scheduling is requested.
|
// Update emails (iTIP REQUEST/REPLY) are sent by the server via the
|
||||||
if (sendSchedulingMessages) {
|
// `sendSchedulingMessages` argument already passed above - a manual
|
||||||
const mergedParticipants = cleanUpdates.participants ?? storeEvent?.participants;
|
// iMIP send here produced duplicate emails.
|
||||||
if (mergedParticipants) {
|
|
||||||
const eventForInvitation = {
|
|
||||||
...(storeEvent ?? {}),
|
|
||||||
...cleanUpdates,
|
|
||||||
id: realId,
|
|
||||||
participants: mergedParticipants,
|
|
||||||
} as CalendarEvent;
|
|
||||||
try {
|
|
||||||
await client.sendImipInvitation(eventForInvitation);
|
|
||||||
} catch (e) {
|
|
||||||
debug.error('Failed to send invitation emails:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to update event:', error);
|
debug.error('Failed to update event:', error);
|
||||||
if (isNetworkError(error)) {
|
|
||||||
const accountId = targetAccountId || useAccountStore.getState().activeAccountId;
|
|
||||||
if (accountId) {
|
|
||||||
enqueueOperation({ type: 'updateEvent', accountId, payload: { id: realId, updates: cleanUpdates } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set({ error: 'Failed to update event' });
|
set({ error: 'Failed to update event' });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -823,12 +788,15 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
|
|
||||||
deleteEvent: async (client, id, sendSchedulingMessages) => {
|
deleteEvent: async (client, id, sendSchedulingMessages) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
const storeEvent = get().events.find(e => e.id === id);
|
|
||||||
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
|
||||||
const targetAccountId = storeEvent?.accountId;
|
|
||||||
try {
|
try {
|
||||||
// Resolve shared event IDs and client-side expanded occurrence IDs
|
// Resolve shared event IDs and client-side expanded occurrence IDs
|
||||||
|
const storeEvent = get().events.find(e => e.id === id);
|
||||||
|
const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId);
|
||||||
|
const targetAccountId = storeEvent?.accountId;
|
||||||
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
client = resolveAccountClient(client, storeEvent?.localAccountId);
|
||||||
|
// Cancellation emails (iTIP CANCEL) are sent by the server via the
|
||||||
|
// `sendSchedulingMessages` argument on the destroy below - a manual
|
||||||
|
// iMIP send here produced duplicate emails.
|
||||||
debug.log('calendar', 'Calendar deleteEvent', {
|
debug.log('calendar', 'Calendar deleteEvent', {
|
||||||
storeId: id,
|
storeId: id,
|
||||||
realId,
|
realId,
|
||||||
@@ -841,22 +809,8 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
events: state.events.filter(e => e.id !== id),
|
events: state.events.filter(e => e.id !== id),
|
||||||
selectedEventId: state.selectedEventId === id ? null : state.selectedEventId,
|
selectedEventId: state.selectedEventId === id ? null : state.selectedEventId,
|
||||||
}));
|
}));
|
||||||
// Send cancellation emails (iTIP CANCEL) to participants.
|
|
||||||
if (sendSchedulingMessages && storeEvent?.participants) {
|
|
||||||
try {
|
|
||||||
await client.sendImipCancellation(storeEvent);
|
|
||||||
} catch (e) {
|
|
||||||
debug.error('Failed to send cancellation emails:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug.error('Failed to delete event:', error);
|
debug.error('Failed to delete event:', error);
|
||||||
if (isNetworkError(error)) {
|
|
||||||
const accountId = targetAccountId || useAccountStore.getState().activeAccountId;
|
|
||||||
if (accountId) {
|
|
||||||
enqueueOperation({ type: 'deleteEvent', accountId, payload: realId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set({ error: 'Failed to delete event' });
|
set({ error: 'Failed to delete event' });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -1348,27 +1302,3 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
import { registerPushHandler } from '@/lib/push-event-bus';
|
|
||||||
|
|
||||||
registerPushHandler('Calendar', async (client) => {
|
|
||||||
const store = useCalendarStore.getState();
|
|
||||||
if (store.supportsCalendar) {
|
|
||||||
store.fetchCalendars(client);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
registerPushHandler('CalendarEvent', async (client) => {
|
|
||||||
const store = useCalendarStore.getState();
|
|
||||||
if (store.supportsCalendar) {
|
|
||||||
const { dateRange, selectedCalendarIds } = store;
|
|
||||||
if (dateRange && selectedCalendarIds.length > 0) {
|
|
||||||
store.fetchEvents(client, dateRange.start, dateRange.end);
|
|
||||||
}
|
|
||||||
const { useTaskStore } = await import('./task-store');
|
|
||||||
const taskStore = useTaskStore.getState();
|
|
||||||
if (taskStore.tasks.length > 0 || store.viewMode === 'tasks') {
|
|
||||||
taskStore.fetchTasks(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
+9
-38
@@ -5,8 +5,6 @@ import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|||||||
import { generateUUID } from '@/lib/utils';
|
import { generateUUID } from '@/lib/utils';
|
||||||
import { debug } from '@/lib/debug';
|
import { debug } from '@/lib/debug';
|
||||||
import { getClientByLocalAccountId } from './client-registry';
|
import { getClientByLocalAccountId } from './client-registry';
|
||||||
import { enqueueOperation, isNetworkError } from '@/lib/offline-write-queue';
|
|
||||||
import { useAccountStore } from '@/stores/account-store';
|
|
||||||
|
|
||||||
/** One connected JMAP account for contact multi-account aggregation. */
|
/** One connected JMAP account for contact multi-account aggregation. */
|
||||||
export interface ContactAccountClient {
|
export interface ContactAccountClient {
|
||||||
@@ -377,12 +375,12 @@ export const useContactStore = create<ContactStore>()(
|
|||||||
|
|
||||||
createContact: async (client, contact) => {
|
createContact: async (client, contact) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
let accountId: string | undefined = contact.isShared ? contact.accountId : undefined;
|
|
||||||
let cleanedContact = contact;
|
|
||||||
try {
|
try {
|
||||||
// Determine target account from the selected address book. Also
|
// Determine target account from the selected address book. Also
|
||||||
// pin the local account so we route through the right server's
|
// pin the local account so we route through the right server's
|
||||||
// client in multi-account Pro mode.
|
// client in multi-account Pro mode.
|
||||||
|
let accountId = contact.isShared ? contact.accountId : undefined;
|
||||||
|
let cleanedContact = contact;
|
||||||
let localAccountId = contact.localAccountId;
|
let localAccountId = contact.localAccountId;
|
||||||
|
|
||||||
// De-namespace addressBookIds if they reference a shared address book
|
// De-namespace addressBookIds if they reference a shared address book
|
||||||
@@ -426,12 +424,6 @@ export const useContactStore = create<ContactStore>()(
|
|||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error instanceof Error ? error.message : 'Failed to create contact';
|
const msg = error instanceof Error ? error.message : 'Failed to create contact';
|
||||||
if (isNetworkError(error)) {
|
|
||||||
const queueAccountId = accountId || useAccountStore.getState().activeAccountId;
|
|
||||||
if (queueAccountId) {
|
|
||||||
enqueueOperation({ type: 'createContact', accountId: queueAccountId, payload: cleanedContact });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set({ error: msg, isLoading: false });
|
set({ error: msg, isLoading: false });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -439,14 +431,14 @@ export const useContactStore = create<ContactStore>()(
|
|||||||
|
|
||||||
updateContact: async (client, id, updates) => {
|
updateContact: async (client, id, updates) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
const contact = get().contacts.find(c => c.id === id);
|
|
||||||
const originalId = contact?.originalId || stripLocalAccountPrefix(id, contact?.localAccountId);
|
|
||||||
const accountId = contact?.isShared ? contact.accountId : undefined;
|
|
||||||
let cleanedUpdates = updates;
|
|
||||||
try {
|
try {
|
||||||
|
const contact = get().contacts.find(c => c.id === id);
|
||||||
|
const originalId = contact?.originalId || stripLocalAccountPrefix(id, contact?.localAccountId);
|
||||||
|
const accountId = contact?.isShared ? contact.accountId : undefined;
|
||||||
client = resolveAccountClient(client, contact?.localAccountId);
|
client = resolveAccountClient(client, contact?.localAccountId);
|
||||||
|
|
||||||
// De-namespace addressBookIds for shared contacts before sending to JMAP server
|
// De-namespace addressBookIds for shared contacts before sending to JMAP server
|
||||||
|
let cleanedUpdates = updates;
|
||||||
if (contact?.isShared && contact?.accountId && updates.addressBookIds) {
|
if (contact?.isShared && contact?.accountId && updates.addressBookIds) {
|
||||||
const prefix = `${contact.accountId}:`;
|
const prefix = `${contact.accountId}:`;
|
||||||
const deNamespaced = Object.fromEntries(
|
const deNamespaced = Object.fromEntries(
|
||||||
@@ -466,12 +458,6 @@ export const useContactStore = create<ContactStore>()(
|
|||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error instanceof Error ? error.message : 'Failed to update contact';
|
const msg = error instanceof Error ? error.message : 'Failed to update contact';
|
||||||
if (isNetworkError(error)) {
|
|
||||||
const queueAccountId = accountId || useAccountStore.getState().activeAccountId;
|
|
||||||
if (queueAccountId) {
|
|
||||||
enqueueOperation({ type: 'updateContact', accountId: queueAccountId, payload: { id: originalId, updates: cleanedUpdates } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set({ error: msg });
|
set({ error: msg });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -479,10 +465,10 @@ export const useContactStore = create<ContactStore>()(
|
|||||||
|
|
||||||
deleteContact: async (client, id) => {
|
deleteContact: async (client, id) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
const contact = get().contacts.find(c => c.id === id);
|
|
||||||
const originalId = contact?.originalId || stripLocalAccountPrefix(id, contact?.localAccountId);
|
|
||||||
const accountId = contact?.isShared ? contact.accountId : undefined;
|
|
||||||
try {
|
try {
|
||||||
|
const contact = get().contacts.find(c => c.id === id);
|
||||||
|
const originalId = contact?.originalId || stripLocalAccountPrefix(id, contact?.localAccountId);
|
||||||
|
const accountId = contact?.isShared ? contact.accountId : undefined;
|
||||||
client = resolveAccountClient(client, contact?.localAccountId);
|
client = resolveAccountClient(client, contact?.localAccountId);
|
||||||
await client.deleteContact(originalId, accountId);
|
await client.deleteContact(originalId, accountId);
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -495,12 +481,6 @@ export const useContactStore = create<ContactStore>()(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error instanceof Error ? error.message : 'Failed to delete contact';
|
const msg = error instanceof Error ? error.message : 'Failed to delete contact';
|
||||||
if (isNetworkError(error)) {
|
|
||||||
const queueAccountId = accountId || useAccountStore.getState().activeAccountId;
|
|
||||||
if (queueAccountId) {
|
|
||||||
enqueueOperation({ type: 'deleteContact', accountId: queueAccountId, payload: { id: originalId, targetAccountId: accountId } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set({ error: msg });
|
set({ error: msg });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -1163,13 +1143,4 @@ export const useContactStore = create<ContactStore>()(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
import { registerPushHandler } from '@/lib/push-event-bus';
|
|
||||||
|
|
||||||
registerPushHandler('ContactCard', async (client) => {
|
|
||||||
const store = useContactStore.getState();
|
|
||||||
store.fetchContacts(client).catch((err) => {
|
|
||||||
console.error('Failed to refresh contacts on push:', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export type { ContactName };
|
export type { ContactName };
|
||||||
|
|||||||
+47
-19
@@ -3,6 +3,7 @@ import { Email, Mailbox, StateChange, ScheduledEmail, SendEmailResult, isUnified
|
|||||||
import type { UnifiedMailboxRole, CrossView } from "@/lib/jmap/types";
|
import type { UnifiedMailboxRole, CrossView } from "@/lib/jmap/types";
|
||||||
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
|
import { useCalendarStore } from "@/stores/calendar-store";
|
||||||
import { SearchFilters, DEFAULT_SEARCH_FILTERS, buildJMAPFilter, isFilterEmpty } from "@/lib/jmap/search-utils";
|
import { SearchFilters, DEFAULT_SEARCH_FILTERS, buildJMAPFilter, isFilterEmpty } from "@/lib/jmap/search-utils";
|
||||||
import { emailHooks } from "@/lib/plugin-hooks";
|
import { emailHooks } from "@/lib/plugin-hooks";
|
||||||
import type { ExternalSearchResult } from "@/lib/plugin-types";
|
import type { ExternalSearchResult } from "@/lib/plugin-types";
|
||||||
@@ -10,7 +11,6 @@ import { fetchUnifiedEmails, fetchUnifiedMailboxCounts, searchUnifiedEmails, adv
|
|||||||
import { useAuthStore } from "@/stores/auth-store";
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
import { useAccountStore } from "@/stores/account-store";
|
import { useAccountStore } from "@/stores/account-store";
|
||||||
import { useMessageListTabsStore } from "@/stores/message-list-tabs-store";
|
import { useMessageListTabsStore } from "@/stores/message-list-tabs-store";
|
||||||
import { enqueueOperation, isNetworkError } from "@/lib/offline-write-queue";
|
|
||||||
|
|
||||||
type ScheduledSubmissionMetadata = {
|
type ScheduledSubmissionMetadata = {
|
||||||
submissionId: string;
|
submissionId: string;
|
||||||
@@ -1367,16 +1367,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isNetworkError(error)) {
|
|
||||||
const accountId = useAccountStore.getState().activeAccountId;
|
|
||||||
if (accountId) {
|
|
||||||
enqueueOperation({
|
|
||||||
type: 'sendEmail',
|
|
||||||
accountId,
|
|
||||||
payload: { to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, delayedUntil, envelopeMailFrom, options },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set({
|
set({
|
||||||
error: error instanceof Error ? error.message : "Failed to send email",
|
error: error instanceof Error ? error.message : "Failed to send email",
|
||||||
isLoading: false
|
isLoading: false
|
||||||
@@ -2921,15 +2911,53 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
await get().fetchMailboxes(client);
|
await get().fetchMailboxes(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate Calendar/CalendarEvent, SieveScript, ContactCard, FileNode
|
// Handle Calendar/CalendarEvent state changes - refresh calendar data
|
||||||
// push handling to the push event bus where each feature store
|
if (accountChanges?.Calendar || accountChanges?.CalendarEvent) {
|
||||||
// registers itself. Decouples email-store from the 5+ other stores
|
const calendarStore = useCalendarStore.getState();
|
||||||
// it previously imported directly for push handling.
|
if (calendarStore.supportsCalendar) {
|
||||||
import('@/lib/push-event-bus').then(({ dispatchPushEvent }) => {
|
calendarStore.fetchCalendars(client);
|
||||||
dispatchPushEvent(client, change.changed, accountId).catch((err) => {
|
const { dateRange, selectedCalendarIds } = calendarStore;
|
||||||
console.error('Push event bus dispatch failed:', err);
|
if (dateRange && selectedCalendarIds.length > 0) {
|
||||||
|
calendarStore.fetchEvents(client, dateRange.start, dateRange.end);
|
||||||
|
}
|
||||||
|
// Refresh tasks when calendar events change (e.g. task created via CalDAV)
|
||||||
|
const { useTaskStore } = await import('./task-store');
|
||||||
|
const taskStore = useTaskStore.getState();
|
||||||
|
if (taskStore.tasks.length > 0 || calendarStore.viewMode === 'tasks') {
|
||||||
|
taskStore.fetchTasks(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SieveScript state changes - refresh filter rules
|
||||||
|
if (accountChanges?.SieveScript) {
|
||||||
|
const { useFilterStore } = await import('./filter-store');
|
||||||
|
const filterStore = useFilterStore.getState();
|
||||||
|
if (filterStore.isSupported) {
|
||||||
|
filterStore.fetchFilters(client).catch((err) => {
|
||||||
|
console.error('Failed to refresh filters:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ContactCard state changes - refresh contacts
|
||||||
|
if (accountChanges?.ContactCard) {
|
||||||
|
const { useContactStore } = await import('./contact-store');
|
||||||
|
const contactStore = useContactStore.getState();
|
||||||
|
contactStore.fetchContacts(client).catch((err) => {
|
||||||
|
console.error('Failed to refresh contacts on push:', err);
|
||||||
});
|
});
|
||||||
}).catch(() => {});
|
}
|
||||||
|
|
||||||
|
// Handle FileNode state changes - refresh current directory
|
||||||
|
if (accountChanges?.FileNode) {
|
||||||
|
const { useFileStore } = await import('./file-store');
|
||||||
|
const fileStore = useFileStore.getState();
|
||||||
|
const currentParentId = fileStore.currentParentId;
|
||||||
|
fileStore.navigate(currentParentId).catch((err) => {
|
||||||
|
console.error('Failed to refresh files on push:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Local search index last, with the refreshed ids (see above).
|
// Local search index last, with the refreshed ids (see above).
|
||||||
scheduleIndexUpdate();
|
scheduleIndexUpdate();
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import { encryptValue, decryptValue, isEncryptionAvailable } from '@/lib/auth/local-storage-crypto';
|
|
||||||
|
|
||||||
const ENCRYPTED_PREFIX = 'ENC:';
|
|
||||||
|
|
||||||
function isEncrypted(value: string): boolean {
|
|
||||||
return value.startsWith(ENCRYPTED_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripPrefix(value: string): string {
|
|
||||||
return value.slice(ENCRYPTED_PREFIX.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache of recently decrypted values. The Zustand persist middleware calls
|
|
||||||
// getItem frequently during rehydration, and we want to avoid re-decrypting
|
|
||||||
// the same ciphertext on every read. Keyed by storage key.
|
|
||||||
const decryptedCache = new Map<string, string | null>();
|
|
||||||
|
|
||||||
function cacheKey(name: string): string {
|
|
||||||
return `vncmail:decrypted:${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCachedDecrypted(name: string): string | null | undefined {
|
|
||||||
return decryptedCache.get(cacheKey(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCachedDecrypted(name: string, value: string | null): void {
|
|
||||||
decryptedCache.set(cacheKey(name), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidateDecryptedCache(name: string): void {
|
|
||||||
decryptedCache.delete(cacheKey(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createEncryptedStorage(): {
|
|
||||||
getItem: (name: string) => Promise<string | null>;
|
|
||||||
setItem: (name: string, value: string) => Promise<void>;
|
|
||||||
removeItem: (name: string) => Promise<void>;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
getItem: async (name: string): Promise<string | null> => {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(name);
|
|
||||||
if (raw === null) return null;
|
|
||||||
|
|
||||||
if (!isEncrypted(raw)) {
|
|
||||||
if (isEncryptionAvailable()) {
|
|
||||||
// Legacy plaintext value found — return as-is, but re-encrypt on
|
|
||||||
// the next write (setItem below always encrypts when available).
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cached = getCachedDecrypted(name);
|
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
|
|
||||||
const ciphertext = stripPrefix(raw);
|
|
||||||
const decrypted = await decryptValue(ciphertext);
|
|
||||||
setCachedDecrypted(name, decrypted);
|
|
||||||
return decrypted;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setItem: async (name: string, value: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
if (isEncryptionAvailable()) {
|
|
||||||
const ciphertext = await encryptValue(value);
|
|
||||||
localStorage.setItem(name, `${ENCRYPTED_PREFIX}${ciphertext}`);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(name, value);
|
|
||||||
}
|
|
||||||
invalidateDecryptedCache(name);
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(name, value);
|
|
||||||
} catch {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeItem: async (name: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem(name);
|
|
||||||
} catch {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
invalidateDecryptedCache(name);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const encryptedStorage = createEncryptedStorage();
|
|
||||||
@@ -1048,13 +1048,3 @@ export const useFileStore = create<FileState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { registerPushHandler } from '@/lib/push-event-bus';
|
|
||||||
|
|
||||||
registerPushHandler('FileNode', async (_client) => {
|
|
||||||
const store = useFileStore.getState();
|
|
||||||
const currentParentId = store.currentParentId;
|
|
||||||
store.navigate(currentParentId).catch((err) => {
|
|
||||||
console.error('Failed to refresh files on push:', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -252,14 +252,3 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
|||||||
selectedAccountId: null,
|
selectedAccountId: null,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { registerPushHandler } from '@/lib/push-event-bus';
|
|
||||||
|
|
||||||
registerPushHandler('SieveScript', async (client) => {
|
|
||||||
const store = useFilterStore.getState();
|
|
||||||
if (store.isSupported) {
|
|
||||||
store.fetchFilters(client).catch((err) => {
|
|
||||||
console.error('Failed to refresh filters on push:', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ interface MessageListTabsStore {
|
|||||||
|
|
||||||
registerTabs: (pluginId: string, config: MessageListTabsConfig) => void;
|
registerTabs: (pluginId: string, config: MessageListTabsConfig) => void;
|
||||||
clearTabs: (pluginId: string) => void;
|
clearTabs: (pluginId: string) => void;
|
||||||
clearState: () => void;
|
|
||||||
setActiveTab: (tabId: string, mailboxId: string | null) => void;
|
setActiveTab: (tabId: string, mailboxId: string | null) => void;
|
||||||
/**
|
/**
|
||||||
* JMAP filter fragment for the active tab (to AND into the mailbox query),
|
* JMAP filter fragment for the active tab (to AND into the mailbox query),
|
||||||
@@ -337,13 +336,4 @@ export const useMessageListTabsStore = create<MessageListTabsStore>()((set, get)
|
|||||||
void messageListTabHooks.onEmailCategorize.emit(ctx);
|
void messageListTabHooks.onEmailCategorize.emit(ctx);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
clearState: () => set({
|
|
||||||
registrations: {},
|
|
||||||
tabs: [],
|
|
||||||
mailboxRoles: [],
|
|
||||||
activeTabId: null,
|
|
||||||
tabCounts: {},
|
|
||||||
isCountsLoading: false,
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -121,11 +121,7 @@ export const useResourceStore = create<ResourceState>()((set, get) => ({
|
|||||||
cancelBooking: async (bookingId: string) => {
|
cancelBooking: async (bookingId: string) => {
|
||||||
const { bookings } = get();
|
const { bookings } = get();
|
||||||
const booking = bookings.find((b) => b.id === bookingId);
|
const booking = bookings.find((b) => b.id === bookingId);
|
||||||
if (!booking) {
|
if (!booking) return;
|
||||||
console.error(`cancelBooking: booking with id "${bookingId}" not found`);
|
|
||||||
set({ bookingError: `Booking ${bookingId} not found` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(
|
const res = await apiFetch(
|
||||||
@@ -140,10 +136,9 @@ export const useResourceStore = create<ResourceState>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelEventBookings: async (eventId: string) => {
|
cancelEventBookings: async (_eventId: string) => {
|
||||||
const { bookings } = get();
|
const { bookings } = get();
|
||||||
const eventBookings = bookings.filter((b) => b.eventId === eventId);
|
for (const booking of bookings) {
|
||||||
for (const booking of eventBookings) {
|
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(
|
const res = await apiFetch(
|
||||||
`/api/resources/${booking.resourceId}/book/${booking.id}`,
|
`/api/resources/${booking.resourceId}/book/${booking.id}`,
|
||||||
@@ -154,6 +149,6 @@ export const useResourceStore = create<ResourceState>()((set, get) => ({
|
|||||||
// silently fail
|
// silently fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set({ bookings: bookings.filter((b) => b.eventId !== eventId) });
|
set({ bookings: [] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
rtlEditingSupport: false,
|
rtlEditingSupport: false,
|
||||||
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER,
|
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||||
sendDelaySeconds: 0 as SendDelaySeconds,
|
sendDelaySeconds: 0 as SendDelaySeconds,
|
||||||
signaturePosition: 'above_quote' as SignaturePosition,
|
signaturePosition: 'below_quote' as SignaturePosition,
|
||||||
signatureSeparatorEnabled: true,
|
signatureSeparatorEnabled: true,
|
||||||
requestReadReceiptDefault: false,
|
requestReadReceiptDefault: false,
|
||||||
readReceiptResponse: 'ask' as ReadReceiptResponse,
|
readReceiptResponse: 'ask' as ReadReceiptResponse,
|
||||||
@@ -998,7 +998,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
version: 8,
|
version: 7,
|
||||||
migrate: migrateSettings,
|
migrate: migrateSettings,
|
||||||
onRehydrateStorage: () => {
|
onRehydrateStorage: () => {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
@@ -1085,12 +1085,6 @@ export function migrateSettings(persisted: unknown, version: number): SettingsSt
|
|||||||
if (version < 6 || !isPlainRecord(state.preferredIdentityIds)) {
|
if (version < 6 || !isPlainRecord(state.preferredIdentityIds)) {
|
||||||
state.preferredIdentityIds = {};
|
state.preferredIdentityIds = {};
|
||||||
}
|
}
|
||||||
// v8: reply/forward signatures now sit above the quoted message by
|
|
||||||
// default (previously below). Migrate any persisted value so existing
|
|
||||||
// accounts pick up the new default.
|
|
||||||
if (version < 8) {
|
|
||||||
state.signaturePosition = 'above_quote';
|
|
||||||
}
|
|
||||||
return state as unknown as SettingsState;
|
return state as unknown as SettingsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+181
-70
@@ -7,19 +7,13 @@ import type {
|
|||||||
FileNodeRights,
|
FileNodeRights,
|
||||||
MailboxRights,
|
MailboxRights,
|
||||||
} from "@/lib/jmap/types";
|
} from "@/lib/jmap/types";
|
||||||
import {
|
import { toast } from "@/stores/toast-store";
|
||||||
type SharedResourceKind,
|
|
||||||
MAILBOX_ROLE_LABELS,
|
|
||||||
CALENDAR_ROLE_LABELS,
|
|
||||||
ADDRESSBOOK_ROLE_LABELS,
|
|
||||||
FILE_ROLE_LABELS,
|
|
||||||
resolveRights,
|
|
||||||
detectMailboxPreset,
|
|
||||||
detectCalendarPreset,
|
|
||||||
detectAddressBookPreset,
|
|
||||||
} from "@/lib/sharing-rights";
|
|
||||||
|
|
||||||
export type { SharedResourceKind } from "@/lib/sharing-rights";
|
export type SharedResourceKind =
|
||||||
|
| "mailbox"
|
||||||
|
| "calendar"
|
||||||
|
| "addressBook"
|
||||||
|
| "file";
|
||||||
|
|
||||||
export interface SharedFolder {
|
export interface SharedFolder {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -40,7 +34,6 @@ interface SharingState {
|
|||||||
sharedWithMe: SharedFolder[];
|
sharedWithMe: SharedFolder[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
principalsCache: Principal[];
|
principalsCache: Principal[];
|
||||||
lastMessage: { type: 'success' | 'error'; text: string } | null;
|
|
||||||
|
|
||||||
loadPrincipals: (client: IJMAPClient) => Promise<Principal[]>;
|
loadPrincipals: (client: IJMAPClient) => Promise<Principal[]>;
|
||||||
fetchShares: (client: IJMAPClient) => Promise<void>;
|
fetchShares: (client: IJMAPClient) => Promise<void>;
|
||||||
@@ -74,17 +67,148 @@ interface SharingState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function roleLabel(kind: SharedResourceKind, role: string): string {
|
function roleLabel(kind: SharedResourceKind, role: string): string {
|
||||||
|
if (kind === "mailbox") return MAILBOX_ROLE_LABELS[role] ?? role;
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAILBOX_PRESETS: Record<string, MailboxRights> = {
|
||||||
|
read: {
|
||||||
|
mayReadItems: true,
|
||||||
|
mayAddItems: false,
|
||||||
|
mayRemoveItems: false,
|
||||||
|
maySetSeen: false,
|
||||||
|
maySetKeywords: false,
|
||||||
|
mayCreateChild: false,
|
||||||
|
mayRename: false,
|
||||||
|
mayDelete: false,
|
||||||
|
maySubmit: false,
|
||||||
|
},
|
||||||
|
readWrite: {
|
||||||
|
mayReadItems: true,
|
||||||
|
mayAddItems: true,
|
||||||
|
mayRemoveItems: false,
|
||||||
|
maySetSeen: true,
|
||||||
|
maySetKeywords: true,
|
||||||
|
mayCreateChild: false,
|
||||||
|
mayRename: false,
|
||||||
|
mayDelete: false,
|
||||||
|
maySubmit: true,
|
||||||
|
},
|
||||||
|
manager: {
|
||||||
|
mayReadItems: true,
|
||||||
|
mayAddItems: true,
|
||||||
|
mayRemoveItems: true,
|
||||||
|
maySetSeen: true,
|
||||||
|
maySetKeywords: true,
|
||||||
|
mayCreateChild: true,
|
||||||
|
mayRename: true,
|
||||||
|
mayDelete: true,
|
||||||
|
maySubmit: true,
|
||||||
|
mayShare: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAILBOX_ROLE_LABELS: Record<string, string> = {
|
||||||
|
read: "Viewer",
|
||||||
|
readWrite: "Editor",
|
||||||
|
manager: "Manager",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CALENDAR_PRESETS: Record<string, CalendarRights> = {
|
||||||
|
read: {
|
||||||
|
mayReadFreeBusy: true,
|
||||||
|
mayReadItems: true,
|
||||||
|
mayWriteAll: false,
|
||||||
|
mayWriteOwn: false,
|
||||||
|
mayUpdatePrivate: false,
|
||||||
|
mayRSVP: false,
|
||||||
|
mayShare: false,
|
||||||
|
mayDelete: false,
|
||||||
|
},
|
||||||
|
readWrite: {
|
||||||
|
mayReadFreeBusy: true,
|
||||||
|
mayReadItems: true,
|
||||||
|
mayWriteAll: true,
|
||||||
|
mayWriteOwn: true,
|
||||||
|
mayUpdatePrivate: true,
|
||||||
|
mayRSVP: true,
|
||||||
|
mayShare: false,
|
||||||
|
mayDelete: false,
|
||||||
|
},
|
||||||
|
manager: {
|
||||||
|
mayReadFreeBusy: true,
|
||||||
|
mayReadItems: true,
|
||||||
|
mayWriteAll: true,
|
||||||
|
mayWriteOwn: true,
|
||||||
|
mayUpdatePrivate: true,
|
||||||
|
mayRSVP: true,
|
||||||
|
mayShare: true,
|
||||||
|
mayDelete: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ADDRESS_BOOK_PRESETS: Record<string, AddressBookRights> = {
|
||||||
|
read: { mayRead: true, mayWrite: false, mayShare: false, mayDelete: false },
|
||||||
|
readWrite: {
|
||||||
|
mayRead: true,
|
||||||
|
mayWrite: true,
|
||||||
|
mayShare: false,
|
||||||
|
mayDelete: false,
|
||||||
|
},
|
||||||
|
manager: {
|
||||||
|
mayRead: true,
|
||||||
|
mayWrite: true,
|
||||||
|
mayShare: true,
|
||||||
|
mayDelete: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const FILE_PRESETS: Record<string, FileNodeRights> = {
|
||||||
|
read: {
|
||||||
|
mayRead: true,
|
||||||
|
mayAddChildren: false,
|
||||||
|
mayRename: false,
|
||||||
|
mayDelete: false,
|
||||||
|
mayModifyContent: false,
|
||||||
|
mayShare: false,
|
||||||
|
},
|
||||||
|
readWrite: {
|
||||||
|
mayRead: true,
|
||||||
|
mayAddChildren: true,
|
||||||
|
mayRename: true,
|
||||||
|
mayDelete: true,
|
||||||
|
mayModifyContent: true,
|
||||||
|
mayShare: false,
|
||||||
|
},
|
||||||
|
manager: {
|
||||||
|
mayRead: true,
|
||||||
|
mayAddChildren: true,
|
||||||
|
mayRename: true,
|
||||||
|
mayDelete: true,
|
||||||
|
mayModifyContent: true,
|
||||||
|
mayShare: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveRights(
|
||||||
|
kind: SharedResourceKind,
|
||||||
|
role: string,
|
||||||
|
): MailboxRights | CalendarRights | AddressBookRights | FileNodeRights {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "mailbox":
|
case "mailbox":
|
||||||
return MAILBOX_ROLE_LABELS[role] ?? role;
|
return (
|
||||||
|
MAILBOX_PRESETS[role] ?? MAILBOX_PRESETS.read
|
||||||
|
);
|
||||||
case "calendar":
|
case "calendar":
|
||||||
return CALENDAR_ROLE_LABELS[role] ?? role;
|
return (
|
||||||
|
CALENDAR_PRESETS[role] ?? CALENDAR_PRESETS.read
|
||||||
|
);
|
||||||
case "addressBook":
|
case "addressBook":
|
||||||
return ADDRESSBOOK_ROLE_LABELS[role] ?? role;
|
return (
|
||||||
|
ADDRESS_BOOK_PRESETS[role] ?? ADDRESS_BOOK_PRESETS.read
|
||||||
|
);
|
||||||
case "file":
|
case "file":
|
||||||
return FILE_ROLE_LABELS[role] ?? role;
|
return FILE_PRESETS[role] ?? FILE_PRESETS.read;
|
||||||
default:
|
|
||||||
return role;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +217,6 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
sharedWithMe: [],
|
sharedWithMe: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
principalsCache: [],
|
principalsCache: [],
|
||||||
lastMessage: null,
|
|
||||||
|
|
||||||
async loadPrincipals(client) {
|
async loadPrincipals(client) {
|
||||||
const cached = get().principalsCache;
|
const cached = get().principalsCache;
|
||||||
@@ -135,21 +258,6 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mb.isShared && mb.myRights) {
|
|
||||||
withMe.push({
|
|
||||||
id: `mb-withme-${mb.id}`,
|
|
||||||
resourceId: mb.id,
|
|
||||||
resourceName: mb.name,
|
|
||||||
resourceKind: "mailbox",
|
|
||||||
principalId: mb.accountId || "unknown",
|
|
||||||
principalName: mb.accountName || "Unknown",
|
|
||||||
principalEmail: null,
|
|
||||||
role: roleLabel("mailbox", detectMailboxPreset(mb.myRights)),
|
|
||||||
direction: "withMe",
|
|
||||||
pending: false,
|
|
||||||
accountId: mb.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* mailboxes may not be available */
|
/* mailboxes may not be available */
|
||||||
@@ -178,21 +286,6 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cal.isShared && cal.myRights) {
|
|
||||||
withMe.push({
|
|
||||||
id: `cal-withme-${cal.id}`,
|
|
||||||
resourceId: cal.id,
|
|
||||||
resourceName: cal.name,
|
|
||||||
resourceKind: "calendar",
|
|
||||||
principalId: cal.accountId || "unknown",
|
|
||||||
principalName: cal.accountName || "Unknown",
|
|
||||||
principalEmail: null,
|
|
||||||
role: roleLabel("calendar", detectCalendarPreset(cal.myRights)),
|
|
||||||
direction: "withMe",
|
|
||||||
pending: false,
|
|
||||||
accountId: cal.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -225,21 +318,6 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (book.isShared && book.myRights) {
|
|
||||||
withMe.push({
|
|
||||||
id: `ab-withme-${book.id}`,
|
|
||||||
resourceId: book.id,
|
|
||||||
resourceName: book.name,
|
|
||||||
resourceKind: "addressBook",
|
|
||||||
principalId: book.accountId || "unknown",
|
|
||||||
principalName: book.accountName || "Unknown",
|
|
||||||
principalEmail: null,
|
|
||||||
role: roleLabel("addressBook", detectAddressBookPreset(book.myRights)),
|
|
||||||
direction: "withMe",
|
|
||||||
pending: false,
|
|
||||||
accountId: book.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -293,7 +371,7 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
entry,
|
entry,
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
set({ lastMessage: { type: 'success', text: `Shared "${resourceName}"` } });
|
toast.success(`Shared "${resourceName}"`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async revokeShare(client, resourceId, resourceKind, principalId, accountId) {
|
async revokeShare(client, resourceId, resourceKind, principalId, accountId) {
|
||||||
@@ -317,7 +395,7 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
set({ lastMessage: { type: 'success', text: "Access revoked" } });
|
toast.success("Access revoked");
|
||||||
},
|
},
|
||||||
|
|
||||||
async changeRole(
|
async changeRole(
|
||||||
@@ -340,7 +418,7 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
: f,
|
: f,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
set({ lastMessage: { type: 'success', text: "Role updated" } });
|
toast.success("Role updated");
|
||||||
},
|
},
|
||||||
|
|
||||||
async acceptShare(_client, share) {
|
async acceptShare(_client, share) {
|
||||||
@@ -349,14 +427,14 @@ export const useSharingStore = create<SharingState>((set, get) => ({
|
|||||||
f.id === share.id ? { ...f, pending: false } : f,
|
f.id === share.id ? { ...f, pending: false } : f,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
set({ lastMessage: { type: 'success', text: `Accepted share: ${share.resourceName}` } });
|
toast.success(`Accepted share: ${share.resourceName}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async declineShare(_client, share) {
|
async declineShare(_client, share) {
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
sharedWithMe: s.sharedWithMe.filter((f) => f.id !== share.id),
|
sharedWithMe: s.sharedWithMe.filter((f) => f.id !== share.id),
|
||||||
}));
|
}));
|
||||||
set({ lastMessage: { type: 'success', text: `Declined share: ${share.resourceName}` } });
|
toast.success(`Declined share: ${share.resourceName}`);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -409,4 +487,37 @@ async function applyShare(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectMailboxPreset(r: MailboxRights): string {
|
||||||
|
for (const [name, preset] of Object.entries(MAILBOX_PRESETS)) {
|
||||||
|
const keys = Object.keys(preset) as (keyof MailboxRights)[];
|
||||||
|
if (
|
||||||
|
keys.every(
|
||||||
|
(k) =>
|
||||||
|
(preset[k] ?? false) === (r[k as keyof MailboxRights] ?? false),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectCalendarPreset(r: CalendarRights): string {
|
||||||
|
for (const [name, preset] of Object.entries(CALENDAR_PRESETS)) {
|
||||||
|
const keys = Object.keys(preset) as (keyof CalendarRights)[];
|
||||||
|
if (keys.every((k) => (preset[k] ?? false) === (r[k as keyof CalendarRights] ?? false))) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectAddressBookPreset(r: AddressBookRights): string {
|
||||||
|
for (const [name, preset] of Object.entries(ADDRESS_BOOK_PRESETS)) {
|
||||||
|
const keys = Object.keys(preset) as (keyof AddressBookRights)[];
|
||||||
|
if (keys.every((k) => (preset[k] ?? false) === (r[k as keyof AddressBookRights] ?? false))) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "custom";
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user