feat(glossaries): add target_language support - DB migration, API, and UI language pair display
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m52s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m52s
This commit is contained in:
@@ -40,10 +40,12 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { SUPPORTED_LANGUAGES } from './types';
|
||||
|
||||
interface CreateGlossaryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onCreate: (data: { name: string; terms: GlossaryTermInput[] }) => Promise<void>;
|
||||
onCreate: (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => Promise<void>;
|
||||
onImportTemplate: (templateId: string, name?: string) => Promise<void>;
|
||||
isCreating: boolean;
|
||||
isImportingTemplate: boolean;
|
||||
@@ -289,6 +291,8 @@ export function CreateGlossaryDialog({
|
||||
const [activeTab, setActiveTab] = useState<'templates' | 'file' | 'manual'>('templates');
|
||||
const [name, setName] = useState('');
|
||||
const [nameAutoFilled, setNameAutoFilled] = useState(false);
|
||||
const [sourceLanguage, setSourceLanguage] = useState('fr');
|
||||
const [targetLanguage, setTargetLanguage] = useState('en');
|
||||
const [terms, setTerms] = useState<GlossaryTermInput[]>([{ source: '', target: '' }]);
|
||||
const [fileTerms, setFileTerms] = useState<GlossaryTermInput[]>([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<GlossaryTemplate | null>(null);
|
||||
@@ -300,6 +304,8 @@ export function CreateGlossaryDialog({
|
||||
const reset = useCallback(() => {
|
||||
setName('');
|
||||
setNameAutoFilled(false);
|
||||
setSourceLanguage('fr');
|
||||
setTargetLanguage('en');
|
||||
setTerms([{ source: '', target: '' }]);
|
||||
setFileTerms([]);
|
||||
setSelectedTemplate(null);
|
||||
@@ -340,9 +346,9 @@ export function CreateGlossaryDialog({
|
||||
const termsToSave = activeTab === 'file'
|
||||
? fileTerms
|
||||
: terms.filter(t => t.source.trim() && t.target.trim());
|
||||
await onCreate({ name: name.trim(), terms: termsToSave });
|
||||
await onCreate({ name: name.trim(), source_language: sourceLanguage, target_language: targetLanguage, terms: termsToSave });
|
||||
reset();
|
||||
}, [activeTab, selectedTemplate, name, fileTerms, terms, onCreate, onImportTemplate, reset]);
|
||||
}, [activeTab, selectedTemplate, name, fileTerms, terms, sourceLanguage, targetLanguage, onCreate, onImportTemplate, reset]);
|
||||
|
||||
const canSubmit = (() => {
|
||||
if (!name.trim() || isProcessing) return false;
|
||||
@@ -381,15 +387,48 @@ export function CreateGlossaryDialog({
|
||||
<DialogDescription>{t('glossaries.dialog.description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="shrink-0 space-y-2 pt-2">
|
||||
<Label htmlFor="glossary-name">{t('glossaries.dialog.nameLabel')}</Label>
|
||||
<Input
|
||||
id="glossary-name"
|
||||
value={name}
|
||||
onChange={(e) => { setName(e.target.value); setNameAutoFilled(false); }}
|
||||
placeholder={t('glossaries.dialog.namePlaceholder')}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<div className="shrink-0 space-y-3 pt-2">
|
||||
<div>
|
||||
<Label htmlFor="glossary-name">{t('glossaries.dialog.nameLabel')}</Label>
|
||||
<Input
|
||||
id="glossary-name"
|
||||
value={name}
|
||||
onChange={(e) => { setName(e.target.value); setNameAutoFilled(false); }}
|
||||
placeholder={t('glossaries.dialog.namePlaceholder')}
|
||||
disabled={isProcessing}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
{/* Language pair selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Langue source</Label>
|
||||
<select
|
||||
value={sourceLanguage}
|
||||
onChange={e => setSourceLanguage(e.target.value)}
|
||||
disabled={isProcessing}
|
||||
className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
{SUPPORTED_LANGUAGES.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="pt-5 text-muted-foreground font-bold">→</div>
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Langue cible</Label>
|
||||
<select
|
||||
value={targetLanguage}
|
||||
onChange={e => setTargetLanguage(e.target.value)}
|
||||
disabled={isProcessing}
|
||||
className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
{SUPPORTED_LANGUAGES.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function GlossariesPage() {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateGlossary = async (data: { name: string; terms: GlossaryTermInput[] }) => {
|
||||
const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => {
|
||||
try {
|
||||
await createGlossary(data);
|
||||
setCreateDialogOpen(false);
|
||||
@@ -501,8 +501,12 @@ export default function GlossariesPage() {
|
||||
<h3 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight mb-1 truncate">
|
||||
{glossary.name}
|
||||
</h3>
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-medium">
|
||||
{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.flag ?? '🌐'} {SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.label ?? glossary.source_language}
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-medium flex items-center gap-1.5">
|
||||
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.flag ?? '🌐'}</span>
|
||||
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.label ?? glossary.source_language}</span>
|
||||
<span className="text-brand-accent font-bold">→</span>
|
||||
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.flag ?? '🌐'}</span>
|
||||
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.label ?? glossary.target_language}</span>
|
||||
</p>
|
||||
<div className="flex justify-between items-center pt-4 mt-5 border-t border-black/5 dark:border-white/10 text-xs text-brand-dark/40 dark:text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Glossary {
|
||||
id: string;
|
||||
name: string;
|
||||
source_language: string;
|
||||
target_language: string;
|
||||
terms: GlossaryTerm[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -19,6 +20,7 @@ export interface GlossaryListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
source_language: string;
|
||||
target_language: string;
|
||||
terms_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -61,12 +63,14 @@ export interface GlossaryTermInputWithId extends GlossaryTermInput {
|
||||
export interface GlossaryCreateInput {
|
||||
name: string;
|
||||
source_language?: string;
|
||||
target_language?: string;
|
||||
terms?: GlossaryTermInput[];
|
||||
}
|
||||
|
||||
export interface GlossaryUpdateInput {
|
||||
name?: string;
|
||||
source_language?: string;
|
||||
target_language?: string;
|
||||
terms?: GlossaryTermInput[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user