import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import type { FormEvent } from 'react'; import { createTranslation, deleteTranslation, listTranslations, updateTranslation, } from '../api/translations'; import type { Translation } from '../types/api'; import { Alert } from './ui/alert'; import { Button } from './ui/button'; import { Card, CardContent } from './ui/card'; import { EmptyState } from './ui/empty-state'; import { FormField } from './ui/form-field'; import { Input } from './ui/input'; import { LoadingState } from './ui/loading-state'; import { Modal } from './ui/modal'; import { Select } from './ui/select'; import { Textarea } from './ui/textarea'; /** The translation keys that the banner script expects. */ const TRANSLATION_KEYS = [ { key: 'title', label: 'Banner title', placeholder: 'We use cookies' }, { key: 'description', label: 'Banner description', placeholder: 'We use cookies and similar technologies...', multiline: true, }, { key: 'acceptAll', label: 'Accept all button', placeholder: 'Accept all' }, { key: 'rejectAll', label: 'Reject all button', placeholder: 'Reject all' }, { key: 'managePreferences', label: 'Manage preferences button', placeholder: 'Manage preferences', }, { key: 'savePreferences', label: 'Save preferences button', placeholder: 'Save preferences' }, { key: 'privacyPolicyLink', label: 'Privacy policy link text', placeholder: 'Privacy Policy' }, { key: 'closeLabel', label: 'Close button label', placeholder: 'Close' }, { key: 'categoryNecessary', label: 'Necessary category', placeholder: 'Necessary' }, { key: 'categoryNecessaryDesc', label: 'Necessary description', placeholder: 'Essential for the website to function.', }, { key: 'categoryFunctional', label: 'Functional category', placeholder: 'Functional' }, { key: 'categoryFunctionalDesc', label: 'Functional description', placeholder: 'Enable enhanced functionality.', }, { key: 'categoryAnalytics', label: 'Analytics category', placeholder: 'Analytics' }, { key: 'categoryAnalyticsDesc', label: 'Analytics description', placeholder: 'Help us understand how visitors interact.', }, { key: 'categoryMarketing', label: 'Marketing category', placeholder: 'Marketing' }, { key: 'categoryMarketingDesc', label: 'Marketing description', placeholder: 'Used to deliver personalised advertisements.', }, { key: 'categoryPersonalisation', label: 'Personalisation category', placeholder: 'Personalisation', }, { key: 'categoryPersonalisationDesc', label: 'Personalisation description', placeholder: 'Enable content personalisation.', }, { key: 'cookieCount', label: 'Cookie count text', placeholder: '{{count}} cookies used on this site', }, ]; const COMMON_LOCALES = [ { code: 'en', name: 'English' }, { code: 'fr', name: 'French' }, { code: 'de', name: 'German' }, { code: 'es', name: 'Spanish' }, { code: 'it', name: 'Italian' }, { code: 'nl', name: 'Dutch' }, { code: 'pt', name: 'Portuguese' }, { code: 'pl', name: 'Polish' }, { code: 'sv', name: 'Swedish' }, { code: 'da', name: 'Danish' }, { code: 'fi', name: 'Finnish' }, { code: 'no', name: 'Norwegian' }, { code: 'cs', name: 'Czech' }, { code: 'ro', name: 'Romanian' }, { code: 'hu', name: 'Hungarian' }, { code: 'bg', name: 'Bulgarian' }, { code: 'hr', name: 'Croatian' }, { code: 'sk', name: 'Slovak' }, { code: 'sl', name: 'Slovenian' }, { code: 'el', name: 'Greek' }, { code: 'ja', name: 'Japanese' }, { code: 'ko', name: 'Korean' }, { code: 'zh', name: 'Chinese' }, { code: 'ar', name: 'Arabic' }, ]; interface Props { siteId: string; } export default function SiteTranslationsTab({ siteId }: Props) { const queryClient = useQueryClient(); const [selectedLocale, setSelectedLocale] = useState(null); const [showCreate, setShowCreate] = useState(false); const { data: translations, isLoading } = useQuery({ queryKey: ['sites', siteId, 'translations'], queryFn: () => listTranslations(siteId), }); const deleteMutation = useMutation({ mutationFn: (locale: string) => deleteTranslation(siteId, locale), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sites', siteId, 'translations'] }); setSelectedLocale(null); }, }); if (isLoading) { return ; } const existing = translations ?? []; const selected = existing.find((t) => t.locale === selectedLocale); return (

Translations

Manage banner text for different languages. English is the default fallback.

{existing.length === 0 ? ( ) : (
{existing.map((t) => ( ))}
)}
{selected && ( { if (confirm(`Delete ${localeName(selected.locale)} translation?`)) { deleteMutation.mutate(selected.locale); } }} /> )} t.locale)} onClose={() => setShowCreate(false)} onCreated={(locale) => { setShowCreate(false); setSelectedLocale(locale); }} />
); } /* ── Translation editor ──────────────────────────────────────────────── */ function TranslationEditor({ siteId, translation, onDelete, }: { siteId: string; translation: Translation; onDelete: () => void; }) { const queryClient = useQueryClient(); const [strings, setStrings] = useState>(translation.strings); const [saved, setSaved] = useState(false); // Reset state when switching locales const [currentLocale, setCurrentLocale] = useState(translation.locale); if (translation.locale !== currentLocale) { setStrings(translation.strings); setCurrentLocale(translation.locale); setSaved(false); } const mutation = useMutation({ mutationFn: (body: { strings: Record }) => updateTranslation(siteId, translation.locale, body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sites', siteId, 'translations'] }); setSaved(true); setTimeout(() => setSaved(false), 2000); }, }); const handleSubmit = (e: FormEvent) => { e.preventDefault(); mutation.mutate({ strings }); }; const filledCount = TRANSLATION_KEYS.filter((k) => strings[k.key]?.trim()).length; return (

{localeName(translation.locale)}{' '} ({translation.locale})

{filledCount}/{TRANSLATION_KEYS.length} strings translated. Empty strings fall back to English.

{TRANSLATION_KEYS.map(({ key, label, placeholder, multiline }) => (
{multiline ? (