@@ -57,20 +42,21 @@ export function KeyboardShortcutsModal({ isOpen, onClose }: KeyboardShortcutsMod
+
{/* Navigation Section */}
diff --git a/components/layout/mobile-header.tsx b/components/layout/mobile-header.tsx
index 3d88dee1..a3ce30f6 100644
--- a/components/layout/mobile-header.tsx
+++ b/components/layout/mobile-header.tsx
@@ -50,8 +50,12 @@ export function MobileHeader({
variant="ghost"
size="icon"
onClick={handleLeftAction}
- className="h-10 w-10"
+ className={cn(
+ "h-11 w-11",
+ !showBack && sidebarOpen && "bg-accent"
+ )}
aria-label={showBack ? "Go back" : "Toggle menu"}
+ aria-expanded={!showBack ? sidebarOpen : undefined}
>
{showBack ? (
@@ -73,7 +77,7 @@ export function MobileHeader({
variant="ghost"
size="icon"
onClick={onSearch}
- className="h-10 w-10"
+ className="h-11 w-11"
aria-label={t('mobile.search')}
>
@@ -84,7 +88,7 @@ export function MobileHeader({
variant="ghost"
size="icon"
onClick={onCompose}
- className="h-10 w-10 text-primary"
+ className="h-11 w-11 text-primary"
aria-label={t('mobile.compose')}
>
@@ -127,7 +131,7 @@ export function MobileViewerHeader({
variant="ghost"
size="icon"
onClick={onBack}
- className="h-10 w-10"
+ className="h-11 w-11"
aria-label={t('mobile.go_back')}
>
diff --git a/components/layout/navigation-rail.tsx b/components/layout/navigation-rail.tsx
new file mode 100644
index 00000000..08ae3ebe
--- /dev/null
+++ b/components/layout/navigation-rail.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { Mail, Calendar, BookUser, Settings } from "lucide-react";
+import { usePathname, Link } from "@/i18n/navigation";
+import { useTranslations } from "next-intl";
+import { useCalendarStore } from "@/stores/calendar-store";
+import { useEmailStore } from "@/stores/email-store";
+import { cn } from "@/lib/utils";
+
+interface NavItem {
+ id: string;
+ icon: typeof Mail;
+ labelKey: string;
+ href: string;
+ hidden?: boolean;
+ badge?: number;
+}
+
+interface NavigationRailProps {
+ orientation?: "vertical" | "horizontal";
+ collapsed?: boolean;
+ className?: string;
+}
+
+export function NavigationRail({
+ orientation = "vertical",
+ collapsed = false,
+ className,
+}: NavigationRailProps) {
+ const t = useTranslations("sidebar");
+ const pathname = usePathname();
+ const { supportsCalendar } = useCalendarStore();
+ const { mailboxes } = useEmailStore();
+ const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0;
+
+ const navItems: NavItem[] = [
+ { id: "mail", icon: Mail, labelKey: "mail", href: "/", badge: inboxUnread },
+ { id: "calendar", icon: Calendar, labelKey: "calendar", href: "/calendar", hidden: !supportsCalendar },
+ { id: "contacts", icon: BookUser, labelKey: "contacts", href: "/contacts" },
+ { id: "settings", icon: Settings, labelKey: "settings", href: "/settings" },
+ ];
+
+ const visibleItems = navItems.filter((item) => !item.hidden);
+
+ const getIsActive = (href: string) => {
+ if (href === "/") {
+ return pathname === "/" || pathname === "";
+ }
+ return pathname.startsWith(href);
+ };
+
+ if (orientation === "horizontal") {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx
index 6b5bed0b..3b2b3bd2 100644
--- a/components/layout/sidebar.tsx
+++ b/components/layout/sidebar.tsx
@@ -20,14 +20,11 @@ import {
ChevronDown,
Folder,
FolderOpen,
- Settings,
- ChevronUp,
Users,
User,
- BookUser,
Palmtree,
SlidersHorizontal,
- Calendar,
+ Settings,
X,
} from "lucide-react";
import { cn, buildMailboxTree, MailboxNode, formatFileSize } from "@/lib/utils";
@@ -37,8 +34,8 @@ import { useMailboxDrop } from "@/hooks/use-mailbox-drop";
import { useEmailStore } from "@/stores/email-store";
import { activeFilterCount } from "@/lib/jmap/search-utils";
import { useVacationStore } from "@/stores/vacation-store";
-import { useCalendarStore } from "@/stores/calendar-store";
import { toast } from "@/stores/toast-store";
+import { debug } from "@/lib/debug";
interface SidebarProps {
mailboxes: Mailbox[];
@@ -55,27 +52,22 @@ interface SidebarProps {
className?: string;
}
-// Map role to icon
const getIconForMailbox = (role?: string, name?: string, hasChildren?: boolean, isExpanded?: boolean, isShared?: boolean, id?: string) => {
const lowerName = name?.toLowerCase() || "";
- // Shared folders root node
if (id === 'shared-folders-root') {
return isExpanded ? FolderOpen : Users;
}
- // Shared account nodes
if (id?.startsWith('shared-account-')) {
return isExpanded ? FolderOpen : User;
}
- // Shared mailboxes (but not virtual nodes)
if (isShared && hasChildren && !id?.startsWith('shared-')) {
return isExpanded ? FolderOpen : Folder;
}
if (hasChildren) {
- // For folders with children, return open/closed folder icon
return isExpanded ? FolderOpen : Folder;
}
@@ -85,10 +77,9 @@ const getIconForMailbox = (role?: string, name?: string, hasChildren?: boolean,
if (role === "trash" || lowerName.includes("trash")) return Trash2;
if (role === "archive" || lowerName.includes("archive")) return Archive;
if (lowerName.includes("star") || lowerName.includes("flag")) return Star;
- return Inbox; // Default icon
+ return Inbox;
};
-// Component for rendering a single mailbox node with its children
function MailboxTreeItem({
node,
selectedMailbox,
@@ -109,10 +100,9 @@ function MailboxTreeItem({
const hasChildren = node.children.length > 0;
const isExpanded = expandedFolders.has(node.id);
const Icon = getIconForMailbox(node.role, node.name, hasChildren, isExpanded, node.isShared, node.id);
- const indentPixels = node.depth * 16; // 16px per depth level
- const isVirtualNode = node.id.startsWith('shared-'); // Virtual nodes for shared folder organization
+ const indentPixels = node.depth * 16;
+ const isVirtualNode = node.id.startsWith('shared-');
- // Drag and drop functionality
const { isDragging: globalDragging } = useDragDropContext();
const { dropHandlers, isValidDropTarget, isInvalidDropTarget } = useMailboxDrop({
mailbox: node,
@@ -140,15 +130,16 @@ function MailboxTreeItem({
{...(globalDragging ? dropHandlers : {})}
className={cn(
"group w-full flex items-center px-2 py-1 lg:py-1 max-lg:py-3 max-lg:min-h-[44px] text-sm transition-all duration-200",
- selectedMailbox === node.id
- ? "bg-accent text-accent-foreground"
- : "hover:bg-muted text-foreground",
- node.depth === 0 && "font-medium",
+ isVirtualNode
+ ? "text-muted-foreground"
+ : selectedMailbox === node.id
+ ? "bg-accent text-accent-foreground"
+ : "hover:bg-muted text-foreground",
+ node.depth === 0 && !isVirtualNode && "font-medium",
isValidDropTarget && "bg-primary/20 ring-2 ring-primary ring-inset",
isInvalidDropTarget && "bg-destructive/10 ring-2 ring-destructive/30 ring-inset opacity-50"
)}
>
- {/* Expand/Collapse Chevron */}
{hasChildren && (
)}
- {/* Mailbox Button */}
- {/* Render children if expanded */}
{hasChildren && isExpanded && !isCollapsed && (
{node.children.map((child) => (
@@ -229,27 +218,26 @@ function MailboxTreeItem({
);
}
-function VacationIndicator() {
+function VacationBanner() {
const t = useTranslations('sidebar');
+ const router = useRouter();
const { isEnabled, isSupported } = useVacationStore();
if (!isSupported || !isEnabled) return null;
return (
-
router.push('/settings')}
+ className={cn(
+ "flex items-center gap-2 w-full px-3 py-2 text-xs",
+ "bg-amber-500/10 dark:bg-amber-400/10 text-amber-700 dark:text-amber-400",
+ "hover:bg-amber-500/15 dark:hover:bg-amber-400/15 transition-colors"
+ )}
>
-
-
- {t("vacation_active")}
-
-
+
+
{t("vacation_active")}
+
+
);
}
@@ -280,6 +268,43 @@ function AdvancedSearchToggle() {
);
}
+function StorageQuota({ quota, isCollapsed }: { quota: { used: number; total: number } | null; isCollapsed: boolean }) {
+ const t = useTranslations('sidebar');
+
+ if (!quota || quota.total <= 0) return null;
+
+ const usagePercent = Math.min((quota.used / quota.total) * 100, 100);
+ const barColor = usagePercent > 90
+ ? "bg-red-500 dark:bg-red-400"
+ : usagePercent > 70
+ ? "bg-amber-500 dark:bg-amber-400"
+ : "bg-green-500 dark:bg-green-400";
+
+ if (isCollapsed) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {t("storage")}
+
+ {formatFileSize(quota.used)} / {formatFileSize(quota.total)}
+
+
+
+
+ );
+}
+
export function Sidebar({
mailboxes = [],
selectedMailbox = "",
@@ -297,17 +322,12 @@ export function Sidebar({
const [isCollapsed, setIsCollapsed] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [expandedFolders, setExpandedFolders] = useState
>(new Set());
- const [showMenu, setShowMenu] = useState(false);
const t = useTranslations('sidebar');
- const { supportsCalendar } = useCalendarStore();
- // Sync local search query with store's active search query
useEffect(() => {
setSearchQuery(activeSearchQuery);
}, [activeSearchQuery]);
- const router = useRouter();
- // Load expanded folders from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem('expandedMailboxes');
if (stored) {
@@ -315,10 +335,9 @@ export function Sidebar({
const parsed = JSON.parse(stored);
setExpandedFolders(new Set(parsed));
} catch (e) {
- console.error('Failed to parse expanded mailboxes:', e);
+ debug.error('Failed to parse expanded mailboxes:', e);
}
} else {
- // By default, expand root folders that have children
const tree = buildMailboxTree(mailboxes);
const defaultExpanded = tree
.filter(node => node.children.length > 0)
@@ -327,7 +346,6 @@ export function Sidebar({
}
}, [mailboxes]);
- // Save expanded folders to localStorage when changed
const handleToggleExpand = (mailboxId: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev);
@@ -336,7 +354,9 @@ export function Sidebar({
} else {
next.add(mailboxId);
}
- localStorage.setItem('expandedMailboxes', JSON.stringify(Array.from(next)));
+ try {
+ localStorage.setItem('expandedMailboxes', JSON.stringify(Array.from(next)));
+ } catch { /* storage full or unavailable */ }
return next;
});
};
@@ -348,15 +368,12 @@ export function Sidebar({
}
};
- // Build hierarchical mailbox tree
const mailboxTree = buildMailboxTree(mailboxes);
- // Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!selectedMailbox || isCollapsed) return;
- // Find the selected node in the tree
const findNode = (nodes: MailboxNode[]): MailboxNode | null => {
for (const node of nodes) {
if (node.id === selectedMailbox) return node;
@@ -369,14 +386,11 @@ export function Sidebar({
const selectedNode = findNode(mailboxTree);
if (!selectedNode) return;
- // Handle arrow keys for expand/collapse
if (e.key === 'ArrowRight' && selectedNode.children.length > 0) {
- // Expand folder
if (!expandedFolders.has(selectedMailbox)) {
handleToggleExpand(selectedMailbox);
}
} else if (e.key === 'ArrowLeft' && selectedNode.children.length > 0) {
- // Collapse folder
if (expandedFolders.has(selectedMailbox)) {
handleToggleExpand(selectedMailbox);
}
@@ -399,7 +413,6 @@ export function Sidebar({
>
{/* Header */}
- {/* Mobile/Tablet: Close button */}
- {/* Desktop: Collapse toggle */}
+ {/* Vacation Banner */}
+ {!isCollapsed && }
+
{/* Search + Advanced Filter Toggle */}
{!isCollapsed && (
@@ -436,7 +451,7 @@ export function Sidebar({
setSearchQuery(e.target.value)}
className={cn("pl-9", searchQuery && "pr-8")}
@@ -470,7 +485,6 @@ export function Sidebar({
) : (
<>
- {/* Render hierarchical mailbox tree */}
{mailboxTree.map((node) => (
- {/* Footer */}
- {!isCollapsed && (
- <>
- {/* Sliding Menu Panel */}
-
-
- {/* Storage Info */}
- {quota && quota.total > 0 && (
-
-
- {t("storage")}
-
- {formatFileSize(quota.used)} / {formatFileSize(quota.total)}
-
-
-
-
- )}
+ {/* Footer: Storage Quota + Sign Out + Push Status */}
+
+
-
- {/* Contacts */}
- router.push('/contacts')}
- className="w-full px-4 py-2 flex items-center justify-between hover:bg-muted transition-colors text-sm"
- >
-
-
- {t("contacts")}
-
-
-
-
- {/* Calendar */}
- {supportsCalendar && (
- router.push('/calendar')}
- className="w-full px-4 py-2 flex items-center justify-between hover:bg-muted transition-colors text-sm"
- >
-
-
- {t("calendar")}
-
-
-
- )}
-
- {/* Settings */}
- router.push('/settings')}
- className="w-full px-4 py-2 flex items-center justify-between hover:bg-muted transition-colors text-sm"
- >
-
-
- {t("settings")}
-
-
-
-
- {/* Sign Out */}
- {onLogout && (
-
-
- {t("sign_out")}
-
- )}
-
-
-
-
- {/* Menu Toggle Button */}
-
+
+ {onLogout && (
setShowMenu(!showMenu)}
+ onClick={onLogout}
className={cn(
- "w-full px-4 py-3 flex items-center justify-between",
- "hover:bg-muted transition-colors",
- "text-sm text-foreground"
+ "flex items-center gap-2 rounded-md transition-colors text-sm text-muted-foreground hover:text-foreground hover:bg-muted",
+ isCollapsed ? "p-2" : "px-2 py-1.5"
)}
+ title={t("sign_out")}
>
-
-
- Menu
-
- {/* Push Connection Status Indicator */}
-
-
- {/* Tooltip on hover */}
-
- {isPushConnected ? t("push_connected") : t("push_disconnected")}
-
-
-
-
+
+ {!isCollapsed && t("sign_out")}
-
- >
- )}
+ )}
+
+ {!isCollapsed && (
+
+
+
+ {isPushConnected ? t("push_connected") : t("push_disconnected")}
+
+
+ )}
+
+
);
-}
\ No newline at end of file
+}
diff --git a/components/settings/vacation-settings.tsx b/components/settings/vacation-settings.tsx
index f9d0bc0e..44761fbe 100644
--- a/components/settings/vacation-settings.tsx
+++ b/components/settings/vacation-settings.tsx
@@ -62,7 +62,9 @@ export function VacationSettings() {
warnings.push(t('warnings.end_before_start'));
}
- if (localFromDate && new Date(localFromDate) < new Date()) {
+ const todayStart = new Date();
+ todayStart.setHours(0, 0, 0, 0);
+ if (localFromDate && new Date(localFromDate) < todayStart) {
warnings.push(t('warnings.start_in_past'));
}
diff --git a/components/ui/confirm-dialog.tsx b/components/ui/confirm-dialog.tsx
new file mode 100644
index 00000000..d6acb44e
--- /dev/null
+++ b/components/ui/confirm-dialog.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useEffect, useId } from "react";
+import { useFocusTrap } from "@/hooks/use-focus-trap";
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { AlertTriangle } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface ConfirmDialogProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ title: string;
+ message: string;
+ confirmText?: string;
+ cancelText?: string;
+ variant?: "default" | "destructive";
+}
+
+export function ConfirmDialog({
+ isOpen,
+ onClose,
+ onConfirm,
+ title,
+ message,
+ confirmText,
+ cancelText,
+ variant = "default",
+}: ConfirmDialogProps) {
+ const t = useTranslations("confirm_dialog");
+ const id = useId();
+
+ const dialogRef = useFocusTrap({
+ isActive: isOpen,
+ onEscape: onClose,
+ restoreFocus: true,
+ });
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const handleBackdropClick = (e: MouseEvent) => {
+ if (dialogRef.current && !dialogRef.current.contains(e.target as Node)) {
+ onClose();
+ }
+ };
+
+ document.addEventListener("mousedown", handleBackdropClick);
+ return () => document.removeEventListener("mousedown", handleBackdropClick);
+ }, [isOpen, onClose, dialogRef]);
+
+ if (!isOpen) return null;
+
+ const resolvedConfirmText = confirmText || t("confirm");
+ const resolvedCancelText = cancelText || t("cancel");
+
+ return (
+
+
+
+
+ {variant === "destructive" && (
+
+ )}
+
+
+ {title}
+
+
+ {message}
+
+
+
+
+
+
+
+ {resolvedCancelText}
+
+ {
+ try {
+ onConfirm();
+ } finally {
+ onClose();
+ }
+ }}
+ className={cn(
+ variant === "destructive" && "shadow-sm"
+ )}
+ >
+ {resolvedConfirmText}
+
+
+
+
+ );
+}
diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx
index f9191d87..da0c046e 100644
--- a/components/ui/toast.tsx
+++ b/components/ui/toast.tsx
@@ -6,6 +6,11 @@ import { cn } from "@/lib/utils";
export type ToastType = "success" | "error" | "info" | "warning";
+export interface ToastAction {
+ label: string;
+ onClick: () => void;
+}
+
export interface Toast {
id: string;
type: ToastType;
@@ -14,6 +19,7 @@ export interface Toast {
duration?: number;
onClick?: () => void;
icon?: React.ReactNode;
+ action?: ToastAction;
}
interface ToastProps {
@@ -52,41 +58,63 @@ export function ToastItem({ toast, onClose }: ToastProps) {
className={cn(
"flex items-start gap-3 p-4 rounded-lg border shadow-lg bg-background animate-slide-in",
styles[toast.type],
- toast.onClick && "cursor-pointer hover:opacity-90 transition-opacity"
+ toast.onClick && !toast.action && "cursor-pointer hover:opacity-90 transition-opacity"
)}
onClick={() => {
- if (toast.onClick) {
+ if (toast.onClick && !toast.action) {
toast.onClick();
onClose(toast.id);
}
}}
>
{toast.icon !== undefined ? toast.icon :
}
-
+
{toast.title}
{toast.message && (
{toast.message}
)}
-
{
- e.stopPropagation();
- onClose(toast.id);
- }}
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
-
-
+
+ {toast.action && (
+ {
+ e.stopPropagation();
+ try {
+ toast.action!.onClick();
+ onClose(toast.id);
+ } catch {
+ // Don't close toast on error so user can retry
+ }
+ }}
+ className="text-sm font-medium underline underline-offset-2 hover:opacity-80 transition-opacity whitespace-nowrap"
+ >
+ {toast.action.label}
+
+ )}
+ {
+ e.stopPropagation();
+ onClose(toast.id);
+ }}
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+
+
+
);
}
export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: (id: string) => void }) {
return (
-
+
{toasts.map((toast) => (
))}
);
-}
\ No newline at end of file
+}
diff --git a/components/ui/welcome-banner.tsx b/components/ui/welcome-banner.tsx
new file mode 100644
index 00000000..08367211
--- /dev/null
+++ b/components/ui/welcome-banner.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { useTranslations } from "next-intl";
+import { X, Lightbulb } from "lucide-react";
+import { Button } from "@/components/ui/button";
+
+const ONBOARDING_KEY = "onboarding_completed";
+
+export function WelcomeBanner() {
+ const t = useTranslations("welcome");
+ const [visible, setVisible] = useState(false);
+ const [dismissed, setDismissed] = useState(false);
+
+ useEffect(() => {
+ try {
+ if (!localStorage.getItem(ONBOARDING_KEY)) {
+ setVisible(true);
+ }
+ } catch { /* localStorage unavailable */ }
+ }, []);
+
+ const dismiss = useCallback(() => {
+ setDismissed(true);
+ try {
+ localStorage.setItem(ONBOARDING_KEY, "true");
+ } catch { /* localStorage unavailable */ }
+ }, []);
+
+ useEffect(() => {
+ if (!visible) return;
+ const handle = (e: KeyboardEvent) => {
+ if (e.key === "Escape") dismiss();
+ };
+ window.addEventListener("keydown", handle);
+ return () => window.removeEventListener("keydown", handle);
+ }, [visible, dismiss]);
+
+ if (!visible) return null;
+
+ return (
+
{
+ if (dismissed) setVisible(false);
+ }}
+ >
+
+
+
+
+
+
+
+
+ {t("title")}
+
+
+ - {t("tip_compose")}
+ - {t("tip_shortcuts")}
+ - {t("tip_sidebar")}
+ - {t("tip_settings")}
+
+
+
+
+
+
+
+
+
+ {t("got_it")}
+
+
+
+
+ );
+}
diff --git a/hooks/use-confirm-dialog.ts b/hooks/use-confirm-dialog.ts
new file mode 100644
index 00000000..4801eeec
--- /dev/null
+++ b/hooks/use-confirm-dialog.ts
@@ -0,0 +1,84 @@
+import { useState, useCallback, useRef, useEffect } from "react";
+
+interface ConfirmDialogState {
+ isOpen: boolean;
+ title: string;
+ message: string;
+ confirmText?: string;
+ cancelText?: string;
+ variant: "default" | "destructive";
+ onConfirm: () => void;
+}
+
+const INITIAL_STATE: ConfirmDialogState = {
+ isOpen: false,
+ title: "",
+ message: "",
+ variant: "default",
+ onConfirm: () => {},
+};
+
+interface ConfirmOptions {
+ title: string;
+ message: string;
+ confirmText?: string;
+ cancelText?: string;
+ variant?: "default" | "destructive";
+}
+
+export function useConfirmDialog() {
+ const [state, setState] = useState
(INITIAL_STATE);
+ const resolveRef = useRef<((value: boolean) => void) | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (resolveRef.current) {
+ resolveRef.current(false);
+ resolveRef.current = null;
+ }
+ };
+ }, []);
+
+ const confirm = useCallback(
+ (options: ConfirmOptions): Promise => {
+ return new Promise((resolve) => {
+ resolveRef.current = resolve;
+ setState({
+ isOpen: true,
+ title: options.title,
+ message: options.message,
+ confirmText: options.confirmText,
+ cancelText: options.cancelText,
+ variant: options.variant || "default",
+ onConfirm: () => {
+ resolveRef.current = null;
+ resolve(true);
+ },
+ });
+ });
+ },
+ []
+ );
+
+ const close = useCallback(() => {
+ if (resolveRef.current) {
+ resolveRef.current(false);
+ resolveRef.current = null;
+ }
+ setState(INITIAL_STATE);
+ }, []);
+
+ return {
+ dialogProps: {
+ isOpen: state.isOpen,
+ onClose: close,
+ onConfirm: state.onConfirm,
+ title: state.title,
+ message: state.message,
+ confirmText: state.confirmText,
+ cancelText: state.cancelText,
+ variant: state.variant,
+ },
+ confirm,
+ };
+}
diff --git a/lib/email-sanitization.ts b/lib/email-sanitization.ts
index dfd0f860..54d64e92 100644
--- a/lib/email-sanitization.ts
+++ b/lib/email-sanitization.ts
@@ -8,7 +8,7 @@ import DOMPurify from 'dompurify';
*/
export const EMAIL_SANITIZE_CONFIG = {
ADD_TAGS: [],
- ADD_ATTR: ['target', 'style', 'class', 'width', 'height', 'align', 'valign', 'bgcolor', 'color'],
+ ADD_ATTR: ['target', 'rel', 'style', 'class', 'width', 'height', 'align', 'valign', 'bgcolor', 'color'],
ALLOW_DATA_ATTR: false,
FORCE_BODY: true,
FORBID_TAGS: [
diff --git a/locales/de/common.json b/locales/de/common.json
index 0278dd18..09cb1823 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -12,7 +12,8 @@
"invalid_credentials": "Ungültige E-Mail-Adresse oder Passwort",
"connection_failed": "Verbindung zum Server fehlgeschlagen",
"generic": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
- "totp_invalid": "Ungültiger Authentifizierungscode. Überprüfen Sie Ihre Authenticator-App."
+ "totp_invalid": "Ungültiger Authentifizierungscode. Überprüfen Sie Ihre Authenticator-App.",
+ "server_error": "Der Server ist vorübergehend nicht erreichbar. Bitte versuchen Sie es später erneut."
},
"config_error": {
"title": "Konfigurationsfehler",
@@ -23,7 +24,13 @@
"totp_toggle": "Ich habe Zwei-Faktor-Authentifizierung",
"totp_label": "Authentifizierungscode",
"totp_placeholder": "000000",
- "totp_hide": "Zwei-Faktor-Authentifizierung ausblenden"
+ "totp_hide": "Zwei-Faktor-Authentifizierung ausblenden",
+ "show_password": "Passwort anzeigen",
+ "hide_password": "Passwort verbergen",
+ "totp_hint": "Aktivieren Sie dies, wenn Ihr Konto einen Zwei-Faktor-Code erfordert",
+ "totp_checkbox": "Zwei-Faktor-Authentifizierungscode verwenden",
+ "session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
+ "dismiss": "Schließen"
},
"sidebar": {
"close": "Schließen",
@@ -66,7 +73,11 @@
"go_back": "Zurück"
},
"clear_search": "Suche löschen",
- "vacation_active": "Abwesenheitsnotiz ist aktiv"
+ "vacation_active": "Abwesenheitsnotiz ist aktiv",
+ "compose_hint": "Verfassen (c)",
+ "search_placeholder_hint": "E-Mails suchen... (drücke /)",
+ "mail": "E-Mail",
+ "nav_label": "Navigation"
},
"email_list": {
"no_emails": "Keine E-Mails",
@@ -202,7 +213,12 @@
"tooltips": {
"reply": "Antworten",
"archive": "Archivieren",
- "delete": "Löschen"
+ "delete": "Löschen",
+ "reply_all": "Allen antworten (a)",
+ "forward": "Weiterleiten (f)",
+ "star": "Markieren (s)",
+ "unstar": "Markierung entfernen (s)",
+ "compose": "Verfassen (c)"
},
"spam": {
"button_title": "Spam melden",
@@ -242,7 +258,8 @@
"no_calendar": "Kalender nicht verfügbar",
"select_calendar": "Kalender auswählen",
"already_in_calendar": "Bereits in deinem Kalender"
- }
+ },
+ "keyboard_shortcuts": "Tastaturkürzel (?)"
},
"email_composer": {
"new_message": "Neue Nachricht",
@@ -290,7 +307,17 @@
},
"remove_sub_address": "Sub-Adresse entfernen",
"use_template": "Vorlage",
- "save_as_template": "Als Vorlage speichern"
+ "save_as_template": "Als Vorlage speichern",
+ "validation": {
+ "recipient_required": "Empfänger hinzufügen zum Senden",
+ "subject_required": "Betreff hinzufügen",
+ "body_required": "Nachricht schreiben oder Datei anhängen"
+ },
+ "upload_progress": "Hochladen {uploaded} / {total}",
+ "upload_cancel": "Hochladen abbrechen",
+ "upload_failed": "Hochladen von {filename} fehlgeschlagen",
+ "send_failed": "E-Mail konnte nicht gesendet werden",
+ "discard_draft_title": "Entwurf verwerfen?"
},
"common": {
"loading": "Lädt...",
@@ -788,7 +815,12 @@
"rule_summary": {
"conditions_count": "{count, plural, one {# Bedingung} other {# Bedingungen}}",
"actions_count": "{count, plural, one {# Aktion} other {# Aktionen}}"
- }
+ },
+ "templates_section": "Von Vorlage starten",
+ "template_newsletters": "Newsletter in Ordner verschieben",
+ "template_receipts": "Quittungen automatisch archivieren",
+ "template_important": "Wichtige E-Mails markieren",
+ "template_notifications": "Benachrichtigungen filtern"
},
"templates": {
"title": "E-Mail-Vorlagen",
@@ -822,7 +854,11 @@
"empty": "Vorlagenname ist erforderlich",
"too_long": "Vorlagenname darf maximal 200 Zeichen haben"
}
- }
+ },
+ "unsaved_changes": "Sie haben ungespeicherte Änderungen",
+ "discard_changes": "Änderungen verwerfen?",
+ "discard": "Verwerfen",
+ "keep_editing": "Weiter bearbeiten"
},
"errors": {
"page_error_title": "Etwas ist schiefgelaufen",
@@ -910,7 +946,9 @@
"archive": "Unterhaltung archivieren",
"delete": "Unterhaltung löschen",
"star": "Unterhaltung mit Stern markieren",
- "unstar": "Stern von Unterhaltung entfernen"
+ "unstar": "Stern von Unterhaltung entfernen",
+ "messages_tooltip": "{count, plural, one {# Nachricht in dieser Unterhaltung} other {# Nachrichten in dieser Unterhaltung}}",
+ "toggle_thread": "Unterhaltung ein-/ausklappen"
},
"identities": {
"modal_title": "Sendeidentitäten verwalten",
@@ -1019,7 +1057,12 @@
"notes": "Notizen",
"no_contact_selected": "Kontakt auswählen, um Details anzuzeigen",
"created": "Erstellt",
- "updated": "Zuletzt aktualisiert"
+ "updated": "Zuletzt aktualisiert",
+ "compose_email": "E-Mail schreiben",
+ "copy_email": "E-Mail kopieren",
+ "copy_phone": "Telefonnummer kopieren",
+ "copied": "In die Zwischenablage kopiert",
+ "copy_failed": "Kopieren in die Zwischenablage fehlgeschlagen"
},
"form": {
"create_title": "Neuer Kontakt",
@@ -1044,7 +1087,8 @@
"updating": "Wird aktualisiert...",
"name_required": "Mindestens ein Vor- oder Nachname ist erforderlich",
"email_invalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
- "save_failed": "Kontakt konnte nicht gespeichert werden"
+ "save_failed": "Kontakt konnte nicht gespeichert werden",
+ "email_error_inline": "Ungültiges E-Mail-Format"
},
"groups": {
"create": "Neue Gruppe",
@@ -1102,7 +1146,12 @@
"error_create": "Kontakt konnte nicht erstellt werden",
"error_update": "Kontakt konnte nicht aktualisiert werden",
"error_delete": "Kontakt konnte nicht gelöscht werden"
- }
+ },
+ "empty_state_title": "Keine Kontakte",
+ "empty_state_subtitle": "Erstellen Sie Ihren ersten Kontakt oder importieren Sie aus einer vCard-Datei",
+ "empty_search_hint": "Versuchen Sie einen anderen Suchbegriff",
+ "clear_search": "Suche löschen",
+ "import_vcard": "vCard importieren"
},
"calendar": {
"title": "Kalender",
@@ -1113,7 +1162,11 @@
"week": "Woche",
"day": "Tag",
"agenda": "Agenda",
- "today": "Heute"
+ "today": "Heute",
+ "month_hint": "Monat (m)",
+ "week_hint": "Woche (w)",
+ "day_hint": "Tag (d)",
+ "agenda_hint": "Agenda (a)"
},
"events": {
"create": "Termin erstellen",
@@ -1159,7 +1212,10 @@
"you_organizer": "Sie sind der Organisator",
"you_attendee": "Sie sind ein Teilnehmer",
"no_participants": "Keine Teilnehmer",
- "count": "{count, plural, one {# Teilnehmer} other {# Teilnehmer}}"
+ "count": "{count, plural, one {# Teilnehmer} other {# Teilnehmer}}",
+ "invited_by": "Einladung von {name}",
+ "respond_below": "Antworten Sie mit den Schaltflächen unten",
+ "rsvp_label": "Ihre Antwort"
},
"recurrence": {
"title": "Wiederholung",
@@ -1249,7 +1305,8 @@
"error": "Kalender konnte nicht importiert werden",
"file_too_large": "Datei überschreitet das 5-MB-Limit",
"invalid_format": "Ungültiges Kalenderdateiformat"
- }
+ },
+ "mini_calendar_change": "Klicken, um den Monat zu wechseln"
},
"advanced_search": {
"title": "Erweiterte Suche",
@@ -1272,6 +1329,21 @@
"clear_all": "Alle löschen",
"filters_active": "{count} Filter",
"filters_active_plural": "{count} Filter",
- "toggle_filters": "Filter"
+ "toggle_filters": "Filter",
+ "search_hint": "Verwenden Sie erweiterte Filter für eine präzise Suche",
+ "advanced_filters_tooltip": "Erweiterte Suchfilter"
+ },
+ "confirm_dialog": {
+ "confirm": "Bestätigen",
+ "cancel": "Abbrechen"
+ },
+ "welcome": {
+ "title": "Willkommen in Ihrem Postfach",
+ "tip_compose": "Drücken Sie c, um eine neue E-Mail zu verfassen",
+ "tip_shortcuts": "Drücken Sie ?, um alle Tastaturkürzel anzuzeigen",
+ "tip_sidebar": "Finden Sie Kalender, Kontakte und Einstellungen im Seitenmenü",
+ "tip_settings": "Passen Sie Ihre Erfahrung in den Einstellungen an",
+ "got_it": "Verstanden",
+ "dismiss": "Schließen"
}
}
diff --git a/locales/en/common.json b/locales/en/common.json
index 85914c4b..9390ac3f 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -9,11 +9,15 @@
"signing_in": "Signing in...",
"loading": "Loading...",
"error": {
- "invalid_credentials": "Invalid email or password",
- "connection_failed": "Failed to connect to the server",
- "generic": "An error occurred. Please try again.",
- "totp_invalid": "Invalid authentication code. Please check your authenticator app."
+ "invalid_credentials": "Invalid email or password. Please check your credentials and try again.",
+ "connection_failed": "Unable to reach the server. Check your internet connection and try again.",
+ "server_error": "The server is temporarily unavailable. Please try again later.",
+ "generic": "An unexpected error occurred. If this persists, contact your administrator.",
+ "totp_invalid": "Invalid authentication code. Please check your authenticator app and try again."
},
+ "show_password": "Show password",
+ "hide_password": "Hide password",
+ "totp_hint": "Enable this if your account requires a two-factor authentication code",
"config_error": {
"title": "Configuration Error",
"fetch_failed": "Unable to load application configuration. Please try again later.",
@@ -23,12 +27,17 @@
"totp_toggle": "I have two-factor authentication",
"totp_label": "Authentication code",
"totp_placeholder": "000000",
- "totp_hide": "Hide two-factor authentication"
+ "totp_hide": "Hide two-factor authentication",
+ "totp_checkbox": "Use two-factor authentication code",
+ "session_expired": "Your session has expired. Please sign in again.",
+ "dismiss": "Dismiss"
},
"sidebar": {
"close": "Close",
"compose": "Compose",
+ "compose_hint": "Compose (c)",
"search_placeholder": "Search mail...",
+ "search_placeholder_hint": "Search mail... (press /)",
"storage": "Storage",
"sign_out": "Sign out",
"contacts": "Contacts",
@@ -66,7 +75,9 @@
"go_back": "Go back"
},
"clear_search": "Clear search",
- "vacation_active": "Vacation responder is active"
+ "vacation_active": "Vacation responder is active",
+ "mail": "Mail",
+ "nav_label": "Navigation"
},
"email_list": {
"no_emails": "No emails",
@@ -110,6 +121,7 @@
"mark_read": "Mark as read",
"print": "Print",
"view_source": "View source",
+ "keyboard_shortcuts": "Keyboard shortcuts (?)",
"email_source": "Email Source",
"copy_source": "Copy to clipboard",
"source_copied": "Source copied to clipboard",
@@ -200,9 +212,14 @@
"none": "None"
},
"tooltips": {
- "reply": "Reply",
- "archive": "Archive",
- "delete": "Delete"
+ "reply": "Reply (r)",
+ "reply_all": "Reply All (a)",
+ "forward": "Forward (f)",
+ "archive": "Archive (e)",
+ "delete": "Delete (# or Del)",
+ "star": "Star (s)",
+ "unstar": "Unstar (s)",
+ "compose": "Compose (c)"
},
"spam": {
"button_title": "Report spam",
@@ -262,6 +279,7 @@
"cancel": "Cancel",
"attach": "Attach",
"discard": "Discard",
+ "discard_draft_title": "Discard draft?",
"discard_draft_confirm": "You have unsaved changes. Do you want to discard this draft?",
"saving": "Saving...",
"draft_saved": "Draft saved",
@@ -290,7 +308,20 @@
},
"remove_sub_address": "Remove sub-address",
"use_template": "Template",
- "save_as_template": "Save as Template"
+ "save_as_template": "Save as Template",
+ "validation": {
+ "recipient_required": "Add a recipient to send",
+ "subject_required": "Add a subject",
+ "body_required": "Write a message or attach a file"
+ },
+ "upload_progress": "Uploading {uploaded} / {total}",
+ "upload_cancel": "Cancel upload",
+ "upload_failed": "Failed to upload {filename}",
+ "send_failed": "Failed to send email"
+ },
+ "confirm_dialog": {
+ "confirm": "Confirm",
+ "cancel": "Cancel"
},
"common": {
"loading": "Loading...",
@@ -393,6 +424,10 @@
"import_success": "Settings imported successfully",
"import_error": "Failed to import settings",
"reset_confirm": "Are you sure you want to reset all settings to defaults?",
+ "unsaved_changes": "You have unsaved changes",
+ "discard_changes": "Discard unsaved changes?",
+ "discard": "Discard",
+ "keep_editing": "Keep editing",
"tabs": {
"appearance": "Appearance",
"language": "Language & Region",
@@ -771,6 +806,11 @@
"validation_empty_name": "Rule name is required",
"validation_empty_conditions": "At least one condition with a value is required",
"validation_empty_actions": "At least one action is required",
+ "templates_section": "Start from template",
+ "template_newsletters": "Move newsletters to folder",
+ "template_receipts": "Auto-archive receipts",
+ "template_important": "Flag important emails",
+ "template_notifications": "Filter notifications",
"sieve_editor": {
"title": "Sieve Script Editor",
"warning": "Editing the raw Sieve script may break visual rule editing. Changes made here override the visual builder.",
@@ -902,6 +942,7 @@
"threads": {
"messages_one": "{count} message",
"messages_other": "{count} messages",
+ "messages_tooltip": "{count, plural, one {# message in this conversation} other {# messages in this conversation}}",
"expand": "Expand conversation",
"collapse": "Collapse conversation",
"loading": "Loading conversation...",
@@ -910,7 +951,8 @@
"archive": "Archive conversation",
"delete": "Delete conversation",
"star": "Star conversation",
- "unstar": "Unstar conversation"
+ "unstar": "Unstar conversation",
+ "toggle_thread": "Toggle thread"
},
"identities": {
"modal_title": "Manage Sending Identities",
@@ -1003,7 +1045,12 @@
"search_placeholder": "Search contacts...",
"create_new": "New Contact",
"empty_state": "No contacts yet",
+ "empty_state_title": "No contacts yet",
+ "empty_state_subtitle": "Create your first contact or import from a vCard file",
"empty_search": "No contacts match your search",
+ "empty_search_hint": "Try a different search term",
+ "clear_search": "Clear search",
+ "import_vcard": "Import vCard",
"delete_confirm": "Are you sure you want to delete this contact?",
"local_mode": "Contacts are stored locally (server does not support JMAP Contacts)",
"back_to_mail": "Back to mail",
@@ -1018,6 +1065,11 @@
"addresses": "Addresses",
"notes": "Notes",
"no_contact_selected": "Select a contact to view details",
+ "compose_email": "Compose email",
+ "copy_email": "Copy email",
+ "copy_phone": "Copy phone number",
+ "copied": "Copied to clipboard",
+ "copy_failed": "Failed to copy to clipboard",
"created": "Created",
"updated": "Last updated"
},
@@ -1044,6 +1096,7 @@
"updating": "Updating...",
"name_required": "At least a first name or last name is required",
"email_invalid": "Please enter a valid email address",
+ "email_error_inline": "Invalid email format",
"save_failed": "Failed to save contact"
},
"groups": {
@@ -1107,13 +1160,18 @@
"calendar": {
"title": "Calendar",
"back_to_email": "Back to email",
- "my_calendars": "My calendars",
+ "my_calendars": "Calendars",
+ "mini_calendar_change": "Click to change month",
"views": {
"month": "Month",
"week": "Week",
"day": "Day",
"agenda": "Agenda",
- "today": "Today"
+ "today": "Today",
+ "month_hint": "Month (m)",
+ "week_hint": "Week (w)",
+ "day_hint": "Day (d)",
+ "agenda_hint": "Agenda (a)"
},
"events": {
"create": "Create event",
@@ -1155,6 +1213,9 @@
"email_placeholder": "Add email address or search contacts",
"send_invitations": "Send invitations to participants",
"status_summary": "{accepted} accepted, {pending} pending",
+ "invited_by": "Invitation from {name}",
+ "respond_below": "Respond using the buttons below",
+ "rsvp_label": "Your response",
"cancel_notification": "Participants will be notified of the cancellation",
"you_organizer": "You are the organizer",
"you_attendee": "You are an attendee",
@@ -1272,6 +1333,17 @@
"clear_all": "Clear all",
"filters_active": "{count} filter",
"filters_active_plural": "{count} filters",
- "toggle_filters": "Filters"
+ "toggle_filters": "Filters",
+ "search_hint": "Use advanced filters for precise search",
+ "advanced_filters_tooltip": "Advanced search filters"
+ },
+ "welcome": {
+ "title": "Welcome to your mailbox",
+ "tip_compose": "Press c to compose a new email",
+ "tip_shortcuts": "Press ? to see all keyboard shortcuts",
+ "tip_sidebar": "Find Calendar, Contacts and Settings in the sidebar menu",
+ "tip_settings": "Customize your experience in Settings",
+ "got_it": "Got it",
+ "dismiss": "Dismiss"
}
}
diff --git a/locales/es/common.json b/locales/es/common.json
index 7a4f8b72..814badf9 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -12,7 +12,8 @@
"invalid_credentials": "Correo electrónico o contraseña inválidos",
"connection_failed": "No se pudo conectar con el servidor",
"generic": "Ocurrió un error. Por favor, inténtelo de nuevo.",
- "totp_invalid": "Código de autenticación inválido. Verifica tu aplicación de autenticación."
+ "totp_invalid": "Código de autenticación inválido. Verifica tu aplicación de autenticación.",
+ "server_error": "El servidor no está disponible temporalmente. Inténtalo más tarde."
},
"config_error": {
"title": "Error de Configuración",
@@ -23,7 +24,13 @@
"totp_toggle": "Tengo autenticación de dos factores",
"totp_label": "Código de autenticación",
"totp_placeholder": "000000",
- "totp_hide": "Ocultar autenticación de dos factores"
+ "totp_hide": "Ocultar autenticación de dos factores",
+ "show_password": "Mostrar contraseña",
+ "hide_password": "Ocultar contraseña",
+ "totp_hint": "Activa si tu cuenta requiere un código de autenticación en dos pasos",
+ "totp_checkbox": "Usar código de autenticación en dos pasos",
+ "session_expired": "Tu sesión ha expirado. Inicia sesión de nuevo.",
+ "dismiss": "Cerrar"
},
"sidebar": {
"close": "Cerrar",
@@ -66,7 +73,11 @@
"go_back": "Volver"
},
"clear_search": "Limpiar búsqueda",
- "vacation_active": "Respuesta automática activa"
+ "vacation_active": "Respuesta automática activa",
+ "compose_hint": "Redactar (c)",
+ "search_placeholder_hint": "Buscar correo... (pulsa /)",
+ "mail": "Correo",
+ "nav_label": "Navegación"
},
"email_list": {
"no_emails": "Sin correos",
@@ -202,7 +213,12 @@
"tooltips": {
"reply": "Responder",
"archive": "Archivar",
- "delete": "Eliminar"
+ "delete": "Eliminar",
+ "reply_all": "Responder a todos (a)",
+ "forward": "Reenviar (f)",
+ "star": "Destacar (s)",
+ "unstar": "Quitar estrella (s)",
+ "compose": "Redactar (c)"
},
"spam": {
"button_title": "Reportar spam",
@@ -242,7 +258,8 @@
"no_calendar": "Calendario no disponible",
"select_calendar": "Seleccionar calendario",
"already_in_calendar": "Ya está en tu calendario"
- }
+ },
+ "keyboard_shortcuts": "Atajos de teclado (?)"
},
"email_composer": {
"new_message": "Nuevo Mensaje",
@@ -290,7 +307,17 @@
},
"remove_sub_address": "Eliminar sub-dirección",
"use_template": "Plantilla",
- "save_as_template": "Guardar como plantilla"
+ "save_as_template": "Guardar como plantilla",
+ "validation": {
+ "recipient_required": "Agregue un destinatario para enviar",
+ "subject_required": "Agregue un asunto",
+ "body_required": "Escriba un mensaje o adjunte un archivo"
+ },
+ "upload_progress": "Subiendo {uploaded} / {total}",
+ "upload_cancel": "Cancelar subida",
+ "upload_failed": "Error al subir {filename}",
+ "send_failed": "Error al enviar el correo",
+ "discard_draft_title": "¿Descartar borrador?"
},
"common": {
"loading": "Cargando...",
@@ -788,7 +815,12 @@
"rule_summary": {
"conditions_count": "{count, plural, one {# condición} other {# condiciones}}",
"actions_count": "{count, plural, one {# acción} other {# acciones}}"
- }
+ },
+ "templates_section": "Usar una plantilla",
+ "template_newsletters": "Mover newsletters a carpeta",
+ "template_receipts": "Archivar recibos automáticamente",
+ "template_important": "Marcar correos importantes",
+ "template_notifications": "Filtrar notificaciones"
},
"templates": {
"title": "Plantillas de correo",
@@ -822,7 +854,11 @@
"empty": "El nombre de la plantilla es obligatorio",
"too_long": "El nombre de la plantilla no debe superar los 200 caracteres"
}
- }
+ },
+ "unsaved_changes": "Tienes cambios sin guardar",
+ "discard_changes": "¿Descartar los cambios?",
+ "discard": "Descartar",
+ "keep_editing": "Seguir editando"
},
"errors": {
"page_error_title": "Algo salió mal",
@@ -910,7 +946,9 @@
"archive": "Archivar conversación",
"delete": "Eliminar conversación",
"star": "Destacar conversación",
- "unstar": "Quitar destacado de conversación"
+ "unstar": "Quitar destacado de conversación",
+ "messages_tooltip": "{count, plural, one {# mensaje en esta conversación} other {# mensajes en esta conversación}}",
+ "toggle_thread": "Expandir/contraer hilo"
},
"identities": {
"modal_title": "Administrar Identidades de Envío",
@@ -1019,7 +1057,12 @@
"notes": "Notas",
"no_contact_selected": "Selecciona un contacto para ver los detalles",
"created": "Creado",
- "updated": "Última actualización"
+ "updated": "Última actualización",
+ "compose_email": "Escribir correo",
+ "copy_email": "Copiar correo",
+ "copy_phone": "Copiar teléfono",
+ "copied": "Copiado al portapapeles",
+ "copy_failed": "Error al copiar al portapapeles"
},
"form": {
"create_title": "Nuevo contacto",
@@ -1044,7 +1087,8 @@
"updating": "Actualizando...",
"name_required": "Se requiere al menos un nombre o apellido",
"email_invalid": "Introduce una dirección de correo válida",
- "save_failed": "Error al guardar el contacto"
+ "save_failed": "Error al guardar el contacto",
+ "email_error_inline": "Formato de correo inválido"
},
"groups": {
"create": "Nuevo grupo",
@@ -1102,7 +1146,12 @@
"error_create": "Error al crear el contacto",
"error_update": "Error al actualizar el contacto",
"error_delete": "Error al eliminar el contacto"
- }
+ },
+ "empty_state_title": "Sin contactos",
+ "empty_state_subtitle": "Crea tu primer contacto o importa desde un archivo vCard",
+ "empty_search_hint": "Prueba con otro término de búsqueda",
+ "clear_search": "Borrar búsqueda",
+ "import_vcard": "Importar vCard"
},
"calendar": {
"title": "Calendario",
@@ -1113,7 +1162,11 @@
"week": "Semana",
"day": "Día",
"agenda": "Agenda",
- "today": "Hoy"
+ "today": "Hoy",
+ "month_hint": "Mes (m)",
+ "week_hint": "Semana (w)",
+ "day_hint": "Día (d)",
+ "agenda_hint": "Agenda (a)"
},
"events": {
"create": "Crear evento",
@@ -1159,7 +1212,10 @@
"you_organizer": "Eres el organizador",
"you_attendee": "Eres un participante",
"no_participants": "Sin participantes",
- "count": "{count, plural, one {# participante} other {# participantes}}"
+ "count": "{count, plural, one {# participante} other {# participantes}}",
+ "invited_by": "Invitación de {name}",
+ "respond_below": "Responde con los botones de abajo",
+ "rsvp_label": "Tu respuesta"
},
"recurrence": {
"title": "Recurrencia",
@@ -1249,7 +1305,8 @@
"error": "Error al importar el calendario",
"file_too_large": "El archivo supera el límite de 5 MB",
"invalid_format": "Formato de archivo de calendario no válido"
- }
+ },
+ "mini_calendar_change": "Clic para cambiar de mes"
},
"advanced_search": {
"title": "Búsqueda avanzada",
@@ -1272,6 +1329,21 @@
"clear_all": "Limpiar todo",
"filters_active": "{count} filtro",
"filters_active_plural": "{count} filtros",
- "toggle_filters": "Filtros"
+ "toggle_filters": "Filtros",
+ "search_hint": "Usa los filtros avanzados para una búsqueda precisa",
+ "advanced_filters_tooltip": "Filtros de búsqueda avanzados"
+ },
+ "confirm_dialog": {
+ "confirm": "Confirmar",
+ "cancel": "Cancelar"
+ },
+ "welcome": {
+ "title": "Bienvenido a tu buzón",
+ "tip_compose": "Pulsa c para redactar un nuevo correo",
+ "tip_shortcuts": "Pulsa ? para ver todos los atajos de teclado",
+ "tip_sidebar": "Encuentra Calendario, Contactos y Ajustes en el menú lateral",
+ "tip_settings": "Personaliza tu experiencia en Ajustes",
+ "got_it": "Entendido",
+ "dismiss": "Cerrar"
}
}
diff --git a/locales/fr/common.json b/locales/fr/common.json
index cb33e8bd..1a59242b 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -12,7 +12,8 @@
"invalid_credentials": "Email ou mot de passe invalide",
"connection_failed": "Échec de la connexion au serveur",
"generic": "Une erreur s'est produite. Veuillez réessayer.",
- "totp_invalid": "Code d'authentification invalide. Vérifiez votre application d'authentification."
+ "totp_invalid": "Code d'authentification invalide. Vérifiez votre application d'authentification.",
+ "server_error": "Le serveur est temporairement indisponible. Veuillez réessayer plus tard."
},
"config_error": {
"title": "Erreur de configuration",
@@ -23,12 +24,20 @@
"totp_toggle": "J'ai l'authentification à deux facteurs",
"totp_label": "Code d'authentification",
"totp_placeholder": "000000",
- "totp_hide": "Masquer l'authentification à deux facteurs"
+ "totp_hide": "Masquer l'authentification à deux facteurs",
+ "totp_checkbox": "Utiliser un code d'authentification à deux facteurs",
+ "session_expired": "Votre session a expiré. Veuillez vous reconnecter.",
+ "dismiss": "Fermer",
+ "show_password": "Afficher le mot de passe",
+ "hide_password": "Masquer le mot de passe",
+ "totp_hint": "Activez si votre compte nécessite un code d'authentification à deux facteurs"
},
"sidebar": {
"close": "Fermer",
"compose": "Composer",
+ "compose_hint": "Composer (c)",
"search_placeholder": "Rechercher un email...",
+ "search_placeholder_hint": "Rechercher... (appuyez sur /)",
"storage": "Stockage",
"sign_out": "Se déconnecter",
"contacts": "Contacts",
@@ -66,7 +75,9 @@
"go_back": "Retour"
},
"clear_search": "Effacer la recherche",
- "vacation_active": "Répondeur d'absence activé"
+ "vacation_active": "Répondeur d'absence activé",
+ "mail": "Messagerie",
+ "nav_label": "Navigation"
},
"email_list": {
"no_emails": "Aucun email",
@@ -202,7 +213,12 @@
"tooltips": {
"reply": "Répondre",
"archive": "Archiver",
- "delete": "Supprimer"
+ "delete": "Supprimer",
+ "reply_all": "Répondre à tous (a)",
+ "forward": "Transférer (f)",
+ "star": "Suivre (s)",
+ "unstar": "Ne plus suivre (s)",
+ "compose": "Rédiger (c)"
},
"spam": {
"button_title": "Signaler comme spam",
@@ -242,7 +258,8 @@
"no_calendar": "Calendrier non disponible",
"select_calendar": "Choisir un calendrier",
"already_in_calendar": "Déjà dans votre calendrier"
- }
+ },
+ "keyboard_shortcuts": "Raccourcis clavier (?)"
},
"email_composer": {
"new_message": "Nouveau message",
@@ -290,7 +307,17 @@
},
"remove_sub_address": "Retirer le sous-adressage",
"use_template": "Modèle",
- "save_as_template": "Enregistrer comme modèle"
+ "save_as_template": "Enregistrer comme modèle",
+ "validation": {
+ "recipient_required": "Ajoutez un destinataire pour envoyer",
+ "subject_required": "Ajoutez un objet",
+ "body_required": "Rédigez un message ou joignez un fichier"
+ },
+ "upload_progress": "Envoi {uploaded} / {total}",
+ "upload_cancel": "Annuler l'envoi",
+ "upload_failed": "Échec du téléversement de {filename}",
+ "send_failed": "Échec de l'envoi de l'e-mail",
+ "discard_draft_title": "Supprimer le brouillon ?"
},
"common": {
"loading": "Chargement...",
@@ -788,7 +815,12 @@
"rule_summary": {
"conditions_count": "{count, plural, one {# condition} other {# conditions}}",
"actions_count": "{count, plural, one {# action} other {# actions}}"
- }
+ },
+ "templates_section": "Partir d'un modèle",
+ "template_newsletters": "Déplacer les newsletters dans un dossier",
+ "template_receipts": "Archiver les reçus automatiquement",
+ "template_important": "Marquer les e-mails importants",
+ "template_notifications": "Filtrer les notifications"
},
"templates": {
"title": "Modèles d'e-mails",
@@ -822,7 +854,11 @@
"empty": "Le nom du modèle est requis",
"too_long": "Le nom du modèle ne doit pas dépasser 200 caractères"
}
- }
+ },
+ "unsaved_changes": "Vous avez des modifications non enregistrées",
+ "discard_changes": "Abandonner les modifications ?",
+ "discard": "Abandonner",
+ "keep_editing": "Continuer l'édition"
},
"errors": {
"page_error_title": "Une erreur s'est produite",
@@ -910,7 +946,9 @@
"archive": "Archiver la conversation",
"delete": "Supprimer la conversation",
"star": "Marquer la conversation comme favorite",
- "unstar": "Retirer des favoris"
+ "unstar": "Retirer des favoris",
+ "messages_tooltip": "{count, plural, one {# message dans cette conversation} other {# messages dans cette conversation}}",
+ "toggle_thread": "Déplier/replier la discussion"
},
"identities": {
"modal_title": "Gérer les identités d'envoi",
@@ -1019,7 +1057,12 @@
"notes": "Notes",
"no_contact_selected": "Sélectionnez un contact pour voir les détails",
"created": "Créé",
- "updated": "Dernière mise à jour"
+ "updated": "Dernière mise à jour",
+ "compose_email": "Écrire un e-mail",
+ "copy_email": "Copier l'e-mail",
+ "copy_phone": "Copier le numéro",
+ "copied": "Copié dans le presse-papiers",
+ "copy_failed": "Échec de la copie dans le presse-papiers"
},
"form": {
"create_title": "Nouveau contact",
@@ -1044,7 +1087,8 @@
"updating": "Mise à jour...",
"name_required": "Un prénom ou un nom est requis",
"email_invalid": "Veuillez saisir une adresse e-mail valide",
- "save_failed": "Échec de l'enregistrement du contact"
+ "save_failed": "Échec de l'enregistrement du contact",
+ "email_error_inline": "Format d'e-mail invalide"
},
"groups": {
"create": "Nouveau groupe",
@@ -1102,7 +1146,12 @@
"error_create": "Échec de la création du contact",
"error_update": "Échec de la mise à jour du contact",
"error_delete": "Échec de la suppression du contact"
- }
+ },
+ "empty_state_title": "Aucun contact",
+ "empty_state_subtitle": "Créez votre premier contact ou importez depuis un fichier vCard",
+ "empty_search_hint": "Essayez un autre terme de recherche",
+ "clear_search": "Effacer la recherche",
+ "import_vcard": "Importer vCard"
},
"calendar": {
"title": "Calendrier",
@@ -1113,7 +1162,11 @@
"week": "Semaine",
"day": "Jour",
"agenda": "Agenda",
- "today": "Aujourd'hui"
+ "today": "Aujourd'hui",
+ "month_hint": "Mois (m)",
+ "week_hint": "Semaine (w)",
+ "day_hint": "Jour (d)",
+ "agenda_hint": "Agenda (a)"
},
"events": {
"create": "Créer un événement",
@@ -1159,7 +1212,10 @@
"you_organizer": "Vous êtes l'organisateur",
"you_attendee": "Vous êtes un participant",
"no_participants": "Aucun participant",
- "count": "{count, plural, one {# participant} other {# participants}}"
+ "count": "{count, plural, one {# participant} other {# participants}}",
+ "invited_by": "Invitation de {name}",
+ "respond_below": "Répondez avec les boutons ci-dessous",
+ "rsvp_label": "Votre réponse"
},
"recurrence": {
"title": "Récurrence",
@@ -1249,7 +1305,8 @@
"error": "Échec de l'importation du calendrier",
"file_too_large": "Le fichier dépasse la limite de 5 Mo",
"invalid_format": "Format de fichier calendrier invalide"
- }
+ },
+ "mini_calendar_change": "Cliquer pour changer de mois"
},
"advanced_search": {
"title": "Recherche avancée",
@@ -1272,6 +1329,21 @@
"clear_all": "Tout effacer",
"filters_active": "{count} filtre",
"filters_active_plural": "{count} filtres",
- "toggle_filters": "Filtres"
+ "toggle_filters": "Filtres",
+ "search_hint": "Utilisez les filtres avancés pour une recherche précise",
+ "advanced_filters_tooltip": "Filtres de recherche avancés"
+ },
+ "welcome": {
+ "title": "Bienvenue dans votre boite mail",
+ "tip_compose": "Appuyez sur c pour composer un nouvel email",
+ "tip_shortcuts": "Appuyez sur ? pour voir tous les raccourcis clavier",
+ "tip_sidebar": "Retrouvez le Calendrier, les Contacts et les Parametres dans le menu lateral",
+ "tip_settings": "Personnalisez votre experience dans les Parametres",
+ "got_it": "Compris",
+ "dismiss": "Fermer"
+ },
+ "confirm_dialog": {
+ "confirm": "Confirmer",
+ "cancel": "Annuler"
}
}
diff --git a/locales/it/common.json b/locales/it/common.json
index e90d897b..1684bd23 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -12,7 +12,8 @@
"invalid_credentials": "Email o password non valida",
"connection_failed": "Impossibile connettersi al server",
"generic": "Si è verificato un errore. Riprova.",
- "totp_invalid": "Codice di autenticazione non valido. Controlla la tua app di autenticazione."
+ "totp_invalid": "Codice di autenticazione non valido. Controlla la tua app di autenticazione.",
+ "server_error": "Il server non è temporaneamente disponibile. Riprova più tardi."
},
"config_error": {
"title": "Errore di configurazione",
@@ -23,7 +24,13 @@
"totp_toggle": "Ho l'autenticazione a due fattori",
"totp_label": "Codice di autenticazione",
"totp_placeholder": "000000",
- "totp_hide": "Nascondi autenticazione a due fattori"
+ "totp_hide": "Nascondi autenticazione a due fattori",
+ "show_password": "Mostra password",
+ "hide_password": "Nascondi password",
+ "totp_hint": "Attiva se il tuo account richiede un codice di autenticazione a due fattori",
+ "totp_checkbox": "Usa codice di autenticazione a due fattori",
+ "session_expired": "La sessione è scaduta. Accedi di nuovo.",
+ "dismiss": "Chiudi"
},
"sidebar": {
"close": "Chiudi",
@@ -66,7 +73,11 @@
"go_back": "Indietro"
},
"clear_search": "Cancella ricerca",
- "vacation_active": "Risponditore automatico attivo"
+ "vacation_active": "Risponditore automatico attivo",
+ "compose_hint": "Scrivi (c)",
+ "search_placeholder_hint": "Cerca posta... (premi /)",
+ "mail": "Posta",
+ "nav_label": "Navigazione"
},
"email_list": {
"no_emails": "Nessun messaggio",
@@ -202,7 +213,12 @@
"tooltips": {
"reply": "Rispondi",
"archive": "Archivia",
- "delete": "Elimina"
+ "delete": "Elimina",
+ "reply_all": "Rispondi a tutti (a)",
+ "forward": "Inoltra (f)",
+ "star": "Aggiungi stella (s)",
+ "unstar": "Rimuovi stella (s)",
+ "compose": "Scrivi (c)"
},
"spam": {
"button_title": "Segnala come spam",
@@ -242,7 +258,8 @@
"no_calendar": "Calendario non disponibile",
"select_calendar": "Seleziona calendario",
"already_in_calendar": "Già nel tuo calendario"
- }
+ },
+ "keyboard_shortcuts": "Scorciatoie da tastiera (?)"
},
"email_composer": {
"new_message": "Nuovo messaggio",
@@ -290,7 +307,17 @@
},
"remove_sub_address": "Rimuovi sotto-indirizzo",
"use_template": "Modello",
- "save_as_template": "Salva come modello"
+ "save_as_template": "Salva come modello",
+ "validation": {
+ "recipient_required": "Aggiungi un destinatario per inviare",
+ "subject_required": "Aggiungi un oggetto",
+ "body_required": "Scrivi un messaggio o allega un file"
+ },
+ "upload_progress": "Caricamento {uploaded} / {total}",
+ "upload_cancel": "Annulla caricamento",
+ "upload_failed": "Caricamento di {filename} non riuscito",
+ "send_failed": "Invio dell'e-mail non riuscito",
+ "discard_draft_title": "Eliminare la bozza?"
},
"common": {
"loading": "Caricamento...",
@@ -788,7 +815,12 @@
"rule_summary": {
"conditions_count": "{count, plural, one {# condizione} other {# condizioni}}",
"actions_count": "{count, plural, one {# azione} other {# azioni}}"
- }
+ },
+ "templates_section": "Parti da un modello",
+ "template_newsletters": "Sposta newsletter in cartella",
+ "template_receipts": "Archivia ricevute automaticamente",
+ "template_important": "Contrassegna email importanti",
+ "template_notifications": "Filtra notifiche"
},
"templates": {
"title": "Modelli email",
@@ -822,7 +854,11 @@
"empty": "Il nome del modello è obbligatorio",
"too_long": "Il nome del modello non deve superare i 200 caratteri"
}
- }
+ },
+ "unsaved_changes": "Hai modifiche non salvate",
+ "discard_changes": "Annullare le modifiche?",
+ "discard": "Annulla",
+ "keep_editing": "Continua a modificare"
},
"errors": {
"page_error_title": "Qualcosa è andato storto",
@@ -910,7 +946,9 @@
"archive": "Archivia conversazione",
"delete": "Elimina conversazione",
"star": "Aggiungi stella alla conversazione",
- "unstar": "Rimuovi stella dalla conversazione"
+ "unstar": "Rimuovi stella dalla conversazione",
+ "messages_tooltip": "{count, plural, one {# messaggio in questa conversazione} other {# messaggi in questa conversazione}}",
+ "toggle_thread": "Espandi/comprimi discussione"
},
"identities": {
"modal_title": "Gestisci identità di invio",
@@ -1019,7 +1057,12 @@
"notes": "Note",
"no_contact_selected": "Seleziona un contatto per vedere i dettagli",
"created": "Creato",
- "updated": "Ultimo aggiornamento"
+ "updated": "Ultimo aggiornamento",
+ "compose_email": "Scrivi email",
+ "copy_email": "Copia email",
+ "copy_phone": "Copia telefono",
+ "copied": "Copiato negli appunti",
+ "copy_failed": "Impossibile copiare negli appunti"
},
"form": {
"create_title": "Nuovo contatto",
@@ -1044,7 +1087,8 @@
"updating": "Aggiornamento...",
"name_required": "È richiesto almeno un nome o cognome",
"email_invalid": "Inserisci un indirizzo email valido",
- "save_failed": "Impossibile salvare il contatto"
+ "save_failed": "Impossibile salvare il contatto",
+ "email_error_inline": "Formato email non valido"
},
"groups": {
"create": "Nuovo gruppo",
@@ -1102,7 +1146,12 @@
"error_create": "Impossibile creare il contatto",
"error_update": "Impossibile aggiornare il contatto",
"error_delete": "Impossibile eliminare il contatto"
- }
+ },
+ "empty_state_title": "Nessun contatto",
+ "empty_state_subtitle": "Crea il tuo primo contatto o importa da un file vCard",
+ "empty_search_hint": "Prova con un altro termine di ricerca",
+ "clear_search": "Cancella ricerca",
+ "import_vcard": "Importa vCard"
},
"calendar": {
"title": "Calendario",
@@ -1113,7 +1162,11 @@
"week": "Settimana",
"day": "Giorno",
"agenda": "Agenda",
- "today": "Oggi"
+ "today": "Oggi",
+ "month_hint": "Mese (m)",
+ "week_hint": "Settimana (w)",
+ "day_hint": "Giorno (d)",
+ "agenda_hint": "Agenda (a)"
},
"events": {
"create": "Crea evento",
@@ -1159,7 +1212,10 @@
"you_organizer": "Sei l'organizzatore",
"you_attendee": "Sei un partecipante",
"no_participants": "Nessun partecipante",
- "count": "{count, plural, one {# partecipante} other {# partecipanti}}"
+ "count": "{count, plural, one {# partecipante} other {# partecipanti}}",
+ "invited_by": "Invito da {name}",
+ "respond_below": "Rispondi con i pulsanti qui sotto",
+ "rsvp_label": "La tua risposta"
},
"recurrence": {
"title": "Ricorrenza",
@@ -1249,7 +1305,8 @@
"error": "Importazione del calendario fallita",
"file_too_large": "Il file supera il limite di 5 MB",
"invalid_format": "Formato del file calendario non valido"
- }
+ },
+ "mini_calendar_change": "Clicca per cambiare mese"
},
"advanced_search": {
"title": "Ricerca avanzata",
@@ -1272,6 +1329,21 @@
"clear_all": "Cancella tutto",
"filters_active": "{count} filtro",
"filters_active_plural": "{count} filtri",
- "toggle_filters": "Filtri"
+ "toggle_filters": "Filtri",
+ "search_hint": "Usa i filtri avanzati per una ricerca precisa",
+ "advanced_filters_tooltip": "Filtri di ricerca avanzati"
+ },
+ "confirm_dialog": {
+ "confirm": "Conferma",
+ "cancel": "Annulla"
+ },
+ "welcome": {
+ "title": "Benvenuto nella tua casella di posta",
+ "tip_compose": "Premi c per scrivere una nuova email",
+ "tip_shortcuts": "Premi ? per vedere tutte le scorciatoie da tastiera",
+ "tip_sidebar": "Trova Calendario, Contatti e Impostazioni nel menu laterale",
+ "tip_settings": "Personalizza la tua esperienza nelle Impostazioni",
+ "got_it": "Ho capito",
+ "dismiss": "Chiudi"
}
}
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 47c76a41..a59fa9b6 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -12,7 +12,8 @@
"invalid_credentials": "メールアドレスまたはパスワードが無効です",
"connection_failed": "サーバーへの接続に失敗しました",
"generic": "エラーが発生しました。もう一度お試しください。",
- "totp_invalid": "認証コードが無効です。認証アプリを確認してください。"
+ "totp_invalid": "認証コードが無効です。認証アプリを確認してください。",
+ "server_error": "サーバーが一時的に利用できません。後でもう一度お試しください。"
},
"config_error": {
"title": "設定エラー",
@@ -23,12 +24,20 @@
"totp_toggle": "二要素認証を使用",
"totp_label": "認証コード",
"totp_placeholder": "000000",
- "totp_hide": "二要素認証を非表示"
+ "totp_hide": "二要素認証を非表示",
+ "totp_checkbox": "二要素認証コードを使用する",
+ "session_expired": "セッションが期限切れになりました。再度サインインしてください。",
+ "dismiss": "閉じる",
+ "show_password": "パスワードを表示",
+ "hide_password": "パスワードを隠す",
+ "totp_hint": "アカウントに二要素認証コードが必要な場合に有効にしてください"
},
"sidebar": {
"close": "閉じる",
"compose": "作成",
+ "compose_hint": "作成 (c)",
"search_placeholder": "メールを検索...",
+ "search_placeholder_hint": "メールを検索... (/ を押す)",
"storage": "ストレージ",
"sign_out": "サインアウト",
"contacts": "連絡先",
@@ -66,7 +75,9 @@
"go_back": "戻る"
},
"clear_search": "検索をクリア",
- "vacation_active": "不在応答が有効です"
+ "vacation_active": "不在応答が有効です",
+ "mail": "メール",
+ "nav_label": "ナビゲーション"
},
"email_list": {
"no_emails": "メールがありません",
@@ -202,7 +213,12 @@
"tooltips": {
"reply": "返信",
"archive": "アーカイブ",
- "delete": "削除"
+ "delete": "削除",
+ "reply_all": "全員に返信 (a)",
+ "forward": "転送 (f)",
+ "star": "スター (s)",
+ "unstar": "スター解除 (s)",
+ "compose": "新規作成 (c)"
},
"spam": {
"button_title": "迷惑メールを報告",
@@ -242,7 +258,8 @@
"no_calendar": "カレンダーが利用できません",
"select_calendar": "カレンダーを選択",
"already_in_calendar": "カレンダーに登録済み"
- }
+ },
+ "keyboard_shortcuts": "キーボードショートカット (?)"
},
"email_composer": {
"new_message": "新規メッセージ",
@@ -290,7 +307,17 @@
},
"remove_sub_address": "サブアドレスを削除",
"use_template": "テンプレート",
- "save_as_template": "テンプレートとして保存"
+ "save_as_template": "テンプレートとして保存",
+ "validation": {
+ "recipient_required": "送信するには宛先を追加してください",
+ "subject_required": "件名を追加してください",
+ "body_required": "メッセージを入力するかファイルを添付してください"
+ },
+ "upload_progress": "アップロード中 {uploaded} / {total}",
+ "upload_cancel": "アップロードをキャンセル",
+ "upload_failed": "{filename} のアップロードに失敗しました",
+ "send_failed": "メールの送信に失敗しました",
+ "discard_draft_title": "下書きを破棄しますか?"
},
"common": {
"loading": "読み込み中...",
@@ -788,7 +815,12 @@
"rule_summary": {
"conditions_count": "{count, plural, other {#個の条件}}",
"actions_count": "{count, plural, other {#個のアクション}}"
- }
+ },
+ "templates_section": "テンプレートから作成",
+ "template_newsletters": "ニュースレターをフォルダに移動",
+ "template_receipts": "領収書を自動アーカイブ",
+ "template_important": "重要なメールにフラグを付ける",
+ "template_notifications": "通知をフィルター"
},
"templates": {
"title": "メールテンプレート",
@@ -822,7 +854,11 @@
"empty": "テンプレート名は必須です",
"too_long": "テンプレート名は200文字以内にしてください"
}
- }
+ },
+ "unsaved_changes": "保存されていない変更があります",
+ "discard_changes": "変更を破棄しますか?",
+ "discard": "破棄",
+ "keep_editing": "編集を続ける"
},
"errors": {
"page_error_title": "問題が発生しました",
@@ -910,7 +946,9 @@
"archive": "会話をアーカイブ",
"delete": "会話を削除",
"star": "会話にスターを付ける",
- "unstar": "会話のスターを外す"
+ "unstar": "会話のスターを外す",
+ "messages_tooltip": "{count, plural, other {この会話の#件のメッセージ}}",
+ "toggle_thread": "スレッドの展開/折りたたみ"
},
"identities": {
"modal_title": "送信者情報を管理",
@@ -1019,7 +1057,12 @@
"notes": "メモ",
"no_contact_selected": "連絡先を選択して詳細を表示",
"created": "作成日",
- "updated": "最終更新"
+ "updated": "最終更新",
+ "compose_email": "メールを作成",
+ "copy_email": "メールをコピー",
+ "copy_phone": "電話番号をコピー",
+ "copied": "クリップボードにコピーしました",
+ "copy_failed": "クリップボードへのコピーに失敗しました"
},
"form": {
"create_title": "新しい連絡先",
@@ -1044,7 +1087,8 @@
"updating": "更新中...",
"name_required": "名前は必須です",
"email_invalid": "有効なメールアドレスを入力してください",
- "save_failed": "連絡先の保存に失敗しました"
+ "save_failed": "連絡先の保存に失敗しました",
+ "email_error_inline": "メールアドレスの形式が正しくありません"
},
"groups": {
"create": "新しいグループ",
@@ -1102,7 +1146,12 @@
"error_create": "連絡先の作成に失敗しました",
"error_update": "連絡先の更新に失敗しました",
"error_delete": "連絡先の削除に失敗しました"
- }
+ },
+ "empty_state_title": "連絡先がありません",
+ "empty_state_subtitle": "最初の連絡先を作成するか、vCardファイルからインポートしてください",
+ "empty_search_hint": "別の検索語をお試しください",
+ "clear_search": "検索をクリア",
+ "import_vcard": "vCardをインポート"
},
"calendar": {
"title": "カレンダー",
@@ -1113,7 +1162,11 @@
"week": "週",
"day": "日",
"agenda": "予定リスト",
- "today": "今日"
+ "today": "今日",
+ "month_hint": "月 (m)",
+ "week_hint": "週 (w)",
+ "day_hint": "日 (d)",
+ "agenda_hint": "予定一覧 (a)"
},
"events": {
"create": "予定を作成",
@@ -1159,7 +1212,10 @@
"you_organizer": "あなたは主催者です",
"you_attendee": "あなたは参加者です",
"no_participants": "参加者なし",
- "count": "{count}人の参加者"
+ "count": "{count}人の参加者",
+ "invited_by": "{name}からの招待",
+ "respond_below": "以下のボタンで返答してください",
+ "rsvp_label": "あなたの返答"
},
"recurrence": {
"title": "繰り返し",
@@ -1249,7 +1305,8 @@
"error": "カレンダーのインポートに失敗しました",
"file_too_large": "ファイルサイズが5MBを超えています",
"invalid_format": "無効なカレンダーファイル形式"
- }
+ },
+ "mini_calendar_change": "クリックで月を変更"
},
"advanced_search": {
"title": "詳細検索",
@@ -1272,6 +1329,21 @@
"clear_all": "すべてクリア",
"filters_active": "{count}件のフィルター",
"filters_active_plural": "{count}件のフィルター",
- "toggle_filters": "フィルター"
+ "toggle_filters": "フィルター",
+ "search_hint": "詳細フィルターで正確に検索",
+ "advanced_filters_tooltip": "詳細検索フィルター"
+ },
+ "confirm_dialog": {
+ "confirm": "確認",
+ "cancel": "キャンセル"
+ },
+ "welcome": {
+ "title": "メールボックスへようこそ",
+ "tip_compose": "cキーで新しいメールを作成",
+ "tip_shortcuts": "?キーですべてのショートカットを表示",
+ "tip_sidebar": "サイドバーからカレンダー、連絡先、設定にアクセス",
+ "tip_settings": "設定でカスタマイズできます",
+ "got_it": "了解",
+ "dismiss": "閉じる"
}
}
diff --git a/locales/nl/common.json b/locales/nl/common.json
index 519f2697..6704e4d0 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -12,7 +12,8 @@
"invalid_credentials": "Ongeldig e-mailadres of wachtwoord",
"connection_failed": "Kan geen verbinding maken met de server",
"generic": "Er is een fout opgetreden. Probeer het opnieuw.",
- "totp_invalid": "Ongeldige authenticatiecode. Controleer uw authenticator-app."
+ "totp_invalid": "Ongeldige authenticatiecode. Controleer uw authenticator-app.",
+ "server_error": "De server is tijdelijk niet beschikbaar. Probeer het later opnieuw."
},
"config_error": {
"title": "Configuratiefout",
@@ -23,7 +24,13 @@
"totp_toggle": "Ik heb tweefactorauthenticatie",
"totp_label": "Authenticatiecode",
"totp_placeholder": "000000",
- "totp_hide": "Tweefactorauthenticatie verbergen"
+ "totp_hide": "Tweefactorauthenticatie verbergen",
+ "show_password": "Wachtwoord tonen",
+ "hide_password": "Wachtwoord verbergen",
+ "totp_hint": "Schakel in als uw account een tweefactorauthenticatiecode vereist",
+ "totp_checkbox": "Tweefactorauthenticatiecode gebruiken",
+ "session_expired": "Uw sessie is verlopen. Meld u opnieuw aan.",
+ "dismiss": "Sluiten"
},
"sidebar": {
"close": "Sluiten",
@@ -66,7 +73,11 @@
"go_back": "Terug"
},
"clear_search": "Zoekopdracht wissen",
- "vacation_active": "Afwezigheidsmelder is actief"
+ "vacation_active": "Afwezigheidsmelder is actief",
+ "compose_hint": "Opstellen (c)",
+ "search_placeholder_hint": "E-mail zoeken... (druk /)",
+ "mail": "E-mail",
+ "nav_label": "Navigatie"
},
"email_list": {
"no_emails": "Geen e-mails",
@@ -202,7 +213,12 @@
"tooltips": {
"reply": "Beantwoorden",
"archive": "Archiveren",
- "delete": "Verwijderen"
+ "delete": "Verwijderen",
+ "reply_all": "Allen beantwoorden (a)",
+ "forward": "Doorsturen (f)",
+ "star": "Ster toevoegen (s)",
+ "unstar": "Ster verwijderen (s)",
+ "compose": "Opstellen (c)"
},
"spam": {
"button_title": "Spam melden",
@@ -242,7 +258,8 @@
"no_calendar": "Agenda niet beschikbaar",
"select_calendar": "Agenda selecteren",
"already_in_calendar": "Staat al in je agenda"
- }
+ },
+ "keyboard_shortcuts": "Sneltoetsen (?)"
},
"email_composer": {
"new_message": "Nieuw bericht",
@@ -290,7 +307,17 @@
},
"remove_sub_address": "Sub-adres verwijderen",
"use_template": "Sjabloon",
- "save_as_template": "Opslaan als sjabloon"
+ "save_as_template": "Opslaan als sjabloon",
+ "validation": {
+ "recipient_required": "Voeg een ontvanger toe om te verzenden",
+ "subject_required": "Voeg een onderwerp toe",
+ "body_required": "Schrijf een bericht of voeg een bestand toe"
+ },
+ "upload_progress": "Uploaden {uploaded} / {total}",
+ "upload_cancel": "Upload annuleren",
+ "upload_failed": "Uploaden van {filename} mislukt",
+ "send_failed": "E-mail verzenden mislukt",
+ "discard_draft_title": "Concept verwijderen?"
},
"common": {
"loading": "Laden...",
@@ -788,7 +815,12 @@
"rule_summary": {
"conditions_count": "{count, plural, one {# voorwaarde} other {# voorwaarden}}",
"actions_count": "{count, plural, one {# actie} other {# acties}}"
- }
+ },
+ "templates_section": "Starten vanuit sjabloon",
+ "template_newsletters": "Nieuwsbrieven naar map verplaatsen",
+ "template_receipts": "Bonnen automatisch archiveren",
+ "template_important": "Belangrijke e-mails markeren",
+ "template_notifications": "Meldingen filteren"
},
"templates": {
"title": "E-mailsjablonen",
@@ -822,7 +854,11 @@
"empty": "Sjabloonnaam is verplicht",
"too_long": "Sjabloonnaam mag maximaal 200 tekens zijn"
}
- }
+ },
+ "unsaved_changes": "U heeft niet-opgeslagen wijzigingen",
+ "discard_changes": "Wijzigingen verwerpen?",
+ "discard": "Verwerpen",
+ "keep_editing": "Doorgaan met bewerken"
},
"errors": {
"page_error_title": "Er is iets misgegaan",
@@ -910,7 +946,9 @@
"archive": "Gesprek archiveren",
"delete": "Gesprek verwijderen",
"star": "Ster toevoegen aan gesprek",
- "unstar": "Ster verwijderen van gesprek"
+ "unstar": "Ster verwijderen van gesprek",
+ "messages_tooltip": "{count, plural, one {# bericht in dit gesprek} other {# berichten in dit gesprek}}",
+ "toggle_thread": "Gesprek in-/uitvouwen"
},
"identities": {
"modal_title": "Verzendidentiteiten beheren",
@@ -1019,7 +1057,12 @@
"notes": "Notities",
"no_contact_selected": "Selecteer een contact om details te bekijken",
"created": "Aangemaakt",
- "updated": "Laatst bijgewerkt"
+ "updated": "Laatst bijgewerkt",
+ "compose_email": "E-mail schrijven",
+ "copy_email": "E-mail kopiëren",
+ "copy_phone": "Telefoonnummer kopiëren",
+ "copied": "Gekopieerd naar klembord",
+ "copy_failed": "Kopiëren naar klembord mislukt"
},
"form": {
"create_title": "Nieuw contact",
@@ -1044,7 +1087,8 @@
"updating": "Bijwerken...",
"name_required": "Ten minste een voor- of achternaam is vereist",
"email_invalid": "Voer een geldig e-mailadres in",
- "save_failed": "Kon contact niet opslaan"
+ "save_failed": "Kon contact niet opslaan",
+ "email_error_inline": "Ongeldig e-mailformaat"
},
"groups": {
"create": "Nieuwe groep",
@@ -1102,7 +1146,12 @@
"error_create": "Kon contact niet aanmaken",
"error_update": "Kon contact niet bijwerken",
"error_delete": "Kon contact niet verwijderen"
- }
+ },
+ "empty_state_title": "Geen contacten",
+ "empty_state_subtitle": "Maak uw eerste contact aan of importeer vanuit een vCard-bestand",
+ "empty_search_hint": "Probeer een andere zoekterm",
+ "clear_search": "Zoekopdracht wissen",
+ "import_vcard": "vCard importeren"
},
"calendar": {
"title": "Agenda",
@@ -1113,7 +1162,11 @@
"week": "Week",
"day": "Dag",
"agenda": "Agenda",
- "today": "Vandaag"
+ "today": "Vandaag",
+ "month_hint": "Maand (m)",
+ "week_hint": "Week (w)",
+ "day_hint": "Dag (d)",
+ "agenda_hint": "Agenda (a)"
},
"events": {
"create": "Evenement aanmaken",
@@ -1159,7 +1212,10 @@
"you_organizer": "Je bent de organisator",
"you_attendee": "Je bent een deelnemer",
"no_participants": "Geen deelnemers",
- "count": "{count, plural, one {# deelnemer} other {# deelnemers}}"
+ "count": "{count, plural, one {# deelnemer} other {# deelnemers}}",
+ "invited_by": "Uitnodiging van {name}",
+ "respond_below": "Reageer met de knoppen hieronder",
+ "rsvp_label": "Uw reactie"
},
"recurrence": {
"title": "Herhaling",
@@ -1249,7 +1305,8 @@
"error": "Agenda importeren mislukt",
"file_too_large": "Bestand overschrijdt de limiet van 5 MB",
"invalid_format": "Ongeldig agendabestandsformaat"
- }
+ },
+ "mini_calendar_change": "Klik om van maand te wisselen"
},
"advanced_search": {
"title": "Geavanceerd zoeken",
@@ -1272,6 +1329,21 @@
"clear_all": "Alles wissen",
"filters_active": "{count} filter",
"filters_active_plural": "{count} filters",
- "toggle_filters": "Filters"
+ "toggle_filters": "Filters",
+ "search_hint": "Gebruik geavanceerde filters voor een nauwkeurige zoekopdracht",
+ "advanced_filters_tooltip": "Geavanceerde zoekfilters"
+ },
+ "confirm_dialog": {
+ "confirm": "Bevestigen",
+ "cancel": "Annuleren"
+ },
+ "welcome": {
+ "title": "Welkom bij uw mailbox",
+ "tip_compose": "Druk op c om een nieuwe e-mail te schrijven",
+ "tip_shortcuts": "Druk op ? om alle sneltoetsen te bekijken",
+ "tip_sidebar": "Vind Agenda, Contacten en Instellingen in het zijmenu",
+ "tip_settings": "Pas uw ervaring aan in Instellingen",
+ "got_it": "Begrepen",
+ "dismiss": "Sluiten"
}
}
diff --git a/locales/pt/common.json b/locales/pt/common.json
index 152528d7..9ad17579 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -12,7 +12,8 @@
"invalid_credentials": "E-mail ou senha inválidos",
"connection_failed": "Falha ao conectar com o servidor",
"generic": "Ocorreu um erro. Por favor, tente novamente.",
- "totp_invalid": "Código de autenticação inválido. Verifique seu aplicativo de autenticação."
+ "totp_invalid": "Código de autenticação inválido. Verifique seu aplicativo de autenticação.",
+ "server_error": "O servidor está temporariamente indisponível. Tente novamente mais tarde."
},
"config_error": {
"title": "Erro de Configuração",
@@ -23,7 +24,13 @@
"totp_toggle": "Tenho autenticação de dois fatores",
"totp_label": "Código de autenticação",
"totp_placeholder": "000000",
- "totp_hide": "Ocultar autenticação de dois fatores"
+ "totp_hide": "Ocultar autenticação de dois fatores",
+ "show_password": "Mostrar senha",
+ "hide_password": "Ocultar senha",
+ "totp_hint": "Ative se sua conta requer um código de autenticação de dois fatores",
+ "totp_checkbox": "Usar código de autenticação de dois fatores",
+ "session_expired": "Sua sessão expirou. Faça login novamente.",
+ "dismiss": "Fechar"
},
"sidebar": {
"close": "Fechar",
@@ -66,7 +73,11 @@
"go_back": "Voltar"
},
"clear_search": "Limpar busca",
- "vacation_active": "Resposta automática ativa"
+ "vacation_active": "Resposta automática ativa",
+ "compose_hint": "Compor (c)",
+ "search_placeholder_hint": "Pesquisar e-mail... (pressione /)",
+ "mail": "E-mail",
+ "nav_label": "Navegação"
},
"email_list": {
"no_emails": "Nenhum e-mail",
@@ -202,7 +213,12 @@
"tooltips": {
"reply": "Responder",
"archive": "Arquivar",
- "delete": "Excluir"
+ "delete": "Excluir",
+ "reply_all": "Responder a todos (a)",
+ "forward": "Encaminhar (f)",
+ "star": "Favoritar (s)",
+ "unstar": "Remover favorito (s)",
+ "compose": "Compor (c)"
},
"spam": {
"button_title": "Reportar spam",
@@ -242,7 +258,8 @@
"no_calendar": "Calendário não disponível",
"select_calendar": "Selecionar calendário",
"already_in_calendar": "Já está no seu calendário"
- }
+ },
+ "keyboard_shortcuts": "Atalhos de teclado (?)"
},
"email_composer": {
"new_message": "Nova Mensagem",
@@ -290,7 +307,17 @@
},
"remove_sub_address": "Remover sub-endereço",
"use_template": "Modelo",
- "save_as_template": "Salvar como modelo"
+ "save_as_template": "Salvar como modelo",
+ "validation": {
+ "recipient_required": "Adicione um destinatário para enviar",
+ "subject_required": "Adicione um assunto",
+ "body_required": "Escreva uma mensagem ou anexe um arquivo"
+ },
+ "upload_progress": "Enviando {uploaded} / {total}",
+ "upload_cancel": "Cancelar envio",
+ "upload_failed": "Falha ao enviar {filename}",
+ "send_failed": "Falha ao enviar o e-mail",
+ "discard_draft_title": "Descartar rascunho?"
},
"common": {
"loading": "Carregando...",
@@ -788,7 +815,12 @@
"rule_summary": {
"conditions_count": "{count, plural, one {# condição} other {# condições}}",
"actions_count": "{count, plural, one {# ação} other {# ações}}"
- }
+ },
+ "templates_section": "Começar com modelo",
+ "template_newsletters": "Mover newsletters para pasta",
+ "template_receipts": "Arquivar recibos automaticamente",
+ "template_important": "Marcar e-mails importantes",
+ "template_notifications": "Filtrar notificações"
},
"templates": {
"title": "Modelos de e-mail",
@@ -822,7 +854,11 @@
"empty": "O nome do modelo é obrigatório",
"too_long": "O nome do modelo não pode exceder 200 caracteres"
}
- }
+ },
+ "unsaved_changes": "Você tem alterações não salvas",
+ "discard_changes": "Descartar alterações?",
+ "discard": "Descartar",
+ "keep_editing": "Continuar editando"
},
"errors": {
"page_error_title": "Algo deu errado",
@@ -910,7 +946,9 @@
"archive": "Arquivar conversa",
"delete": "Excluir conversa",
"star": "Adicionar estrela à conversa",
- "unstar": "Remover estrela da conversa"
+ "unstar": "Remover estrela da conversa",
+ "messages_tooltip": "{count, plural, one {# mensagem nesta conversa} other {# mensagens nesta conversa}}",
+ "toggle_thread": "Expandir/recolher conversa"
},
"identities": {
"modal_title": "Gerenciar Identidades de Envio",
@@ -1019,7 +1057,12 @@
"notes": "Notas",
"no_contact_selected": "Selecione um contato para ver os detalhes",
"created": "Criado",
- "updated": "Última atualização"
+ "updated": "Última atualização",
+ "compose_email": "Escrever e-mail",
+ "copy_email": "Copiar e-mail",
+ "copy_phone": "Copiar telefone",
+ "copied": "Copiado para a área de transferência",
+ "copy_failed": "Falha ao copiar para a área de transferência"
},
"form": {
"create_title": "Novo contato",
@@ -1044,7 +1087,8 @@
"updating": "Atualizando...",
"name_required": "É necessário pelo menos um nome ou sobrenome",
"email_invalid": "Por favor, insira um endereço de e-mail válido",
- "save_failed": "Falha ao salvar contato"
+ "save_failed": "Falha ao salvar contato",
+ "email_error_inline": "Formato de e-mail inválido"
},
"groups": {
"create": "Novo grupo",
@@ -1102,7 +1146,12 @@
"error_create": "Falha ao criar contato",
"error_update": "Falha ao atualizar contato",
"error_delete": "Falha ao excluir contato"
- }
+ },
+ "empty_state_title": "Sem contatos",
+ "empty_state_subtitle": "Crie seu primeiro contato ou importe de um arquivo vCard",
+ "empty_search_hint": "Tente outro termo de pesquisa",
+ "clear_search": "Limpar pesquisa",
+ "import_vcard": "Importar vCard"
},
"calendar": {
"title": "Calendário",
@@ -1113,7 +1162,11 @@
"week": "Semana",
"day": "Dia",
"agenda": "Agenda",
- "today": "Hoje"
+ "today": "Hoje",
+ "month_hint": "Mês (m)",
+ "week_hint": "Semana (w)",
+ "day_hint": "Dia (d)",
+ "agenda_hint": "Agenda (a)"
},
"events": {
"create": "Criar evento",
@@ -1159,7 +1212,10 @@
"you_organizer": "Você é o organizador",
"you_attendee": "Você é um participante",
"no_participants": "Sem participantes",
- "count": "{count, plural, one {# participante} other {# participantes}}"
+ "count": "{count, plural, one {# participante} other {# participantes}}",
+ "invited_by": "Convite de {name}",
+ "respond_below": "Responda com os botões abaixo",
+ "rsvp_label": "Sua resposta"
},
"recurrence": {
"title": "Recorrência",
@@ -1249,7 +1305,8 @@
"error": "Falha ao importar calendário",
"file_too_large": "Arquivo excede o limite de 5 MB",
"invalid_format": "Formato de arquivo de calendário inválido"
- }
+ },
+ "mini_calendar_change": "Clique para mudar o mês"
},
"advanced_search": {
"title": "Pesquisa avançada",
@@ -1272,6 +1329,21 @@
"clear_all": "Limpar tudo",
"filters_active": "{count} filtro",
"filters_active_plural": "{count} filtros",
- "toggle_filters": "Filtros"
+ "toggle_filters": "Filtros",
+ "search_hint": "Use os filtros avançados para uma pesquisa precisa",
+ "advanced_filters_tooltip": "Filtros de pesquisa avançados"
+ },
+ "confirm_dialog": {
+ "confirm": "Confirmar",
+ "cancel": "Cancelar"
+ },
+ "welcome": {
+ "title": "Bem-vindo à sua caixa de entrada",
+ "tip_compose": "Pressione c para escrever um novo e-mail",
+ "tip_shortcuts": "Pressione ? para ver todos os atalhos de teclado",
+ "tip_sidebar": "Encontre Calendário, Contatos e Configurações no menu lateral",
+ "tip_settings": "Personalize sua experiência nas Configurações",
+ "got_it": "Entendi",
+ "dismiss": "Fechar"
}
}
diff --git a/stores/auth-store.ts b/stores/auth-store.ts
index 81603399..ad2ad0c3 100644
--- a/stores/auth-store.ts
+++ b/stores/auth-store.ts
@@ -43,17 +43,11 @@ export const useAuthStore = create()(
set({ isLoading: true, error: null });
try {
- // Create JMAP client
const client = new JMAPClient(serverUrl, username, effectivePassword);
-
- // Try to connect
await client.connect();
- // Fetch identities from the server
const identities = await client.getIdentities();
const primaryIdentity = identities.length > 0 ? identities[0] : null;
-
- // Sync identities to identity store
useIdentityStore.getState().setIdentities(identities);
// Fetch contacts if server supports JMAP Contacts
@@ -66,7 +60,6 @@ export const useAuthStore = create()(
useContactStore.getState().setSupportsSync(false);
}
- // Initialize vacation responder if supported
const vacationStore = useVacationStore.getState();
if (client.supportsVacationResponse()) {
vacationStore.setSupported(true);
@@ -75,14 +68,12 @@ export const useAuthStore = create()(
vacationStore.setSupported(false);
}
- // Initialize calendar if supported
if (client.supportsCalendars()) {
const calendarStore = useCalendarStore.getState();
calendarStore.setSupported(true);
calendarStore.fetchCalendars(client).catch((err) => console.error('Failed to fetch calendars:', err));
}
- // Initialize Sieve filters if supported
if (client.supportsSieve()) {
const filterStore = useFilterStore.getState();
filterStore.setSupported(true);
@@ -106,15 +97,23 @@ export const useAuthStore = create()(
debug.error('Login error:', error);
let errorKey = 'generic';
- // Map common errors to translation keys
if (error instanceof Error) {
if (error.message.includes('Invalid username or password') ||
error.message.includes('401') ||
error.message.includes('Unauthorized')) {
errorKey = 'invalid_credentials';
} else if (error.message.includes('network') ||
- error.message.includes('Failed to fetch')) {
+ error.message.includes('Failed to fetch') ||
+ error.message.includes('NetworkError') ||
+ error.message.includes('ECONNREFUSED')) {
errorKey = 'connection_failed';
+ } else if (error.message.includes('500') ||
+ error.message.includes('502') ||
+ error.message.includes('503') ||
+ error.message.includes('504') ||
+ error.message.includes('Internal Server Error') ||
+ error.message.includes('Service Unavailable')) {
+ errorKey = 'server_error';
}
}
@@ -131,7 +130,6 @@ export const useAuthStore = create()(
logout: () => {
const state = get();
- // Disconnect the JMAP client if it exists
if (state.client) {
state.client.disconnect();
}
@@ -146,10 +144,8 @@ export const useAuthStore = create()(
error: null,
});
- // Clear persisted storage
localStorage.removeItem('auth-storage');
- // Clear email store state
useEmailStore.setState({
emails: [],
mailboxes: [],
@@ -161,29 +157,21 @@ export const useAuthStore = create()(
quota: null,
});
- // Clear identity store state
useIdentityStore.getState().clearIdentities();
-
- // Clear contact store state
useContactStore.getState().clearContacts();
-
- // Clear vacation store state
useVacationStore.getState().clearState();
-
- // Clear calendar store state
useCalendarStore.getState().clearState();
-
- // Clear filter store state
useFilterStore.getState().clearState();
},
checkAuth: async () => {
const state = get();
- // If authenticated but no client (e.g., after page refresh), we can't restore the session
- // because we don't store passwords for security reasons
if (state.isAuthenticated && !state.client) {
- // Reset auth state - user will need to log in again
+ try {
+ sessionStorage.setItem('session_expired', 'true');
+ } catch { /* sessionStorage unavailable */ }
+
set({
isAuthenticated: false,
isLoading: false,
@@ -193,7 +181,6 @@ export const useAuthStore = create()(
});
}
- // Mark loading as complete
set({ isLoading: false });
},
diff --git a/stores/toast-store.ts b/stores/toast-store.ts
index 3577754f..61796830 100644
--- a/stores/toast-store.ts
+++ b/stores/toast-store.ts
@@ -1,5 +1,5 @@
import { create } from "zustand";
-import { Toast } from "@/components/ui/toast";
+import { Toast, ToastAction } from "@/components/ui/toast";
interface ToastStore {
toasts: Toast[];
@@ -16,7 +16,7 @@ export const useToastStore = create((set) => ({
const newToast: Toast = {
...toast,
id,
- duration: toast.duration ?? 5000, // Default 5 seconds
+ duration: toast.duration ?? 5000,
};
set((state) => ({
@@ -35,18 +35,26 @@ export const useToastStore = create((set) => ({
},
}));
-// Helper functions for common toast types
+interface ToastOptions {
+ message?: string;
+ action?: ToastAction;
+ duration?: number;
+}
+
+function showToast(type: Toast["type"], title: string, options?: string | ToastOptions, defaultDuration?: number): void {
+ const opts = typeof options === "string" ? { message: options } : options;
+ useToastStore.getState().addToast({
+ type,
+ title,
+ message: opts?.message,
+ action: opts?.action,
+ duration: opts?.duration ?? defaultDuration,
+ });
+}
+
export const toast = {
- success: (title: string, message?: string) => {
- useToastStore.getState().addToast({ type: "success", title, message });
- },
- error: (title: string, message?: string) => {
- useToastStore.getState().addToast({ type: "error", title, message, duration: 10000 });
- },
- info: (title: string, message?: string) => {
- useToastStore.getState().addToast({ type: "info", title, message });
- },
- warning: (title: string, message?: string) => {
- useToastStore.getState().addToast({ type: "warning", title, message });
- },
-};
\ No newline at end of file
+ success: (title: string, options?: string | ToastOptions) => showToast("success", title, options),
+ error: (title: string, options?: string | ToastOptions) => showToast("error", title, options, 10000),
+ info: (title: string, options?: string | ToastOptions) => showToast("info", title, options),
+ warning: (title: string, options?: string | ToastOptions) => showToast("warning", title, options),
+};