refactor: Remove unused DB translation system

Static dictionary (i18n.ts CAR_TRANSLATIONS) already covers all terms.
DB translations table had only 179 entries used as fallback and was
never actually reached. Simplifies useTranslate hook to static-only.
DB table preserved for safety.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-02-18 23:24:38 +09:00
parent 46973c8508
commit 3f27297c4a
10 changed files with 10 additions and 2140 deletions

View File

@@ -16,7 +16,6 @@ const menuItems = [
{ href: '/admin/withdrawals', label: 'Withdrawals', icon: '💸' },
{ href: '/admin/sns-shares', label: 'SNS Shares', icon: '📢' },
{ href: '/admin/notifications', label: 'Notifications', icon: '🔔' },
{ href: '/admin/translations', label: 'Translations', icon: '🌐' },
{ href: '/admin/dealer-translations', label: 'Dealer Descriptions', icon: '📝' },
{ href: '/admin/users', label: 'Users', icon: '👥' },
{ href: '/admin/inquiries', label: 'Inquiries', icon: '💬' },

View File

@@ -1,765 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { translationsApi, Translation, TranslationListResponse } from '@/lib/api';
const CATEGORY_LABELS: Record<string, string> = {
maker: 'Maker (제조사)',
model: 'Model (모델)',
fuel: 'Fuel (연료)',
transmission: 'Transmission (변속기)',
color: 'Color (색상)',
car_name: 'Car Name (차량명)',
general: 'General (일반)',
};
interface TranslationStats {
total_entries: number;
by_category: Record<string, number>;
translation_coverage: {
english: { translated: number; total: number; percentage: number };
mongolian: { translated: number; total: number; percentage: number };
russian: { translated: number; total: number; percentage: number };
};
}
export default function TranslationsPage() {
const [translations, setTranslations] = useState<Translation[]>([]);
const [categories, setCategories] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [selectedCategory, setSelectedCategory] = useState<string>('');
const [searchTerm, setSearchTerm] = useState('');
const [editingId, setEditingId] = useState<number | null>(null);
const [editData, setEditData] = useState<Partial<Translation>>({});
const [showAddModal, setShowAddModal] = useState(false);
const [showBatchModal, setShowBatchModal] = useState(false);
const [translatingId, setTranslatingId] = useState<number | null>(null);
const [batchTranslating, setBatchTranslating] = useState(false);
const [stats, setStats] = useState<TranslationStats | null>(null);
const [batchOptions, setBatchOptions] = useState({
category: '',
overwriteExisting: false,
targetLangs: ['en', 'mn', 'ru'] as string[],
});
const [batchResult, setBatchResult] = useState<{
total_processed: number;
successful: number;
failed: number;
} | null>(null);
const [newTranslation, setNewTranslation] = useState({
source_text: '',
category: 'general',
text_en: '',
text_mn: '',
text_ru: '',
});
const pageSize = 20;
useEffect(() => {
loadCategories();
loadStats();
}, []);
useEffect(() => {
loadTranslations();
}, [page, selectedCategory, searchTerm]);
const loadCategories = async () => {
try {
const data = await translationsApi.getCategories();
setCategories(data);
} catch (err) {
console.error('Failed to load categories:', err);
}
};
const loadStats = async () => {
try {
const data = await translationsApi.getStats();
setStats(data);
} catch (err) {
console.error('Failed to load stats:', err);
}
};
const loadTranslations = async () => {
setLoading(true);
try {
const data = await translationsApi.getList({
page,
page_size: pageSize,
category: selectedCategory || undefined,
search: searchTerm || undefined,
});
setTranslations(data.translations);
setTotal(data.total);
} catch (err) {
console.error('Failed to load translations:', err);
} finally {
setLoading(false);
}
};
const handleAutoExtract = async () => {
try {
const result = await translationsApi.autoExtract();
alert(result.message);
loadTranslations();
loadStats();
} catch (err) {
console.error('Failed to auto-extract:', err);
alert('Failed to auto-extract translations');
}
};
const handleSeedAllDefaults = async () => {
try {
const result = await translationsApi.seedAllDefaults();
alert(`${result.message}\n\nCategories: ${result.categories.join(', ')}`);
loadTranslations();
loadStats();
} catch (err) {
console.error('Failed to seed defaults:', err);
alert('Failed to seed default translations');
}
};
const handleAutoTranslate = async (translation: Translation) => {
setTranslatingId(translation.id);
try {
const result = await translationsApi.autoTranslate(translation.id);
alert(`Auto-translated: ${result.message}`);
loadTranslations();
loadStats();
} catch (err: any) {
console.error('Failed to auto-translate:', err);
alert(err.response?.data?.detail || 'Failed to auto-translate');
} finally {
setTranslatingId(null);
}
};
const handleBatchTranslate = async () => {
setBatchTranslating(true);
setBatchResult(null);
try {
const result = await translationsApi.autoTranslateBatch(
batchOptions.targetLangs,
batchOptions.category || undefined,
batchOptions.overwriteExisting
);
setBatchResult({
total_processed: result.total_processed,
successful: result.successful,
failed: result.failed,
});
loadTranslations();
loadStats();
} catch (err: any) {
console.error('Failed to batch translate:', err);
alert(err.response?.data?.detail || 'Failed to batch translate');
} finally {
setBatchTranslating(false);
}
};
const handleEdit = (translation: Translation) => {
setEditingId(translation.id);
setEditData({
text_en: translation.text_en || '',
text_mn: translation.text_mn || '',
text_ru: translation.text_ru || '',
});
};
const handleSave = async (id: number) => {
try {
await translationsApi.update(id, editData);
setEditingId(null);
loadTranslations();
loadStats();
} catch (err) {
console.error('Failed to save:', err);
alert('Failed to save translation');
}
};
const handleDelete = async (id: number) => {
if (!confirm('Delete this translation?')) return;
try {
await translationsApi.delete(id);
loadTranslations();
loadStats();
} catch (err) {
console.error('Failed to delete:', err);
alert('Failed to delete translation');
}
};
const handleAdd = async () => {
if (!newTranslation.source_text.trim()) {
alert('Source text is required');
return;
}
try {
await translationsApi.create(newTranslation);
setShowAddModal(false);
setNewTranslation({
source_text: '',
category: 'general',
text_en: '',
text_mn: '',
text_ru: '',
});
loadTranslations();
loadStats();
} catch (err: any) {
console.error('Failed to add:', err);
alert(err.response?.data?.detail || 'Failed to add translation');
}
};
const totalPages = Math.ceil(total / pageSize);
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">Translations Management</h1>
<div className="flex gap-2">
<button
onClick={handleSeedAllDefaults}
className="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 flex items-center gap-2"
title="Load all predefined translations (makers, models, colors, fuels, etc.)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
Seed Defaults
</button>
<button
onClick={handleAutoExtract}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2"
title="Extract terms from cars in database"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Auto Extract
</button>
<button
onClick={() => setShowBatchModal(true)}
className="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
</svg>
Batch Translate
</button>
<button
onClick={() => setShowAddModal(true)}
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add New
</button>
</div>
</div>
{/* Translation Statistics */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl shadow-sm p-4">
<div className="text-sm text-gray-500">Total Entries</div>
<div className="text-2xl font-bold text-gray-800">{stats.total_entries}</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-4">
<div className="text-sm text-gray-500">English Coverage</div>
<div className="flex items-center gap-2">
<div className="text-2xl font-bold text-blue-600">{stats.translation_coverage.english.percentage.toFixed(1)}%</div>
<div className="text-xs text-gray-400">({stats.translation_coverage.english.translated}/{stats.translation_coverage.english.total})</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${stats.translation_coverage.english.percentage}%` }}></div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-4">
<div className="text-sm text-gray-500">Mongolian Coverage</div>
<div className="flex items-center gap-2">
<div className="text-2xl font-bold text-green-600">{stats.translation_coverage.mongolian.percentage.toFixed(1)}%</div>
<div className="text-xs text-gray-400">({stats.translation_coverage.mongolian.translated}/{stats.translation_coverage.mongolian.total})</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${stats.translation_coverage.mongolian.percentage}%` }}></div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-4">
<div className="text-sm text-gray-500">Russian Coverage</div>
<div className="flex items-center gap-2">
<div className="text-2xl font-bold text-red-600">{stats.translation_coverage.russian.percentage.toFixed(1)}%</div>
<div className="text-xs text-gray-400">({stats.translation_coverage.russian.translated}/{stats.translation_coverage.russian.total})</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div className="bg-red-600 h-2 rounded-full" style={{ width: `${stats.translation_coverage.russian.percentage}%` }}></div>
</div>
</div>
</div>
)}
{/* Category Stats */}
{stats && Object.keys(stats.by_category).length > 0 && (
<div className="bg-white rounded-xl shadow-sm p-4 mb-6">
<h3 className="text-sm font-medium text-gray-700 mb-3">Entries by Category</h3>
<div className="flex flex-wrap gap-2">
{Object.entries(stats.by_category).map(([cat, count]) => (
<span key={cat} className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">
{CATEGORY_LABELS[cat] || cat}: {count}
</span>
))}
</div>
</div>
)}
{/* Filters */}
<div className="bg-white rounded-xl shadow-sm p-4 mb-6">
<div className="flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px]">
<label className="block text-sm font-medium text-gray-700 mb-1">Search</label>
<input
type="text"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setPage(1);
}}
placeholder="Search translations..."
className="w-full border border-gray-300 rounded-lg px-3 py-2"
/>
</div>
<div className="w-48">
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
<select
value={selectedCategory}
onChange={(e) => {
setSelectedCategory(e.target.value);
setPage(1);
}}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
<option value="">All Categories</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{CATEGORY_LABELS[cat] || cat}
</option>
))}
</select>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="py-3 px-4 text-left text-sm font-medium text-gray-600">Category</th>
<th className="py-3 px-4 text-left text-sm font-medium text-gray-600">Korean (Source)</th>
<th className="py-3 px-4 text-left text-sm font-medium text-gray-600">English</th>
<th className="py-3 px-4 text-left text-sm font-medium text-gray-600">Mongolian</th>
<th className="py-3 px-4 text-left text-sm font-medium text-gray-600">Russian</th>
<th className="py-3 px-4 text-left text-sm font-medium text-gray-600 w-40">Actions</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={6} className="py-8 text-center">
<div className="flex justify-center">
<div className="w-8 h-8 border-4 border-primary-600 border-t-transparent rounded-full animate-spin"></div>
</div>
</td>
</tr>
) : translations.length === 0 ? (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-500">
No translations found
</td>
</tr>
) : (
translations.map((trans) => (
<tr key={trans.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-3 px-4">
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
{CATEGORY_LABELS[trans.category] || trans.category}
</span>
</td>
<td className="py-3 px-4 font-medium text-gray-800">{trans.source_text}</td>
<td className="py-3 px-4">
{editingId === trans.id ? (
<input
type="text"
value={editData.text_en || ''}
onChange={(e) => setEditData({ ...editData, text_en: e.target.value })}
className="w-full border border-gray-300 rounded px-2 py-1 text-sm"
/>
) : (
<span className={trans.text_en ? 'text-gray-700' : 'text-gray-400 italic'}>
{trans.text_en || 'Not set'}
</span>
)}
</td>
<td className="py-3 px-4">
{editingId === trans.id ? (
<input
type="text"
value={editData.text_mn || ''}
onChange={(e) => setEditData({ ...editData, text_mn: e.target.value })}
className="w-full border border-gray-300 rounded px-2 py-1 text-sm"
/>
) : (
<span className={trans.text_mn ? 'text-gray-700' : 'text-gray-400 italic'}>
{trans.text_mn || 'Not set'}
</span>
)}
</td>
<td className="py-3 px-4">
{editingId === trans.id ? (
<input
type="text"
value={editData.text_ru || ''}
onChange={(e) => setEditData({ ...editData, text_ru: e.target.value })}
className="w-full border border-gray-300 rounded px-2 py-1 text-sm"
/>
) : (
<span className={trans.text_ru ? 'text-gray-700' : 'text-gray-400 italic'}>
{trans.text_ru || 'Not set'}
</span>
)}
</td>
<td className="py-3 px-4">
{editingId === trans.id ? (
<div className="flex gap-2">
<button
onClick={() => handleSave(trans.id)}
className="text-green-600 hover:text-green-700"
title="Save"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
<button
onClick={() => setEditingId(null)}
className="text-gray-600 hover:text-gray-700"
title="Cancel"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
) : (
<div className="flex gap-2">
<button
onClick={() => handleAutoTranslate(trans)}
disabled={translatingId === trans.id}
className={`text-purple-600 hover:text-purple-700 ${translatingId === trans.id ? 'opacity-50' : ''}`}
title="Auto Translate"
>
{translatingId === trans.id ? (
<div className="w-5 h-5 border-2 border-purple-600 border-t-transparent rounded-full animate-spin"></div>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
</svg>
)}
</button>
<button
onClick={() => handleEdit(trans)}
className="text-blue-600 hover:text-blue-700"
title="Edit"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDelete(trans.id)}
className="text-red-600 hover:text-red-700"
title="Delete"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center items-center gap-2 py-4 border-t border-gray-200">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50"
>
Prev
</button>
<span className="px-4 text-gray-600">
Page {page} of {totalPages} ({total} total)
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
)}
</div>
{/* Add Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl max-w-lg w-full p-6">
<h2 className="text-xl font-bold text-gray-800 mb-4">Add Translation</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Korean (Source Text) *
</label>
<input
type="text"
value={newTranslation.source_text}
onChange={(e) => setNewTranslation({ ...newTranslation, source_text: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
placeholder="Enter Korean text"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
<select
value={newTranslation.category}
onChange={(e) => setNewTranslation({ ...newTranslation, category: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
{categories.map((cat) => (
<option key={cat} value={cat}>
{CATEGORY_LABELS[cat] || cat}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">English</label>
<input
type="text"
value={newTranslation.text_en}
onChange={(e) => setNewTranslation({ ...newTranslation, text_en: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
placeholder="English translation"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Mongolian</label>
<input
type="text"
value={newTranslation.text_mn}
onChange={(e) => setNewTranslation({ ...newTranslation, text_mn: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
placeholder="Mongolian translation"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Russian</label>
<input
type="text"
value={newTranslation.text_ru}
onChange={(e) => setNewTranslation({ ...newTranslation, text_ru: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
placeholder="Russian translation"
/>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setShowAddModal(false)}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleAdd}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Add
</button>
</div>
</div>
</div>
)}
{/* Batch Translate Modal */}
{showBatchModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl max-w-lg w-full p-6">
<h2 className="text-xl font-bold text-gray-800 mb-4">Batch Auto-Translate</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Category (Optional)</label>
<select
value={batchOptions.category}
onChange={(e) => setBatchOptions({ ...batchOptions, category: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
<option value="">All Categories</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{CATEGORY_LABELS[cat] || cat}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">Leave empty to translate all categories</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Target Languages</label>
<div className="flex gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={batchOptions.targetLangs.includes('en')}
onChange={(e) => {
if (e.target.checked) {
setBatchOptions({ ...batchOptions, targetLangs: [...batchOptions.targetLangs, 'en'] });
} else {
setBatchOptions({ ...batchOptions, targetLangs: batchOptions.targetLangs.filter(l => l !== 'en') });
}
}}
className="rounded"
/>
<span>English</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={batchOptions.targetLangs.includes('mn')}
onChange={(e) => {
if (e.target.checked) {
setBatchOptions({ ...batchOptions, targetLangs: [...batchOptions.targetLangs, 'mn'] });
} else {
setBatchOptions({ ...batchOptions, targetLangs: batchOptions.targetLangs.filter(l => l !== 'mn') });
}
}}
className="rounded"
/>
<span>Mongolian</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={batchOptions.targetLangs.includes('ru')}
onChange={(e) => {
if (e.target.checked) {
setBatchOptions({ ...batchOptions, targetLangs: [...batchOptions.targetLangs, 'ru'] });
} else {
setBatchOptions({ ...batchOptions, targetLangs: batchOptions.targetLangs.filter(l => l !== 'ru') });
}
}}
className="rounded"
/>
<span>Russian</span>
</label>
</div>
</div>
<div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={batchOptions.overwriteExisting}
onChange={(e) => setBatchOptions({ ...batchOptions, overwriteExisting: e.target.checked })}
className="rounded"
/>
<span className="text-sm text-gray-700">Overwrite existing translations</span>
</label>
<p className="text-xs text-gray-500 mt-1">If unchecked, only empty translations will be filled</p>
</div>
{batchResult && (
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-medium text-gray-700 mb-2">Translation Results</h3>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-gray-800">{batchResult.total_processed}</div>
<div className="text-xs text-gray-500">Processed</div>
</div>
<div>
<div className="text-2xl font-bold text-green-600">{batchResult.successful}</div>
<div className="text-xs text-gray-500">Successful</div>
</div>
<div>
<div className="text-2xl font-bold text-red-600">{batchResult.failed}</div>
<div className="text-xs text-gray-500">Failed</div>
</div>
</div>
</div>
)}
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => {
setShowBatchModal(false);
setBatchResult(null);
}}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Close
</button>
<button
onClick={handleBatchTranslate}
disabled={batchTranslating || batchOptions.targetLangs.length === 0}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 flex items-center gap-2"
>
{batchTranslating ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Translating...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
</svg>
Start Batch Translate
</>
)}
</button>
</div>
</div>
</div>
)}
</div>
);
}