Files
office_translator/frontend/src/app/dashboard/glossaries/EditGlossaryDialog.tsx
sepehr b2d918c832
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m31s
feat: add multilingual glossary support (backend + frontend types)
Backend:
- Add source_language column to glossaries table
- Add translations JSON column to glossary_terms table
- Alembic migration for schema changes
- format_glossary_for_prompt now language-aware: extracts correct
  translation per target language, falls back to EN reference for
  templates with only FR→EN data
- CRUD routes accept/return source_language and translations
- Pydantic schemas updated

Frontend:
- Types updated: GlossaryTerm now has translations: Record<string, string>
- Glossary/GlossaryListItem now have source_language
- Added SUPPORTED_LANGUAGES constant (13 languages)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 15:25:28 +02:00

218 lines
6.2 KiB
TypeScript

'use client';
import { useState, useCallback, useRef } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Download, Upload } from 'lucide-react';
import { TermEditor } from './TermEditor';
import { exportGlossaryToCsv, parseCsvToTerms } from './csvUtils';
import { useToast } from '@/components/ui/toast';
import type { Glossary, GlossaryTermInput } from './types';
import { MAX_TERMS_PER_GLOSSARY } from './types';
interface EditGlossaryDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
glossary: Glossary | null;
onSave: (id: string, data: { name: string; terms: GlossaryTermInput[] }) => Promise<void>;
isSaving: boolean;
}
export function EditGlossaryDialog({
open,
onOpenChange,
glossary,
onSave,
isSaving,
}: EditGlossaryDialogProps) {
const [name, setName] = useState('');
const [terms, setTerms] = useState<GlossaryTermInput[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
const isInitialized = useRef(false);
if (glossary && !isInitialized.current) {
setName(glossary.name);
setTerms(glossary.terms.map(t => ({ source: t.source, target: t.target })));
isInitialized.current = true;
}
if (!open && isInitialized.current) {
isInitialized.current = false;
}
const handleSave = useCallback(async () => {
if (!glossary || !name.trim()) return;
const validTerms = terms.filter(t => t.source.trim() && t.target.trim());
await onSave(glossary.id, {
name: name.trim(),
terms: validTerms,
});
}, [glossary, name, terms, onSave]);
const handleExport = useCallback(() => {
if (!glossary) return;
const glossaryWithCurrentTerms: Glossary = {
...glossary,
name,
terms: terms.map((t, i) => ({
id: `temp-${i}`,
source: t.source,
target: t.target,
translations: t.translations || {},
created_at: null,
})),
};
exportGlossaryToCsv(glossaryWithCurrentTerms);
}, [glossary, name, terms]);
const handleImportClick = useCallback(() => {
fileInputRef.current?.click();
}, []);
const { toast } = useToast();
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result;
if (typeof text === 'string') {
const importedTerms = parseCsvToTerms(text);
if (importedTerms.length > 0) {
if (importedTerms.length > MAX_TERMS_PER_GLOSSARY) {
toast({
variant: 'destructive',
title: 'Import failed',
description: `CSV contains ${importedTerms.length} terms, but maximum is ${MAX_TERMS_PER_GLOSSARY}. Please reduce the number of terms.`,
});
e.target.value = '';
return;
}
setTerms(importedTerms);
toast({
title: 'Import successful',
description: `${importedTerms.length} terms imported successfully.`,
});
} else {
toast({
variant: 'destructive',
title: 'Import failed',
description: 'No valid terms found in CSV file.',
});
}
}
};
reader.onerror = () => {
toast({
variant: 'destructive',
title: 'Import failed',
description: 'Failed to read CSV file.',
});
};
reader.readAsText(file);
e.target.value = '';
}, [toast]);
const validTermsCount = terms.filter(t => t.source.trim() && t.target.trim()).length;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Glossary</DialogTitle>
<DialogDescription>
Update the glossary name and term pairs.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="space-y-2">
<Label htmlFor="glossary-name">Glossary Name</Label>
<Input
id="glossary-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter glossary name..."
disabled={isSaving}
/>
</div>
<div className="space-y-2">
<Label>Terms ({validTermsCount} valid)</Label>
<TermEditor
terms={terms}
onChange={setTerms}
disabled={isSaving}
/>
</div>
<div className="flex items-center gap-2 pt-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleExport}
disabled={isSaving || validTermsCount === 0}
className="gap-1.5"
>
<Download className="size-3.5" />
Export CSV
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleImportClick}
disabled={isSaving}
className="gap-1.5"
>
<Upload className="size-3.5" />
Import CSV
</Button>
<input
ref={fileInputRef}
type="file"
accept=".csv"
onChange={handleFileChange}
className="hidden"
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={isSaving || !name.trim()}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}