feat: Implement i18n infrastructure
This commit is contained in:
@@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { PlexConnectionSettings, PlexServerConnection, PlexLibrary } from '../types';
|
||||
import { apiService } from '../services/api';
|
||||
import { X, Server, Lock, User, Key, Globe, Eye, EyeOff, CheckCircle, Library, ChevronDown, ChevronRight, Settings, Loader2 } from 'lucide-react';
|
||||
import { useLanguage } from '../LanguageContext';
|
||||
|
||||
interface ConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -13,6 +14,7 @@ interface ConnectionModalProps {
|
||||
}
|
||||
|
||||
const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onConnectSuccess, onShowMessage, initialSettings }) => {
|
||||
const { t } = useLanguage();
|
||||
const [formData, setFormData] = useState<PlexConnectionSettings>({
|
||||
protocol: 'http',
|
||||
address: '',
|
||||
@@ -90,9 +92,9 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
onConnectSuccess(updatedInfo);
|
||||
const saveResult = await apiService.updateLibrary(lib.title);
|
||||
if (saveResult.status !== 'success') {
|
||||
onShowMessage(saveResult.message || 'Failed to save library selection');
|
||||
onShowMessage(saveResult.message || t('toasts.librarySaveFailed'));
|
||||
} else {
|
||||
onShowMessage(`Library switched to ${lib.title}`);
|
||||
onShowMessage(t('toasts.librarySwitched', { library: lib.title }));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -112,7 +114,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
setIsConnecting(false);
|
||||
setError("Connection cancelled by user.");
|
||||
setError(t('toasts.connectionCancelled'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -141,7 +143,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
|
||||
const info = result.data.serverInfo;
|
||||
setConnectedServerInfo(info);
|
||||
onShowMessage(`Successfully connected to ${info.name || 'Plex Server'}`);
|
||||
onShowMessage(t('toasts.connectedTo', { name: info.name || 'Plex Server' }));
|
||||
|
||||
const libs = info.libraries || [];
|
||||
const musicLibraries = libs.filter((lib) => lib.type === 'artist').sort((a, b) => a.title.localeCompare(b.title));
|
||||
@@ -157,13 +159,13 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
});
|
||||
const saveResult = await apiService.updateLibrary(defaultLib.title);
|
||||
if (saveResult.status !== 'success') {
|
||||
setError(saveResult.message || 'Failed to save library selection');
|
||||
setError(saveResult.message || t('toasts.librarySaveFailed'));
|
||||
}
|
||||
} else {
|
||||
onConnectSuccess(info);
|
||||
}
|
||||
} else {
|
||||
setError(result.message || "Connection failed");
|
||||
setError(result.message || t('server.connectionFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -183,7 +185,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
<div className="px-6 py-4 bg-gray-800 border-b border-gray-700 flex items-center justify-between flex-none">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Server size={18} className={isConnected ? "text-green-400" : "text-plex-orange"} />
|
||||
{isConnected ? 'Server Connected' : 'Connect Plex Server'}
|
||||
{isConnected ? t('connection.titleConnected') : t('connection.titleConnect')}
|
||||
</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
|
||||
<X size={20} />
|
||||
@@ -202,7 +204,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
|
||||
{/* Server Connection */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Server Details</label>
|
||||
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">{t('connection.serverDetails')}</label>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="col-span-1">
|
||||
<select
|
||||
@@ -226,7 +228,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
name="address"
|
||||
required
|
||||
disabled={isConnected || isConnecting}
|
||||
placeholder="IP Address or Domain"
|
||||
placeholder={t('connection.address')}
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 pl-9 pr-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
@@ -240,7 +242,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
type="text"
|
||||
name="port"
|
||||
disabled={isConnected || isConnecting}
|
||||
placeholder="Port (e.g. 32400)"
|
||||
placeholder={t('connection.port')}
|
||||
value={formData.port}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 px-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
@@ -252,7 +254,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
|
||||
{/* Authentication */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Authentication</label>
|
||||
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">{t('connection.authentication')}</label>
|
||||
|
||||
{/* Token */}
|
||||
<div className="relative">
|
||||
@@ -263,7 +265,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
type="text"
|
||||
name="token"
|
||||
disabled={isConnected || isConnecting}
|
||||
placeholder="X-Plex-Token (Optional)"
|
||||
placeholder={t('connection.token')}
|
||||
value={formData.token}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 pl-9 pr-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all font-mono ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
@@ -285,7 +287,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
type="text"
|
||||
name="username"
|
||||
disabled={isTokenProvided || isConnecting}
|
||||
placeholder="Username / Email"
|
||||
placeholder={t('connection.username')}
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 pl-9 pr-3 rounded-md text-sm transition-all focus:outline-none ${disabledInputClass}`}
|
||||
@@ -301,7 +303,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
disabled={isTokenProvided || isConnecting}
|
||||
placeholder="Password"
|
||||
placeholder={t('connection.password')}
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className={`w-full h-10 pl-9 pr-10 rounded-md text-sm transition-all focus:outline-none ${disabledInputClass}`}
|
||||
@@ -329,7 +331,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings size={14} />
|
||||
<span>Advanced Options</span>
|
||||
<span>{t('connection.advanced')}</span>
|
||||
</div>
|
||||
{showAdvanced ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
@@ -337,7 +339,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
{showAdvanced && (
|
||||
<div className="p-3 bg-gray-900/50 space-y-3 animate-in slide-in-from-top-2">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 mb-1 block">Connection Timeout (Seconds)</label>
|
||||
<label className="text-xs text-gray-500 mb-1 block">{t('connection.timeout')}</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -366,15 +368,15 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
{isConnecting ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>Connecting... <span className="opacity-75 font-normal ml-1">(Cancel)</span></span>
|
||||
<span>{t('connection.connecting')} <span className="opacity-75 font-normal ml-1">({t('common.cancel')})</span></span>
|
||||
</>
|
||||
) : 'Connect Server'}
|
||||
) : t('connection.connectBtn')}
|
||||
</button>
|
||||
) : (
|
||||
<div className="mt-2 p-2 bg-green-500/10 border border-green-500/20 rounded-lg text-center">
|
||||
<p className="text-green-400 text-sm font-semibold flex items-center justify-center gap-2">
|
||||
<CheckCircle size={16} />
|
||||
Connected Successfully
|
||||
{t('connection.connectedSuccess')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -383,7 +385,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
{/* Library Selection - Appears after connection */}
|
||||
{isConnected && libraries.length > 0 && (
|
||||
<div className="mt-6 pt-5 border-t border-gray-700 animate-in slide-in-from-top-2 fade-in">
|
||||
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider block mb-2">Select Library to Sync</label>
|
||||
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider block mb-2">{t('connection.selectLibrary')}</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Library size={14} className="text-plex-orange" />
|
||||
@@ -407,7 +409,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
|
||||
onClick={onClose}
|
||||
className="px-6 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm font-medium rounded-lg transition-colors border border-gray-600 hover:border-gray-500"
|
||||
>
|
||||
Done
|
||||
{t('common.done')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user