turash/bugulma/frontend/pages/admin/LocalizationUIPage.tsx
2025-12-15 10:06:41 +01:00

265 lines
9.1 KiB
TypeScript

import {
Button,
Card,
CardContent,
CardHeader,
CardTitle,
FormField,
Input,
Label,
Badge,
} from '@/components/ui';
import {
useTranslationKeys,
useUpdateUITranslation,
useBulkUpdateUITranslations,
useAutoTranslateMissing,
} from '@/hooks/api/useAdminAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { Save, Download, Upload, Sparkles, Search } from 'lucide-react';
import { useState, useMemo } from 'react';
import { Combobox } from '@/components/ui/Combobox.tsx';
const LocalizationUIPage = () => {
const { t, currentLocale } = useTranslation();
const [selectedLocale, setSelectedLocale] = useState<string>('en');
const [searchTerm, setSearchTerm] = useState('');
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [translationValue, setTranslationValue] = useState('');
const [isBulkMode, setIsBulkMode] = useState(false);
const [bulkUpdates, setBulkUpdates] = useState<Record<string, string>>({});
const { data: translationKeys, isLoading } = useTranslationKeys(selectedLocale);
const { mutate: updateTranslation, isPending: isUpdating } = useUpdateUITranslation();
const { mutate: bulkUpdate, isPending: isBulkUpdating } = useBulkUpdateUITranslations();
const { mutate: autoTranslate, isPending: isAutoTranslating } = useAutoTranslateMissing();
const filteredKeys = useMemo(() => {
if (!translationKeys?.keys) return [];
if (!searchTerm) return translationKeys.keys;
const term = searchTerm.toLowerCase();
return translationKeys.keys.filter(
(key) => key.key.toLowerCase().includes(term) || key.source.toLowerCase().includes(term)
);
}, [translationKeys?.keys, searchTerm]);
const selectedKeyData = useMemo(() => {
if (!selectedKey || !translationKeys?.keys) return null;
return translationKeys.keys.find((k) => k.key === selectedKey);
}, [selectedKey, translationKeys?.keys]);
const handleSave = () => {
if (!selectedKey) return;
updateTranslation(
{
locale: selectedLocale,
key: selectedKey,
value: translationValue,
},
{
onSuccess: () => {
// Query will refetch automatically
},
}
);
};
const handleBulkSave = () => {
const updates = Object.entries(bulkUpdates).map(([key, value]) => ({
locale: selectedLocale,
key,
value,
}));
bulkUpdate(updates, {
onSuccess: () => {
setBulkUpdates({});
setIsBulkMode(false);
},
});
};
const handleAutoTranslate = () => {
autoTranslate(
{
sourceLocale: 'ru', // Russian is the source
targetLocale: selectedLocale,
},
{
onSuccess: () => {
// Query will refetch automatically
},
}
);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'translated':
return <Badge variant="success"> Translated</Badge>;
case 'missing':
return <Badge variant="destructive">Missing</Badge>;
default:
return <Badge variant="secondary">{status}</Badge>;
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">
{t('adminPage.localization.ui.title') || 'UI Translations'}
</h1>
<p className="text-muted-foreground">
{t('adminPage.localization.ui.description') ||
'Manage all frontend UI text translations'}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setIsBulkMode(!isBulkMode)}>
{isBulkMode ? 'Single Edit' : 'Bulk Edit'}
</Button>
<Button variant="outline" onClick={handleAutoTranslate} disabled={isAutoTranslating}>
<Sparkles className="h-4 w-4 mr-2" />
{isAutoTranslating ? 'Translating...' : 'Auto-Translate Missing'}
</Button>
<Button variant="outline">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
<Button variant="outline">
<Upload className="h-4 w-4 mr-2" />
Import
</Button>
</div>
</div>
{/* Locale Selector and Stats */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<FormField>
<Label>Target Locale</Label>
<Combobox
value={selectedLocale}
onChange={setSelectedLocale}
options={[
{ value: 'en', label: 'English (en)' },
{ value: 'tt', label: 'Tatar (tt)' },
]}
/>
</FormField>
{translationKeys && (
<div className="flex gap-4 text-sm">
<div>
<span className="text-muted-foreground">Total: </span>
<span className="font-medium">{translationKeys.total}</span>
</div>
<div>
<span className="text-muted-foreground">Translated: </span>
<span className="font-medium text-success">{translationKeys.translated}</span>
</div>
<div>
<span className="text-muted-foreground">Missing: </span>
<span className="font-medium text-destructive">{translationKeys.missing}</span>
</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Translation Keys List */}
<Card>
<CardHeader>
<CardTitle>Translation Keys</CardTitle>
<Input
placeholder="Search keys..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
icon={<Search className="h-4 w-4" />}
/>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{filteredKeys.map((keyData) => (
<div
key={keyData.key}
className={`p-3 rounded-md border cursor-pointer transition-colors ${
selectedKey === keyData.key
? 'border-primary bg-primary/5'
: 'border-border hover:bg-muted/50'
}`}
onClick={() => {
setSelectedKey(keyData.key);
setTranslationValue(keyData.value || keyData.source);
}}
>
<div className="flex items-center justify-between mb-1">
<span className="font-mono text-sm font-medium">{keyData.key}</span>
{getStatusBadge(keyData.status)}
</div>
<p className="text-sm text-muted-foreground line-clamp-2">{keyData.source}</p>
</div>
))}
</div>
</CardContent>
</Card>
{/* Translation Editor */}
<Card>
<CardHeader>
<CardTitle>
{selectedKey ? `Edit: ${selectedKey}` : 'Select a translation key'}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{selectedKeyData && (
<>
<FormField>
<Label>Source (Russian)</Label>
<Input value={selectedKeyData.source} disabled className="bg-muted" />
</FormField>
<FormField>
<Label>Translation ({selectedLocale.toUpperCase()})</Label>
<textarea
className="min-h-[200px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
value={translationValue}
onChange={(e) => setTranslationValue(e.target.value)}
placeholder="Enter translation..."
/>
</FormField>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setTranslationValue(selectedKeyData.source)}
>
Copy from Source
</Button>
<Button onClick={handleSave} disabled={isUpdating || !translationValue}>
<Save className="h-4 w-4 mr-2" />
{isUpdating ? 'Saving...' : 'Save'}
</Button>
</div>
</>
)}
{!selectedKey && (
<div className="text-center py-12 text-muted-foreground">
Select a translation key from the list to edit
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
};
export default LocalizationUIPage;