feat(i18n): Complete internationalization with timezone fixes and language switching

Completes full internationalization coverage for the webmail application with enhanced language support:

- Replace all remaining hardcoded English strings with translation keys
- Add timezone auto-detection to prevent hydration mismatches
- Enable instant client-side language switching without page reload
- Add 60+ new translation keys in English and French locales
- Improve language switcher component with proper state management
- Add health check endpoint for container orchestration

All user-facing text now supports English and French with no hardcoded fallbacks, providing a fully localized experience.
This commit is contained in:
Matthieu MALVACHE
2026-01-08 04:29:19 +01:00
committed by Matthieu MALVACHE
parent b2b479359f
commit 0d68851b63
20 changed files with 638 additions and 192 deletions
+27 -11
View File
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import { useRouter } from "@/i18n/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -37,6 +37,7 @@ interface SidebarProps {
onMailboxSelect?: (mailboxId: string) => void;
onCompose?: () => void;
onLogout?: () => void;
onSidebarClose?: () => void;
onSearch?: (query: string) => void;
onClearSearch?: () => void;
activeSearchQuery?: string;
@@ -94,6 +95,7 @@ function MailboxTreeItem({
onToggleExpand: (id: string) => void;
isCollapsed: boolean;
}) {
const t = useTranslations('sidebar');
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);
@@ -111,12 +113,11 @@ function MailboxTreeItem({
<div
{...(globalDragging ? dropHandlers : {})}
className={cn(
"group w-full flex items-center px-2 py-1 text-sm transition-all duration-200",
"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", // Root folders are slightly bolder
// Drop target visual feedback
node.depth === 0 && "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"
)}
@@ -133,7 +134,7 @@ function MailboxTreeItem({
"hover:bg-muted active:bg-accent"
)}
style={{ marginLeft: indentPixels }}
title={isExpanded ? "Collapse" : "Expand"}
title={isExpanded ? t('collapse_tooltip') : t('expand_tooltip')}
>
{isExpanded ? (
<ChevronDown className="w-3 h-3 text-muted-foreground" />
@@ -148,7 +149,7 @@ function MailboxTreeItem({
onClick={() => !isVirtualNode && onMailboxSelect?.(node.id)}
disabled={isVirtualNode}
className={cn(
"flex-1 flex items-center text-left py-1 px-1 rounded",
"flex-1 flex items-center text-left py-1 lg:py-1 max-lg:py-2 px-1 rounded",
"transition-colors duration-150",
isVirtualNode && "cursor-default"
)}
@@ -208,6 +209,7 @@ export function Sidebar({
onMailboxSelect,
onCompose,
onLogout,
onSidebarClose,
onSearch,
onClearSearch,
activeSearchQuery = "",
@@ -225,7 +227,6 @@ export function Sidebar({
useEffect(() => {
setSearchQuery(activeSearchQuery);
}, [activeSearchQuery]);
const params = useParams();
const router = useRouter();
// Load expanded folders from localStorage on mount
@@ -313,21 +314,36 @@ export function Sidebar({
className={cn(
"relative flex flex-col h-full border-r transition-all duration-300 overflow-hidden",
"bg-secondary border-border",
isCollapsed ? "w-16" : "w-64",
"max-lg:w-full",
isCollapsed ? "lg:w-16" : "lg:w-64",
className
)}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="flex items-center gap-2 px-4 py-3 border-b border-border">
{/* Mobile/Tablet: Close button */}
<Button
variant="ghost"
size="icon"
onClick={onSidebarClose}
className="lg:hidden h-11 w-11 flex-shrink-0"
aria-label={t("close")}
>
<X className="w-5 h-5" />
</Button>
{/* Desktop: Collapse toggle */}
<Button
variant="ghost"
size="icon"
onClick={() => setIsCollapsed(!isCollapsed)}
className="hidden lg:flex"
>
<Menu className="w-5 h-5" />
</Button>
{!isCollapsed && (
<Button onClick={onCompose} className="ml-2 flex-1">
<Button onClick={onCompose} className="flex-1">
<PenSquare className="w-4 h-4 mr-2" />
{t("compose")}
</Button>
@@ -421,7 +437,7 @@ export function Sidebar({
<div className="border-t border-border mt-2 pt-2">
{/* Settings */}
<button
onClick={() => router.push(`/${params.locale}/settings`)}
onClick={() => router.push('/settings')}
className="w-full px-4 py-2 flex items-center justify-between hover:bg-muted transition-colors text-sm"
>
<span className="flex items-center gap-2">