feat(translations): add French language support and improve localization handling

This commit is contained in:
PiR1
2026-05-09 18:23:11 +02:00
parent fd98a78123
commit a5db4bd46e
12 changed files with 2352 additions and 126 deletions
@@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom';
import { Menu, User, LogOut, Settings, Bell, Lock, CheckCircle, Trash2, Sun, Moon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { useLocalizedTimeAgo } from '../../hooks/useLocalizedTimeAgo';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAdminAuth } from '../../contexts';
@@ -25,8 +24,7 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
const { user, logout } = useAdminAuth();
const { isDark, toggle: toggleDarkMode, forcedMode } = useAdminDarkMode();
const { t } = useTranslation();
const { format } = useLocalizedDate();
const { formatTimeAgo } = useLocalizedTimeAgo();
const { format, formatDistanceToNow } = useLocalizedDate();
const [showUserMenu, setShowUserMenu] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
@@ -190,7 +188,7 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
{notificationsService.formatNotificationMessage(notification)}
</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{formatTimeAgo(notification.createdAt)}
{formatDistanceToNow(notification.createdAt, { addSuffix: true })}
</p>
</div>
</div>
@@ -106,7 +106,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('backup.dashboard.health.title')}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium bg-${healthColors[health.status]}-100 dark:bg-${healthColors[health.status]}-900/40 text-${healthColors[health.status]}-700 dark:text-${healthColors[health.status]}-300`}>
{health.status.charAt(0).toUpperCase() + health.status.slice(1)}
{t(`backup.dashboard.healthStatus.${health.status}`)}
</span>
</div>
@@ -46,19 +46,28 @@ const NLFlag: React.FC<{ className?: string }> = ({ className = "w-5 h-5" }) =>
</svg>
);
const languages = [
const FRFlag: React.FC<{ className?: string }> = ({ className = "w-5 h-5" }) => (
<svg className={className} viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg">
<path fill="#002395" d="M0 0h213.3v480H0z"/>
<path fill="#fff" d="M213.3 0h213.4v480H213.3z"/>
<path fill="#ED2939" d="M426.7 0H640v480H426.7z"/>
</svg>
);
export const SUPPORTED_LANGUAGES = [
{ code: 'en', name: 'English', Flag: GBFlag },
{ code: 'de', name: 'Deutsch', Flag: DEFlag },
{ code: 'ru', name: 'Русский', Flag: RUFlag },
{ code: 'pt', name: 'Português', Flag: PTBRFlag },
{ code: 'nl', name: 'Nederlands', Flag: NLFlag },
{ code: 'fr', name: 'Français', Flag: FRFlag },
];
export const LanguageSelector: React.FC = () => {
const { i18n } = useTranslation();
const [isOpen, setIsOpen] = React.useState(false);
const currentLanguage = languages.find(lang => lang.code === i18n.language) || languages[0];
const currentLanguage = SUPPORTED_LANGUAGES.find(lang => lang.code === i18n.language) || SUPPORTED_LANGUAGES[0];
const handleLanguageChange = (languageCode: string) => {
i18n.changeLanguage(languageCode);
@@ -78,7 +87,7 @@ export const LanguageSelector: React.FC = () => {
{isOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 py-1 z-50">
{languages.map((language) => (
{SUPPORTED_LANGUAGES.map((language) => (
<button
key={language.code}
onClick={() => handleLanguageChange(language.code)}
@@ -81,12 +81,13 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
onClick={() => setShowSortMenu(!showSortMenu)}
className="w-full md:w-auto text-sm md:text-base"
>
<span className="hidden md:inline">{t('common.sortBy')} </span>
{sortBy === 'date' ? t('gallery.sortByDate').replace('Sort by ', '') :
sortBy === 'name' ? t('gallery.sortByName').replace('Sort by ', '') :
sortBy === 'size' ? t('gallery.sortBySize').replace('Sort by ', '') :
<span className="hidden md:inline">{t('common.sortBy')}{" "}
{sortBy === 'date' ? t('gallery.sortByDate').replace(t('common.sortBy'), '') :
sortBy === 'name' ? t('gallery.sortByName').replace(t('common.sortBy'), '') :
sortBy === 'size' ? t('gallery.sortBySize').replace(t('common.sortBy'), '') :
sortBy === 'capture_date' ? t('photoSort.dateTaken', 'Date Taken') :
t('gallery.sortByRating', 'Rating')}
</span>
</Button>
{showSortMenu && (