feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useUser } from '@/app/dashboard/useUser';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useGlossaries, useGlossary } from './useGlossaries';
|
||||
import type { Glossary, GlossaryTermInput, GlossaryListItem } from './types';
|
||||
import { ProUpgradePrompt } from './ProUpgradePrompt';
|
||||
@@ -16,6 +17,7 @@ import { DeleteGlossaryDialog } from './DeleteGlossaryDialog';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
|
||||
export default function GlossariesPage() {
|
||||
const { t } = useI18n();
|
||||
const { data: user, isLoading: isLoadingUser } = useUser();
|
||||
const {
|
||||
glossaries,
|
||||
@@ -24,9 +26,11 @@ export default function GlossariesPage() {
|
||||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
isImportingTemplate,
|
||||
createGlossary,
|
||||
updateGlossary,
|
||||
deleteGlossary,
|
||||
importTemplate,
|
||||
} = useGlossaries();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -62,14 +66,34 @@ export default function GlossariesPage() {
|
||||
await createGlossary(data);
|
||||
setCreateDialogOpen(false);
|
||||
toast({
|
||||
title: 'Glossary created',
|
||||
description: `"${data.name}" has been created successfully.`,
|
||||
title: t('glossaries.toast.created'),
|
||||
description: t('glossaries.toast.createdDesc', { name: data.name }),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Error',
|
||||
description: 'Failed to create glossary. Please try again.',
|
||||
title: t('glossaries.toast.error'),
|
||||
description: t('glossaries.toast.errorCreate'),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportTemplate = async (templateId: string, name?: string) => {
|
||||
try {
|
||||
await importTemplate(templateId, name);
|
||||
setCreateDialogOpen(false);
|
||||
toast({
|
||||
title: t('glossaries.toast.imported'),
|
||||
description: name
|
||||
? t('glossaries.toast.importedDesc', { name })
|
||||
: t('glossaries.toast.importedDesc', { name: templateId }),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('glossaries.toast.error'),
|
||||
description: t('glossaries.toast.errorImport'),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -81,14 +105,14 @@ export default function GlossariesPage() {
|
||||
setEditDialogOpen(false);
|
||||
setSelectedGlossary(null);
|
||||
toast({
|
||||
title: 'Glossary updated',
|
||||
description: `"${data.name}" has been updated successfully.`,
|
||||
title: t('glossaries.toast.updated'),
|
||||
description: t('glossaries.toast.updatedDesc', { name: data.name }),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Error',
|
||||
description: 'Failed to update glossary. Please try again.',
|
||||
title: t('glossaries.toast.error'),
|
||||
description: t('glossaries.toast.errorUpdate'),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -101,14 +125,14 @@ export default function GlossariesPage() {
|
||||
setDeleteDialogOpen(false);
|
||||
setGlossaryToDelete(null);
|
||||
toast({
|
||||
title: 'Glossary deleted',
|
||||
description: 'The glossary has been deleted successfully.',
|
||||
title: t('glossaries.toast.deleted'),
|
||||
description: t('glossaries.toast.deletedDesc'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Error',
|
||||
description: 'Failed to delete glossary. Please try again.',
|
||||
title: t('glossaries.toast.error'),
|
||||
description: t('glossaries.toast.errorDelete'),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -131,10 +155,8 @@ export default function GlossariesPage() {
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Glossaries</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage custom terminology for your LLM translations.
|
||||
</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t('glossaries.title')}</h1>
|
||||
<p className="text-muted-foreground">{t('glossaries.description')}</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
@@ -144,8 +166,8 @@ export default function GlossariesPage() {
|
||||
<BookText className="size-4 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">Your Glossaries</CardTitle>
|
||||
<CardDescription>Create and manage glossaries for consistent translations</CardDescription>
|
||||
<CardTitle className="text-base">{t('glossaries.yourGlossaries')}</CardTitle>
|
||||
<CardDescription>{t('glossaries.yourGlossariesDesc')}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -154,11 +176,9 @@ export default function GlossariesPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{total} glossarie{total !== 1 ? 's' : ''}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Define term pairs to customize your LLM translations
|
||||
{t(total !== 1 ? 'glossaries.count_other' : 'glossaries.count_one', { count: String(total) })}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t('glossaries.defineTerms')}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
@@ -166,17 +186,15 @@ export default function GlossariesPage() {
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
Create New Glossary
|
||||
{t('glossaries.createNew')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{glossaries.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<BookText className="size-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||
<p className="text-muted-foreground">No glossaries yet</p>
|
||||
<p className="text-sm text-muted-foreground/80">
|
||||
Create your first glossary to customize translations
|
||||
</p>
|
||||
<p className="text-muted-foreground">{t('glossaries.empty')}</p>
|
||||
<p className="text-sm text-muted-foreground/80">{t('glossaries.emptyDesc')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -198,15 +216,11 @@ export default function GlossariesPage() {
|
||||
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">About Glossaries</CardTitle>
|
||||
<CardTitle className="text-sm">{t('glossaries.aboutTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground space-y-2">
|
||||
<p>
|
||||
Glossaries let you define custom terminology for your translations. When using LLM translation modes, your terms will be applied to ensure consistent translations.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Format:</strong> Each term has a source (original) and target (translation) pair.
|
||||
</p>
|
||||
<p>{t('glossaries.aboutDesc')}</p>
|
||||
<p><strong>{t('glossaries.aboutFormat')}</strong></p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -214,7 +228,9 @@ export default function GlossariesPage() {
|
||||
open={createDialogOpen}
|
||||
onOpenChange={setCreateDialogOpen}
|
||||
onCreate={handleCreateGlossary}
|
||||
onImportTemplate={handleImportTemplate}
|
||||
isCreating={isCreating}
|
||||
isImportingTemplate={isImportingTemplate}
|
||||
/>
|
||||
|
||||
{editDialogOpen && (fullGlossary || !isLoadingGlossaryDetail) && (
|
||||
|
||||
Reference in New Issue
Block a user